diff --git a/frontend/src/lib/components/AgentResultDisplay.svelte b/frontend/src/lib/components/AgentResultDisplay.svelte
index ec10bbaf03..1397643e4b 100644
--- a/frontend/src/lib/components/AgentResultDisplay.svelte
+++ b/frontend/src/lib/components/AgentResultDisplay.svelte
@@ -3,6 +3,7 @@
import { Badge } from '$lib/components/common'
import GfmMarkdown from './GfmMarkdown.svelte'
import AgentTrace from './AgentTrace.svelte'
+ import ChatCollapsibleCard from './copilot/chat/ChatCollapsibleCard.svelte'
import LabeledDivider from './LabeledDivider.svelte'
import WebSearchSourcesDisplay from './copilot/chat/WebSearchSourcesDisplay.svelte'
import { buildAgentTrace, splitFinalAnswer } from './agentTrace'
@@ -35,6 +36,8 @@
// answered itself. Its citations move down with it.
let answer = $derived(splitFinalAnswer(buildAgentTrace(result.messages), result.output))
let trace = $derived(answer.trace)
+ let reasoning = $derived(result.reasoning?.trim())
+ let reasoningExpanded = $state(false)
let anchor: HTMLElement | undefined = $state()
const sticker = createBottomSticker()
@@ -43,6 +46,7 @@
// so the end has moved from wherever the stream had the reader parked.
runKey
trace.length
+ reasoning
sticker.scrollToEnd(runPane(anchor))
})
@@ -50,6 +54,20 @@
{#if trace.length > 0}
+ {/if}
+ {#if reasoning}
+
+
(reasoningExpanded = !reasoningExpanded)}
+ contentClass="font-main"
+ >
+
+
+ {/if}
+ {#if trace.length > 0 || reasoning}
Output
diff --git a/frontend/src/lib/components/aiAgentResult.test.ts b/frontend/src/lib/components/aiAgentResult.test.ts
index 75a7ccc896..4c6499ce73 100644
--- a/frontend/src/lib/components/aiAgentResult.test.ts
+++ b/frontend/src/lib/components/aiAgentResult.test.ts
@@ -22,6 +22,11 @@ describe('parseAgentResult', () => {
it('accepts the envelope with and without its optional keys', () => {
expect(parseAgentResult(envelope)?.output).toBe('the answer')
expect(parseAgentResult({ output: 1, messages: [{ role: 'user' }] })?.messages).toHaveLength(1)
+ // A key the worker serializes and the signature does not know costs every run
+ // carrying it the pretty display, so each one is pinned here.
+ expect(parseAgentResult({ ...envelope, reasoning: 'let me think' })?.reasoning).toBe(
+ 'let me think'
+ )
})
// The signature is the only thing separating an agent result from any other
diff --git a/frontend/src/lib/components/aiAgentResult.ts b/frontend/src/lib/components/aiAgentResult.ts
index e8328a1a43..5b314e552c 100644
--- a/frontend/src/lib/components/aiAgentResult.ts
+++ b/frontend/src/lib/components/aiAgentResult.ts
@@ -31,10 +31,16 @@ export type AgentResult = {
messages: AgentMessage[]
usage?: AgentTokenUsage
wm_stream?: string
+ /** The model's thinking across every iteration, blank-line separated. */
+ reasoning?: string
}
-/** Every key `AIAgentResult` can serialize. `usage` and `wm_stream` are skipped when empty. */
-const ENVELOPE_KEYS = ['output', 'messages', 'usage', 'wm_stream']
+/**
+ * Every key `AIAgentResult` can serialize; the optional ones are skipped when
+ * empty. The signature below is closed, so a key the worker gains and this list
+ * does not makes every run carrying it fall back to the raw JSON.
+ */
+const ENVELOPE_KEYS = ['output', 'messages', 'usage', 'wm_stream', 'reasoning']
function isRecord(value: unknown): value is Record
{
return typeof value === 'object' && value !== null && !Array.isArray(value)
@@ -129,7 +135,8 @@ export function parseAgentResult(result: unknown): AgentResult | undefined {
output: result.output,
messages: toAgentMessages(result.messages),
usage: isRecord(result.usage) ? (result.usage as AgentTokenUsage) : undefined,
- wm_stream: typeof result.wm_stream === 'string' ? result.wm_stream : undefined
+ wm_stream: typeof result.wm_stream === 'string' ? result.wm_stream : undefined,
+ reasoning: typeof result.reasoning === 'string' ? result.reasoning : undefined
}
}