mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-09-24 16:29:01 +08:00
fix(chat): show model reasoning inline in agent timeline
Keep thinking events in the compact tool timeline and render expanded reasoning inline instead of hiding it or using a separate title block.
This commit is contained in:
@@ -20,18 +20,23 @@ test('agent steps use compact muted timeline styling', () => {
|
||||
assert.doesNotMatch(source, /\.action-title \.action-title-icon,\s*\n\s*\.icon-mask\s*\{/)
|
||||
})
|
||||
|
||||
test('expanded agent step log hides raw thinking narration', () => {
|
||||
assert.match(source, /visibleIntermediateEvents\s*=\s*computed/)
|
||||
assert.match(source, /e\.type === 'thinking'\)\s*return false/)
|
||||
assert.match(source, /e\.type === 'tool_call' && e\.tool_name === 'thinking'\)\s*return false/)
|
||||
test('expanded agent step log keeps model thinking in the tool timeline', () => {
|
||||
assert.match(source, /visibleIntermediateEvents\s*=\s*computed\(\(\) => intermediateEvents\.value\)/)
|
||||
assert.match(source, /v-for="\(event, index\) in visibleIntermediateEvents"/)
|
||||
})
|
||||
|
||||
test('streaming log also hides raw thinking narration', () => {
|
||||
assert.match(source, /if \(!isConversationDone\.value\)\s*\{\s*return result\.filter/)
|
||||
assert.match(source, /e\.type === 'thinking'\) return false/)
|
||||
assert.match(source, /e\.type === 'tool_call' && e\.tool_name === 'thinking'\) return false/)
|
||||
assert.doesNotMatch(source, /if \(!isConversationDone\.value\)\s*\{\s*return result;\s*\}/)
|
||||
test('streaming log renders reasoning alongside tool calls', () => {
|
||||
assert.match(source, /if \(!isConversationDone\.value\)\s*\{\s*return result;\s*\}/)
|
||||
})
|
||||
|
||||
test('expanded model reasoning stays inline without a separate thinking title', () => {
|
||||
assert.match(source, /class="thinking-inline-content markdown-content"/)
|
||||
assert.match(source, /class="thinking-inline-markdown" v-html="renderMarkdownContent\(event\.content\)"/)
|
||||
assert.match(source, /event\.title && event\.content && isEventExpanded\(event\.event_id\)/)
|
||||
assert.match(source, /\.thinking-inline-title\s*\{[\s\S]*align-items:\s*flex-start/)
|
||||
assert.match(source, /\.thinking-inline-content\s*\{[\s\S]*margin-top:\s*0/)
|
||||
assert.doesNotMatch(source, /\.thinking-inline-title > \.action-title-icon/)
|
||||
assert.match(source, /\.tree-child \.thinking-event-card \.action-title\s*\{[\s\S]*position:\s*static/)
|
||||
})
|
||||
|
||||
test('streaming tool log uses the same timeline structure', () => {
|
||||
@@ -95,6 +100,7 @@ test('agent mode shows a native placeholder before answer whenever nothing is pe
|
||||
assert.match(source, /if \(isConversationDone\.value\) return false/)
|
||||
assert.match(source, /return !hasPendingStreamingActivity\.value/)
|
||||
assert.match(source, /const hasPendingStreamingActivity = computed/)
|
||||
assert.match(source, /event\.thinking === true \|\| isThinkingActive\(event\.event_id\)/)
|
||||
assert.match(source, /event\.type === 'tool_approval_required' \|\| event\.type === 'mcp_oauth_required'/)
|
||||
assert.match(source, /class="action-card action-pending"/)
|
||||
assert.match(source, /t\('chat\.thinkingAlt'\)/)
|
||||
|
||||
@@ -36,19 +36,24 @@
|
||||
preamble was folded in, it becomes the card title and the
|
||||
reasoning is the expandable body. -->
|
||||
<div v-if="event.type === 'thinking'" class="tool-event">
|
||||
<div class="action-card" :class="{ 'action-pending': isThinkingActive(event.event_id) }">
|
||||
<div class="action-card thinking-event-card"
|
||||
:class="{ 'action-pending': isThinkingActive(event.event_id) }">
|
||||
<div class="action-header" @click="toggleEvent(event.event_id)">
|
||||
<div class="action-title">
|
||||
<div class="action-title" :class="{
|
||||
'thinking-inline-title': !event.title && isEventExpanded(event.event_id),
|
||||
}">
|
||||
<span class="action-title-icon icon-mask" :style="maskIconStyle(thinkingIcon)"
|
||||
aria-hidden="true" />
|
||||
<span v-if="event.title" class="action-name action-preamble-title">{{ event.title }}</span>
|
||||
<span v-else-if="isEventExpanded(event.event_id)" class="action-name">{{ $t('agent.think')
|
||||
}}</span>
|
||||
<div v-else-if="event.content && isEventExpanded(event.event_id)"
|
||||
class="thinking-inline-content markdown-content">
|
||||
<div class="thinking-inline-markdown" v-html="renderMarkdownContent(event.content)"></div>
|
||||
</div>
|
||||
<span v-else-if="getThinkingSummary(event)" class="action-summary">{{ getThinkingSummary(event)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="event.content && isEventExpanded(event.event_id)" class="action-details">
|
||||
<div v-if="event.title && event.content && isEventExpanded(event.event_id)" class="action-details">
|
||||
<div class="thinking-detail-content markdown-content">
|
||||
<div v-html="renderMarkdownContent(event.content)"></div>
|
||||
</div>
|
||||
@@ -230,17 +235,23 @@
|
||||
from the answer area) is shown as the card title; the reasoning is
|
||||
the expandable body. -->
|
||||
<div v-if="event.type === 'thinking'" class="tool-event">
|
||||
<div class="action-card" :class="{ 'action-pending': isThinkingActive(event.event_id) }">
|
||||
<div class="action-card thinking-event-card"
|
||||
:class="{ 'action-pending': isThinkingActive(event.event_id) }">
|
||||
<div class="action-header" @click="toggleEvent(event.event_id)">
|
||||
<div class="action-title">
|
||||
<div class="action-title" :class="{
|
||||
'thinking-inline-title': !event.title && isEventExpanded(event.event_id),
|
||||
}">
|
||||
<span class="action-title-icon icon-mask" :style="maskIconStyle(thinkingIcon)" aria-hidden="true" />
|
||||
<span v-if="event.title" class="action-name action-preamble-title">{{ event.title }}</span>
|
||||
<span v-else class="action-name">{{ $t('agent.think') }}</span>
|
||||
<span v-if="!event.title && getThinkingSummary(event) && !isEventExpanded(event.event_id)"
|
||||
<div v-else-if="event.content && isEventExpanded(event.event_id)"
|
||||
class="thinking-inline-content markdown-content">
|
||||
<div class="thinking-inline-markdown" v-html="renderMarkdownContent(event.content)"></div>
|
||||
</div>
|
||||
<span v-else-if="getThinkingSummary(event) && !isEventExpanded(event.event_id)"
|
||||
class="action-summary">{{ getThinkingSummary(event) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="event.content && isEventExpanded(event.event_id)" class="action-details">
|
||||
<div v-if="event.title && event.content && isEventExpanded(event.event_id)" class="action-details">
|
||||
<div class="thinking-detail-content markdown-content">
|
||||
<div v-html="renderMarkdownContent(event.content)"></div>
|
||||
</div>
|
||||
@@ -1299,6 +1310,12 @@ const hasPendingStreamingActivity = computed(() => {
|
||||
return displayEvents.value.some((event: any) => {
|
||||
if (!event) return false;
|
||||
if (event.pending === true) return true;
|
||||
if (
|
||||
event.type === 'thinking' &&
|
||||
(event.thinking === true || isThinkingActive(event.event_id))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return event.type === 'tool_approval_required' || event.type === 'mcp_oauth_required';
|
||||
});
|
||||
});
|
||||
@@ -1665,14 +1682,10 @@ const intermediateEvents = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const visibleIntermediateEvents = computed(() => {
|
||||
return intermediateEvents.value.filter((e: any) => {
|
||||
if (!e) return false;
|
||||
if (e.type === 'thinking') return false;
|
||||
if (e.type === 'tool_call' && e.tool_name === 'thinking') return false;
|
||||
return true;
|
||||
});
|
||||
});
|
||||
// Keep reasoning in the same compact timeline as tool calls. The template
|
||||
// auto-expands the active reasoning event and folds it again once a tool or
|
||||
// answer follows, so tool activity remains the primary structure.
|
||||
const visibleIntermediateEvents = computed(() => intermediateEvents.value);
|
||||
|
||||
// Events to display (non-tree: before answer starts show all, after answer starts show only answer)
|
||||
const displayEvents = computed(() => {
|
||||
@@ -1689,15 +1702,10 @@ const displayEvents = computed(() => {
|
||||
return result.filter((e: any) => e.type === 'answer');
|
||||
}
|
||||
|
||||
// While the conversation is still running, keep the same lightweight tool-log
|
||||
// surface as the completed tree. Raw thinking narration is noisy during
|
||||
// streaming; real tool rows carry their own pending state.
|
||||
// Keep the active reasoning event inline with tool activity. It uses the same
|
||||
// compact timeline card and is auto-collapsed when a tool or answer follows.
|
||||
if (!isConversationDone.value) {
|
||||
return result.filter((e: any) => {
|
||||
if (e.type === 'thinking') return false;
|
||||
if (e.type === 'tool_call' && e.tool_name === 'thinking') return false;
|
||||
return true;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Done: the steps live in the collapsed tree; show only the answer here.
|
||||
@@ -3365,6 +3373,34 @@ const handleAddToKnowledge = (answerEvent: any) => {
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
.thinking-inline-title {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
// Anchor the bulb to the fixed header instead of the variable-height title.
|
||||
// Otherwise expanding inline reasoning shifts the icon along the timeline.
|
||||
.tree-child .thinking-event-card .action-title {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.thinking-inline-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: var(--agent-step-summary-size);
|
||||
line-height: 1.6;
|
||||
overflow-wrap: anywhere;
|
||||
user-select: text;
|
||||
|
||||
:deep(.thinking-inline-markdown > :first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
:deep(.thinking-inline-markdown > :last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-results-summary-fixed,
|
||||
.plan-status-summary-fixed {
|
||||
padding: 2px 0 0 0;
|
||||
|
||||
Reference in New Issue
Block a user