@deepseek-ai/dsh-client-ui-tool 0.1.2-alpha.5 → 0.1.3-alpha.2

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: e0b49a3b582f5f1e6e9aa19e0b026dedb9ba2a18
6
+ README.zh.md: 032f048f2501923c4c9f010b76afe27d98fe7889
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. Running and settled foreground standard `bash`/`pwsh` and `terminal_send` calls use terminal cards at the root and in Code Dispatch children, subject to the same argument, result, and error checks. Persistent `bash`/`pwsh` calls use terminal cards only while running. Shell output ending in a recognized spill-policy notice uses expandable generic output in shell rows and generic output in Details; a displaced or omitted exit marker cannot establish success. Settled persistent-shell results stay generic because reset and partial-output diagnostics do not always describe one process exit status; root persistent results are expandable, while 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,10 @@ 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, and Session path facts. Diff, read, search, and web models also reject Code Dispatch children; terminal eligibility does not depend on `parentCallId`. Unsupported or malformed inputs use flattened Tool result text. Card-specific limits and fallback rules for the terminal, diff, read, search, and web cards remain in [the ui-primitives README](../ui-primitives/README.md); the image card's model in this package carries its own fallback rules.
66
+
67
+ The terminal model uses `hasSpillNotice` from the browser-safe `@deepseek-ai/dsh-spill-policy/notice` entry, not an independent UI pattern. The [spill-policy README](../../spill/spill-policy/README.md#shared-notice-ownership) owns notice formatting and recognition. This check conservatively selects generic output; matching text cannot authenticate its source, and replay leaves recorded result bytes untouched.
66
68
  </details>
67
69
 
68
70
  -----
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。运行中与已完成的前台标准 `bash`/`pwsh` `terminal_send` 调用,无论位于根还是 Code Dispatch 子调用中,都在通过相同的参数、结果和错误检查后使用 terminal 卡片。持久 `bash`/`pwsh` 调用仅在运行中使用 terminal 卡片。以已识别的 spill 策略提示结尾的 shell 输出,在 shell 行中使用可展开的 generic 输出,在 Details 中使用 generic 输出;位置被改变或被省略的退出标记无法证明成功。已完成的持久 shell 结果保持 generic 展示,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;根调用的持久 shell 结果可展开,后台启动回执则保持折叠。成功的问题行按稳定 id 配对调用中的问题与结果中的回答,展开后显示可读的问答行。已取消或已中断的问题行显示其裁决与原始问题,不虚构回答。不受支持、格式错误或含糊的输入回退为压平的工具输入/结果文本。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。
47
47
 
48
48
  -----
49
49
 
@@ -61,8 +61,10 @@ 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 与 Session 路径事实。diff、read、search 和 web model 还会拒绝 Code Dispatch 子调用;terminal 适用性不依赖 `parentCallId`。不受支持或格式错误的输入使用压平的工具结果文本。terminal、diff、read、search 与 web 卡片的上限与 fallback 规则仍由 [ui-primitives README](../ui-primitives/README.zh.md) 负责;image 卡片的 fallback 规则由本包内的 card model 自行承载。
66
+
67
+ terminal model 使用浏览器安全入口 `@deepseek-ai/dsh-spill-policy/notice` 的 `hasSpillNotice`,而非独立的 UI 匹配规则。[spill-policy README](../../spill/spill-policy/README.zh.md#shared-notice-ownership) 负责通知的格式化与识别。该检查保守地选择通用输出;匹配文本不能认证其来源,回放也不改变已记录的结果字节。
66
68
  </details>
67
69
 
68
70
  -----
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.
@@ -555,6 +558,65 @@ window.__ModuleLoader__.load({
555
558
  }
556
559
  };
557
560
  }
561
+ new TextEncoder();
562
+ new TextDecoder();
563
+ /**
564
+ * Standardized, false-precision-safe wording for one {@link Omitted} value —
565
+ * the "may standardize omission wording" half the library owns. `exact` prints
566
+ * the count (`Omitted 3 items`); `unknown` prints NO count because the caller
567
+ * did not provide one. `none` is the empty string.
568
+ *
569
+ * @param omitted The omission metadata from a retainer result.
570
+ * @param unit The noun for the omitted quantity (`items`, `bytes`, `chars`, `lines`).
571
+ * @returns A neutral clause (no trailing space), or `''` when nothing was omitted.
572
+ */
573
+ function describeOmitted(omitted, unit) {
574
+ switch (omitted.kind) {
575
+ case "none": return "";
576
+ case "exact": return `Omitted ${omitted.count} ${unit}.`;
577
+ case "unknown": return `More ${unit} were omitted.`;
578
+ }
579
+ }
580
+ //#endregion
581
+ //#region ../../spill/spill-policy/src/notice.ts
582
+ /** Browser-safe formatting and recognition of persisted spill-policy notices. */
583
+ const OPEN = "(";
584
+ const CLOSE = ")";
585
+ const LOCATION = " Full formatted result stored at: ";
586
+ const GUIDANCE_SEPARATOR = ". ";
587
+ const SEPARATOR = "\n\n";
588
+ const EXACT_OMISSION = describeOmitted({
589
+ kind: "exact",
590
+ count: 0
591
+ }, "bytes");
592
+ const COUNT_OFFSET = EXACT_OMISSION.indexOf("0");
593
+ const COUNT_SUFFIX = EXACT_OMISSION.slice(COUNT_OFFSET + 1);
594
+ function isOmission(text) {
595
+ if (text === describeOmitted({ kind: "none" }, "bytes") || text === describeOmitted({ kind: "unknown" }, "bytes")) return true;
596
+ const count = Number(text.slice(COUNT_OFFSET, text.length - COUNT_SUFFIX.length));
597
+ return Number.isSafeInteger(count) && count >= 0 && text === describeOmitted({
598
+ kind: "exact",
599
+ count
600
+ }, "bytes");
601
+ }
602
+ /**
603
+ * Recognize a final spill-policy notice in persisted text, including notice-only output.
604
+ * This identifies the text convention, not authenticated provenance of tool output.
605
+ * @param text - complete recorded text result.
606
+ * @returns whether a complete notice occupies the end of the result.
607
+ */
608
+ function hasSpillNotice(text) {
609
+ if (!text.endsWith(CLOSE)) return false;
610
+ let start = 0;
611
+ while (true) {
612
+ const next = text.indexOf(`${SEPARATOR}${OPEN}`, start);
613
+ const candidate = text.slice(start, next < 0 ? -1 : next);
614
+ const location = candidate.indexOf(LOCATION, 1);
615
+ if (candidate.startsWith(OPEN) && location >= 0 && isOmission(candidate.slice(1, location))) return text.indexOf(GUIDANCE_SEPARATOR, start + location + 34) >= 0;
616
+ if (next < 0) return false;
617
+ start = next + 2;
618
+ }
619
+ }
558
620
  //#endregion
559
621
  //#region lib/types/client/tool/models/terminal-card-model.js
560
622
  /**
@@ -720,6 +782,20 @@ window.__ModuleLoader__.load({
720
782
  if (parsed === null) return false;
721
783
  return shellCall(parsed.name, parsed.args)?.persistent === true;
722
784
  }
785
+ /**
786
+ * Identify a settled foreground shell preview whose spill footer can hide the exit marker.
787
+ * @param block - running or settled Tool block.
788
+ * @returns whether the shell output must remain generic without an inferred exit status.
789
+ */
790
+ function isSpilledShellCall(block) {
791
+ if (!("kind" in block)) return false;
792
+ const parsed = parsedToolCall(block);
793
+ if (parsed === null) return false;
794
+ const call = shellCall(parsed.name, parsed.args);
795
+ if (call === null || call.background) return false;
796
+ const output = singleResultText(block);
797
+ return output !== void 0 && hasSpillNotice(output);
798
+ }
723
799
  function terminalSendCall(name, args) {
724
800
  if (name !== "terminal_send") return null;
725
801
  const { sessionId, text, submit, run_in_background: background } = args;
@@ -756,17 +832,16 @@ window.__ModuleLoader__.load({
756
832
  };
757
833
  }
758
834
  /**
759
- * Derive terminal props for supported root shell and terminal-send calls.
760
- * Standard shell results parse their final status marker; persistent shell
761
- * results, background calls, errors, malformed input, or child dispatches use
762
- * the generic path. {@link isSettledPersistentShellCall} lets that generic
835
+ * Derive terminal props for supported shell and terminal-send calls, including
836
+ * nested Code Dispatch calls. Standard shell results parse their final status
837
+ * marker; persistent shell results, spill previews, background calls, errors,
838
+ * and malformed input use the generic path. {@link isSettledPersistentShellCall} lets that generic
763
839
  * persistent result remain expandable without inventing one process status.
764
840
  * @param block - running or settled Tool block.
765
841
  * @param sessionCwd - session workspace root used to resolve workdir.
766
842
  * @returns locale-neutral terminal-card data, or null for the generic path.
767
843
  */
768
844
  function terminalCardModel(block, sessionCwd) {
769
- if (block.parentCallId !== void 0) return null;
770
845
  const parsed = parsedToolCall(block);
771
846
  if (parsed === null) return null;
772
847
  const call = shellCall(parsed.name, parsed.args) ?? terminalSendCall(parsed.name, parsed.args);
@@ -791,7 +866,7 @@ window.__ModuleLoader__.load({
791
866
  running: true
792
867
  }
793
868
  };
794
- if (block.isError || call.kind === "shell" && call.persistent) return null;
869
+ if (block.isError || call.kind === "shell" && call.persistent || isSpilledShellCall(block)) return null;
795
870
  const output = singleResultText(block);
796
871
  if (output === void 0) return null;
797
872
  const status = call.kind === "terminal-send" ? { output } : parseExitStatus(output);
@@ -1042,7 +1117,7 @@ window.__ModuleLoader__.load({
1042
1117
  }
1043
1118
  //#endregion
1044
1119
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css.mjs
1045
- const css$3 = ".o3BgMG_root{flex-direction:column;display:flex}.o3BgMG_row{position:relative;overflow:hidden}.o3BgMG_root[data-state=running] .o3BgMG_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 o3BgMG_dsh-tool-row-sweep;position:absolute;top:0;bottom:0;left:0}@keyframes o3BgMG_dsh-tool-row-sweep{0%{left:-300px}90%,to{left:100%}}.o3BgMG_leading{flex-shrink:0}.o3BgMG_root[data-tool^=cordis_] .o3BgMG_leading,.o3BgMG_root[data-tool^=cordis_] .o3BgMG_title{color:var(--dsw-alias-state-business-primary)}.o3BgMG_root[data-tool^=cordis_] .o3BgMG_title{font-weight:500}.o3BgMG_root[data-tool^=cordis_] .o3BgMG_sep{background:var(--dsw-alias-state-business-primary)}.o3BgMG_chevron{color:var(--dsw-alias-label-secondary)}.o3BgMG_title{font-weight:400}.o3BgMG_sep{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.o3BgMG_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}.o3BgMG_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}.o3BgMG_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)}.o3BgMG_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}.o3BgMG_fileLink:hover{color:var(--dsw-alias-label-primary);text-decoration-color:currentColor}.o3BgMG_errorSummary{color:var(--dsw-alias-state-error-primary)}.o3BgMG_bodyWrap{flex-direction:column;display:flex}.o3BgMG_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}.o3BgMG_root:hover .o3BgMG_inspectButton,.o3BgMG_inspectButton:focus-visible{opacity:1}.o3BgMG_inspectButton:hover{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-label-primary)}.o3BgMG_bodyScroll{max-height:260px;overflow-y:auto}.o3BgMG_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}.o3BgMG_ioSection{grid-template-columns:max-content 1fr;align-items:baseline;column-gap:14px;max-height:150px;padding:12px 16px;display:grid;overflow-y:auto}.o3BgMG_ioSection::-webkit-scrollbar-thumb{background-clip:padding-box;border:2px solid #0000;border-radius:6px}.o3BgMG_ioSection::-webkit-scrollbar-track{margin:6px 0}.o3BgMG_ioLabel{color:var(--dsw-alias-label-caption);align-self:start;position:sticky;top:0}.o3BgMG_ioDivider{background:var(--dsw-alias-border-l2);flex:none;height:.5px}.o3BgMG_ioText{white-space:pre-wrap;word-break:break-word;min-width:0;color:var(--dsw-alias-label-secondary)}.o3BgMG_ioText[data-error]{color:var(--dsw-alias-state-error-primary)}.o3BgMG_codeBody,.o3BgMG_terminalBody,.o3BgMG_diffBody,.o3BgMG_readBody,.o3BgMG_searchBody,.o3BgMG_webBody{margin:4px 0 4px 4px}.o3BgMG_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}.o3BgMG_codeBody{--dsl-code-block-content-font:var(--dsw-font-markdown-code-block-small)}.o3BgMG_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)}.o3BgMG_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}";
1120
+ const css$3 = ".o3BgMG_root{flex-direction:column;display:flex}.o3BgMG_row{position:relative;overflow:hidden}.o3BgMG_root[data-state=running] .o3BgMG_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 o3BgMG_dsh-tool-row-sweep;position:absolute;top:0;bottom:0;left:0}@keyframes o3BgMG_dsh-tool-row-sweep{0%{left:-300px}90%,to{left:100%}}.o3BgMG_leading{flex-shrink:0}.o3BgMG_root[data-tool^=cordis_] .o3BgMG_leading,.o3BgMG_root[data-tool^=cordis_] .o3BgMG_title{color:var(--dsw-alias-state-business-primary)}.o3BgMG_root[data-tool^=cordis_] .o3BgMG_title{font-weight:500}.o3BgMG_root[data-tool^=cordis_] .o3BgMG_sep{background:var(--dsw-alias-state-business-primary)}.o3BgMG_chevron{color:var(--dsw-alias-label-secondary)}.o3BgMG_title{font-weight:400}.o3BgMG_sep{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.o3BgMG_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}.o3BgMG_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}.o3BgMG_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)}.o3BgMG_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}.o3BgMG_fileLink:hover{color:var(--dsw-alias-label-primary);text-decoration-color:currentColor}.o3BgMG_errorSummary{color:var(--dsw-alias-state-error-primary)}.o3BgMG_bodyWrap{flex-direction:column;display:flex}.o3BgMG_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}.o3BgMG_root:hover .o3BgMG_inspectButton,.o3BgMG_inspectButton:focus-visible{opacity:1}.o3BgMG_inspectButton:hover{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-label-primary)}.o3BgMG_bodyScroll{max-height:260px;overflow-y:auto}.o3BgMG_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}.o3BgMG_ioSection{grid-template-columns:max-content 1fr;align-items:baseline;column-gap:14px;max-height:150px;padding:12px 16px;display:grid;overflow-y:auto}.o3BgMG_ioSection::-webkit-scrollbar-thumb{background-clip:padding-box;border:2px solid #0000;border-radius:6px}.o3BgMG_ioSection::-webkit-scrollbar-track{margin:6px 0}.o3BgMG_ioLabel{color:var(--dsw-alias-label-caption);align-self:start;position:sticky;top:0}.o3BgMG_ioDivider{background:var(--dsw-alias-border-l2);flex:none;height:.5px}.o3BgMG_ioText{white-space:pre-wrap;word-break:break-word;min-width:0;color:var(--dsw-alias-label-secondary)}.o3BgMG_ioText[data-error]{color:var(--dsw-alias-state-error-primary)}.o3BgMG_codeBody,.o3BgMG_terminalBody,.o3BgMG_diffBody,.o3BgMG_readBody,.o3BgMG_imageBody,.o3BgMG_searchBody,.o3BgMG_webBody{margin:4px 0 4px 4px}.o3BgMG_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}.o3BgMG_imageLabel{overflow-wrap:anywhere;font:var(--dsw-font-sm-13);color:var(--dsw-alias-label-secondary);margin-bottom:4px}.o3BgMG_imageMeta{white-space:pre-wrap;overflow-wrap:anywhere;font:var(--dsw-font-xs-13);color:var(--dsw-alias-label-tertiary)}.o3BgMG_codeBody{--dsl-code-block-content-font:var(--dsw-font-markdown-code-block-small)}.o3BgMG_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)}.o3BgMG_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}";
1046
1121
  const tagId$3 = "@deepseek-ai/dsh-client-ui-tool/ToolRow.module.css";
1047
1122
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
1048
1123
  const tag = document.createElement("style");
@@ -1061,6 +1136,9 @@ window.__ModuleLoader__.load({
1061
1136
  "dsh-tool-row-sweep": "o3BgMG_dsh-tool-row-sweep",
1062
1137
  "errorSummary": "o3BgMG_errorSummary",
1063
1138
  "fileLink": "o3BgMG_fileLink",
1139
+ "imageBody": "o3BgMG_imageBody",
1140
+ "imageLabel": "o3BgMG_imageLabel",
1141
+ "imageMeta": "o3BgMG_imageMeta",
1064
1142
  "inspectButton": "o3BgMG_inspectButton",
1065
1143
  "ioCard": "o3BgMG_ioCard",
1066
1144
  "ioDivider": "o3BgMG_ioDivider",
@@ -1102,7 +1180,7 @@ window.__ModuleLoader__.load({
1102
1180
  default: return null;
1103
1181
  }
1104
1182
  }
1105
- function ToolRow({ t, variant, toolName, icon, title, summary, summarySuffix, bodyRaw, output, askQuestion, errorSummary, terminal, diff, read, search, web, state, filePath, onOpenFile, inspect }) {
1183
+ 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
1184
  const [expanded, setExpanded] = (0, react.useState)(false);
1107
1185
  const terminalLabels = (0, react.useMemo)(() => terminalBlockLabels(t), [t]);
1108
1186
  const diffLabels = (0, react.useMemo)(() => diffBlockLabels(t), [t]);
@@ -1112,11 +1190,12 @@ window.__ModuleLoader__.load({
1112
1190
  const terminalBody = terminal === void 0 || terminal === null ? null : localizeTerminalCardModel(terminal, t);
1113
1191
  const diffBody = diff ?? null;
1114
1192
  const readBody = read ?? null;
1193
+ const imageBody = image !== void 0 && image !== null && renderSlot !== void 0 && loadImage !== void 0 ? image : null;
1115
1194
  const searchBody = search ?? null;
1116
1195
  const webBody = web ?? null;
1117
1196
  const askQuestionBody = askQuestion ?? null;
1118
1197
  const outputText = output ?? null;
1119
- const card = askQuestionBody ?? terminalBody ?? diffBody ?? readBody ?? searchBody ?? webBody;
1198
+ const card = askQuestionBody ?? terminalBody ?? diffBody ?? readBody ?? imageBody ?? searchBody ?? webBody;
1120
1199
  const expandable = bodyRaw != null || outputText !== null || card !== null;
1121
1200
  const open = expanded && expandable;
1122
1201
  const bodyText = (0, react.useMemo)(() => open && card === null && bodyRaw != null ? formatToolBody(variant, bodyRaw) : null, [
@@ -1203,6 +1282,23 @@ window.__ModuleLoader__.load({
1203
1282
  labels: readLabels,
1204
1283
  maxLines: 8,
1205
1284
  className: ToolRow_module_css_default.readBody
1285
+ }) : imageBody !== null ? (0, react_jsx_runtime.jsxs)("div", {
1286
+ className: ToolRow_module_css_default.imageBody,
1287
+ children: [
1288
+ (0, react_jsx_runtime.jsx)("div", {
1289
+ className: ToolRow_module_css_default.imageLabel,
1290
+ children: imageBody.label
1291
+ }),
1292
+ renderSlot !== void 0 && loadImage !== void 0 && renderSlot("tool.call.images", {
1293
+ images: imageBody.images,
1294
+ loadImage,
1295
+ align: "start"
1296
+ }),
1297
+ (0, react_jsx_runtime.jsx)("div", {
1298
+ className: ToolRow_module_css_default.imageMeta,
1299
+ children: imageBody.text
1300
+ })
1301
+ ]
1206
1302
  }) : searchBody !== null ? (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SearchBlock, {
1207
1303
  ...searchBody.card,
1208
1304
  labels: searchLabels,
@@ -1328,7 +1424,7 @@ window.__ModuleLoader__.load({
1328
1424
  return "kind" in node ? node.call?.name ?? "" : node.name;
1329
1425
  }
1330
1426
  /** 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 }) {
1427
+ const ToolCall = (0, react.memo)(function ToolCall({ renderSlot, callId, toolName, block, openFile, selected, cwd, home, inspectCall, loadImage, t, children }) {
1332
1428
  const owner = (0, react.useMemo)(() => ({
1333
1429
  callId,
1334
1430
  toolName,
@@ -1336,6 +1432,7 @@ window.__ModuleLoader__.load({
1336
1432
  openFile,
1337
1433
  cwd,
1338
1434
  home,
1435
+ loadImage,
1339
1436
  inspect: () => {
1340
1437
  inspectCall(callId);
1341
1438
  }
@@ -1346,6 +1443,7 @@ window.__ModuleLoader__.load({
1346
1443
  openFile,
1347
1444
  cwd,
1348
1445
  home,
1446
+ loadImage,
1349
1447
  inspectCall
1350
1448
  ]);
1351
1449
  return (0, react_jsx_runtime.jsxs)("div", {
@@ -1362,7 +1460,7 @@ window.__ModuleLoader__.load({
1362
1460
  }), children]
1363
1461
  });
1364
1462
  });
1365
- const ToolCallBranch = (0, react.memo)(function ToolCallBranch({ renderSlot, block, selectedCallId, cwd, home, openFile, inspectCall, t }) {
1463
+ const ToolCallBranch = (0, react.memo)(function ToolCallBranch({ renderSlot, block, selectedCallId, cwd, home, openFile, inspectCall, loadImage, t }) {
1366
1464
  return (0, react_jsx_runtime.jsx)(ToolCall, {
1367
1465
  renderSlot,
1368
1466
  callId: block.callId,
@@ -1373,6 +1471,7 @@ window.__ModuleLoader__.load({
1373
1471
  cwd,
1374
1472
  home,
1375
1473
  inspectCall,
1474
+ loadImage,
1376
1475
  t,
1377
1476
  children: block.subCalls.length > 0 ? (0, react_jsx_runtime.jsx)("div", {
1378
1477
  className: ToolCallTree_module_css_default.subCalls,
@@ -1385,6 +1484,7 @@ window.__ModuleLoader__.load({
1385
1484
  home,
1386
1485
  openFile,
1387
1486
  inspectCall,
1487
+ loadImage,
1388
1488
  t
1389
1489
  }, child.callId))
1390
1490
  }) : null
@@ -1396,7 +1496,7 @@ window.__ModuleLoader__.load({
1396
1496
  * @param props - whole-Tool owner data and the Tool-owned child-slot share.
1397
1497
  * @returns the Tool call tree.
1398
1498
  */
1399
- function ToolCallTree({ renderSlot, node, selectedCallId, cwd, openFile, inspectCall, useHostInfo, t }) {
1499
+ function ToolCallTree({ renderSlot, node, selectedCallId, cwd, openFile, inspectCall, loadImage, useHostInfo, t }) {
1400
1500
  const home = useHostInfo((info) => info.home);
1401
1501
  const block = node.data.root;
1402
1502
  return (0, react_jsx_runtime.jsx)(ToolCallBranch, {
@@ -1407,6 +1507,7 @@ window.__ModuleLoader__.load({
1407
1507
  home,
1408
1508
  openFile,
1409
1509
  inspectCall,
1510
+ loadImage,
1410
1511
  t
1411
1512
  });
1412
1513
  }
@@ -1435,7 +1536,7 @@ window.__ModuleLoader__.load({
1435
1536
  /** Card-aware output body for the selected Tool call in details. */
1436
1537
  /**
1437
1538
  * Render the selected Tool call's structured output when its raw fields form a
1438
- * supported root card, otherwise preserve the flattened result text.
1539
+ * supported card, otherwise preserve the flattened result text.
1439
1540
  * @param props - selected call slice, workspace root, host home, and locale seat.
1440
1541
  * @returns the details output body.
1441
1542
  */
@@ -1714,7 +1815,7 @@ window.__ModuleLoader__.load({
1714
1815
  const state = model.state === "ok" && terminalModel !== null && terminalFailed(terminalModel) ? "error" : model.state;
1715
1816
  const status = stateStatus(state, t);
1716
1817
  const [expanded, setExpanded] = (0, react.useState)(false);
1717
- const genericBody = terminal === null && (model.state === "error" || isSettledPersistentShellCall(block)) && (model.bodyRaw !== null || model.output !== null);
1818
+ const genericBody = terminal === null && (model.state === "error" || isSettledPersistentShellCall(block) || isSpilledShellCall(block)) && (model.bodyRaw !== null || model.output !== null);
1718
1819
  const expandable = terminal !== null || genericBody;
1719
1820
  const open = expanded && expandable;
1720
1821
  const body = (0, react.useMemo)(() => open && genericBody && model.bodyRaw !== null ? formatToolBody(model.variant, model.bodyRaw) : null, [
@@ -1872,13 +1973,16 @@ window.__ModuleLoader__.load({
1872
1973
  }
1873
1974
  };
1874
1975
  //#endregion
1875
- //#region lib/types/client/tool/toolviews/read-row.js
1976
+ //#region lib/types/client/tool/toolviews/read-family-row.js
1876
1977
  /**
1877
- * Lets users expand a completed read result and open its reported path.
1978
+ * Compose a read-family row: the shared chrome and model-derived fields, plus the
1979
+ * caller's card material.
1980
+ * @param props - the toolview runtime share and locale seat.
1981
+ * @param card - the card props this row owns.
1982
+ * @returns the assembled ToolRow.
1878
1983
  */
1879
- function ReadRow({ toolName, block, cwd, home, openFile, inspect, t }) {
1984
+ function readFamilyRow({ toolName, block, cwd, home, openFile, inspect, t }, card) {
1880
1985
  const model = toolRowModel(toolName, block, cwd, home);
1881
- const read = readCardModel(block, cwd, home);
1882
1986
  return (0, react_jsx_runtime.jsx)(ToolRow, {
1883
1987
  t,
1884
1988
  variant: model.variant,
@@ -1886,15 +1990,25 @@ window.__ModuleLoader__.load({
1886
1990
  icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBrowseOutline16, { size: 14 }),
1887
1991
  title: t(model.titleKey),
1888
1992
  summary: model.summary,
1993
+ bodyRaw: null,
1889
1994
  output: model.output,
1890
1995
  errorSummary: model.errorSummary,
1891
- read,
1996
+ ...card,
1892
1997
  state: model.state,
1893
1998
  filePath: model.filePath,
1894
1999
  onOpenFile: openFile,
1895
2000
  inspect
1896
2001
  });
1897
2002
  }
2003
+ //#endregion
2004
+ //#region lib/types/client/tool/toolviews/read-row.js
2005
+ /**
2006
+ * Lets users expand a completed read result and open its reported path.
2007
+ */
2008
+ function ReadRow(props) {
2009
+ const { block, cwd, home } = props;
2010
+ return readFamilyRow(props, { read: readCardModel(block, cwd, home) });
2011
+ }
1898
2012
  /** Registers the read tool's conversation row. */
1899
2013
  const readToolview = {
1900
2014
  name: "read-toolview",
@@ -1908,6 +2022,214 @@ window.__ModuleLoader__.load({
1908
2022
  }
1909
2023
  };
1910
2024
  //#endregion
2025
+ //#region lib/types/client/tool/models/image-card-model.js
2026
+ /**
2027
+ * Whether a wire value is a usable pixel or byte measure.
2028
+ * @param value - unvalidated wire value.
2029
+ * @returns true when it is a positive integer.
2030
+ */
2031
+ function positiveInteger(value) {
2032
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
2033
+ }
2034
+ /** The envelope `formatImageReadOutput` writes, matched by shape. */
2035
+ const IMAGE_ENVELOPE = /^<path>[^\n]*<\/path>\n<type>image<\/type>\n<content>\n[\s\S]*\n<\/content>$/u;
2036
+ /** The media types a durable image block may claim; anything else declines.
2037
+ * Hand-written mirror of `ImageMediaType` from dsh-attachment — the wire
2038
+ * boundary needs a runtime check and feature plugins must not import values
2039
+ * from each other; a new member added there must be added here too, or the
2040
+ * decline point below silently degrades that image to the generic card. */
2041
+ const IMAGE_MEDIA_TYPES = new Set([
2042
+ "image/png",
2043
+ "image/jpeg",
2044
+ "image/webp",
2045
+ "image/gif"
2046
+ ]);
2047
+ /** Runtime membership check; the cast is safe because the set holds exactly the four members. */
2048
+ function isImageMediaType(value) {
2049
+ return IMAGE_MEDIA_TYPES.has(value);
2050
+ }
2051
+ /**
2052
+ * Narrow the persisted metadata, defensively. Every field arrives unvalidated on
2053
+ * replay (an obsolete or hand-edited log reaches here), so any mismatch declines
2054
+ * to the generic card rather than throwing. An absent `meta` (a nested call
2055
+ * persists none) leaves the path to the call's own `file_path` argument.
2056
+ *
2057
+ * The attachment id is checked for existence only: it is opaque and
2058
+ * provider-owned, and consumers must not parse that representation, so
2059
+ * pattern-matching the local content-address form would reject a legitimate id
2060
+ * minted by an alternative store.
2061
+ * @param meta - persisted presentation metadata of unknown shape.
2062
+ * @returns the narrowed path, or null when it does not match.
2063
+ */
2064
+ function imageMeta(meta) {
2065
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return null;
2066
+ const { path } = meta;
2067
+ if (typeof path !== "string" || path === "") return null;
2068
+ return { path };
2069
+ }
2070
+ /**
2071
+ * Narrow every attachment reference carried by the result's image blocks, in
2072
+ * order.
2073
+ *
2074
+ * The content is the single source of truth for the references: it is what the
2075
+ * tool actually returned and what a post-execute hook would replace together
2076
+ * with the rest of the content. Every field arrives unvalidated over the wire,
2077
+ * so any malformed image block declines to the generic card rather than
2078
+ * rendering a partial gallery.
2079
+ *
2080
+ * The attachment id is checked for existence only — it is opaque and
2081
+ * provider-owned, so pattern-matching the local content-address form would reject
2082
+ * a legitimate id minted by an alternative store.
2083
+ * @param content - the settled result's content blocks.
2084
+ * @returns the narrowed references, or null when no valid image block is present.
2085
+ */
2086
+ function imageReferences(content) {
2087
+ const refs = [];
2088
+ for (const part of content) {
2089
+ if (typeof part !== "object" || part === null) continue;
2090
+ const { type, attachment } = part;
2091
+ if (type !== "image") continue;
2092
+ if (typeof attachment !== "object" || attachment === null || Array.isArray(attachment)) return null;
2093
+ const { attachmentId, mediaType, bytes, width, height, name, originalDimensions } = attachment;
2094
+ if (typeof attachmentId !== "string" || attachmentId === "") return null;
2095
+ if (typeof mediaType !== "string" || !isImageMediaType(mediaType)) return null;
2096
+ if (!positiveInteger(bytes) || !positiveInteger(width) || !positiveInteger(height)) return null;
2097
+ if (name !== void 0 && typeof name !== "string") return null;
2098
+ let inputDimensions;
2099
+ if (originalDimensions !== void 0) {
2100
+ if (typeof originalDimensions !== "object" || originalDimensions === null || Array.isArray(originalDimensions)) return null;
2101
+ const { width: inputWidth, height: inputHeight } = originalDimensions;
2102
+ if (!positiveInteger(inputWidth) || !positiveInteger(inputHeight)) return null;
2103
+ inputDimensions = {
2104
+ width: inputWidth,
2105
+ height: inputHeight
2106
+ };
2107
+ }
2108
+ refs.push({
2109
+ attachmentId,
2110
+ mediaType,
2111
+ bytes,
2112
+ width,
2113
+ height,
2114
+ ...name === void 0 ? {} : { name },
2115
+ ...inputDimensions === void 0 ? {} : { originalDimensions: inputDimensions }
2116
+ });
2117
+ }
2118
+ return refs.length > 0 ? refs : null;
2119
+ }
2120
+ /**
2121
+ * Read the text of every text block of a settled image result, joined in order.
2122
+ *
2123
+ * The envelope is one of them; a post-execute hook that appends further text
2124
+ * blocks keeps them visible under the gallery instead of being dropped. The
2125
+ * envelope shape is still the recognition gate: a result without it is not a
2126
+ * well-formed image read and declines.
2127
+ * @param content - the settled result's content blocks.
2128
+ * @returns the joined text, or null when no envelope-shaped block is present.
2129
+ */
2130
+ function imageTexts(content) {
2131
+ const parts = [];
2132
+ let sawEnvelope = false;
2133
+ for (const part of content) {
2134
+ if (part.type !== "text" || typeof part.text !== "string") continue;
2135
+ if (IMAGE_ENVELOPE.test(part.text)) sawEnvelope = true;
2136
+ parts.push(part.text);
2137
+ }
2138
+ return sawEnvelope && parts.length > 0 ? parts.join("\n") : null;
2139
+ }
2140
+ /**
2141
+ * Whether the content carries only blocks the card consumes.
2142
+ *
2143
+ * `ContentBlock` is a merge-extensible union, so a post-execute hook could
2144
+ * append a block of a type this card does not render (reasoning, an extension
2145
+ * type, or a non-object). Rendering the card anyway would silently hide that
2146
+ * block, so anything beyond a well-formed text or image object declines to the
2147
+ * generic card, which shows the flattened content.
2148
+ * @param content - the settled result's content blocks.
2149
+ * @returns true when every block is a text or image object with usable fields.
2150
+ */
2151
+ function fullyRendered(content) {
2152
+ return content.every((part) => {
2153
+ if (typeof part !== "object" || part === null) return false;
2154
+ const { type, text } = part;
2155
+ return type === "image" || type === "text" && typeof text === "string";
2156
+ });
2157
+ }
2158
+ /**
2159
+ * Derive a settled image card after validating the call head, persisted
2160
+ * metadata (or its argument fallback), and the model-facing image envelope.
2161
+ *
2162
+ * The card is result-side only: a call carries no content until `execute`
2163
+ * returns, so a running `read_image` has none and this returns null for it.
2164
+ * Both root and nested calls settle as ToolResultNode; the nested one (a
2165
+ * read_image dispatched from inside run_code) persists no presentationMeta, so
2166
+ * its label falls back to the call's own `file_path` argument.
2167
+ * @param block - running or settled Tool block.
2168
+ * @param sessionCwd - the session workspace root; a workspace-rooted absolute
2169
+ * path label displays relative to it. Absent leaves the path as authored.
2170
+ * @param home - host account home; a leftover POSIX home path displays as `~`.
2171
+ * @returns the image-card props, or null for the generic path.
2172
+ */
2173
+ function imageCardModel(block, sessionCwd, home) {
2174
+ if (!("kind" in block) || block.isError) return null;
2175
+ const call = parsedToolCall(block);
2176
+ if (call?.name !== "read_image") return null;
2177
+ const { file_path: filePath } = call.args;
2178
+ if (typeof filePath !== "string" || filePath.trim() === "") return null;
2179
+ const path = imageMeta(block.meta)?.path ?? (block.parentCallId !== void 0 ? filePath : null);
2180
+ if (path === null) return null;
2181
+ if (!fullyRendered(block.content)) return null;
2182
+ const refs = imageReferences(block.content);
2183
+ if (refs === null) return null;
2184
+ const text = imageTexts(block.content);
2185
+ if (text === null) return null;
2186
+ return {
2187
+ label: abbreviateHomePath(relativizeToCwd(path, sessionCwd), home),
2188
+ images: refs.map((ref) => ({ attachment: ref })),
2189
+ text
2190
+ };
2191
+ }
2192
+ //#endregion
2193
+ //#region lib/types/client/tool/toolviews/read-image-row.js
2194
+ /**
2195
+ * read_image row: the read-family chrome with the durably committed image as the
2196
+ * row's collapsed-by-default card body, rendered through the `tool.call.images`
2197
+ * slot this entry declares.
2198
+ */
2199
+ function ReadImageRow(props) {
2200
+ const { block, cwd, home, renderSlot, loadImage } = props;
2201
+ return readFamilyRow(props, {
2202
+ image: imageCardModel(block, cwd, home),
2203
+ renderSlot,
2204
+ loadImage
2205
+ });
2206
+ }
2207
+ /**
2208
+ * The read_image row as a plain registrant plugin following the atomic Tool-view
2209
+ * declaration across independent activation and reload lifetimes. Declaring
2210
+ * `tool.call.images` as a child slot authorizes this entry's `renderSlot` to
2211
+ * dispatch the gallery.
2212
+ */
2213
+ const readImageToolview = {
2214
+ name: "read-image-toolview",
2215
+ inject: ["slots"],
2216
+ /**
2217
+ * Register the read_image row into the Tool-owned keyed view slot.
2218
+ * @param ctx - registrant context (disposal rides ctx.effect inside slots.register).
2219
+ */
2220
+ apply(ctx) {
2221
+ ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
2222
+ name: "tool.call.toolview",
2223
+ key: "read_image",
2224
+ locale: CONVERSATION_NS,
2225
+ children: { "tool.call.images": {
2226
+ kind: "single",
2227
+ scope: "session"
2228
+ } }
2229
+ }, ReadImageRow));
2230
+ }
2231
+ };
2232
+ //#endregion
1911
2233
  //#region lib/types/client/tool/toolviews/search-row.js
1912
2234
  const SEARCH_TITLE_KEYS = {
1913
2235
  grep: "tool.title.grep",
@@ -2120,6 +2442,7 @@ window.__ModuleLoader__.load({
2120
2442
  }, ToolDetails));
2121
2443
  ctx.plugin(bashToolviewSample);
2122
2444
  ctx.plugin(readToolview);
2445
+ ctx.plugin(readImageToolview);
2123
2446
  ctx.plugin(fileMutationToolview);
2124
2447
  ctx.plugin(searchToolview);
2125
2448
  ctx.plugin(webToolview);
@@ -2,6 +2,7 @@
2
2
  import type { HostObservable, InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
3
3
  import type { RemoteHostFacts } from '@deepseek-ai/dsh-api-remotes/client';
4
4
  import type { ToolCallBlock } from '@deepseek-ai/dsh-client-ui-chat/client';
5
+ import type { MessageImageLoader, MessageImageSource } from '@deepseek-ai/dsh-client-ui-conversation/client';
5
6
  declare module '@deepseek-ai/dsh-client-ui-slots' {
6
7
  interface SlotMap {
7
8
  /**
@@ -23,8 +24,35 @@ declare module '@deepseek-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,7 @@
1
1
  import type { ToolDetailsProps } from '../contract/slots.ts';
2
2
  /**
3
3
  * Render the selected Tool call's structured output when its raw fields form a
4
- * supported root card, otherwise preserve the flattened result text.
4
+ * supported card, otherwise preserve the flattened result text.
5
5
  * @param props - selected call slice, workspace root, host home, and locale seat.
6
6
  * @returns the details output body.
7
7
  */
@@ -1,7 +1,9 @@
1
1
  import { type ReactNode } from 'react';
2
- import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import type { PropsRenderSlots, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
3
+ import type { MessageImageLoader } from '@deepseek-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 '@deepseek-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
@@ -67,10 +67,16 @@ export declare function terminalFailed(model: TerminalCardModel): boolean;
67
67
  */
68
68
  export declare function isSettledPersistentShellCall(block: ToolCallBlock): boolean;
69
69
  /**
70
- * Derive terminal props for supported root shell and terminal-send calls.
71
- * Standard shell results parse their final status marker; persistent shell
72
- * results, background calls, errors, malformed input, or child dispatches use
73
- * the generic path. {@link isSettledPersistentShellCall} lets that generic
70
+ * Identify a settled foreground shell preview whose spill footer can hide the exit marker.
71
+ * @param block - running or settled Tool block.
72
+ * @returns whether the shell output must remain generic without an inferred exit status.
73
+ */
74
+ export declare function isSpilledShellCall(block: ToolCallBlock): boolean;
75
+ /**
76
+ * Derive terminal props for supported shell and terminal-send calls, including
77
+ * nested Code Dispatch calls. Standard shell results parse their final status
78
+ * marker; persistent shell results, spill previews, background calls, errors,
79
+ * and malformed input use the generic path. {@link isSettledPersistentShellCall} lets that generic
74
80
  * persistent result remain expandable without inventing one process status.
75
81
  * @param block - running or settled Tool block.
76
82
  * @param sessionCwd - session workspace root used to resolve workdir.
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { PropsRenderSlots } from '@deepseek-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 '@deepseek-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": "@deepseek-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.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -48,19 +48,28 @@
48
48
  "@types/react": "~18.3.1",
49
49
  "react": "^18.2.0",
50
50
  "react-dom": "^18.2.0",
51
- "@deepseek-ai/dsh-api-remotes": "^0.1.2-alpha.5",
52
- "@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.5",
51
+ "@deepseek-ai/dsh-spill-policy": "^0.1.3-alpha.2",
52
+ "@deepseek-ai/dsh-agent": "^0.1.3-alpha.2",
53
+ "@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
54
+ "@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
55
+ "@deepseek-ai/dsh-system-prompt": "^0.1.3-alpha.2",
56
+ "@deepseek-ai/dsh-tools": "^0.1.3-alpha.2",
57
+ "@deepseek-ai/dsh-spill": "^0.1.3-alpha.2",
58
+ "@deepseek-ai/dsh-code-runtime-worker-thread": "^0.1.3-alpha.2",
53
59
  "@deepseek-ai/cordis": "^4.0.2",
54
- "@deepseek-ai/dsh-client-test-runtime": "^0.1.2-alpha.5",
55
- "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.5",
56
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-alpha.5",
57
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.5",
58
- "@deepseek-ai/dsh-api-workspace-controller": "^0.1.2-alpha.5",
59
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.5",
60
- "@deepseek-ai/dsh-client-ui-chat": "^0.1.2-alpha.5",
61
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-alpha.5",
62
- "@deepseek-ai/dsh-client-ui-session": "^0.1.2-alpha.5",
63
- "@deepseek-ai/dsh-util-workspace-path": "^0.1.2-alpha.5"
60
+ "@deepseek-ai/dsh-api-remotes": "^0.1.3-alpha.2",
61
+ "@deepseek-ai/dsh-client-connection": "^0.1.3-alpha.2",
62
+ "@deepseek-ai/dsh-client-locale": "^0.1.3-alpha.2",
63
+ "@deepseek-ai/dsh-client-test-runtime": "^0.1.3-alpha.2",
64
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.3-alpha.2",
65
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.3-alpha.2",
66
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.3-alpha.2",
67
+ "@deepseek-ai/dsh-api-workspace-controller": "^0.1.3-alpha.2",
68
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.3-alpha.2",
69
+ "@deepseek-ai/dsh-util-workspace-path": "^0.1.3-alpha.2",
70
+ "@deepseek-ai/dsh-attachment": "^0.1.3-alpha.2",
71
+ "@deepseek-ai/dsh-client-ui-session": "^0.1.3-alpha.2",
72
+ "@deepseek-ai/dsh-client-ui-chat": "^0.1.3-alpha.2"
64
73
  },
65
74
  "files": [
66
75
  "lib/index.js",