fix(trajectory): retain failure fallback for empty text blocks

This commit is contained in:
_Kerman committed 2026-10-27 12:44:43 +08:00
1 parent 86bf2734c6
commit c099ecdfd5
6 files changed
+63 -6

No files matched your search

@@ -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
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -57,7 +57,7 @@ Trajectory 标签页让你以按轮次组织的事件记录表和交互式时间
用户消息、中途引导、上下文输入及请求提示词变更按日志顺序放在所属请求的 Step 分组内,位于请求分隔行和助手输出之前。
原生与嵌套 PTC 工具结果保留原始结构化错误详情和展示元数据。失败记录在记录表预览、检查器输出和输出复制中使用已记录的结果文本。仅在失败结果没有文本时,记录表才回退到错误代码,检查器才回退到错误名称和代码;图片内容仍显示在回退信息旁边。
原生与嵌套 PTC 工具结果保留原始结构化错误详情和展示元数据。失败记录在记录表预览、检查器输出和输出复制中使用已记录的非空结果文本,并保留仅含空白字符的文本。存在这类文本时,结构化错误名称和代码既不显示,也不单独纳入搜索索引。仅在失败结果没有任何非空文本块时,记录表才回退到错误代码,检查器才回退到错误名称和代码;图片内容仍显示在回退信息旁边。
工具记录从持久 tool/call 事件开始,保留完整参数文本和由配对结果复用的懒计算 `args` 视图。Chat 的瞬态 preparing 阶段不创建 Trajectory 工具行,也不改变历史工具计时。
@@ -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']
@@ -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'
@@ -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(<Harness cell={cell} />)
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(<Harness cell={cell} />)
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 } })