@prettier-ai/dsh-client-ui-tool 0.1.2-alpha.5 → 0.1.3-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/ui-tool/README.md
5
- README.md: ac2105c5932717bf58e7bba9268240765c4c3ae3
6
- README.zh.md: df9a8afe1096856afac6fbc7dfda88a6190d4ba8
5
+ README.md: 2fe410425935aed46ab95844c81568037ac929e9
6
+ README.zh.md: 83440881fe16582a75fd8dd762577dc7b4d5335b
package/README.md CHANGED
@@ -39,11 +39,11 @@ ctx.slots.inject('tool.call.toolview', () =>
39
39
  }, BusinessToolRow))
40
40
  ```
41
41
 
42
- The owner payload is `ToolCallOwnerProps`: `callId`, `toolName`, the frozen `block`, optional `cwd` and `home`, 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.
42
+ 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.
43
43
 
44
44
  ### Built-in views
45
45
 
46
- This package owns the generic fallback and the built-in shell/pwsh, read, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and Code Dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Foreground one-shot shell results use terminal cards. Settled persistent-shell results use the expandable generic input/output card because reset and partial-output diagnostics do not always describe one process exit status; background acknowledgements remain collapsed. A successful question row pairs call questions with result answers by their stable ids and shows readable question/answer lines when expanded. A cancelled or interrupted row shows its verdict and original questions without inventing answers. Unsupported, malformed, or ambiguous inputs fall back to flattened Tool input/result text. `ui-skill` demonstrates a business-owned registration for `skill`.
46
+ This package owns the generic fallback and the built-in shell/pwsh, read, read_image, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and Code Dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Foreground one-shot shell results use terminal cards. Settled persistent-shell results use the expandable generic input/output card because reset and partial-output diagnostics do not always describe one process exit status; background acknowledgements remain collapsed. A successful question row pairs call questions with result answers by their stable ids and shows readable question/answer lines when expanded. A cancelled or interrupted row shows its verdict and original questions without inventing answers. Unsupported, malformed, or ambiguous inputs fall back to flattened Tool input/result text. `ui-skill` demonstrates a business-owned registration for `skill`.
47
47
 
48
48
  -----
49
49
 
@@ -61,8 +61,8 @@ The package realizes one dispatch rule: atomic Tool views are keyed by wire Tool
61
61
 
62
62
  ### Details and cards
63
63
 
64
- 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.
65
64
 
65
+ 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.
66
66
  </details>
67
67
 
68
68
  -----
package/README.zh.md CHANGED
@@ -39,11 +39,11 @@ ctx.slots.inject('tool.call.toolview', () =>
39
39
  }, BusinessToolRow))
40
40
  ```
41
41
 
42
- owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、冻结的 `block`、可选 `cwd` 与 `home`,以及普通的 `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。
42
+ 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。
43
43
 
44
44
  ### 内置视图
45
45
 
46
- 本包拥有 generic fallback,以及 shell/pwsh、read、write/edit、running `str_replace_editor` `create`/`str_replace`、grep/glob、web、todo、question 与 Code Dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。前台一次性 shell 结果使用 terminal 卡片。已完成的持久 shell 结果使用可展开的 generic 输入/输出卡片,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;后台启动回执保持折叠。成功的问题行按稳定 id 配对调用中的问题与结果中的回答,展开后显示可读的问答行。已取消或已中断的问题行显示其裁决与原始问题,不虚构回答。不受支持、格式错误或含糊的输入回退为压平的工具输入/结果文本。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。
46
+ 本包拥有 generic fallback,以及 shell/pwsh、read、read_image、write/edit、running `str_replace_editor` `create`/`str_replace`、grep/glob、web、todo、question 与 Code Dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。前台一次性 shell 结果使用 terminal 卡片。已完成的持久 shell 结果使用可展开的 generic 输入/输出卡片,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;后台启动回执保持折叠。成功的问题行按稳定 id 配对调用中的问题与结果中的回答,展开后显示可读的问答行。已取消或已中断的问题行显示其裁决与原始问题,不虚构回答。不受支持、格式错误或含糊的输入回退为压平的工具输入/结果文本。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。
47
47
 
48
48
  -----
49
49
 
@@ -61,8 +61,8 @@ owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、冻结的 `block`
61
61
 
62
62
  ### 详情与卡片
63
63
 
64
- 本包通过 `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) 笔记负责。
65
64
 
65
+ 本包通过 `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) 笔记负责。
66
66
  </details>
67
67
 
68
68
  -----
package/lib/client.js CHANGED
@@ -25,7 +25,8 @@ window.__ModuleLoader__.load({
25
25
  function resolveWorkspacePath(cwd, path) {
26
26
  if (path.startsWith("/") || isWindowsStylePath(path)) return path;
27
27
  if (cwd === void 0 || cwd === "") return path;
28
- return `${cwd.replace(/[/\\]+$/, "")}/${path.replace(/^[/\\]+/, "")}`;
28
+ const separator = isWindowsStylePath(cwd) && cwd.includes("\\") ? "\\" : "/";
29
+ return `${cwd.replace(/[/\\]+$/, "")}${separator}${path.replace(/^[/\\]+/, "")}`;
29
30
  }
30
31
  /**
31
32
  * Abbreviate a POSIX home directory for display.
@@ -67,6 +68,7 @@ window.__ModuleLoader__.load({
67
68
  bash: "bash",
68
69
  pwsh: "bash",
69
70
  read: "read",
71
+ read_image: "read",
70
72
  web_fetch: "read",
71
73
  web_search: "search",
72
74
  grep: "search",
@@ -87,7 +89,8 @@ window.__ModuleLoader__.load({
87
89
  cordis_run: "tool.title.runCordis",
88
90
  cordis_stop: "tool.title.stopCordis",
89
91
  cordis_undefine: "tool.title.removeCordis",
90
- pwsh: "tool.title.pwsh"
92
+ pwsh: "tool.title.pwsh",
93
+ read_image: "tool.title.readImage"
91
94
  };
92
95
  /**
93
96
  * Classify a tool name into its row variant.
@@ -1042,7 +1045,7 @@ window.__ModuleLoader__.load({
1042
1045
  }
1043
1046
  //#endregion
1044
1047
  //#region \0dsh-css:/home/runner/work/dsh-publisher/dsh-publisher/upstream/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css.mjs
1045
- const css$3 = ".RAJz1a_root{flex-direction:column;display:flex}.RAJz1a_row{position:relative;overflow:hidden}.RAJz1a_root[data-state=running] .RAJz1a_row:after{content:\"\";background:linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);pointer-events:none;width:300px;animation:2.6s ease-out infinite RAJz1a_dsh-tool-row-sweep;position:absolute;top:0;bottom:0;left:0}@keyframes RAJz1a_dsh-tool-row-sweep{0%{left:-300px}90%,to{left:100%}}.RAJz1a_leading{flex-shrink:0}.RAJz1a_root[data-tool^=cordis_] .RAJz1a_leading,.RAJz1a_root[data-tool^=cordis_] .RAJz1a_title{color:var(--dsw-alias-state-business-primary)}.RAJz1a_root[data-tool^=cordis_] .RAJz1a_title{font-weight:500}.RAJz1a_root[data-tool^=cordis_] .RAJz1a_sep{background:var(--dsw-alias-state-business-primary)}.RAJz1a_chevron{color:var(--dsw-alias-label-secondary)}.RAJz1a_title{font-weight:400}.RAJz1a_sep{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.RAJz1a_summary{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);flex:auto;overflow:hidden}.RAJz1a_summarySuffix{white-space:nowrap;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);flex:none;margin-left:4px}.RAJz1a_diffStat{font-family:var(--ds-font-family-code);font-size:calc(var(--dsh-content-font-size-secondary,13px) - 2px);color:var(--dsw-alias-label-caption);margin-left:10px;transform:translateY(.5px)}.RAJz1a_fileLink{text-overflow:ellipsis;white-space:nowrap;min-width:0;font:inherit;text-align:left;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-secondary);text-decoration:underline dotted;text-decoration-color:var(--dsw-alias-label-tertiary);text-underline-offset:3px;cursor:pointer;background:0 0;border:none;flex:0 auto;margin:0;padding:0;text-decoration-thickness:1px;overflow:hidden}.RAJz1a_fileLink:hover{color:var(--dsw-alias-label-primary);text-decoration-color:currentColor}.RAJz1a_errorSummary{color:var(--dsw-alias-state-error-primary)}.RAJz1a_bodyWrap{flex-direction:column;display:flex}.RAJz1a_inspectButton{border:.5px solid var(--dsw-alias-border-l3);corner-shape:round;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-secondary);cursor:pointer;opacity:0;border-radius:999px;align-self:flex-start;align-items:center;gap:4px;margin:4px 0 2px 4px;padding:2px 8px;font-size:11px;line-height:16px;transition:opacity .1s;display:inline-flex}.RAJz1a_root:hover .RAJz1a_inspectButton,.RAJz1a_inspectButton:focus-visible{opacity:1}.RAJz1a_inspectButton:hover{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-label-primary)}.RAJz1a_bodyScroll{max-height:260px;overflow-y:auto}.RAJz1a_ioCard{border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-markdown-code-block);font:var(--dsw-font-markdown-code-block-small);border-radius:12px;flex-direction:column;margin:4px 0 4px 4px;display:flex}.RAJz1a_ioSection{grid-template-columns:max-content 1fr;align-items:baseline;column-gap:14px;max-height:150px;padding:12px 16px;display:grid;overflow-y:auto}.RAJz1a_ioSection::-webkit-scrollbar-thumb{background-clip:padding-box;border:2px solid #0000;border-radius:6px}.RAJz1a_ioSection::-webkit-scrollbar-track{margin:6px 0}.RAJz1a_ioLabel{color:var(--dsw-alias-label-caption);align-self:start;position:sticky;top:0}.RAJz1a_ioDivider{background:var(--dsw-alias-border-l2);flex:none;height:.5px}.RAJz1a_ioText{white-space:pre-wrap;word-break:break-word;min-width:0;color:var(--dsw-alias-label-secondary)}.RAJz1a_ioText[data-error]{color:var(--dsw-alias-state-error-primary)}.RAJz1a_codeBody,.RAJz1a_terminalBody,.RAJz1a_diffBody,.RAJz1a_readBody,.RAJz1a_searchBody,.RAJz1a_webBody{margin:4px 0 4px 4px}.RAJz1a_searchRecovery{white-space:pre-wrap;overflow-wrap:anywhere;font:var(--dsw-font-xs-13);color:var(--dsw-alias-label-tertiary);margin:4px 0 4px 4px}.RAJz1a_codeBody{--dsl-code-block-content-font:var(--dsw-font-markdown-code-block-small)}.RAJz1a_terminalBody{--dsl-terminal-font:var(--dsw-font-markdown-code-block-small);--dsl-terminal-line-height:18px;--dsl-terminal-output-max-height:224px;border:.5px solid var(--dsw-alias-border-l1)}.RAJz1a_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}";
1048
+ const css$3 = ".RAJz1a_root{flex-direction:column;display:flex}.RAJz1a_row{position:relative;overflow:hidden}.RAJz1a_root[data-state=running] .RAJz1a_row:after{content:\"\";background:linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);pointer-events:none;width:300px;animation:2.6s ease-out infinite RAJz1a_dsh-tool-row-sweep;position:absolute;top:0;bottom:0;left:0}@keyframes RAJz1a_dsh-tool-row-sweep{0%{left:-300px}90%,to{left:100%}}.RAJz1a_leading{flex-shrink:0}.RAJz1a_root[data-tool^=cordis_] .RAJz1a_leading,.RAJz1a_root[data-tool^=cordis_] .RAJz1a_title{color:var(--dsw-alias-state-business-primary)}.RAJz1a_root[data-tool^=cordis_] .RAJz1a_title{font-weight:500}.RAJz1a_root[data-tool^=cordis_] .RAJz1a_sep{background:var(--dsw-alias-state-business-primary)}.RAJz1a_chevron{color:var(--dsw-alias-label-secondary)}.RAJz1a_title{font-weight:400}.RAJz1a_sep{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.RAJz1a_summary{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);flex:auto;overflow:hidden}.RAJz1a_summarySuffix{white-space:nowrap;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);flex:none;margin-left:4px}.RAJz1a_diffStat{font-family:var(--ds-font-family-code);font-size:calc(var(--dsh-content-font-size-secondary,13px) - 2px);color:var(--dsw-alias-label-caption);margin-left:10px;transform:translateY(.5px)}.RAJz1a_fileLink{text-overflow:ellipsis;white-space:nowrap;min-width:0;font:inherit;text-align:left;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-secondary);text-decoration:underline dotted;text-decoration-color:var(--dsw-alias-label-tertiary);text-underline-offset:3px;cursor:pointer;background:0 0;border:none;flex:0 auto;margin:0;padding:0;text-decoration-thickness:1px;overflow:hidden}.RAJz1a_fileLink:hover{color:var(--dsw-alias-label-primary);text-decoration-color:currentColor}.RAJz1a_errorSummary{color:var(--dsw-alias-state-error-primary)}.RAJz1a_bodyWrap{flex-direction:column;display:flex}.RAJz1a_inspectButton{border:.5px solid var(--dsw-alias-border-l3);corner-shape:round;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-secondary);cursor:pointer;opacity:0;border-radius:999px;align-self:flex-start;align-items:center;gap:4px;margin:4px 0 2px 4px;padding:2px 8px;font-size:11px;line-height:16px;transition:opacity .1s;display:inline-flex}.RAJz1a_root:hover .RAJz1a_inspectButton,.RAJz1a_inspectButton:focus-visible{opacity:1}.RAJz1a_inspectButton:hover{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-label-primary)}.RAJz1a_bodyScroll{max-height:260px;overflow-y:auto}.RAJz1a_ioCard{border:.5px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-markdown-code-block);font:var(--dsw-font-markdown-code-block-small);border-radius:12px;flex-direction:column;margin:4px 0 4px 4px;display:flex}.RAJz1a_ioSection{grid-template-columns:max-content 1fr;align-items:baseline;column-gap:14px;max-height:150px;padding:12px 16px;display:grid;overflow-y:auto}.RAJz1a_ioSection::-webkit-scrollbar-thumb{background-clip:padding-box;border:2px solid #0000;border-radius:6px}.RAJz1a_ioSection::-webkit-scrollbar-track{margin:6px 0}.RAJz1a_ioLabel{color:var(--dsw-alias-label-caption);align-self:start;position:sticky;top:0}.RAJz1a_ioDivider{background:var(--dsw-alias-border-l2);flex:none;height:.5px}.RAJz1a_ioText{white-space:pre-wrap;word-break:break-word;min-width:0;color:var(--dsw-alias-label-secondary)}.RAJz1a_ioText[data-error]{color:var(--dsw-alias-state-error-primary)}.RAJz1a_codeBody,.RAJz1a_terminalBody,.RAJz1a_diffBody,.RAJz1a_readBody,.RAJz1a_imageBody,.RAJz1a_searchBody,.RAJz1a_webBody{margin:4px 0 4px 4px}.RAJz1a_searchRecovery{white-space:pre-wrap;overflow-wrap:anywhere;font:var(--dsw-font-xs-13);color:var(--dsw-alias-label-tertiary);margin:4px 0 4px 4px}.RAJz1a_imageLabel{overflow-wrap:anywhere;font:var(--dsw-font-sm-13);color:var(--dsw-alias-label-secondary);margin-bottom:4px}.RAJz1a_imageMeta{white-space:pre-wrap;overflow-wrap:anywhere;font:var(--dsw-font-xs-13);color:var(--dsw-alias-label-tertiary)}.RAJz1a_codeBody{--dsl-code-block-content-font:var(--dsw-font-markdown-code-block-small)}.RAJz1a_terminalBody{--dsl-terminal-font:var(--dsw-font-markdown-code-block-small);--dsl-terminal-line-height:18px;--dsl-terminal-output-max-height:224px;border:.5px solid var(--dsw-alias-border-l1)}.RAJz1a_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}";
1046
1049
  const tagId$3 = "@prettier-ai/dsh-client-ui-tool/ToolRow.module.css";
1047
1050
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
1048
1051
  const tag = document.createElement("style");
@@ -1061,6 +1064,9 @@ window.__ModuleLoader__.load({
1061
1064
  "dsh-tool-row-sweep": "RAJz1a_dsh-tool-row-sweep",
1062
1065
  "errorSummary": "RAJz1a_errorSummary",
1063
1066
  "fileLink": "RAJz1a_fileLink",
1067
+ "imageBody": "RAJz1a_imageBody",
1068
+ "imageLabel": "RAJz1a_imageLabel",
1069
+ "imageMeta": "RAJz1a_imageMeta",
1064
1070
  "inspectButton": "RAJz1a_inspectButton",
1065
1071
  "ioCard": "RAJz1a_ioCard",
1066
1072
  "ioDivider": "RAJz1a_ioDivider",
@@ -1102,7 +1108,7 @@ window.__ModuleLoader__.load({
1102
1108
  default: return null;
1103
1109
  }
1104
1110
  }
1105
- function ToolRow({ t, variant, toolName, icon, title, summary, summarySuffix, bodyRaw, output, askQuestion, errorSummary, terminal, diff, read, search, web, state, filePath, onOpenFile, inspect }) {
1111
+ function ToolRow({ t, variant, toolName, icon, title, summary, summarySuffix, bodyRaw, output, askQuestion, errorSummary, terminal, diff, read, image, renderSlot, loadImage, search, web, state, filePath, onOpenFile, inspect }) {
1106
1112
  const [expanded, setExpanded] = (0, react.useState)(false);
1107
1113
  const terminalLabels = (0, react.useMemo)(() => terminalBlockLabels(t), [t]);
1108
1114
  const diffLabels = (0, react.useMemo)(() => diffBlockLabels(t), [t]);
@@ -1112,11 +1118,12 @@ window.__ModuleLoader__.load({
1112
1118
  const terminalBody = terminal === void 0 || terminal === null ? null : localizeTerminalCardModel(terminal, t);
1113
1119
  const diffBody = diff ?? null;
1114
1120
  const readBody = read ?? null;
1121
+ const imageBody = image !== void 0 && image !== null && renderSlot !== void 0 && loadImage !== void 0 ? image : null;
1115
1122
  const searchBody = search ?? null;
1116
1123
  const webBody = web ?? null;
1117
1124
  const askQuestionBody = askQuestion ?? null;
1118
1125
  const outputText = output ?? null;
1119
- const card = askQuestionBody ?? terminalBody ?? diffBody ?? readBody ?? searchBody ?? webBody;
1126
+ const card = askQuestionBody ?? terminalBody ?? diffBody ?? readBody ?? imageBody ?? searchBody ?? webBody;
1120
1127
  const expandable = bodyRaw != null || outputText !== null || card !== null;
1121
1128
  const open = expanded && expandable;
1122
1129
  const bodyText = (0, react.useMemo)(() => open && card === null && bodyRaw != null ? formatToolBody(variant, bodyRaw) : null, [
@@ -1203,6 +1210,23 @@ window.__ModuleLoader__.load({
1203
1210
  labels: readLabels,
1204
1211
  maxLines: 8,
1205
1212
  className: ToolRow_module_css_default.readBody
1213
+ }) : imageBody !== null ? (0, react_jsx_runtime.jsxs)("div", {
1214
+ className: ToolRow_module_css_default.imageBody,
1215
+ children: [
1216
+ (0, react_jsx_runtime.jsx)("div", {
1217
+ className: ToolRow_module_css_default.imageLabel,
1218
+ children: imageBody.label
1219
+ }),
1220
+ renderSlot !== void 0 && loadImage !== void 0 && renderSlot("tool.call.images", {
1221
+ images: imageBody.images,
1222
+ loadImage,
1223
+ align: "start"
1224
+ }),
1225
+ (0, react_jsx_runtime.jsx)("div", {
1226
+ className: ToolRow_module_css_default.imageMeta,
1227
+ children: imageBody.text
1228
+ })
1229
+ ]
1206
1230
  }) : searchBody !== null ? (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.SearchBlock, {
1207
1231
  ...searchBody.card,
1208
1232
  labels: searchLabels,
@@ -1328,7 +1352,7 @@ window.__ModuleLoader__.load({
1328
1352
  return "kind" in node ? node.call?.name ?? "" : node.name;
1329
1353
  }
1330
1354
  /** One atomic call dispatched through the Tool-owned keyed slot. */
1331
- const ToolCall = (0, react.memo)(function ToolCall({ renderSlot, callId, toolName, block, openFile, selected, cwd, home, inspectCall, t, children }) {
1355
+ const ToolCall = (0, react.memo)(function ToolCall({ renderSlot, callId, toolName, block, openFile, selected, cwd, home, inspectCall, loadImage, t, children }) {
1332
1356
  const owner = (0, react.useMemo)(() => ({
1333
1357
  callId,
1334
1358
  toolName,
@@ -1336,6 +1360,7 @@ window.__ModuleLoader__.load({
1336
1360
  openFile,
1337
1361
  cwd,
1338
1362
  home,
1363
+ loadImage,
1339
1364
  inspect: () => {
1340
1365
  inspectCall(callId);
1341
1366
  }
@@ -1346,6 +1371,7 @@ window.__ModuleLoader__.load({
1346
1371
  openFile,
1347
1372
  cwd,
1348
1373
  home,
1374
+ loadImage,
1349
1375
  inspectCall
1350
1376
  ]);
1351
1377
  return (0, react_jsx_runtime.jsxs)("div", {
@@ -1362,7 +1388,7 @@ window.__ModuleLoader__.load({
1362
1388
  }), children]
1363
1389
  });
1364
1390
  });
1365
- const ToolCallBranch = (0, react.memo)(function ToolCallBranch({ renderSlot, block, selectedCallId, cwd, home, openFile, inspectCall, t }) {
1391
+ const ToolCallBranch = (0, react.memo)(function ToolCallBranch({ renderSlot, block, selectedCallId, cwd, home, openFile, inspectCall, loadImage, t }) {
1366
1392
  return (0, react_jsx_runtime.jsx)(ToolCall, {
1367
1393
  renderSlot,
1368
1394
  callId: block.callId,
@@ -1373,6 +1399,7 @@ window.__ModuleLoader__.load({
1373
1399
  cwd,
1374
1400
  home,
1375
1401
  inspectCall,
1402
+ loadImage,
1376
1403
  t,
1377
1404
  children: block.subCalls.length > 0 ? (0, react_jsx_runtime.jsx)("div", {
1378
1405
  className: ToolCallTree_module_css_default.subCalls,
@@ -1385,6 +1412,7 @@ window.__ModuleLoader__.load({
1385
1412
  home,
1386
1413
  openFile,
1387
1414
  inspectCall,
1415
+ loadImage,
1388
1416
  t
1389
1417
  }, child.callId))
1390
1418
  }) : null
@@ -1396,7 +1424,7 @@ window.__ModuleLoader__.load({
1396
1424
  * @param props - whole-Tool owner data and the Tool-owned child-slot share.
1397
1425
  * @returns the Tool call tree.
1398
1426
  */
1399
- function ToolCallTree({ renderSlot, node, selectedCallId, cwd, openFile, inspectCall, useHostInfo, t }) {
1427
+ function ToolCallTree({ renderSlot, node, selectedCallId, cwd, openFile, inspectCall, loadImage, useHostInfo, t }) {
1400
1428
  const home = useHostInfo((info) => info.home);
1401
1429
  const block = node.data.root;
1402
1430
  return (0, react_jsx_runtime.jsx)(ToolCallBranch, {
@@ -1407,6 +1435,7 @@ window.__ModuleLoader__.load({
1407
1435
  home,
1408
1436
  openFile,
1409
1437
  inspectCall,
1438
+ loadImage,
1410
1439
  t
1411
1440
  });
1412
1441
  }
@@ -1872,13 +1901,16 @@ window.__ModuleLoader__.load({
1872
1901
  }
1873
1902
  };
1874
1903
  //#endregion
1875
- //#region lib/types/client/tool/toolviews/read-row.js
1904
+ //#region lib/types/client/tool/toolviews/read-family-row.js
1876
1905
  /**
1877
- * Lets users expand a completed read result and open its reported path.
1906
+ * Compose a read-family row: the shared chrome and model-derived fields, plus the
1907
+ * caller's card material.
1908
+ * @param props - the toolview runtime share and locale seat.
1909
+ * @param card - the card props this row owns.
1910
+ * @returns the assembled ToolRow.
1878
1911
  */
1879
- function ReadRow({ toolName, block, cwd, home, openFile, inspect, t }) {
1912
+ function readFamilyRow({ toolName, block, cwd, home, openFile, inspect, t }, card) {
1880
1913
  const model = toolRowModel(toolName, block, cwd, home);
1881
- const read = readCardModel(block, cwd, home);
1882
1914
  return (0, react_jsx_runtime.jsx)(ToolRow, {
1883
1915
  t,
1884
1916
  variant: model.variant,
@@ -1886,15 +1918,25 @@ window.__ModuleLoader__.load({
1886
1918
  icon: (0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.IconBrowseOutline16, { size: 14 }),
1887
1919
  title: t(model.titleKey),
1888
1920
  summary: model.summary,
1921
+ bodyRaw: null,
1889
1922
  output: model.output,
1890
1923
  errorSummary: model.errorSummary,
1891
- read,
1924
+ ...card,
1892
1925
  state: model.state,
1893
1926
  filePath: model.filePath,
1894
1927
  onOpenFile: openFile,
1895
1928
  inspect
1896
1929
  });
1897
1930
  }
1931
+ //#endregion
1932
+ //#region lib/types/client/tool/toolviews/read-row.js
1933
+ /**
1934
+ * Lets users expand a completed read result and open its reported path.
1935
+ */
1936
+ function ReadRow(props) {
1937
+ const { block, cwd, home } = props;
1938
+ return readFamilyRow(props, { read: readCardModel(block, cwd, home) });
1939
+ }
1898
1940
  /** Registers the read tool's conversation row. */
1899
1941
  const readToolview = {
1900
1942
  name: "read-toolview",
@@ -1908,6 +1950,214 @@ window.__ModuleLoader__.load({
1908
1950
  }
1909
1951
  };
1910
1952
  //#endregion
1953
+ //#region lib/types/client/tool/models/image-card-model.js
1954
+ /**
1955
+ * Whether a wire value is a usable pixel or byte measure.
1956
+ * @param value - unvalidated wire value.
1957
+ * @returns true when it is a positive integer.
1958
+ */
1959
+ function positiveInteger(value) {
1960
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
1961
+ }
1962
+ /** The envelope `formatImageReadOutput` writes, matched by shape. */
1963
+ const IMAGE_ENVELOPE = /^<path>[^\n]*<\/path>\n<type>image<\/type>\n<content>\n[\s\S]*\n<\/content>$/u;
1964
+ /** The media types a durable image block may claim; anything else declines.
1965
+ * Hand-written mirror of `ImageMediaType` from dsh-attachment — the wire
1966
+ * boundary needs a runtime check and feature plugins must not import values
1967
+ * from each other; a new member added there must be added here too, or the
1968
+ * decline point below silently degrades that image to the generic card. */
1969
+ const IMAGE_MEDIA_TYPES = new Set([
1970
+ "image/png",
1971
+ "image/jpeg",
1972
+ "image/webp",
1973
+ "image/gif"
1974
+ ]);
1975
+ /** Runtime membership check; the cast is safe because the set holds exactly the four members. */
1976
+ function isImageMediaType(value) {
1977
+ return IMAGE_MEDIA_TYPES.has(value);
1978
+ }
1979
+ /**
1980
+ * Narrow the persisted metadata, defensively. Every field arrives unvalidated on
1981
+ * replay (an obsolete or hand-edited log reaches here), so any mismatch declines
1982
+ * to the generic card rather than throwing. An absent `meta` (a nested call
1983
+ * persists none) leaves the path to the call's own `file_path` argument.
1984
+ *
1985
+ * The attachment id is checked for existence only: it is opaque and
1986
+ * provider-owned, and consumers must not parse that representation, so
1987
+ * pattern-matching the local content-address form would reject a legitimate id
1988
+ * minted by an alternative store.
1989
+ * @param meta - persisted presentation metadata of unknown shape.
1990
+ * @returns the narrowed path, or null when it does not match.
1991
+ */
1992
+ function imageMeta(meta) {
1993
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return null;
1994
+ const { path } = meta;
1995
+ if (typeof path !== "string" || path === "") return null;
1996
+ return { path };
1997
+ }
1998
+ /**
1999
+ * Narrow every attachment reference carried by the result's image blocks, in
2000
+ * order.
2001
+ *
2002
+ * The content is the single source of truth for the references: it is what the
2003
+ * tool actually returned and what a post-execute hook would replace together
2004
+ * with the rest of the content. Every field arrives unvalidated over the wire,
2005
+ * so any malformed image block declines to the generic card rather than
2006
+ * rendering a partial gallery.
2007
+ *
2008
+ * The attachment id is checked for existence only — it is opaque and
2009
+ * provider-owned, so pattern-matching the local content-address form would reject
2010
+ * a legitimate id minted by an alternative store.
2011
+ * @param content - the settled result's content blocks.
2012
+ * @returns the narrowed references, or null when no valid image block is present.
2013
+ */
2014
+ function imageReferences(content) {
2015
+ const refs = [];
2016
+ for (const part of content) {
2017
+ if (typeof part !== "object" || part === null) continue;
2018
+ const { type, attachment } = part;
2019
+ if (type !== "image") continue;
2020
+ if (typeof attachment !== "object" || attachment === null || Array.isArray(attachment)) return null;
2021
+ const { attachmentId, mediaType, bytes, width, height, name, originalDimensions } = attachment;
2022
+ if (typeof attachmentId !== "string" || attachmentId === "") return null;
2023
+ if (typeof mediaType !== "string" || !isImageMediaType(mediaType)) return null;
2024
+ if (!positiveInteger(bytes) || !positiveInteger(width) || !positiveInteger(height)) return null;
2025
+ if (name !== void 0 && typeof name !== "string") return null;
2026
+ let inputDimensions;
2027
+ if (originalDimensions !== void 0) {
2028
+ if (typeof originalDimensions !== "object" || originalDimensions === null || Array.isArray(originalDimensions)) return null;
2029
+ const { width: inputWidth, height: inputHeight } = originalDimensions;
2030
+ if (!positiveInteger(inputWidth) || !positiveInteger(inputHeight)) return null;
2031
+ inputDimensions = {
2032
+ width: inputWidth,
2033
+ height: inputHeight
2034
+ };
2035
+ }
2036
+ refs.push({
2037
+ attachmentId,
2038
+ mediaType,
2039
+ bytes,
2040
+ width,
2041
+ height,
2042
+ ...name === void 0 ? {} : { name },
2043
+ ...inputDimensions === void 0 ? {} : { originalDimensions: inputDimensions }
2044
+ });
2045
+ }
2046
+ return refs.length > 0 ? refs : null;
2047
+ }
2048
+ /**
2049
+ * Read the text of every text block of a settled image result, joined in order.
2050
+ *
2051
+ * The envelope is one of them; a post-execute hook that appends further text
2052
+ * blocks keeps them visible under the gallery instead of being dropped. The
2053
+ * envelope shape is still the recognition gate: a result without it is not a
2054
+ * well-formed image read and declines.
2055
+ * @param content - the settled result's content blocks.
2056
+ * @returns the joined text, or null when no envelope-shaped block is present.
2057
+ */
2058
+ function imageTexts(content) {
2059
+ const parts = [];
2060
+ let sawEnvelope = false;
2061
+ for (const part of content) {
2062
+ if (part.type !== "text" || typeof part.text !== "string") continue;
2063
+ if (IMAGE_ENVELOPE.test(part.text)) sawEnvelope = true;
2064
+ parts.push(part.text);
2065
+ }
2066
+ return sawEnvelope && parts.length > 0 ? parts.join("\n") : null;
2067
+ }
2068
+ /**
2069
+ * Whether the content carries only blocks the card consumes.
2070
+ *
2071
+ * `ContentBlock` is a merge-extensible union, so a post-execute hook could
2072
+ * append a block of a type this card does not render (reasoning, an extension
2073
+ * type, or a non-object). Rendering the card anyway would silently hide that
2074
+ * block, so anything beyond a well-formed text or image object declines to the
2075
+ * generic card, which shows the flattened content.
2076
+ * @param content - the settled result's content blocks.
2077
+ * @returns true when every block is a text or image object with usable fields.
2078
+ */
2079
+ function fullyRendered(content) {
2080
+ return content.every((part) => {
2081
+ if (typeof part !== "object" || part === null) return false;
2082
+ const { type, text } = part;
2083
+ return type === "image" || type === "text" && typeof text === "string";
2084
+ });
2085
+ }
2086
+ /**
2087
+ * Derive a settled image card after validating the call head, persisted
2088
+ * metadata (or its argument fallback), and the model-facing image envelope.
2089
+ *
2090
+ * The card is result-side only: a call carries no content until `execute`
2091
+ * returns, so a running `read_image` has none and this returns null for it.
2092
+ * Both root and nested calls settle as ToolResultNode; the nested one (a
2093
+ * read_image dispatched from inside run_code) persists no presentationMeta, so
2094
+ * its label falls back to the call's own `file_path` argument.
2095
+ * @param block - running or settled Tool block.
2096
+ * @param sessionCwd - the session workspace root; a workspace-rooted absolute
2097
+ * path label displays relative to it. Absent leaves the path as authored.
2098
+ * @param home - host account home; a leftover POSIX home path displays as `~`.
2099
+ * @returns the image-card props, or null for the generic path.
2100
+ */
2101
+ function imageCardModel(block, sessionCwd, home) {
2102
+ if (!("kind" in block) || block.isError) return null;
2103
+ const call = parsedToolCall(block);
2104
+ if (call?.name !== "read_image") return null;
2105
+ const { file_path: filePath } = call.args;
2106
+ if (typeof filePath !== "string" || filePath.trim() === "") return null;
2107
+ const path = imageMeta(block.meta)?.path ?? (block.parentCallId !== void 0 ? filePath : null);
2108
+ if (path === null) return null;
2109
+ if (!fullyRendered(block.content)) return null;
2110
+ const refs = imageReferences(block.content);
2111
+ if (refs === null) return null;
2112
+ const text = imageTexts(block.content);
2113
+ if (text === null) return null;
2114
+ return {
2115
+ label: abbreviateHomePath(relativizeToCwd(path, sessionCwd), home),
2116
+ images: refs.map((ref) => ({ attachment: ref })),
2117
+ text
2118
+ };
2119
+ }
2120
+ //#endregion
2121
+ //#region lib/types/client/tool/toolviews/read-image-row.js
2122
+ /**
2123
+ * read_image row: the read-family chrome with the durably committed image as the
2124
+ * row's collapsed-by-default card body, rendered through the `tool.call.images`
2125
+ * slot this entry declares.
2126
+ */
2127
+ function ReadImageRow(props) {
2128
+ const { block, cwd, home, renderSlot, loadImage } = props;
2129
+ return readFamilyRow(props, {
2130
+ image: imageCardModel(block, cwd, home),
2131
+ renderSlot,
2132
+ loadImage
2133
+ });
2134
+ }
2135
+ /**
2136
+ * The read_image row as a plain registrant plugin following the atomic Tool-view
2137
+ * declaration across independent activation and reload lifetimes. Declaring
2138
+ * `tool.call.images` as a child slot authorizes this entry's `renderSlot` to
2139
+ * dispatch the gallery.
2140
+ */
2141
+ const readImageToolview = {
2142
+ name: "read-image-toolview",
2143
+ inject: ["slots"],
2144
+ /**
2145
+ * Register the read_image row into the Tool-owned keyed view slot.
2146
+ * @param ctx - registrant context (disposal rides ctx.effect inside slots.register).
2147
+ */
2148
+ apply(ctx) {
2149
+ ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
2150
+ name: "tool.call.toolview",
2151
+ key: "read_image",
2152
+ locale: CONVERSATION_NS,
2153
+ children: { "tool.call.images": {
2154
+ kind: "single",
2155
+ scope: "session"
2156
+ } }
2157
+ }, ReadImageRow));
2158
+ }
2159
+ };
2160
+ //#endregion
1911
2161
  //#region lib/types/client/tool/toolviews/search-row.js
1912
2162
  const SEARCH_TITLE_KEYS = {
1913
2163
  grep: "tool.title.grep",
@@ -2120,6 +2370,7 @@ window.__ModuleLoader__.load({
2120
2370
  }, ToolDetails));
2121
2371
  ctx.plugin(bashToolviewSample);
2122
2372
  ctx.plugin(readToolview);
2373
+ ctx.plugin(readImageToolview);
2123
2374
  ctx.plugin(fileMutationToolview);
2124
2375
  ctx.plugin(searchToolview);
2125
2376
  ctx.plugin(webToolview);
@@ -2,6 +2,7 @@
2
2
  import type { HostObservable, InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime } from '@prettier-ai/dsh-client-ui-slots';
3
3
  import type { RemoteHostFacts } from '@prettier-ai/dsh-api-remotes/client';
4
4
  import type { ToolCallBlock } from '@prettier-ai/dsh-client-ui-chat/client';
5
+ import type { MessageImageLoader, MessageImageSource } from '@prettier-ai/dsh-client-ui-conversation/client';
5
6
  declare module '@prettier-ai/dsh-client-ui-slots' {
6
7
  interface SlotMap {
7
8
  /**
@@ -23,8 +24,35 @@ declare module '@prettier-ai/dsh-client-ui-slots' {
23
24
  scope: 'session';
24
25
  owner: ToolCallOwnerProps;
25
26
  };
27
+ /**
28
+ * Durable images of a settled image-bearing Tool call, rendered through
29
+ * the attachment presentation plugin. The Tool layer never imports an
30
+ * attachment implementation: a toolview declares this slot as a child and
31
+ * renders it with the image card's references plus the session-authorized
32
+ * loader it received in its owner, and the attachment plugin fills the
33
+ * gallery. Composing no attachment presentation plugin renders nothing,
34
+ * which is why the image card keeps its own envelope text beside the
35
+ * gallery. A child slot is declared by exactly one entry: registering a
36
+ * second toolview that declares the same child throws at load, so a
37
+ * future image-bearing tool must reuse this entry or own a distinct
38
+ * slot.
39
+ */
40
+ 'tool.call.images': {
41
+ kind: 'single';
42
+ scope: 'session';
43
+ owner: ToolImagesOwnerProps;
44
+ };
26
45
  }
27
46
  }
47
+ /** Owner currency of the Tool image gallery slot: references plus the loader. */
48
+ export interface ToolImagesOwnerProps {
49
+ /** Durable references or submission-echo previews in result order. */
50
+ images: readonly MessageImageSource[];
51
+ /** Session-authorized image URL loader for the durable arm. */
52
+ loadImage: MessageImageLoader;
53
+ /** Horizontal placement inside the owning record. */
54
+ align: 'start' | 'end';
55
+ }
28
56
  /** Standard owner currency supplied to every atomic Tool view. */
29
57
  export interface ToolCallOwnerProps {
30
58
  /** Tool call identity, stable across running and settled forms. */
@@ -39,6 +67,14 @@ export interface ToolCallOwnerProps {
39
67
  home?: string | undefined;
40
68
  /** Open a Tool argument path through the Host. */
41
69
  openFile: (path: string) => void;
70
+ /**
71
+ * Session-authorized image loader for the `tool.call.images` slot, supplied
72
+ * by the chat node that owns this call. A composed chat node always
73
+ * supplies it (`ChatNodeOwnerProps.loadImage` is required), so the tool
74
+ * layer never imports an attachment implementation nor handles URL
75
+ * authorization.
76
+ */
77
+ loadImage: MessageImageLoader;
42
78
  /** Inspect this call in the trajectory view when available. */
43
79
  inspect?: (() => void) | undefined;
44
80
  }
@@ -5,5 +5,5 @@ import type { ToolTreeProps } from '../contract/slots.ts';
5
5
  * @param props - whole-Tool owner data and the Tool-owned child-slot share.
6
6
  * @returns the Tool call tree.
7
7
  */
8
- export declare function ToolCallTree({ renderSlot, node, selectedCallId, cwd, openFile, inspectCall, useHostInfo, t, }: ToolTreeProps): import("react").JSX.Element;
8
+ export declare function ToolCallTree({ renderSlot, node, selectedCallId, cwd, openFile, inspectCall, loadImage, useHostInfo, t, }: ToolTreeProps): import("react").JSX.Element;
9
9
  //# sourceMappingURL=ToolCallTree.d.ts.map
@@ -1,7 +1,9 @@
1
1
  import { type ReactNode } from 'react';
2
- import type { TranslateNS } from '@prettier-ai/dsh-client-ui-slots';
2
+ import type { PropsRenderSlots, TranslateNS } from '@prettier-ai/dsh-client-ui-slots';
3
+ import type { MessageImageLoader } from '@prettier-ai/dsh-client-ui-conversation/client';
3
4
  import { type DiffCardModel } from '../models/diff-card-model.ts';
4
5
  import { type ReadCardModel } from '../models/read-card-model.ts';
6
+ import type { ImageCardModel } from '../models/image-card-model.ts';
5
7
  import { type SearchCardModel } from '../models/search-card-model.ts';
6
8
  import { type TerminalCardModel } from '../models/terminal-card-model.ts';
7
9
  import type { AskQuestionCardModel } from '../models/ask-question-card-model.ts';
@@ -35,6 +37,21 @@ export interface ToolRowProps {
35
37
  terminal?: TerminalCardModel | null | undefined;
36
38
  diff?: DiffCardModel | null | undefined;
37
39
  read?: ReadCardModel | null | undefined;
40
+ /**
41
+ * Image-card material for a call whose result is an image (derived by
42
+ * `imageCardModel`). Rendered through the `tool.call.images` slot, so the
43
+ * tool layer never imports an attachment implementation nor handles URL
44
+ * authorization.
45
+ */
46
+ image?: ImageCardModel | null | undefined;
47
+ /**
48
+ * Dispatch the image gallery through the tool-owned `tool.call.images`
49
+ * slot, supplied by the toolview that owns this row together with the
50
+ * session-authorized loader.
51
+ */
52
+ renderSlot?: PropsRenderSlots<'tool.call.images'>['renderSlot'] | undefined;
53
+ /** Session-authorized image URL loader for the gallery slot. */
54
+ loadImage?: MessageImageLoader | undefined;
38
55
  search?: SearchCardModel | null | undefined;
39
56
  web?: WebCardModelProps | null | undefined;
40
57
  state: ToolRowState;
@@ -51,5 +68,5 @@ export interface ToolRowProps {
51
68
  */
52
69
  inspect?: (() => void) | undefined;
53
70
  }
54
- export declare function ToolRow({ t, variant, toolName, icon, title, summary, summarySuffix, bodyRaw, output, askQuestion, errorSummary, terminal, diff, read, search, web, state, filePath, onOpenFile, inspect, }: ToolRowProps): import("react").JSX.Element;
71
+ export declare function ToolRow({ t, variant, toolName, icon, title, summary, summarySuffix, bodyRaw, output, askQuestion, errorSummary, terminal, diff, read, image, renderSlot, loadImage, search, web, state, filePath, onOpenFile, inspect, }: ToolRowProps): import("react").JSX.Element;
55
72
  //# sourceMappingURL=ToolRow.d.ts.map
@@ -0,0 +1,45 @@
1
+ /** Pure image-card derivation from raw result content and metadata. @module */
2
+ import type { ImageAttachmentRef } from '@prettier-ai/dsh-attachment';
3
+ import { type ToolCallBlock } from './tool-call-model.ts';
4
+ /**
5
+ * The image-card material one settled call contributes: the display label plus
6
+ * the durable references the attachment slot renders as a gallery.
7
+ *
8
+ * The bytes are not here. `attachmentId` is opaque and provider-owned, so a UI
9
+ * resolves it to a session-authorized URL at render time; this model never parses
10
+ * it nor derives a path from it.
11
+ */
12
+ export interface ImageCardModel {
13
+ /** Card label: the read path, shortened the way every other card's is. */
14
+ label: string;
15
+ /** The durable images this result returned, in result order. */
16
+ images: readonly {
17
+ readonly attachment: ImageAttachmentRef;
18
+ }[];
19
+ /**
20
+ * The model-facing envelope text, for the line under the gallery.
21
+ *
22
+ * Taken from the result's own text block rather than the row's flattened
23
+ * result text: an image read's content is `[text envelope, image block]`, and
24
+ * flattening JSON.stringifies the image block, which would print the raw
25
+ * attachment object under the picture — the symptom this card exists to remove.
26
+ */
27
+ text: string;
28
+ }
29
+ /**
30
+ * Derive a settled image card after validating the call head, persisted
31
+ * metadata (or its argument fallback), and the model-facing image envelope.
32
+ *
33
+ * The card is result-side only: a call carries no content until `execute`
34
+ * returns, so a running `read_image` has none and this returns null for it.
35
+ * Both root and nested calls settle as ToolResultNode; the nested one (a
36
+ * read_image dispatched from inside run_code) persists no presentationMeta, so
37
+ * its label falls back to the call's own `file_path` argument.
38
+ * @param block - running or settled Tool block.
39
+ * @param sessionCwd - the session workspace root; a workspace-rooted absolute
40
+ * path label displays relative to it. Absent leaves the path as authored.
41
+ * @param home - host account home; a leftover POSIX home path displays as `~`.
42
+ * @returns the image-card props, or null for the generic path.
43
+ */
44
+ export declare function imageCardModel(block: ToolCallBlock, sessionCwd?: string, home?: string): ImageCardModel | null;
45
+ //# sourceMappingURL=image-card-model.d.ts.map
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { PropsRenderSlots } from '@prettier-ai/dsh-client-ui-slots';
3
+ import type { ToolCallViewProps } from '../../contract/slots.ts';
4
+ import { type ToolRowProps } from '../components/ToolRow.tsx';
5
+ /** Full row props of a read-family toolview: the runtime share plus its locale seat. */
6
+ export type ReadFamilyRowProps = ToolCallViewProps & {
7
+ t: ToolRowProps['t'];
8
+ };
9
+ /** read_image row props: the runtime share, the declared image child slot, and the locale seat. */
10
+ export type ReadImageRowProps = ReadFamilyRowProps & PropsRenderSlots<'tool.call.images'>;
11
+ /**
12
+ * The card material one read-family row contributes: exactly the ToolRow card
13
+ * props that row owns. `read` supplies `read`; `read_image` supplies `image`
14
+ * together with the slot dispatcher and loader that draw it.
15
+ */
16
+ export type ReadFamilyCard = Pick<ToolRowProps, 'read' | 'image' | 'renderSlot' | 'loadImage'>;
17
+ /**
18
+ * Compose a read-family row: the shared chrome and model-derived fields, plus the
19
+ * caller's card material.
20
+ * @param props - the toolview runtime share and locale seat.
21
+ * @param card - the card props this row owns.
22
+ * @returns the assembled ToolRow.
23
+ */
24
+ export declare function readFamilyRow({ toolName, block, cwd, home, openFile, inspect, t }: ReadFamilyRowProps, card: ReadFamilyCard): ReactNode;
25
+ //# sourceMappingURL=read-family-row.d.ts.map
@@ -0,0 +1,24 @@
1
+ import type { Context } from '@prettier-ai/cordis';
2
+ import { type ReadImageRowProps } from './read-family-row.tsx';
3
+ /**
4
+ * read_image row: the read-family chrome with the durably committed image as the
5
+ * row's collapsed-by-default card body, rendered through the `tool.call.images`
6
+ * slot this entry declares.
7
+ */
8
+ export declare function ReadImageRow(props: ReadImageRowProps): import("react").ReactNode;
9
+ /**
10
+ * The read_image row as a plain registrant plugin following the atomic Tool-view
11
+ * declaration across independent activation and reload lifetimes. Declaring
12
+ * `tool.call.images` as a child slot authorizes this entry's `renderSlot` to
13
+ * dispatch the gallery.
14
+ */
15
+ export declare const readImageToolview: {
16
+ name: string;
17
+ inject: string[];
18
+ /**
19
+ * Register the read_image row into the Tool-owned keyed view slot.
20
+ * @param ctx - registrant context (disposal rides ctx.effect inside slots.register).
21
+ */
22
+ apply(ctx: Context): void;
23
+ };
24
+ //# sourceMappingURL=read-image-row.d.ts.map
@@ -5,7 +5,7 @@ type ReadRowProps = ToolCallViewProps & PropsLocale<'conversation'>;
5
5
  /**
6
6
  * Lets users expand a completed read result and open its reported path.
7
7
  */
8
- export declare function ReadRow({ toolName, block, cwd, home, openFile, inspect, t }: ReadRowProps): import("react").JSX.Element;
8
+ export declare function ReadRow(props: ReadRowProps): import("react").ReactNode;
9
9
  /** Registers the read tool's conversation row. */
10
10
  export declare const readToolview: {
11
11
  name: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@prettier-ai/dsh-client-ui-tool",
3
3
  "description": "Client Tool call-tree renderer and keyed per-tool presentation slot",
4
- "version": "0.1.2-alpha.5",
4
+ "version": "0.1.3-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -48,19 +48,20 @@
48
48
  "@types/react": "~18.3.1",
49
49
  "react": "^18.2.0",
50
50
  "react-dom": "^18.2.0",
51
+ "@prettier-ai/dsh-api-remotes": "^0.1.3-alpha.1",
52
+ "@prettier-ai/dsh-client-connection": "^0.1.3-alpha.1",
51
53
  "@prettier-ai/cordis": "^4.0.2",
52
- "@prettier-ai/dsh-api-remotes": "^0.1.2-alpha.5",
53
- "@prettier-ai/dsh-client-locale": "^0.1.2-alpha.5",
54
- "@prettier-ai/dsh-client-connection": "^0.1.2-alpha.5",
55
- "@prettier-ai/dsh-client-ui-conversation": "^0.1.2-alpha.5",
56
- "@prettier-ai/dsh-client-test-runtime": "^0.1.2-alpha.5",
57
- "@prettier-ai/dsh-client-ui-primitives": "^0.1.2-alpha.5",
58
- "@prettier-ai/dsh-client-ui-slots": "^0.1.2-alpha.5",
59
- "@prettier-ai/dsh-api-workspace-controller": "^0.1.2-alpha.5",
60
- "@prettier-ai/dsh-client-ui-renderer": "^0.1.2-alpha.5",
61
- "@prettier-ai/dsh-util-workspace-path": "^0.1.2-alpha.5",
62
- "@prettier-ai/dsh-client-ui-chat": "^0.1.2-alpha.5",
63
- "@prettier-ai/dsh-client-ui-session": "^0.1.2-alpha.5"
54
+ "@prettier-ai/dsh-client-locale": "^0.1.3-alpha.1",
55
+ "@prettier-ai/dsh-client-ui-conversation": "^0.1.3-alpha.1",
56
+ "@prettier-ai/dsh-client-test-runtime": "^0.1.3-alpha.1",
57
+ "@prettier-ai/dsh-client-ui-slots": "^0.1.3-alpha.1",
58
+ "@prettier-ai/dsh-client-ui-chat": "^0.1.3-alpha.1",
59
+ "@prettier-ai/dsh-api-workspace-controller": "^0.1.3-alpha.1",
60
+ "@prettier-ai/dsh-client-ui-renderer": "^0.1.3-alpha.1",
61
+ "@prettier-ai/dsh-client-ui-primitives": "^0.1.3-alpha.1",
62
+ "@prettier-ai/dsh-attachment": "^0.1.3-alpha.1",
63
+ "@prettier-ai/dsh-client-ui-session": "^0.1.3-alpha.1",
64
+ "@prettier-ai/dsh-util-workspace-path": "^0.1.3-alpha.1"
64
65
  },
65
66
  "files": [
66
67
  "lib/index.js",