diff --git a/packages/client/ui-trajectory/README.i18n.yaml b/packages/client/ui-trajectory/README.i18n.yaml index 4205d20bbe..4552b1d742 100644 --- a/packages/client/ui-trajectory/README.i18n.yaml +++ b/packages/client/ui-trajectory/README.i18n.yaml @@ -24,8 +24,8 @@ en: 0f4ee2189324ac06 zh: 12e76fed22c09bc1 /deepseek-ai-dsh-client-ui-trajectory/understand-the-implementation: - en: 422f0f91eeab6707 - zh: 3516d868cfbb0e02 + en: 9b8478a403486fca + zh: 8679d31b30594774 /deepseek-ai-dsh-client-ui-trajectory/understand-the-implementation/virtual-rows: en: c85e342bc7eee4e6 zh: 9feab66ef1755b94 diff --git a/packages/client/ui-trajectory/README.md b/packages/client/ui-trajectory/README.md index 45f0f721b6..07a0c31e6b 100644 --- a/packages/client/ui-trajectory/README.md +++ b/packages/client/ui-trajectory/README.md @@ -57,7 +57,7 @@ The view is a pure projection: Trajectory-owned Definitions assemble business re User, steering, and context inputs and request prompt changes share their request's Step group in log order, before the request separator and assistant output. -Native and nested PTC Tool results retain their raw structured error details and presentation metadata. Failed records show the error code in the ledger and the error name and code in the inspector. +Native and nested PTC Tool results retain their raw structured error details and presentation metadata. The Result tab switches between rendered content and the recorded canonical value with a compact toggle. Rendered content is the default; the browser-wide preference survives record, tab, and Session changes and reloads. Missing values in older or failed records show an explicit notice without resetting the preference. JSON primitives, including `null`, remain inspectable and copyable. Summary previews continue to show rendered content. Failed records show the error code in the ledger and the error name and code in the inspector. Tool records begin at durable tool/call events and retain complete argument text with a lazy `args` view, reused by paired results. Chat's transient preparing stage does not create Trajectory tool rows or alter historical tool timing. diff --git a/packages/client/ui-trajectory/README.zh.md b/packages/client/ui-trajectory/README.zh.md index c682e9d728..241adefde2 100644 --- a/packages/client/ui-trajectory/README.zh.md +++ b/packages/client/ui-trajectory/README.zh.md @@ -57,7 +57,7 @@ Trajectory 标签页让你以按轮次组织的事件记录表和交互式时间 用户消息、中途引导、上下文输入及请求提示词变更按日志顺序放在所属请求的 Step 分组内,位于请求分隔行和助手输出之前。 -原生与嵌套 PTC 工具结果保留原始结构化错误详情和展示元数据。失败记录在记录表中显示错误代码,在检查器中显示错误名称和代码。 +原生与嵌套 PTC 工具结果保留原始结构化错误详情和展示元数据。结果标签页通过紧凑按钮切换渲染内容与已记录的规范值。默认显示渲染内容;浏览器级偏好在切换记录、标签页、Session 及刷新页面后保留。旧记录或失败记录缺少原始值时显示明确提示,不重置偏好。包括 `null` 在内的 JSON 基本值均可查看和复制。概述预览仍显示渲染内容。失败记录在记录表中显示错误代码,在检查器中显示错误名称和代码。 工具记录从持久 tool/call 事件开始,保留完整参数文本和由配对结果复用的懒计算 `args` 视图。Chat 的瞬态 preparing 阶段不创建 Trajectory 工具行,也不改变历史工具计时。 diff --git a/packages/client/ui-trajectory/src/client/TrajectoryCell.tsx b/packages/client/ui-trajectory/src/client/TrajectoryCell.tsx index bb0b247979..a41ca18672 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryCell.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryCell.tsx @@ -50,6 +50,7 @@ export function TrajectoryCell({ promptDetail: _promptDetail, previousPromptDetail: _previousPromptDetail, outputDetail: _outputDetail, + resultValue: _resultValue, thinkingDetail: _thinkingDetail, sourceBlocks: _sourceBlocks, outputBlocks: _outputBlocks, diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx index 2940741317..c38fbcb287 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx @@ -388,6 +388,10 @@ function AssistantTimingPanel({ /** Props for the trajectory ledger. */ export interface TrajectoryTableProps { + /** Browser-wide result mode, independent of record and inspector tab selection. */ + rawResult?: boolean + /** Persist an explicit result mode selection. */ + onResultModeChange?: (raw: boolean) => void /** Wrapping control shared by all JSON inspectors. */ stringWrapping?: JsonTreeProps['stringWrapping'] /** Trajectory locale seat. */ @@ -2083,6 +2087,8 @@ function OverviewSection({ * @returns The ledger and an optional local record inspector. */ export function TrajectoryTable({ + rawResult = false, + onResultModeChange, t, stringWrapping, renderSlot, @@ -3520,9 +3526,39 @@ export function TrajectoryTable({ stringWrapping={stringWrapping} t={t} /> )} {!promptSelected && selected !== undefined && activeTab === 'output' && ( - selectedProgram === undefined - ? - : + <> + {(selected.cell.kind === 'tool' || selected.cell.kind === 'subtool') && ( +
+ {t(rawResult ? 'record.rawResult' : 'record.renderedResult')} + + + + + {rawResult && selected.cell.resultValue !== undefined && ( + + )} + +
+ )} +
+ {rawResult && (selected.cell.kind === 'tool' || selected.cell.kind === 'subtool') + ? selected.cell.resultValue === undefined + ?

{t('record.rawResultUnavailable')}

+ : selected.cell.resultValue !== null && typeof selected.cell.resultValue === 'object' + ? + :
{JSON.stringify(selected.cell.resultValue)}
+ : selectedProgram === undefined + ? + : } +
+ )} {!promptSelected && selected !== undefined && activeTab === 'schema' && ( diff --git a/packages/client/ui-trajectory/src/client/TrajectoryView.tsx b/packages/client/ui-trajectory/src/client/TrajectoryView.tsx index e36386e819..7e393adc33 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryView.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryView.tsx @@ -79,10 +79,12 @@ export interface TrajectoryViewInjected { jsonStringWrapping?: Omit, 'label'> hooks: { duration: SnapshotStore + resultMode: SnapshotStore } loadOlder: () => Promise loadImage: MessageImageLoader setActualDuration: (actualDuration: boolean) => void + setResultMode: (raw: boolean) => void } interface UsageLike { @@ -130,7 +132,7 @@ function addUsage( } export function TrajectoryView({ - useSession, useTrajectory, useDuration, loadOlder, loadImage, setActualDuration, + useSession, useTrajectory, useDuration, useResultMode, setResultMode, loadOlder, loadImage, setActualDuration, viewRequest, completeViewRequest, renderSlot, t, jsonStringWrapping, }: ConvViewProps & Pick, 'renderSlot'> @@ -141,6 +143,7 @@ export function TrajectoryView({ useState>(EMPTY_RECORD_IDS) const [timelineSelection, setTimelineSelection] = useState(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) }} />,