From b9bf3bc4eba494b293aec9056507e5555c19aee2 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 26 Oct 2026 14:55:59 +0800 Subject: [PATCH] fix(trajectory): align live result states and JSON previews --- .../live-result-running.expected.md | 7 ++ apps/web/tests/navigation-panes.e2e.ts | 106 +++++++++++++++++- .../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 | 5 +- .../ui-trajectory/tests/table.client.spec.tsx | 23 +++- 7 files changed, 140 insertions(+), 9 deletions(-) create mode 100644 apps/web/tests/expected/navigation-panes/live-result-running.expected.md diff --git a/apps/web/tests/expected/navigation-panes/live-result-running.expected.md b/apps/web/tests/expected/navigation-panes/live-result-running.expected.md new file mode 100644 index 0000000000..91844f0a86 --- /dev/null +++ b/apps/web/tests/expected/navigation-panes/live-result-running.expected.md @@ -0,0 +1,7 @@ +- tabpanel "Result": + - region "Raw result": + - heading "Raw result" [level=3] + - paragraph: Running… + - region "Rendered result": + - heading "Rendered result" [level=3] + - paragraph: Running… diff --git a/apps/web/tests/navigation-panes.e2e.ts b/apps/web/tests/navigation-panes.e2e.ts index 583b2dbc27..733fc987ac 100644 --- a/apps/web/tests/navigation-panes.e2e.ts +++ b/apps/web/tests/navigation-panes.e2e.ts @@ -13,7 +13,7 @@ import type { Browser, Page, Response } from 'playwright' import { chromium } from 'playwright' import { strFromU8, unzipSync } from 'fflate' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, onTestFailed, onTestFinished } from 'vitest' -import { ToolCallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId, createToolResultMessage } from '@deepseek-ai/dsh-llm' import type {} from '@deepseek-ai/dsh-client-ui-trajectory' import { defineTool } from '@deepseek-ai/dsh-tools' import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay' @@ -38,6 +38,7 @@ const CODE_CARD_EXPECTED = join(CODE_CARD_DIR, 'code-card.expected.md') const COPY_MARKDOWN_EXPECTED = join(CODE_CARD_DIR, 'copy-markdown.expected.md') const EVICTED_RESULT_EXPECTED = join(CODE_CARD_DIR, 'live-result-evicted.expected.md') const LIVE_RESULT_EXPECTED = join(CODE_CARD_DIR, 'live-result.expected.md') +const RUNNING_RESULT_EXPECTED = join(CODE_CARD_DIR, 'live-result-running.expected.md') const MODE = webSnapshotMode() const SEED_ID = 'navigation-panes-web-e2e' const EXPORTED_LOG_FILE = `session.v${SESSION_FORMAT_VERSION}.jsonl` @@ -630,7 +631,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => { it.skipIf(MODE === 'record')('keeps the recorded fixture inventory exact', async () => { await assertFixtureInventory(CODE_CARD_DIR, [ 'code-card.expected.md', 'command-wrap.expected.md', 'copy-markdown.expected.md', - 'live-result-evicted.expected.md', 'live-result.expected.md', + 'live-result-evicted.expected.md', 'live-result.expected.md', 'live-result-running.expected.md', ]) await assertFixtureInventory(SNAPSHOT_DIR, [ 'session.v3.jsonl', 'search-id-results.expected.md', 'search-results.expected.md', 'trajectory.expected.md', @@ -743,4 +744,105 @@ describe('web e2e: navigation & panes over a rich seeded session', () => { await details.getByText('LIVE_RESULT_RENDERED', { exact: true }).waitFor() }) + it.skipIf(MODE === 'record')('keeps long raw strings and PTC output readable in one result scroller', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-long-tool-results')) + await ensureSeedOpen(page) + await expect.poll(() => scaffold.ctx.agents.get(SessionId(SEED_ID)) !== undefined).toBe(true) + const agent = scaffold.ctx.agents.get(SessionId(SEED_ID))! + agent.session.append('turn/start', { turn: 3 }) + onTestFinished(() => { + agent.session.append('step/end', { turn: 3, step: 1 }) + agent.session.append('turn/end', { turn: 3, reason: { kind: 'completed' } }) + }) + agent.session.append('step/start', { turn: 3, step: 1 }) + const callId = ToolCallId('long-ptc-result') + const call = agent.session.append('tool/call', { + turn: 3, step: 1, callId, name: 'run_code', + arguments: JSON.stringify({ code: 'console.log("PTC output")', description: 'Long program output' }), + }) + await page.getByRole('tab', { name: 'Trajectory' }).click() + await page.locator('tr[data-kind="tool"]').filter({ hasText: 'Long program output' }).click() + const details = page.getByRole('complementary', { name: 'Event details' }) + await details.getByRole('tab', { name: 'Result', exact: true }).click() + const raw = details.getByRole('region', { name: 'Raw result', exact: true }) + const rendered = details.getByRole('region', { name: 'Rendered result', exact: true }) + await raw.getByText('Running…', { exact: true }).waitFor() + await compareOrRefreshGolden( + RUNNING_RESULT_EXPECTED, await captureStableAria(page, '#trajectory-detail-panel', scaffold.workspaceCwd), MODE, + ) + const ptcOutput = [...Array.from({ length: 100 }, (_, i) => `PTC_LINE_${i}`), 'PTC_END'].join('\n') + agent.session.append('tool/result', { + turn: 3, step: 1, + message: createToolResultMessage({ callId, content: [{ type: 'text', text: ptcOutput }], isError: false }), + }, { surfaceOp: 'append', sourceEventSeqs: [call.seq] }) + await rendered.getByText(ptcOutput, { exact: true }).waitFor() + const stringValue = 'raw_output_'.repeat(300) + 'RAW_END' + const probes = [ + { name: 'long_string_probe', value: stringValue }, + { name: 'long_json_probe', value: { detail: 'json_output_'.repeat(120) } }, + ] + for (const probe of probes) { + const unregister = scaffold.ctx.tools.register(defineTool({ + name: probe.name, description: 'Long result layout fixture.', parameters: {}, + output: { + schema: { type: 'json' }, + render: (_args, value) => [{ type: 'text', text: typeof value === 'string' ? 'STRING_RENDERED' : JSON.stringify(value) }], + }, + execute: async () => probe.value, + })) + onTestFinished(unregister) + const batch = await scaffold.ctx.tools.execute({ + agent, signal: new AbortController().signal, log: { turn: 3, step: 1 }, + calls: [{ type: 'tool-call', id: ToolCallId(probe.name), name: probe.name, arguments: '{}' }], + }) + expect(batch.results[0]?.result.isError).toBe(false) + } + const viewport = page.viewportSize()! + try { + for (const width of [viewport.width, 390]) { + await page.setViewportSize({ width, height: 600 }) + for (const name of ['Long program output', ...probes.map(probe => probe.name)]) { + await details.getByRole('button', { name: 'Close details' }).click() + await page.locator('tr[data-kind="tool"]').filter({ hasText: name }).click() + await details.getByRole('tab', { name: 'Result', exact: true }).click() + if (name === 'long_json_probe') { + await raw.getByRole('treeitem').waitFor() + const heights = await Promise.all([raw, rendered].map(section => + section.getByRole('treeitem').evaluate(row => row.getBoundingClientRect().height))) + expect(heights[0]).toBeGreaterThan(3 * 16) + expect(heights[0]).toBe(heights[1]) + } else { + const body = name === 'Long program output' ? rendered : raw + await expect.poll(() => body.locator('pre').textContent()).toBe( + name === 'Long program output' ? ptcOutput : JSON.stringify(stringValue), + ) + } + const panel = details.getByRole('tabpanel') + const geometry = await panel.evaluate((element) => { + element.scrollTop = element.scrollHeight + const bounds = element.getBoundingClientRect() + const pre = element.querySelector('pre') + let tailVisible = true + if (pre?.lastChild !== null && pre?.lastChild !== undefined) { + const range = document.createRange() + range.selectNodeContents(pre) + range.setStart(pre.lastChild, (pre.lastChild.textContent?.length ?? 1) - 1) + const tail = range.getBoundingClientRect() + tailVisible = tail.top >= bounds.top && tail.bottom <= bounds.bottom + } + return { + scrolls: element.scrollTop > 0 && getComputedStyle(element).overflowY === 'auto', + tailVisible, + overflowingDescendants: [...element.querySelectorAll('*')].filter(node => + /auto|scroll/.test(getComputedStyle(node).overflowY) && node.scrollHeight > node.clientHeight + 1).length, + horizontalOverflow: element.scrollWidth > element.clientWidth + 1, + } + }) + expect(geometry).toEqual({ scrolls: true, tailVisible: true, overflowingDescendants: 0, horizontalOverflow: false }) + } + } + } finally { + await page.setViewportSize(viewport) + } + }) }) diff --git a/packages/client/ui-trajectory/README.i18n.yaml b/packages/client/ui-trajectory/README.i18n.yaml index 08c68d8541..5c2d165c37 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: ca179a3c87924aa2 - zh: 1cd2ff15c96a6b63 + en: 0eaa2a895454e053 + zh: 6fb5c2682698fca9 /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 248733949e..c2ea1a0094 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. The Result tab stacks the live canonical value above the rendered content, with one scroll area for the entire panel and no section height limits. Historical, failed, evicted, and unloaded raw results show “Raw results are only available for live tool calls” while rendered content remains available. There is no result-mode preference. The inspector subscribes only to the selected result while the Result tab is open, releasing it on selection changes or disposal; Session unload and connection loss clear the displayed value. JSON primitives, including `null`, remain inspectable. 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. +Native and nested PTC Tool results retain their raw structured error details and presentation metadata. The Result tab stacks the live canonical value above the rendered content, with one scroll area for the entire panel and no section height limits. Both JSON trees preview long strings up to 12 lines and allow expansion. In-flight PTC calls show “Running…” until a result is available. Historical, failed, evicted, and unloaded raw results show “Raw results are only available for live tool calls” while rendered content remains available. There is no result-mode preference. The inspector subscribes only to the selected result while the Result tab is open, releasing it on selection changes or disposal; Session unload and connection loss clear the displayed value. JSON primitives, including `null`, remain inspectable. 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 8793dbfb35..96c6fed96e 100644 --- a/packages/client/ui-trajectory/README.zh.md +++ b/packages/client/ui-trajectory/README.zh.md @@ -57,7 +57,7 @@ Trajectory 标签页让你以按轮次组织的事件记录表和交互式时间 用户消息、中途引导、上下文输入及请求提示词变更按日志顺序放在所属请求的 Step 分组内,位于请求分隔行和助手输出之前。 -原生与嵌套 PTC 工具结果保留原始结构化错误详情和展示元数据。结果标签页从上到下展示实时规范值与渲染内容,整个面板共用一个滚动区域,不限制各 section 的高度。历史、失败、已淘汰及已卸载的原始结果显示“原始结果仅在实时工具调用中可用”,渲染内容仍可查看。不再保留结果模式偏好。检查器仅在结果标签页打开时订阅当前选中的结果,在切换选择或销毁时释放;Session 卸载及连接断开会清除已展示的值。包括 `null` 在内的 JSON 基本值均可查看。概述预览仍显示渲染内容。失败记录在记录表中显示错误代码,在检查器中显示错误名称和代码。 +原生与嵌套 PTC 工具结果保留原始结构化错误详情和展示元数据。结果标签页从上到下展示实时规范值与渲染内容,整个面板共用一个滚动区域,不限制各 section 的高度。两侧 JSON 树的长字符串均预览最多 12 行,并可展开。执行中的 PTC 调用在结果可用前显示“运行中”。历史、失败、已淘汰及已卸载的原始结果显示“原始结果仅在实时工具调用中可用”,渲染内容仍可查看。不再保留结果模式偏好。检查器仅在结果标签页打开时订阅当前选中的结果,在切换选择或销毁时释放;Session 卸载及连接断开会清除已展示的值。包括 `null` 在内的 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 738d8deb69..2d9edc6c40 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx @@ -3536,10 +3536,11 @@ export function TrajectoryTable({

{t('record.rawResult')}

{resultValue === undefined - ?

{t('record.rawResultUnavailable')}

+ ?

{t(stateOf(selected) === 'running' ? 'code.running' : 'record.rawResultUnavailable')}

: resultValue !== null && typeof resultValue === 'object' ? + labels={jsonTreeLabels(t)} stringWrapping={stringWrapping} + collapsedStringLines={12} className={css.jsonPayload} /> :
{JSON.stringify(resultValue)}
}
diff --git a/packages/client/ui-trajectory/tests/table.client.spec.tsx b/packages/client/ui-trajectory/tests/table.client.spec.tsx index cf4d466085..84a6af0db8 100644 --- a/packages/client/ui-trajectory/tests/table.client.spec.tsx +++ b/packages/client/ui-trajectory/tests/table.client.spec.tsx @@ -14,7 +14,7 @@ import type { MessageImageLoader, MessageImagesOwnerProps } from '@deepseek-ai/d import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' import { TrajectoryTable as LocalizedTrajectoryTable } from '../src/client/TrajectoryTable.tsx' import { deriveTrajectoryLayout, type TrajectoryTurnModel } from '../src/client/layout.ts' -import { trajectoryRecordId } from '../src/client/trajectory-record.ts' +import { trajectoryRecordId, type TrajectoryCellProps } from '../src/client/trajectory-record.ts' import { t, tZh } from './locale.client.ts' const loadImage: MessageImageLoader = async () => 'blob:trajectory' @@ -1165,6 +1165,27 @@ describe('TrajectoryTable', () => { view.unmount() }) + it.each(['tool', 'subtool'] as const)('shows an in-flight PTC %s as running before retaining its failure output', (kind) => { + const cell: TrajectoryCellProps = { + index: 1, kind, text: 'pending-call', timeSeconds: null, toolName: 'run_code', + inputDetail: JSON.stringify({ code: 'return 42', description: 'pending-call' }), + } + const turns = (current: TrajectoryCellProps): readonly TrajectoryTurnModel[] => [ + { turn: 1, groups: [{ title: 'Step 1', cells: [current] }] }, + ] + const view = render() + fireEvent.click(screen.getByRole('row', { name: /pending-call/ })) + fireEvent.click(screen.getByRole('tab', { name: 'Result' })) + const raw = screen.getByRole('region', { name: 'Raw result' }) + expect(within(raw).getByText('Running…')).toBeTruthy() + expect(within(raw).queryByText('Raw results are only available for live tool calls')).toBeNull() + view.rerender() + expect(within(raw).getByText('Raw results are only available for live tool calls')).toBeTruthy() + expect(screen.getByRole('region', { name: 'Rendered result' }).textContent).toContain('Error: execution failed') + }) + it('keeps rendered output visible when the raw value unloads and ignores stale callbacks', () => { const callbacks: ((value: JsonValue | undefined) => void)[] = [] const dispose = vi.fn()