fix(trajectory): align live result states and JSON previews

This commit is contained in:
_Kerman committed 2026-10-26 14:55:59 +08:00
1 parent 6287b3f387
commit b9bf3bc4eb
7 files changed
+140 -9

No files matched your search

@@ -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…
+104 -2
View File
@@ -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)
}
})
})
@@ -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
+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. 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.
+1 -1
View File
@@ -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 工具行,也不改变历史工具计时。
@@ -3536,10 +3536,11 @@ export function TrajectoryTable({
<h3 className={css.resultTitle}>{t('record.rawResult')}</h3>
</header>
{resultValue === undefined
? <p className={css.noPayload}>{t('record.rawResultUnavailable')}</p>
? <p className={css.noPayload}>{t(stateOf(selected) === 'running' ? 'code.running' : 'record.rawResultUnavailable')}</p>
: resultValue !== null && typeof resultValue === 'object'
? <JsonTree data={resultValue} label={t('record.resultJson')}
labels={jsonTreeLabels(t)} stringWrapping={stringWrapping} className={css.jsonPayload} />
labels={jsonTreeLabels(t)} stringWrapping={stringWrapping}
collapsedStringLines={12} className={css.jsonPayload} />
: <pre className={css.programOutput}>{JSON.stringify(resultValue)}</pre>}
</section>
<section className={css.resultSection} aria-label={t('record.renderedResult')}>
@@ -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(<TrajectoryTable turns={turns(cell)} {...FOLD_PROPS} />)
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(<TrajectoryTable turns={turns({
...cell, isError: true, outputDetail: 'Error: execution failed', timeSeconds: 0.1,
})} {...FOLD_PROPS} />)
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()