fix(ui-tool): show compact image references in code output

This commit is contained in:
_Kerman committed 2026-11-10 16:07:14 +08:00
1 parent 5fd443618f
commit 47fafbbfcb
7 files changed
+27 -11

No files matched your search

+6
View File
@@ -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}`) })
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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、尺寸和字节数展示图片引用。
### 内置视图
@@ -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}`)
@@ -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('')
+1 -1
View File
@@ -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"