diff --git a/frontend/src/views/chat/components/AgentStreamDisplay.style.test.mjs b/frontend/src/views/chat/components/AgentStreamDisplay.style.test.mjs index ac86030b3..ddc17c7ab 100644 --- a/frontend/src/views/chat/components/AgentStreamDisplay.style.test.mjs +++ b/frontend/src/views/chat/components/AgentStreamDisplay.style.test.mjs @@ -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'\)/) diff --git a/frontend/src/views/chat/components/AgentStreamDisplay.vue b/frontend/src/views/chat/components/AgentStreamDisplay.vue index 8f69906c3..ab50cc7f3 100644 --- a/frontend/src/views/chat/components/AgentStreamDisplay.vue +++ b/frontend/src/views/chat/components/AgentStreamDisplay.vue @@ -36,19 +36,24 @@ preamble was folded in, it becomes the card title and the reasoning is the expandable body. -->
-
+
-
+
-
+
@@ -230,17 +235,23 @@ from the answer area) is shown as the card title; the reasoning is the expandable body. -->
-
+
-
+
+ {{ getThinkingSummary(event) }}
-
+
@@ -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;