diff --git a/apps/web/tests/cua-run-code.e2e.ts b/apps/web/tests/cua-run-code.e2e.ts index 643a61d814..0c8fa805d2 100644 --- a/apps/web/tests/cua-run-code.e2e.ts +++ b/apps/web/tests/cua-run-code.e2e.ts @@ -82,12 +82,18 @@ it.skipIf(mode === 'record')('shows standard CUA programs and their recorded sou expect(events.filter(event => event.type === 'tool/ptc-dispatch' && event.data.content.some(block => block.type === 'image'))).toHaveLength(1) expect(events.filter(event => event.type === 'tool/result' && event.data.message.content.some(block => block.type === 'image'))).toHaveLength(1) + expect(await calls.first().textContent()).toContain('(1×1, 90 B)') + expect(await calls.first().textContent()).not.toContain('"type": "image"') await compareOrRefreshGolden(join(snapshotDirectory, 'ui.expected.md'), await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd), mode) if (mode === 'refresh') { const evidence = fileURLToPath(new URL('../../../.playwright-mcp/cua-run-code-ui', import.meta.url)) await mkdir(evidence, { recursive: true }) await page.screenshot({ path: join(evidence, 'standard-code-cards.png'), animations: 'disabled' }) + const viewport = page.viewportSize() + await page.setViewportSize({ width: 800, height: 900 }) + await page.screenshot({ path: join(evidence, 'standard-code-cards-narrow.png'), animations: 'disabled' }) + if (viewport !== null) await page.setViewportSize(viewport) } await page.getByRole('tab', { name: 'Trajectory', exact: true }).click() const row = page.getByRole('row', { name: new RegExp(`cua_run_code ${description}`) }) diff --git a/packages/client/ui-tool/README.i18n.yaml b/packages/client/ui-tool/README.i18n.yaml index 59dfd47110..47c516bed1 100644 --- a/packages/client/ui-tool/README.i18n.yaml +++ b/packages/client/ui-tool/README.i18n.yaml @@ -18,8 +18,8 @@ en: 4d8cb38024095705 zh: 1baf6fa42f3fb80f /deepseek-ai-dsh-client-ui-tool/use-this-package/registering-a-business-tool-view: - en: e458aff61e634190 - zh: 650e417f450b3edf + en: 3dd6bd71dfc5527d + zh: 9fe3c2135014bb7b /deepseek-ai-dsh-client-ui-tool/use-this-package/built-in-views: en: 33f41ad70d0728f7 zh: b7c65108c5921134 diff --git a/packages/client/ui-tool/README.md b/packages/client/ui-tool/README.md index 448a4e197b..04b2254d47 100644 --- a/packages/client/ui-tool/README.md +++ b/packages/client/ui-tool/README.md @@ -47,7 +47,7 @@ ctx.slots.inject('tool.call.toolview', () => }, BusinessToolRow)) ``` -The owner payload is `ToolCallOwnerProps`: `callId`, `toolName`, the `phase` discriminant and its stage-specific `block`, optional `cwd` and `home`, the Gateway-authorized `loadImage` loader (for a view whose result carries durable images), and plain `openFile`/`inspect` callbacks. A PTC dispatch block retains its event's `parentCallId`; a root Session call has no such field, so descendants route through the same keyed dispatch — a registered view such as `read_image` renders its card there, and unregistered descendants keep the generic flattened form. Path summaries relativize to the Session cwd first, then replace a leftover POSIX Host home with `~`; `filePath` and Host open prefer the operation-time absolute result path while displayed labels keep the authored argument. Successful older root results retain their argument fallback; pending relative paths wait for a result. The registration receives the normal Session slot runtime share but no React node or Runtime service. +The owner payload is `ToolCallOwnerProps`: `callId`, `toolName`, the `phase` discriminant and its stage-specific `block`, optional `cwd` and `home`, the Gateway-authorized `loadImage` loader (for a view whose result carries durable images), and plain `openFile`/`inspect` callbacks. A PTC dispatch block retains its event's `parentCallId`; a root Session call has no such field, so descendants route through the same keyed dispatch — a registered view such as `read_image` renders its card there, and unregistered descendants keep the generic flattened form. Path summaries relativize to the Session cwd first, then replace a leftover POSIX Host home with `~`; `filePath` and Host open prefer the operation-time absolute result path while displayed labels keep the authored argument. Successful older root results retain their argument fallback; pending relative paths wait for a result. The registration receives the normal Session slot runtime share but no React node or Runtime service. Generic result output preserves block order and displays image attachment names or IDs with dimensions and byte counts. ### Built-in views diff --git a/packages/client/ui-tool/README.zh.md b/packages/client/ui-tool/README.zh.md index 37757c47a7..65f23151d7 100644 --- a/packages/client/ui-tool/README.zh.md +++ b/packages/client/ui-tool/README.zh.md @@ -47,7 +47,7 @@ ctx.slots.inject('tool.call.toolview', () => }, BusinessToolRow)) ``` -owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、`phase` 判别字段及对应阶段的 `block`、可选 `cwd` 与 `home`、经 Gateway 授权的 `loadImage` loader(供结果携带持久图像的视图使用),以及普通的 `openFile`/`inspect` 回调。PTC dispatch 块保留事件的 `parentCallId`;根会话调用没有该字段,因此后代调用都走同一条按 key 分发路径:已注册视图的调用(如 `read_image`)也会在嵌套处渲染对应卡片,未注册的后代调用则保持通用压平形式。路径摘要先相对会话 cwd 缩短,再把剩余的 POSIX Host home 写成 `~`;`filePath` 与 Host 打开优先使用操作发生时记录的绝对结果路径,显示标签仍保留原始参数。成功的旧根调用结果继续回退到原参数;运行中的相对路径等待结果。注册项会收到常规的会话 slot 运行时共享数据,但不会收到 React 节点或运行时服务。 +owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、`phase` 判别字段及对应阶段的 `block`、可选 `cwd` 与 `home`、经 Gateway 授权的 `loadImage` loader(供结果携带持久图像的视图使用),以及普通的 `openFile`/`inspect` 回调。PTC dispatch 块保留事件的 `parentCallId`;根会话调用没有该字段,因此后代调用都走同一条按 key 分发路径:已注册视图的调用(如 `read_image`)也会在嵌套处渲染对应卡片,未注册的后代调用则保持通用压平形式。路径摘要先相对会话 cwd 缩短,再把剩余的 POSIX Host home 写成 `~`;`filePath` 与 Host 打开优先使用操作发生时记录的绝对结果路径,显示标签仍保留原始参数。成功的旧根调用结果继续回退到原参数;运行中的相对路径等待结果。注册项会收到常规的会话 slot 运行时共享数据,但不会收到 React 节点或运行时服务。 通用结果输出保留块的顺序,并以附件名称或 ID、尺寸和字节数展示图片引用。 ### 内置视图 diff --git a/packages/client/ui-tool/src/client/tool/models/tool-call-model.ts b/packages/client/ui-tool/src/client/tool/models/tool-call-model.ts index f568af3bd6..c9c0ef4644 100644 --- a/packages/client/ui-tool/src/client/tool/models/tool-call-model.ts +++ b/packages/client/ui-tool/src/client/tool/models/tool-call-model.ts @@ -203,8 +203,9 @@ function deriveAutoReviewDenial(block: ToolCallBlock): AutoReviewDenial | null { /** * Flatten a settled result's content blocks to display text: text blocks - * verbatim, other block shapes as pretty JSON. Empty content on a failed call - * falls back to the structured error's `name: code` line. + * verbatim, image references with dimensions and bytes, and other blocks as + * pretty JSON. Empty failure content falls back to the structured error's + * `name: code` line. * @param node - the settled result node. * @returns the flattened result text (may be empty). */ @@ -212,7 +213,10 @@ export function resultText(node: ToolResultNode): string { const parts: string[] = [] for (const block of node.content) { if (block.type === 'text') parts.push(block.text) - else parts.push(JSON.stringify(block, null, 2)) + else if (block.type === 'image') { + const image = block.attachment + parts.push(`${image.name ?? image.attachmentId} (${image.width}×${image.height}, ${image.bytes} B)`) + } else parts.push(JSON.stringify(block, null, 2)) } if (parts.length === 0 && node.error !== undefined) { parts.push(`${node.error.name}: ${node.error.code}`) diff --git a/packages/client/ui-tool/tests/tool-row.client.spec.tsx b/packages/client/ui-tool/tests/tool-row.client.spec.tsx index f215febfc1..54dbc685d3 100644 --- a/packages/client/ui-tool/tests/tool-row.client.spec.tsx +++ b/packages/client/ui-tool/tests/tool-row.client.spec.tsx @@ -1,4 +1,5 @@ import type { SessionId } from '@deepseek-ai/dsh-session/types' +import { AttachmentId } from '@deepseek-ai/dsh-attachment' // @vitest-environment jsdom import { contextMenuHook } from '@deepseek-ai/dsh-client-test-runtime' import { afterEach, describe, expect, it, vi } from 'vitest' @@ -238,10 +239,15 @@ describe('tool-call-model', () => { .toBe('{\n "code": ""\n}') }) - it('resultText flattens text blocks verbatim, other shapes as JSON, empty error content to name: code', () => { + it('resultText retains text, describes images, and uses the structured failure when content is empty', () => { expect(resultText(result({ content: [{ type: 'text', text: 'a\nb' }] }))).toBe('a\nb') - expect(resultText(result({ content: [{ type: 'text', text: 'a' }, { type: 'image', data: 'x' } as never] }))) - .toBe(`a\n${JSON.stringify({ type: 'image', data: 'x' }, null, 2)}`) + const attachment = { attachmentId: AttachmentId('sha256:image'), mediaType: 'image/png' as const, width: 8, height: 4, bytes: 64 } + expect(resultText(result({ content: [{ type: 'text', text: 'a' }, { type: 'image', attachment }] }))) + .toBe('a\nsha256:image (8×4, 64 B)') + expect(resultText(result({ content: [{ type: 'image', attachment: { ...attachment, name: 'shot.png' } }] }))) + .toBe('shot.png (8×4, 64 B)') + const reasoning = { type: 'reasoning' as const, text: 'detail' } + expect(resultText(result({ content: [reasoning] }))).toBe(JSON.stringify(reasoning, null, 2)) expect(resultText(result({ content: [], isError: true, error: { name: 'ToolError', code: 'denied' } }))) .toBe('ToolError: denied') expect(resultText(result({ content: [] }))).toBe('') diff --git a/snapshots/web/cua-run-code/ui.expected.md b/snapshots/web/cua-run-code/ui.expected.md index 7c9627dda4..a3c1608087 100644 --- a/snapshots/web/cua-run-code/ui.expected.md +++ b/snapshots/web/cua-run-code/ui.expected.md @@ -20,7 +20,7 @@ - button "Wrap lines" [pressed] - button "Copy" - code: "globalThis.visits = 1; var app = await cua.getApp(\"test.fixture\"); var win = await app.getWindow(7); await win.getAccessibilityTree({emit:false}); await win.click(\"0\"); var screenshot = await win.getScreenshot(); inspect(JSON.stringify({ visits, bindings: Object.keys(tools), app: app.appHandle, window: win.windowId })); inspect(screenshot)" -- text: "OUT {\"visits\":1,\"bindings\":[\"cua\"],\"app\":\"fixture-app\",\"window\":7} { \"type\": \"image\", \"attachment\": { \"attachmentId\": \"sha256:999f1d1527ee7e79266f16add5430fff76b1225d742464a5b1ff1f02971bb8ee\", \"mediaType\": \"image/png\", \"width\": 1, \"height\": 1, \"bytes\": 90 } }" +- text: "OUT {\"visits\":1,\"bindings\":[\"cua\"],\"app\":\"fixture-app\",\"window\":7} sha256:999f1d1527ee7e79266f16add5430fff76b1225d742464a5b1ff1f02971bb8ee (1×1, 90 B)" - button "Inspect" - button "Tool call cua · getCapabilities" - button "Tool call cua · getApp"