From c099ecdfd5435d8da80eaec32f55876043ae4341 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Tue, 27 Oct 2026 12:44:43 +0800 Subject: [PATCH] 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 } })