(null)
const actualDuration = useDuration(value => value)
+ const rawResult = useResultMode(value => value)
const [actualTime, setActualTime] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [searchIndex] = useState(() => new TrajectorySearchIndex())
@@ -546,6 +549,8 @@ export function TrajectoryView({
{ resultMode.set(value) },
jsonStringWrapping: {
getDefault: () => stringWrapping.getSnapshot(),
setDefault: (value) => { stringWrapping.set(value) },
diff --git a/packages/client/ui-trajectory/src/client/layout.ts b/packages/client/ui-trajectory/src/client/layout.ts
index f43c58b29e..cc6c3b9820 100644
--- a/packages/client/ui-trajectory/src/client/layout.ts
+++ b/packages/client/ui-trajectory/src/client/layout.ts
@@ -465,6 +465,7 @@ export function deriveTrajectoryLayout(
: resultAsText(resultPreview)),
...(node.call !== null ? { inputDetail: node.call.argsRaw } : {}),
outputDetail: detailResult(node, t),
+ ...node.value === undefined ? {} : { resultValue: node.value },
outputBlocks: node.content.map(block => sourceBlock(block)),
...resultPreview,
callId: node.callId,
@@ -785,6 +786,7 @@ function expandAssistant(
...(result !== undefined
? {
outputDetail: detailResult(result, t),
+ ...result.value === undefined ? {} : { resultValue: result.value },
outputBlocks: result.content.map(block => sourceBlock(block)),
...resultPreview,
isError: result.isError,
@@ -1032,6 +1034,7 @@ function expandSubCalls(
...(settled
? {
outputDetail: detailResult(sub, t),
+ ...sub.value === undefined ? {} : { resultValue: sub.value },
outputBlocks: sub.content.map(block => sourceBlock(block)),
...resultPreview,
isError: sub.isError,
diff --git a/packages/client/ui-trajectory/src/client/locales.ts b/packages/client/ui-trajectory/src/client/locales.ts
index b6c8e26929..3c3d95c726 100644
--- a/packages/client/ui-trajectory/src/client/locales.ts
+++ b/packages/client/ui-trajectory/src/client/locales.ts
@@ -108,6 +108,9 @@ export const zh = {
'record.schemaUnavailable': 'Schema 不可用',
'record.parameters': '参数',
'record.resultJson': '结果 JSON',
+ 'record.rawResult': '原始结果',
+ 'record.renderedResult': '渲染结果',
+ 'record.rawResultUnavailable': '此记录未保存原始结果',
'record.json': 'JSON',
'record.parametersJson': '参数 JSON',
'record.namedParametersJson': '{name} 参数 JSON',
@@ -318,6 +321,9 @@ export const en: Record = {
'record.schemaUnavailable': 'Schema unavailable',
'record.parameters': 'Parameters',
'record.resultJson': 'Result JSON',
+ 'record.rawResult': 'Raw result',
+ 'record.renderedResult': 'Rendered result',
+ 'record.rawResultUnavailable': 'No raw result was recorded',
'record.json': 'JSON',
'record.parametersJson': 'parameters JSON',
'record.namedParametersJson': '{name} parameters JSON',
@@ -514,6 +520,9 @@ export const ja = {
'record.schemaUnavailable': 'スキーマを利用できません',
'record.parameters': 'パラメータ',
'record.resultJson': '結果 JSON',
+ 'record.rawResult': '元の結果',
+ 'record.renderedResult': '表示用の結果',
+ 'record.rawResultUnavailable': '元の結果は記録されていません',
'record.json': 'JSON',
'record.parametersJson': 'パラメータ JSON',
'record.namedParametersJson': '{name} パラメータ JSON',
@@ -710,6 +719,9 @@ export const es = {
'record.schemaUnavailable': 'Esquema no disponible',
'record.parameters': 'Parámetros',
'record.resultJson': 'Resultado JSON',
+ 'record.rawResult': 'Resultado original',
+ 'record.renderedResult': 'Resultado renderizado',
+ 'record.rawResultUnavailable': 'No se registró ningún resultado original',
'record.json': 'JSON',
'record.parametersJson': 'parámetros JSON',
'record.namedParametersJson': 'Parámetros de {name} JSON',
@@ -906,6 +918,9 @@ export const fr = {
'record.schemaUnavailable': 'Schéma indisponible',
'record.parameters': 'Paramètres',
'record.resultJson': 'Résultat JSON',
+ 'record.rawResult': 'Résultat brut',
+ 'record.renderedResult': 'Résultat rendu',
+ 'record.rawResultUnavailable': 'Aucun résultat brut enregistré',
'record.json': 'JSON',
'record.parametersJson': 'paramètres JSON',
'record.namedParametersJson': 'Paramètres {name} JSON',
@@ -1102,6 +1117,9 @@ export const de = {
'record.schemaUnavailable': 'Schema nicht verfügbar',
'record.parameters': 'Parameter',
'record.resultJson': 'Ergebnis JSON',
+ 'record.rawResult': 'Rohergebnis',
+ 'record.renderedResult': 'Gerendertes Ergebnis',
+ 'record.rawResultUnavailable': 'Kein Rohergebnis aufgezeichnet',
'record.json': 'JSON',
'record.parametersJson': 'Parameter JSON',
'record.namedParametersJson': '{name} Parameter JSON',
@@ -1298,6 +1316,9 @@ export const ru = {
'record.schemaUnavailable': 'Схема недоступна',
'record.parameters': 'Параметры',
'record.resultJson': 'Результат JSON',
+ 'record.rawResult': 'Исходный результат',
+ 'record.renderedResult': 'Отображаемый результат',
+ 'record.rawResultUnavailable': 'Исходный результат не записан',
'record.json': 'JSON',
'record.parametersJson': 'параметры JSON',
'record.namedParametersJson': '{name} параметры JSON',
@@ -1494,6 +1515,9 @@ export const pt = {
'record.schemaUnavailable': 'Esquema indisponível',
'record.parameters': 'Parâmetros',
'record.resultJson': 'Resultado JSON',
+ 'record.rawResult': 'Resultado original',
+ 'record.renderedResult': 'Resultado renderizado',
+ 'record.rawResultUnavailable': 'Nenhum resultado original foi registrado',
'record.json': 'JSON',
'record.parametersJson': 'parâmetros JSON',
'record.namedParametersJson': '{name} parâmetros JSON',
diff --git a/packages/client/ui-trajectory/src/client/result-mode-store.ts b/packages/client/ui-trajectory/src/client/result-mode-store.ts
new file mode 100644
index 0000000000..f010bbbb97
--- /dev/null
+++ b/packages/client/ui-trajectory/src/client/result-mode-store.ts
@@ -0,0 +1,10 @@
+/** Browser-wide preference for canonical tool result inspection. */
+import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
+
+/**
+ * Create the persisted result mode shared by trajectory views.
+ * @returns True for canonical values, false for rendered content (the default).
+ */
+export function createTrajectoryResultModeStore(): SnapshotStore {
+ return createSnapshotStore(false, { persist: { name: 'dsh.trajectory.rawResult' } })
+}
diff --git a/packages/client/ui-trajectory/src/client/trajectory-record.ts b/packages/client/ui-trajectory/src/client/trajectory-record.ts
index afcc6dabdf..9e3d196829 100644
--- a/packages/client/ui-trajectory/src/client/trajectory-record.ts
+++ b/packages/client/ui-trajectory/src/client/trajectory-record.ts
@@ -1,6 +1,7 @@
/** Shared trajectory record data and formatting contracts. */
import type { HTMLAttributes } from 'react'
+import type { JsonValue } from '@deepseek-ai/dsh-util-values'
import type { FileAttachmentRef, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { ConversationPromptSnapshot } from '@deepseek-ai/dsh-client-ui-conversation/client'
import type { TrajectoryTranslate } from './locales.ts'
@@ -66,6 +67,8 @@ export interface TrajectoryCellProps extends HTMLAttributes {
previousPromptDetail?: ConversationPromptSnapshot
/** Full assistant/tool result content for the details panel. */
outputDetail?: string
+ /** Final canonical tool value; absent on failures and records created before value recording. */
+ resultValue?: JsonValue
/** Full assistant reasoning content for the details panel. */
thinkingDetail?: string
/** Original message blocks in source order for the details panel. */
diff --git a/packages/client/ui-trajectory/tests/result-mode-store.client.spec.ts b/packages/client/ui-trajectory/tests/result-mode-store.client.spec.ts
new file mode 100644
index 0000000000..df3f26c798
--- /dev/null
+++ b/packages/client/ui-trajectory/tests/result-mode-store.client.spec.ts
@@ -0,0 +1,26 @@
+// @vitest-environment jsdom
+import { afterEach, beforeEach, expect, it } from 'vitest'
+import { createTrajectoryResultModeStore } from '../src/client/result-mode-store.ts'
+
+const key = 'dsh.trajectory.jsonResultMode'
+let original: string | null
+
+beforeEach(() => {
+ original = localStorage.getItem(key)
+ localStorage.removeItem(key)
+})
+
+afterEach(() => {
+ if (original === null) localStorage.removeItem(key)
+ else localStorage.setItem(key, original)
+})
+
+it('restores the last result mode when the view is recreated', () => {
+ const first = createTrajectoryResultModeStore()
+ expect(first.getSnapshot()).toBe(false)
+ first.set(true)
+ const second = createTrajectoryResultModeStore()
+ expect(second.getSnapshot()).toBe(true)
+ second.set(false)
+ expect(createTrajectoryResultModeStore().getSnapshot()).toBe(false)
+})
diff --git a/packages/client/ui-trajectory/tests/table.client.spec.tsx b/packages/client/ui-trajectory/tests/table.client.spec.tsx
index 0789d4ae97..bf04e9833b 100644
--- a/packages/client/ui-trajectory/tests/table.client.spec.tsx
+++ b/packages/client/ui-trajectory/tests/table.client.spec.tsx
@@ -1133,6 +1133,38 @@ describe('TrajectoryTable', () => {
expect(turnLabel.textContent).toContain('#1')
})
+ it.each([null, false, 0, '', { answer: 42 }])('switches to the recorded value %j and retains the mode across records and tabs', (value) => {
+ const turns: readonly TrajectoryTurnModel[] = [{ turn: 1, groups: [{ title: 'Step 1', cells: [
+ { index: 1, kind: 'tool', text: 'recorded', outputDetail: 'rendered answer', resultValue: value, timeSeconds: 0.1 },
+ { index: 2, kind: 'subtool', text: 'historical', outputDetail: 'old answer', timeSeconds: 0.1 },
+ ] }] }]
+ const changes = vi.fn()
+ function Harness() {
+ const [raw, setRaw] = useState(false)
+ return { changes(next); setRaw(next) }} />
+ }
+ render()
+ fireEvent.click(screen.getByRole('row', { name: /recorded/ }))
+ fireEvent.click(screen.getByRole('tab', { name: 'Result' }))
+ expect(screen.getByText('rendered answer')).toBeTruthy()
+ fireEvent.click(screen.getByRole('button', { name: 'Raw result' }))
+ expect(document.getElementById('trajectory-result-content')?.textContent).toContain(
+ typeof value === 'object' && value !== null ? 'answer:' : JSON.stringify(value),
+ )
+ fireEvent.click(screen.getByRole('row', { name: /historical/ }))
+ expect(screen.getByText('No raw result was recorded')).toBeTruthy()
+ expect(screen.getByRole('button', { name: 'Raw result' }).getAttribute('aria-pressed')).toBe('true')
+ fireEvent.click(screen.getByRole('tab', { name: 'Summary' }))
+ fireEvent.click(screen.getByRole('tab', { name: 'Result' }))
+ fireEvent.click(screen.getByRole('row', { name: /recorded/ }))
+ expect(screen.queryByText('No raw result was recorded')).toBeNull()
+ expect(changes.mock.calls).toEqual([[true]])
+ fireEvent.click(screen.getByRole('button', { name: 'Raw result' }))
+ expect(screen.getByText('rendered answer')).toBeTruthy()
+ expect(changes.mock.calls).toEqual([[true], [false]])
+ })
+
it('renders a single-text JSON tool result as a JSON tree', () => {
const turns: readonly TrajectoryTurnModel[] = [{
turn: 1,
diff --git a/packages/client/ui-trajectory/tests/views.client.spec.tsx b/packages/client/ui-trajectory/tests/views.client.spec.tsx
index c753bd0889..1f4e4a6f03 100644
--- a/packages/client/ui-trajectory/tests/views.client.spec.tsx
+++ b/packages/client/ui-trajectory/tests/views.client.spec.tsx
@@ -162,10 +162,13 @@ function standaloneHistory(
}
function standaloneDuration(): Pick<
- ComponentProps, 'useDuration' | 'setActualDuration'
+ ComponentProps, 'useDuration' | 'setActualDuration' | 'useResultMode' | 'setResultMode'
> {
const duration = createSnapshotStore(false)
+ const resultMode = createSnapshotStore(false)
return {
+ useResultMode: bindSnapshotSelector(resultMode),
+ setResultMode: (value) => { resultMode.set(value) },
useDuration: bindSnapshotSelector(duration),
setActualDuration: (value) => { duration.set(value) },
}
@@ -204,7 +207,7 @@ const useProjection: UseProjection = emptyProjection
type StandaloneBaseProps = Omit<
ComponentProps,
- 'useSession' | 'useTrajectory' | 'useDuration' | 'loadOlder' | 'setActualDuration'
+ 'useSession' | 'useTrajectory' | 'useDuration' | 'loadOlder' | 'setActualDuration' | 'useResultMode' | 'setResultMode'
>
/** Standalone view props: the session-scope standard kit the outlet would bake. */
@@ -391,6 +394,8 @@ function mount(fixture: Awaited>) {
return {
loadOlder: trajectory.loadOlder,
loadImage: trajectory.loadImage,
+ setResultMode: trajectory.setResultMode,
+ useResultMode: bindSnapshotSelector(trajectory.hooks.resultMode),
setActualDuration: trajectory.setActualDuration,
useDuration: bindSnapshotSelector(trajectory.hooks.duration),
t: tZh,
@@ -496,6 +501,11 @@ describe('plugin registration', () => {
await reference.ready
const second = injectEntry(reference.sessionId)
+ expect(second.hooks.resultMode).toBe(first.hooks.resultMode)
+ first.setResultMode(true)
+ expect(second.hooks.resultMode.getSnapshot()).toBe(true)
+ expect(localStorage.getItem('dsh.trajectory.rawResult')).toBe('true')
+ first.setResultMode(false)
expect(second.hooks.duration).toBe(first.hooks.duration)
first.setActualDuration(true)
expect(second.hooks.duration.getSnapshot()).toBe(true)
@@ -1541,6 +1551,7 @@ describe('TrajectoryView state', () => {
const first = render(
{ firstDuration.set(value) }}
/>,
@@ -1556,6 +1567,7 @@ describe('TrajectoryView state', () => {
render(
{ restoredDuration.set(value) }}
/>,