mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-09 04:02:35 +00:00
fix(ui-tool): scope the image-card path fallback to nested calls; repair merge-broken docs and snapshot fixtures
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-tool/README.md
|
||||
README.md: ee2eabb732f192e4b119421a9fe10dd1a8d93807
|
||||
README.zh.md: f2d8c778b1bb84e381bb638f6e85829e41e252da
|
||||
README.md: 7c4fa98ed245c7102e6380285bba099290221ea9
|
||||
README.zh.md: 5dbdf7d9cbd856d50f3b7c5b79e50ac6025e68f9
|
||||
|
||||
@@ -39,7 +39,7 @@ ctx.slots.inject('tool.call.toolview', () =>
|
||||
}, BusinessToolRow))
|
||||
```
|
||||
|
||||
The owner payload is `ToolCallOwnerProps`: `callId`, `toolName`, the frozen `block`, optional `cwd` and `home`, the session-authorized `loadImage` loader (for a view whose result carries durable images), and plain `openFile`/`inspect` callbacks. A Code Dispatch block retains its event's `parentCallId`; a root Session call has no such field, so descendants keep the generic flattened form without another placement flag. Path summaries relativize to the Session cwd first, then replace a leftover POSIX Host home with `~`; `filePath` and Host open keep the authored filesystem path. The registration receives the normal Session slot runtime share but no React node or Runtime service.
|
||||
The owner payload is `ToolCallOwnerProps`: `callId`, `toolName`, the frozen `block`, optional `cwd` and `home`, the session-authorized `loadImage` loader (for a view whose result carries durable images), and plain `openFile`/`inspect` callbacks. A Code 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 keep the authored filesystem path. The registration receives the normal Session slot runtime share but no React node or Runtime service.
|
||||
|
||||
### Built-in views
|
||||
|
||||
@@ -62,9 +62,8 @@ The package realizes one dispatch rule: atomic Tool views are keyed by wire Tool
|
||||
### Details and cards
|
||||
|
||||
|
||||
The package fills `conversation.details.tool` with `ToolDetails`. Row and Details renderers share one pure card model for each terminal, read, diff, search, and web card. These models validate raw call arguments, result content, failure state, persisted metadata, Code Dispatch `parentCallId`, and Session path facts. Generic rows retain the original `argsRaw` reference and format their input body only while it is expanded; structured cards skip generic-body formatting. Unsupported or malformed inputs use flattened Tool result text. Card-specific limits and fallback rules remain in the owning [terminal](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.md), [diff](../../../.agents/notes/implemented/feature/2026-07-30-web-diff-card.md), [read](../../../.agents/notes/implemented/feature/2026-07-30-web-read-card-frontend.md), [search](../../../.agents/notes/implemented/feature/2026-07-30-web-search-card.md), [web](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card-frontend.md), and [question](../../../.agents/notes/implemented/feature/2026-07-29-ask-question-web-presentation.md) notes.
|
||||
The package fills `conversation.details.tool` with `ToolDetails`. Row and Details renderers share one pure card model for each terminal, read, diff, search, and web card; the image card is row-only because its gallery renders through the tool-owned `tool.call.images` slot the details panel does not declare. These models validate raw call arguments, result content, failure state, persisted metadata, Code Dispatch `parentCallId`, and Session path facts. Unsupported or malformed inputs use flattened Tool result text. Card-specific limits and fallback rules remain in the owning [terminal](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.md), [diff](../../../.agents/notes/implemented/feature/2026-07-30-web-diff-card.md), [read](../../../.agents/notes/implemented/feature/2026-07-30-web-read-card-frontend.md), [image](../../../.agents/notes/implemented/feature/2026-08-20-tool-card-image-results.md), [search](../../../.agents/notes/implemented/feature/2026-07-30-web-search-card.md), [web](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card-frontend.md), and [question](../../../.agents/notes/implemented/feature/2026-07-29-ask-question-web-presentation.md) notes.
|
||||
/details>
|
||||
</details>
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ ctx.slots.inject('tool.call.toolview', () =>
|
||||
}, BusinessToolRow))
|
||||
```
|
||||
|
||||
owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、冻结的 `block`、可选 `cwd` 与 `home`、会话授权的 `loadImage` loader(供结果携带持久图像的视图使用),以及普通的 `openFile`/`inspect` 回调。Code Dispatch block 保留事件的 `parentCallId`;root Session call 没有该字段,因此 descendant 无需另一项 placement 标志即可保持 generic 压平形态。路径摘要先相对 Session cwd 缩短,再把剩余的 POSIX Host home 写成 `~`;`filePath` 与 Host 打开仍使用作者给出的文件系统路径。注册项会收到常规 Session slot runtime share,但不会收到 React node 或 runtime service。
|
||||
owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、冻结的 `block`、可选 `cwd` 与 `home`、会话授权的 `loadImage` loader(供结果携带持久图像的视图使用),以及普通的 `openFile`/`inspect` 回调。Code Dispatch block 保留事件的 `parentCallId`;root Session call 没有该字段,因此 descendant 走同一条按 key 分发——注册过视图的调用(如 `read_image`)在嵌套处也渲染其卡片,未注册的 descendant 保持 generic 压平形式。路径摘要先相对 Session cwd 缩短,再把剩余的 POSIX Host home 写成 `~`;`filePath` 与 Host 打开仍使用作者给出的文件系统路径。注册项会收到常规 Session slot runtime share,但不会收到 React node 或 runtime service。
|
||||
|
||||
### 内置视图
|
||||
|
||||
@@ -62,9 +62,8 @@ owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、冻结的 `block`
|
||||
### 详情与卡片
|
||||
|
||||
|
||||
本包通过 `ToolDetails` 填充 `conversation.details.tool`。行 renderer 与 Details renderer 分别为 terminal、read、diff、search 和 web 卡片复用同一个纯 card model。这些 model 校验原始调用参数、结果内容、失败状态、持久 metadata、Code Dispatch `parentCallId` 与 Session 路径事实。Generic row 保留原始 `argsRaw` 引用,只在展开期间格式化 input body;结构化卡片跳过 generic body 格式化。不受支持或格式错误的输入使用压平的工具结果文本。各类卡片的上限与 fallback 规则仍由对应的 [terminal](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.zh.md)、[diff](../../../.agents/notes/implemented/feature/2026-07-30-web-diff-card.zh.md)、[read](../../../.agents/notes/implemented/feature/2026-07-30-web-read-card-frontend.zh.md)、[search](../../../.agents/notes/implemented/feature/2026-07-30-web-search-card.zh.md)、[web](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card-frontend.zh.md) 与 [question](../../../.agents/notes/implemented/feature/2026-07-29-ask-question-web-presentation.zh.md) 笔记负责。
|
||||
本包通过 `ToolDetails` 填充 `conversation.details.tool`。行 renderer 与 Details renderer 分别为 terminal、read、diff、search 和 web 卡片复用同一个纯 card model;image 卡片仅属于行,因为其图库经由工具自有 `tool.call.images` 槽位渲染,而 details 面板不声明该槽位。这些 model 校验原始调用参数、结果内容、失败状态、持久 metadata、Code Dispatch `parentCallId` 与 Session 路径事实。不受支持或格式错误的输入使用压平的工具结果文本。各类卡片的上限与 fallback 规则仍由对应的 [terminal](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.zh.md)、[diff](../../../.agents/notes/implemented/feature/2026-07-30-web-diff-card.zh.md)、[read](../../../.agents/notes/implemented/feature/2026-07-30-web-read-card-frontend.zh.md)、[image](../../../.agents/notes/implemented/feature/2026-08-20-tool-card-image-results.zh.md)、[search](../../../.agents/notes/implemented/feature/2026-07-30-web-search-card.zh.md)、[web](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card-frontend.zh.md) 与 [question](../../../.agents/notes/implemented/feature/2026-07-29-ask-question-web-presentation.zh.md) 笔记负责。
|
||||
/details>
|
||||
</details>
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -215,9 +215,13 @@ export function imageCardModel(
|
||||
const { file_path: filePath } = call.args
|
||||
if (typeof filePath !== 'string' || filePath.trim() === '') return null
|
||||
// The label path: root calls persist it in presentationMeta; a nested call
|
||||
// persists none, so its own file_path argument fills the label.
|
||||
// (dispatched from inside run_code) persists none, so its own file_path
|
||||
// argument fills the label. A root call with missing or malformed meta
|
||||
// declines — malformed tool data falls back to the generic card, which shows
|
||||
// the flattened content rather than an author-typed path.
|
||||
const metaPath = imageMeta(block.meta)?.path
|
||||
const path = metaPath ?? filePath
|
||||
const path = metaPath ?? (block.parentCallId !== undefined ? filePath : null)
|
||||
if (path === null) return null
|
||||
// The card renders only text and image blocks; a block of any other type must
|
||||
// not be silently hidden, so the whole card declines to the generic form.
|
||||
if (!fullyRendered(block.content)) return null
|
||||
|
||||
@@ -192,17 +192,19 @@ describe('imageCardModel', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('falls back to the call argument when metadata is malformed, instead of throwing', () => {
|
||||
it('declines a root call with missing or malformed metadata, and falls back for a nested one', () => {
|
||||
// Metadata arrives unvalidated on replay, so an obsolete or hand-edited log
|
||||
// must never crash the tool message. A call whose meta does not match still
|
||||
// carries a usable file_path argument, which fills the label; only a call
|
||||
// with no usable argument declines (covered by the call-head case below).
|
||||
// must never crash the tool message. A ROOT call whose meta does not match
|
||||
// declines to the generic card — malformed tool data must not render a
|
||||
// card labeled with the author-typed path. Only a nested call (which
|
||||
// persists no meta by design) falls back to its own file_path argument.
|
||||
for (const meta of [undefined, null, 'meta', [{ path: 'a.png' }], {}, { path: '' }, { path: 7 }]) {
|
||||
expect(() => imageCardModel(settled({ meta }))).not.toThrow()
|
||||
const card = imageCardModel(settled({ meta }))
|
||||
expect(card).not.toBeNull()
|
||||
expect(card?.label).toBe('shots/card.png')
|
||||
expect(imageCardModel(settled({ meta }))).toBeNull()
|
||||
}
|
||||
const nested = imageCardModel(settled({ parentCallId: 'parent', meta: undefined }))
|
||||
expect(nested).not.toBeNull()
|
||||
expect(nested?.label).toBe('shots/card.png')
|
||||
})
|
||||
|
||||
it('declines a malformed attachment reference in the content', () => {
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/fs/tool-fs/README.md
|
||||
README.md: 6792ad6ef45623d9d0ad43275154954283e172db
|
||||
README.zh.md: b891eebace4a5a3d6ec28c82cd630d20a5842187
|
||||
README.zh.md: 0aed51744d3e23a63a06030a841e31375ea1cf7a
|
||||
|
||||
@@ -243,7 +243,6 @@ Use the edit tool for targeted changes to existing UTF-8 text files. It replaces
|
||||
- **媒体类型按扩展名声明**:扩展名选择声明类型,附件存储的魔数校验保持权威;扩展名错误但格式正确的图像会得到改名修复提示,而不是被嗅探接受。只有没有扩展名的路径按文件签名识别格式。
|
||||
- **对象路径重新走源准入**:对规范化附件对象调用 `read_image` 会把其字节作为新来源重新准入,因此把 `maxImageBytes`/`maxMessageImageBytes` 配置得低于规范化图片字节预算的部署可能拒绝 `ctx.attachments.readImage` 仍可读取的对象路径;默认配置下规范化预算(4 MiB)远低于源上限(20 MiB)。
|
||||
- **内嵌图像预览依赖 UI 组合**:工具结果卡片经由浏览器的 `tool.call.images` 槽位渲染图像,由附件呈现插件填充;未组合该插件的 UI 改为显示结果的信封文本。
|
||||
**没有附件区域工具**:agent 在拥有文件系统路径时可以通过其他可用工具裁剪图片;没有路径的粘贴或拖入图片无法按更高分辨率重新读取。
|
||||
- **没有附件区域工具**:agent 在拥有文件系统路径时可以通过其他可用工具裁剪图片;没有路径的粘贴或拖入图片无法按更高分辨率重新读取。
|
||||
- **没有超时接口**:`read`/`write`/`edit` 不接受超时参数,也不声明超时预算;取消只通过 `exec.signal` 传递(见[提供方理由](../README.zh.md))。
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"read-image-source","name":"read_image","arguments":"{\"file_path\":\"red.png\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:3}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[12,13,14,15],"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"read-image-source","name":"read_image","arguments":"{\"file_path\":\"red.png\"}"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"read-image-source"},"content":[{"type":"tool-result","toolCallId":"read-image-source","content":[{"type":"text","text":"<path>{{cwd}}/red.png</path>\n<type>image</type>\n<content>\nimage/png image, 1x1 px, 69 bytes\n</content>"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}],"isError":false}],"role":"user","id":"{{message:4}}"}},"sourceEventSeqs":[17],"surfaceOp":"append"}
|
||||
{"type": "tool/result", "data": {"turn": 1, "step": 1, "message": {"source": {"kind": "tool", "callId": "read-image-source"}, "content": [{"type": "tool-result", "toolCallId": "read-image-source", "content": [{"type": "text", "text": "<path>{{cwd}}/red.png</path>\n<type>image</type>\n<content>\nimage/png image, 1x1 px, 69 bytes\n</content>"}, {"type": "image", "attachment": {"attachmentId": "sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640", "mediaType": "image/png", "bytes": 69, "width": 1, "height": 1, "name": "red.png"}}], "isError": false}], "role": "user", "id": "{{message:4}}"}, "meta": {"path": "{{cwd}}/red.png"}}, "sourceEventSeqs": [17], "surfaceOp": "append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
@@ -26,7 +26,7 @@
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"read-image-object","name":"read_image","arguments":"{\"file_path\":\"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:5}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[21,22,23,24],"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":2,"callId":"read-image-object","name":"read_image","arguments":"{\"file_path\":\"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\"}"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"read-image-object"},"content":[{"type":"tool-result","toolCallId":"read-image-object","content":[{"type":"text","text":"<path>{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640</path>\n<type>image</type>\n<content>\nimage/png image, 1x1 px, 69 bytes\n</content>"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"}}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[26],"surfaceOp":"append"}
|
||||
{"type": "tool/result", "data": {"turn": 1, "step": 2, "message": {"source": {"kind": "tool", "callId": "read-image-object"}, "content": [{"type": "tool-result", "toolCallId": "read-image-object", "content": [{"type": "text", "text": "<path>{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640</path>\n<type>image</type>\n<content>\nimage/png image, 1x1 px, 69 bytes\n</content>"}, {"type": "image", "attachment": {"attachmentId": "sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640", "mediaType": "image/png", "bytes": 69, "width": 1, "height": 1, "name": "b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"}}], "isError": false}], "role": "user", "id": "{{message:6}}"}, "meta": {"path": "{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"}}, "sourceEventSeqs": [26], "surfaceOp": "append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":2}}
|
||||
{"type":"step/start","data":{"turn":1,"step":3}}
|
||||
{"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
|
||||
Reference in New Issue
Block a user