From 86bf2734c6592208c41a8d26b4d9197fd7adb7ce Mon Sep 17 00:00:00 2001 From: _Kerman Date: Tue, 27 Oct 2026 12:21:28 +0800 Subject: [PATCH 1/3] fix(trajectory): preserve recorded tool failure output --- apps/web/tests/auto-review-denial.e2e.ts | 15 ++++---- .../client/ui-trajectory/README.i18n.yaml | 4 +-- packages/client/ui-trajectory/README.md | 2 +- packages/client/ui-trajectory/README.zh.md | 2 +- .../src/client/TrajectoryTable.tsx | 1 + .../client/ui-trajectory/src/client/layout.ts | 16 ++++----- .../tests/code-inspector.client.spec.tsx | 35 +++++++++++++++++++ .../tests/layout.client.spec.tsx | 26 ++++++++++++++ .../web/auto-review-denial/ui.expected.md | 6 ++-- 9 files changed, 86 insertions(+), 21 deletions(-) diff --git a/apps/web/tests/auto-review-denial.e2e.ts b/apps/web/tests/auto-review-denial.e2e.ts index 58371631a9..d967b4cb9b 100644 --- a/apps/web/tests/auto-review-denial.e2e.ts +++ b/apps/web/tests/auto-review-denial.e2e.ts @@ -119,13 +119,16 @@ describe.skipIf(MODE === 'record')('web e2e: cold Auto-review denial', () => { await ledger.locator('table[data-scroll-ready="true"]').waitFor({ timeout: 15_000 }) const nativeRecord = ledger.locator('tr[data-kind="tool"]').filter({ hasText: 'mystery' }) const innerRecord = ledger.locator('tr[data-kind="subtool"]').filter({ hasText: 'bash' }) - await expect.poll(() => nativeRecord.textContent()).toContain('AUTO_REVIEW_DENIED') - await expect.poll(() => innerRecord.textContent()).toContain('AUTO_REVIEW_DENIED') + await expect.poll(() => nativeRecord.textContent()).toContain('Error: Auto review rejected tool "mystery"; its body was not executed') + await expect.poll(() => innerRecord.textContent()).toContain('Error: Auto review rejected tool "bash"; its body was not executed') await innerRecord.click() - await page.getByRole('tab', { name: 'Result', exact: true }).click() - await page.getByRole('tabpanel', { name: 'Result' }) - .getByText('AutoReviewDeniedError: AUTO_REVIEW_DENIED', { exact: true }) - .waitFor({ state: 'visible' }) + for (const tab of ['Summary', 'Result']) { + await page.getByRole('tab', { name: tab, exact: true }).click() + const panel = page.getByRole('tabpanel', { name: tab }) + await panel.getByText('Error: Auto review rejected tool "bash"; its body was not executed', { exact: true }) + .waitFor({ state: 'visible' }) + expect(await panel.getByText('AutoReviewDeniedError: AUTO_REVIEW_DENIED', { exact: true }).count()).toBe(0) + } await captureAutoReviewState(page, 'deny-trajectory') const trajectory = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)) .split(SEED_ID).join('{{seededId}}') diff --git a/packages/client/ui-trajectory/README.i18n.yaml b/packages/client/ui-trajectory/README.i18n.yaml index 4205d20bbe..55ddc72e50 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: 3d90787660ddedfa + zh: cf6f338834f08771 /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..d1933f75b9 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. Failed records use the recorded result text for ledger previews, inspector output, and output copying. Only failures without text fall back to the error code in the ledger and the error name and code in the inspector; image content remains visible beside that fallback. 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..cbb6f857f3 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 工具结果保留原始结构化错误详情和展示元数据。失败记录在记录表预览、检查器输出和输出复制中使用已记录的结果文本。仅在失败结果没有文本时,记录表才回退到错误代码,检查器才回退到错误名称和代码;图片内容仍显示在回退信息旁边。 工具记录从持久 tool/call 事件开始,保留完整参数文本和由配对结果复用的懒计算 `args` 视图。Chat 的瞬态 preparing 阶段不创建 Trajectory 工具行,也不改变历史工具计时。 diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx index 2940741317..deed80f0e4 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx @@ -1499,6 +1499,7 @@ function ToolOutputBlocks({ ].filter((value): value is string => value !== undefined).join(' ')} > {error && errorDetail !== undefined && errorDetail !== '' + && !blocks.some(block => block.type === 'text' && block.content !== '') &&
{errorDetail}
} {blocks.map((block, index) => ( block.attachment !== undefined diff --git a/packages/client/ui-trajectory/src/client/layout.ts b/packages/client/ui-trajectory/src/client/layout.ts index f43c58b29e..ed37adf23a 100644 --- a/packages/client/ui-trajectory/src/client/layout.ts +++ b/packages/client/ui-trajectory/src/client/layout.ts @@ -1069,14 +1069,14 @@ function summarizeResult( node: ToolResultNode, t: TrajectoryTranslate, ): Pick { - if (node.isError) { - return { result: node.error?.code ?? 'error' } - } for (const block of node.content) { if (block.type === 'text' && typeof block.text === 'string' && block.text !== '') { return { result: '', resultPreviewMarkdown: block.text } } } + if (node.isError) { + return { result: node.error?.code ?? 'error' } + } const images = imageBlockCount(node.content) if (images > 0) return { result: t('layout.imageCount', { count: images }) } return { result: t('record.noOutput') } @@ -1094,16 +1094,16 @@ function resultAsText( } function detailResult(node: ToolResultNode, t: TrajectoryTranslate): string { - if (node.isError) { - return node.error === undefined - ? 'error' - : `${node.error.name}: ${node.error.code}` - } const text = node.content .filter(block => block.type === 'text' && typeof block.text === 'string') .map(block => block.type === 'text' ? block.text : '') .join('\n') if (text !== '') return text + if (node.isError) { + return node.error === undefined + ? 'error' + : `${node.error.name}: ${node.error.code}` + } const images = imageBlockCount(node.content) if (images > 0) return t('layout.imageCount', { count: images }) if ( diff --git a/packages/client/ui-trajectory/tests/code-inspector.client.spec.tsx b/packages/client/ui-trajectory/tests/code-inspector.client.spec.tsx index ab9bd12d41..825d8a0bb1 100644 --- a/packages/client/ui-trajectory/tests/code-inspector.client.spec.tsx +++ b/packages/client/ui-trajectory/tests/code-inspector.client.spec.tsx @@ -2,7 +2,9 @@ /** PTC source, output, and independent wrapping through the trajectory inspector. */ import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, fireEvent, render, waitFor, within } from '@testing-library/react' +import { PartialArguments } from '@deepseek-ai/dsh-util-values' import { TrajectoryTable } from '../src/client/TrajectoryTable.tsx' +import { deriveTrajectoryLayout } from '../src/client/layout.ts' import type { TrajectoryCellProps } from '../src/client/trajectory-record.ts' import { t } from './locale.client.ts' @@ -34,6 +36,39 @@ function Harness({ cell = CELL, getDefault = () => false, setDefault = () => {} } describe('PTC trajectory inspector', () => { + it.each(['run_code', 'read'])('preserves the recorded %s failure in the ledger and inspector', async (name) => { + const writeText = vi.fn().mockResolvedValue(undefined) + vi.stubGlobal('navigator', { clipboard: { writeText } }) + const output = name === 'run_code' + ? "Error: code run failed (exception): Identifier 'value' has already been declared\nCaptured output:\nstep one\n" + : 'Error: missing.txt does not exist\nCheck the file path.\n' + const argsRaw = name === 'run_code' ? ARGS : '{"file_path":"missing.txt"}' + const turns = deriveTrajectoryLayout({ + nodes: [{ + kind: 'tool-result', seq: 1, time: 1_000, callId: 'failed-call', name, + args: PartialArguments.fromText(argsRaw), call: { name, argsRaw }, callTime: 500, + content: [{ type: 'text', text: output }], isError: true, + error: { name: 'CodeRunFailedError', code: 'CODE_RUN_FAILED' }, subCalls: [], + }], + partial: null, runningCalls: [], + }, t) + const cell = turns.flatMap(turn => turn.groups.flatMap(group => group.cells))[0]! + const view = render() + const ui = within(view.container) + expect(ui.getByRole('row', { name: /TOOL/ }).textContent).toContain(output.split('\n')[0]) + fireEvent.click(ui.getByRole('row', { name: /TOOL/ })) + for (const tab of ['Summary', 'Result']) { + fireEvent.click(ui.getByRole('tab', { name: tab })) + const panel = ui.getByRole('tabpanel') + expect(panel.textContent).not.toContain('CodeRunFailedError: CODE_RUN_FAILED') + expect([...panel.querySelectorAll('pre')].filter(pre => pre.textContent === output)).toHaveLength(1) + } + if (name === 'run_code') { + fireEvent.click(ui.getByRole('button', { name: 'Copy output' })) + await waitFor(() => { expect(writeText).toHaveBeenLastCalledWith(output) }) + } + }) + it('shows the description, numbered source, original arguments, and verbatim output', () => { const view = render() const ui = within(view.container) diff --git a/packages/client/ui-trajectory/tests/layout.client.spec.tsx b/packages/client/ui-trajectory/tests/layout.client.spec.tsx index 8816ec1ce2..246f16cb9b 100644 --- a/packages/client/ui-trajectory/tests/layout.client.spec.tsx +++ b/packages/client/ui-trajectory/tests/layout.client.spec.tsx @@ -603,6 +603,32 @@ describe('run_code sub-dispatch cells', () => { expect(cells[3]).toMatchObject({ timeSeconds: 0.5 }) }) + it.each([true, false])('retains every failed sub-call text block with structured error present: %s', (withError) => { + const sub = { + ...settledSub(1, 'read', 6_300, 7_300), + isError: true, + ...(withError ? { error: { name: 'ToolError', code: 'ENOENT' } } : {}), + content: [{ type: 'text' as const, text: 'missing.txt does not exist' }, { type: 'text' as const, text: 'Check the file path.\n' }], + } + const turns = deriveTrajectoryLayout({ nodes: withSubCalls([sub]), partial: null, runningCalls: [] }) + const cell = turns[0]!.groups.flatMap(group => group.cells).find(cell => cell.kind === 'subtool') + expect(cell).toMatchObject({ + isError: true, resultPreviewMarkdown: 'missing.txt does not exist', + outputDetail: 'missing.txt does not exist\nCheck the file path.\n', + outputBlocks: sub.content.map(block => ({ type: 'text', content: block.text })), + }) + }) + + it('keeps structured failure details when an interrupted tool has no output', () => { + const sub = { + ...settledSub(1, 'read', 6_300, 7_300), + isError: true, error: { name: 'Interrupted', code: 'interrupted' }, content: [], + } + const turns = deriveTrajectoryLayout({ nodes: withSubCalls([sub]), partial: null, runningCalls: [] }) + expect(turns[0]!.groups.flatMap(group => group.cells).find(cell => cell.kind === 'subtool')) + .toMatchObject({ isError: true, result: 'interrupted', outputDetail: 'Interrupted: interrupted' }) + }) + it('a running (unsettled) sub-call renders a subtool cell with blank time', () => { const running = { phase: 'start' as const, args: PartialArguments.fromText('{"pattern":"x"}'), callId: 'p1:code:1', name: 'grep', argsRaw: '{"pattern":"x"}', diff --git a/snapshots/web/auto-review-denial/ui.expected.md b/snapshots/web/auto-review-denial/ui.expected.md index 98790b262f..c7ed52cf83 100644 --- a/snapshots/web/auto-review-denial/ui.expected.md +++ b/snapshots/web/auto-review-denial/ui.expected.md @@ -84,7 +84,7 @@ - cell "(tool call only)" - 'row "TOOL, mystery {\"secret\":\"hidden-input\"}"': - cell "TOOL" - - 'cell "mystery{\"secret\":\"hidden-input\"} → AUTO_REVIEW_DENIED"' + - 'cell "mystery{\"secret\":\"hidden-input\"} → Error: Auto review rejected tool \"mystery\"; its body was not executed"' - row "Request 2, ASSISTANT, (tool call only)": - 'cell "Request #2 ASSISTANT"': - 'button "Request #2"' @@ -95,7 +95,7 @@ - cell "run_codeCatch an Auto-denied inner call → denial caught" - 'row "SUBTOOL, bash {\"command\":\"echo hidden-input-ptc\"}" [selected]': - cell "SUBTOOL" - - 'cell "bash{\"command\":\"echo hidden-input-ptc\"} → AUTO_REVIEW_DENIED"' + - 'cell "bash{\"command\":\"echo hidden-input-ptc\"} → Error: Auto review rejected tool \"bash\"; its body was not executed"' - complementary "Event details": - separator "Resize event details" - text: SUBTOOL Turn 1 · Step 2 @@ -106,7 +106,7 @@ - tab "Result" [selected] - tab "Schema" - tab "Timing" - - tabpanel "Result": "AutoReviewDeniedError: AUTO_REVIEW_DENIED Error: Auto review rejected tool \"bash\"; its body was not executed" + - tabpanel "Result": "Error: Auto review rejected tool \"bash\"; its body was not executed" - textbox "Message or run a task, / commands, @ files or sessions" - button "Add files or run commands" - 'button "Access mode, current: Auto review"': Auto review From c099ecdfd5435d8da80eaec32f55876043ae4341 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Tue, 27 Oct 2026 12:44:43 +0800 Subject: [PATCH 2/3] fix(trajectory): retain failure fallback for empty text blocks --- .../client/ui-trajectory/README.i18n.yaml | 4 +- packages/client/ui-trajectory/README.md | 2 +- packages/client/ui-trajectory/README.zh.md | 2 +- .../src/client/TrajectoryTable.tsx | 2 +- .../client/ui-trajectory/src/client/layout.ts | 3 +- .../tests/code-inspector.client.spec.tsx | 56 +++++++++++++++++++ 6 files changed, 63 insertions(+), 6 deletions(-) diff --git a/packages/client/ui-trajectory/README.i18n.yaml b/packages/client/ui-trajectory/README.i18n.yaml index 55ddc72e50..c4a66b723a 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: 3d90787660ddedfa - zh: cf6f338834f08771 + en: 14d86ed3863a630b + zh: bd30ea1be9c39025 /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 d1933f75b9..89a6dfa7ca 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 use the recorded result text for ledger previews, inspector output, and output copying. Only failures without text fall back to the error code in the ledger and the error name and code in the inspector; image content remains visible beside that fallback. +Native and nested PTC Tool results retain their raw structured error details and presentation metadata. Failed records use non-empty recorded result text for ledger previews, inspector output, and output copying, preserving whitespace-only text. With that text present, structured error names and codes are neither displayed nor separately indexed for search. Only failures without any non-empty text block fall back to the error code in the ledger and the error name and code in the inspector; image content remains visible beside that fallback. 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 cbb6f857f3..421e92dd08 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 工具结果保留原始结构化错误详情和展示元数据。失败记录在记录表预览、检查器输出和输出复制中使用已记录的非空结果文本,并保留仅含空白字符的文本。存在这类文本时,结构化错误名称和代码既不显示,也不单独纳入搜索索引。仅在失败结果没有任何非空文本块时,记录表才回退到错误代码,检查器才回退到错误名称和代码;图片内容仍显示在回退信息旁边。 工具记录从持久 tool/call 事件开始,保留完整参数文本和由配对结果复用的懒计算 `args` 视图。Chat 的瞬态 preparing 阶段不创建 Trajectory 工具行,也不改变历史工具计时。 diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx index deed80f0e4..eff4b09e4c 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx @@ -1485,7 +1485,7 @@ function ToolOutputBlocks({ }: { blocks: readonly TrajectorySourceBlock[] error: boolean - /** Failure name and code preserved beside image-only error content. */ + /** Failure name and code shown only when no recorded text block is non-empty. */ errorDetail?: string | undefined preview: boolean renderSlot: PropsRenderSlots<'conversation.trajectory.images'>['renderSlot'] diff --git a/packages/client/ui-trajectory/src/client/layout.ts b/packages/client/ui-trajectory/src/client/layout.ts index ed37adf23a..e4c4095c14 100644 --- a/packages/client/ui-trajectory/src/client/layout.ts +++ b/packages/client/ui-trajectory/src/client/layout.ts @@ -1098,7 +1098,8 @@ function detailResult(node: ToolResultNode, t: TrajectoryTranslate): string { .filter(block => block.type === 'text' && typeof block.text === 'string') .map(block => block.type === 'text' ? block.text : '') .join('\n') - if (text !== '') return text + if (text !== '' && (!node.isError || node.content.some(block => + block.type === 'text' && typeof block.text === 'string' && block.text !== ''))) return text if (node.isError) { return node.error === undefined ? 'error' diff --git a/packages/client/ui-trajectory/tests/code-inspector.client.spec.tsx b/packages/client/ui-trajectory/tests/code-inspector.client.spec.tsx index 825d8a0bb1..d3754e43dc 100644 --- a/packages/client/ui-trajectory/tests/code-inspector.client.spec.tsx +++ b/packages/client/ui-trajectory/tests/code-inspector.client.spec.tsx @@ -3,6 +3,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, fireEvent, render, waitFor, within } from '@testing-library/react' import { PartialArguments } from '@deepseek-ai/dsh-util-values' +import type { ToolResultNode } from '@deepseek-ai/dsh-client-ui-conversation/client' import { TrajectoryTable } from '../src/client/TrajectoryTable.tsx' import { deriveTrajectoryLayout } from '../src/client/layout.ts' import type { TrajectoryCellProps } from '../src/client/trajectory-record.ts' @@ -35,7 +36,62 @@ function Harness({ cell = CELL, getDefault = () => false, setDefault = () => {} /> } +function failedCell(name: string, texts: readonly string[], nested: boolean): TrajectoryCellProps { + const argsRaw = name === 'run_code' ? ARGS : '{"file_path":"missing.txt"}' + const result: ToolResultNode = { + kind: 'tool-result', seq: 1, time: 1_000, callId: 'failed-call', name, + args: PartialArguments.fromText(argsRaw), call: { name, argsRaw }, callTime: 500, + content: texts.map(text => ({ type: 'text', text })), isError: true, + error: { name: 'ToolError', code: 'TOOL_FAILED' }, subCalls: [], + } + const turns = deriveTrajectoryLayout({ + nodes: nested ? [{ ...result, callId: 'parent', name: 'run_code', isError: false, subCalls: [result] }] : [result], + partial: null, runningCalls: [], + }, t) + return turns.flatMap(turn => turn.groups.flatMap(group => group.cells)) + .find(cell => cell.kind === (nested ? 'subtool' : 'tool'))! +} + describe('PTC trajectory inspector', () => { + it.each([ + ['run_code', false], ['read', false], ['read', true], + ] as const)('keeps error details for empty %s blocks (nested: %s)', async (name, nested) => { + const writeText = vi.fn().mockResolvedValue(undefined) + vi.stubGlobal('navigator', { clipboard: { writeText } }) + const cell = failedCell(name, ['', ''], nested) + expect(cell.outputDetail).toBe('ToolError: TOOL_FAILED') + const view = render() + const ui = within(view.container) + expect(ui.getByRole('row', { name: /TOOL/ }).textContent).toContain('TOOL_FAILED') + fireEvent.click(ui.getByRole('row', { name: /TOOL/ })) + for (const tab of ['Summary', 'Result']) { + fireEvent.click(ui.getByRole('tab', { name: tab })) + expect(ui.getByRole('tabpanel').textContent).toContain('ToolError: TOOL_FAILED') + } + if (name === 'run_code') { + fireEvent.click(ui.getByRole('button', { name: 'Copy output' })) + await waitFor(() => { expect(writeText).toHaveBeenLastCalledWith('ToolError: TOOL_FAILED') }) + } + }) + + it.each([ + ['run_code', false], ['read', false], ['read', true], + ] as const)('keeps whitespace-only %s failure text verbatim (nested: %s)', (name, nested) => { + const output = ' \n\t ' + const cell = failedCell(name, [output], nested) + expect(cell.resultPreviewMarkdown).toBe(output) + expect(cell.outputDetail).toBe(output) + const view = render() + const ui = within(view.container) + fireEvent.click(ui.getByRole('row', { name: /TOOL/ })) + for (const tab of ['Summary', 'Result']) { + fireEvent.click(ui.getByRole('tab', { name: tab })) + const panel = ui.getByRole('tabpanel') + expect(panel.textContent).not.toContain('ToolError: TOOL_FAILED') + expect([...panel.querySelectorAll('pre')].filter(pre => pre.textContent === output)).toHaveLength(1) + } + }) + it.each(['run_code', 'read'])('preserves the recorded %s failure in the ledger and inspector', async (name) => { const writeText = vi.fn().mockResolvedValue(undefined) vi.stubGlobal('navigator', { clipboard: { writeText } }) From aa1cf9115b4d29d965481e59e3090b1ed6fb0d8e Mon Sep 17 00:00:00 2001 From: _Kerman Date: Tue, 27 Oct 2026 12:48:42 +0800 Subject: [PATCH 3/3] fix(trajectory): show failed JSON results as recorded text --- packages/client/ui-trajectory/README.i18n.yaml | 4 ++-- packages/client/ui-trajectory/README.md | 2 +- packages/client/ui-trajectory/README.zh.md | 2 +- .../ui-trajectory/src/client/TrajectoryTable.tsx | 2 +- .../tests/code-inspector.client.spec.tsx | 15 +++++++++++++++ 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/client/ui-trajectory/README.i18n.yaml b/packages/client/ui-trajectory/README.i18n.yaml index c4a66b723a..18672cd831 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: 14d86ed3863a630b - zh: bd30ea1be9c39025 + en: aa79ae3c504b3f5e + zh: 1fd4d423b46ed08d /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 89a6dfa7ca..b8cc66a8a7 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 use non-empty recorded result text for ledger previews, inspector output, and output copying, preserving whitespace-only text. With that text present, structured error names and codes are neither displayed nor separately indexed for search. Only failures without any non-empty text block fall back to the error code in the ledger and the error name and code in the inspector; image content remains visible beside that fallback. +Native and nested PTC Tool results retain their raw structured error details and presentation metadata. Failed records use non-empty recorded result text for ledger previews, inspector output, and output copying, preserving whitespace-only text. JSON failure output stays as raw text in Summary and Result. With that text present, structured error names and codes are neither displayed nor separately indexed for search. Only failures without any non-empty text block fall back to the error code in the ledger and the error name and code in the inspector; image content remains visible beside that fallback. 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 421e92dd08..4722eea158 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 工具结果保留原始结构化错误详情和展示元数据。失败记录在记录表预览、检查器输出和输出复制中使用已记录的非空结果文本,并保留仅含空白字符的文本。JSON 失败输出在“概述”和“结果”中保持为原始文本。存在这类文本时,结构化错误名称和代码既不显示,也不单独纳入搜索索引。仅在失败结果没有任何非空文本块时,记录表才回退到错误代码,检查器才回退到错误名称和代码;图片内容仍显示在回退信息旁边。 工具记录从持久 tool/call 事件开始,保留完整参数文本和由配对结果复用的懒计算 `args` 视图。Chat 的瞬态 preparing 阶段不创建 Trajectory 工具行,也不改变历史工具计时。 diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx index eff4b09e4c..7d9e5ec80d 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx @@ -1727,7 +1727,7 @@ function RecordPayload({ const payloadClass = preview ? css.jsonPreview : css.jsonPayload const payloadClassName = error ? `${payloadClass} ${css.errorPayload}` : payloadClass - const json = parseJsonContainer(value) + const json = error ? undefined : parseJsonContainer(value) const singleTextResult = direction === 'output' && record.cell.outputBlocks?.length === 1 && record.cell.outputBlocks[0]?.type === 'text' diff --git a/packages/client/ui-trajectory/tests/code-inspector.client.spec.tsx b/packages/client/ui-trajectory/tests/code-inspector.client.spec.tsx index d3754e43dc..9021ad1b5e 100644 --- a/packages/client/ui-trajectory/tests/code-inspector.client.spec.tsx +++ b/packages/client/ui-trajectory/tests/code-inspector.client.spec.tsx @@ -53,6 +53,21 @@ function failedCell(name: string, texts: readonly string[], nested: boolean): Tr } describe('PTC trajectory inspector', () => { + it.each([ + ['run_code', false], ['read', false], ['read', true], + ] as const)('keeps failed %s JSON output as raw text (nested: %s)', (name, nested) => { + const output = '{ "reason" : "missing", "steps": [1, 2] }\n' + const view = render() + const ui = within(view.container) + fireEvent.click(ui.getByRole('row', { name: /TOOL/ })) + for (const tab of ['Summary', 'Result']) { + fireEvent.click(ui.getByRole('tab', { name: tab })) + const panel = ui.getByRole('tabpanel') + expect(within(panel).queryByRole('tree', { name: 'Result JSON' })).toBeNull() + expect([...panel.querySelectorAll('pre')].filter(pre => pre.textContent === output)).toHaveLength(1) + } + }) + it.each([ ['run_code', false], ['read', false], ['read', true], ] as const)('keeps error details for empty %s blocks (nested: %s)', async (name, nested) => {