@xiaoso/dsh-tool-plus 0.1.7 → 0.1.8-beta.3

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.
Files changed (96) hide show
  1. package/lib/{abort-Caqff3M6.mjs → abort-BtBy5Svf.mjs} +1 -1
  2. package/lib/{alibaba-coding-plan-DTupMiAi.mjs → alibaba-coding-plan-oxHCHJtG.mjs} +2 -2
  3. package/lib/{alibaba-token-plan-Dj3OZhEE.mjs → alibaba-token-plan-B0WzvYnB.mjs} +2 -2
  4. package/lib/{amazon-bedrock-DbFSYXfJ.mjs → amazon-bedrock-BvfYGhrD.mjs} +6 -6
  5. package/lib/{anthropic-CXfWlp-x.mjs → anthropic-C6-vdduI.mjs} +1 -1
  6. package/lib/{anthropic-CTJ3IQVa.mjs → anthropic-Id561lwb.mjs} +13 -13
  7. package/lib/{anthropic-wire-CKuhDwZ1.mjs → anthropic-wire-Cb8AQ1tS.mjs} +13 -13
  8. package/lib/{api-key-validation-CGCJFbFr.mjs → api-key-validation-BXJKlsJg.mjs} +1 -1
  9. package/lib/{azure-openai-responses-ChFbWaGj.mjs → azure-openai-responses-CGws_ijz.mjs} +8 -8
  10. package/lib/{browser-render-9o0Rknop.mjs → browser-render-i7uYOl4f.mjs} +2 -2
  11. package/lib/client.js +2205 -19
  12. package/lib/{cloudflare-ai-gateway-BupNXyKK.mjs → cloudflare-ai-gateway-BK6eadm9.mjs} +2 -2
  13. package/lib/{cloudflare-ai-gateway-EWo0VkP5.mjs → cloudflare-ai-gateway-B_kWnF30.mjs} +1 -1
  14. package/lib/{collapse-DOR8sOGf.mjs → collapse-DU3g7d3B.mjs} +1 -1
  15. package/lib/{coreweave-Ct8cF-da.mjs → coreweave-CkR_FlQi.mjs} +2 -2
  16. package/lib/{cursor-LKGjucn2.mjs → cursor-BXx_5h4a.mjs} +2 -2
  17. package/lib/{cursor-DJV_dcgB.mjs → cursor-BcHwv8qq.mjs} +10 -10
  18. package/lib/{cursor-B95LzVWV.mjs → cursor-BjWbJJLe.mjs} +1 -1
  19. package/lib/{cursor-proto-B8Wj8bv9.mjs → cursor-proto-DPXXYx8S.mjs} +1 -1
  20. package/lib/{devin-DSp0Rw_E.mjs → devin-BZBfzyIz.mjs} +6 -6
  21. package/lib/{devin-BMTdx_hx.mjs → devin-Ce3Wz8_E.mjs} +5 -5
  22. package/lib/{devin-proto-Br8fdyNl.mjs → devin-proto-B6HwDwHK.mjs} +1 -1
  23. package/lib/{error-CORgGg5a.mjs → error-DdCn4m1q.mjs} +3 -3
  24. package/lib/{event-stream-ulcDenZD.mjs → event-stream-ldcUvOA0.mjs} +1 -1
  25. package/lib/{github-copilot-Dstq1D3s.mjs → github-copilot-BT5dkIJl.mjs} +2 -2
  26. package/lib/{github-copilot-headers-DOh1gt_w.mjs → github-copilot-headers-BGL8JO5c.mjs} +2 -2
  27. package/lib/{gitlab-duo-DSCMrU0m.mjs → gitlab-duo-B0qSYCa3.mjs} +1 -1
  28. package/lib/{google-CtsKkOQK.mjs → google-6cRgRmh0.mjs} +3 -3
  29. package/lib/{google-antigravity-z-5KsEci.mjs → google-antigravity-D1llOVad.mjs} +3 -3
  30. package/lib/{google-auth-ieLBaneO.mjs → google-auth-DYhqX1rJ.mjs} +5 -5
  31. package/lib/{google-gemini-cli-CAIPSl7e.mjs → google-gemini-cli-ByQUuKp9.mjs} +7 -7
  32. package/lib/{google-gemini-cli-C6NE5mWz.mjs → google-gemini-cli-CjHy6mFG.mjs} +3 -3
  33. package/lib/{google-oauth-shared-DhMllAkt.mjs → google-oauth-shared-CSwCT0Yl.mjs} +1 -1
  34. package/lib/{google-shared-B0aNCYEH.mjs → google-shared-DYjJp0mQ.mjs} +5 -5
  35. package/lib/{google-vertex-Bbd1tYNc.mjs → google-vertex-CbOvlE5j.mjs} +4 -4
  36. package/lib/{idle-iterator-DlUkEfzW.mjs → idle-iterator-DTBaEIbM.mjs} +2 -2
  37. package/lib/index.mjs +1278 -184
  38. package/lib/{kilo-ChJL54wq.mjs → kilo-cVG1aNZM.mjs} +1 -1
  39. package/lib/{kimi-C2c_Zq8A.mjs → kimi-BFfnFDhu.mjs} +4 -4
  40. package/lib/{launch-DKDH71En.mjs → launch-Uq7920ng.mjs} +7 -7
  41. package/lib/{markit-CmRfm4E2.mjs → markit-BmsfOFVh.mjs} +18 -18
  42. package/lib/{muse-code-Di1VZmB1.mjs → muse-code-CtcevJab.mjs} +1 -1
  43. package/lib/{ollama-B8ic3sEp.mjs → ollama-B5hR1P1p.mjs} +8 -8
  44. package/lib/{openai-codex-DuJNC99x.mjs → openai-codex-CxkPyvyt.mjs} +2 -2
  45. package/lib/{openai-codex-responses-BL29nh9K.mjs → openai-codex-responses-BbM3Q7nJ.mjs} +9 -9
  46. package/lib/{openai-completions-BK2f2dBv.mjs → openai-completions-CffXVOCL.mjs} +13 -13
  47. package/lib/{openai-reasoning-fallback-BdB3RCbj.mjs → openai-reasoning-fallback-DFdD1ok5.mjs} +2 -2
  48. package/lib/{openai-responses-CGQPlIIy.mjs → openai-responses-DgqOvSn_.mjs} +9 -9
  49. package/lib/{perplexity-TBjz3VMB.mjs → perplexity-JFp3knZN.mjs} +2 -2
  50. package/lib/{protobuf-aab1UGVn.mjs → protobuf-BJHNU9-E.mjs} +1 -1
  51. package/lib/{proxy-5nWqRL1f.mjs → proxy-B51IWEMt.mjs} +2 -2
  52. package/lib/{scrapers-CtQrQ5kG.mjs → scrapers-DU4u4-Co.mjs} +7 -7
  53. package/lib/{src-BwW51vA5.mjs → src-DUUqyo9y.mjs} +42 -42
  54. package/lib/{stream-B_7qN-bA.mjs → stream-DHdFrAG4.mjs} +54 -54
  55. package/lib/{stream-markup-healing-DKT1lnkW.mjs → stream-markup-healing-DlJ9abTw.mjs} +2 -2
  56. package/lib/{thinking-CiLa2LFB.mjs → thinking-D6LJxkz_.mjs} +1 -1
  57. package/lib/types/client/locales.d.ts +1 -1
  58. package/lib/types/config/settings.d.ts +8 -0
  59. package/lib/types/tools/ast-grep/adapter/index.d.ts +23 -0
  60. package/lib/types/tools/edit/adapter/index.d.ts +3 -0
  61. package/lib/types/tools/glob/adapter/index.d.ts +2 -0
  62. package/lib/types/tools/grep/adapter/index.d.ts +4 -0
  63. package/lib/types/tools/read/adapter/index.d.ts +18 -0
  64. package/lib/types/tools/write/adapter/index.d.ts +2 -0
  65. package/lib/types/web/client/ToolCardShell.d.ts +111 -0
  66. package/lib/types/web/client/labels.d.ts +36 -0
  67. package/lib/types/web/client/registerToolCards.d.ts +35 -0
  68. package/lib/types/web/client/row-utils.d.ts +177 -0
  69. package/lib/types/web/client/rows/AstEditRow.d.ts +22 -0
  70. package/lib/types/web/client/rows/FileMutationRow.d.ts +23 -0
  71. package/lib/types/web/client/rows/ReadRow.d.ts +31 -0
  72. package/lib/types/web/client/rows/SearchRow.d.ts +21 -0
  73. package/lib/types/web/client/rows/TerminalRow.d.ts +24 -0
  74. package/lib/types/web/client/rows/WriteRow.d.ts +22 -0
  75. package/lib/types/web/client/rows/command-card-facts.d.ts +45 -0
  76. package/lib/types/web/client/rows/image-card.d.ts +60 -0
  77. package/lib/types/web/client/rows/read-card-facts.d.ts +33 -0
  78. package/lib/types/web/client/rows/search-card-facts.d.ts +37 -0
  79. package/lib/types/web/contract.d.ts +249 -0
  80. package/lib/types/web/host/ast-edit.d.ts +10 -0
  81. package/lib/types/web/host/bash.d.ts +30 -0
  82. package/lib/types/web/host/edit.d.ts +33 -0
  83. package/lib/types/web/host/read.d.ts +48 -0
  84. package/lib/types/web/host/search.d.ts +41 -0
  85. package/lib/types/web/host/write.d.ts +13 -0
  86. package/lib/{types-PVueGi9S.mjs → types-DT9vgZ3S.mjs} +1 -1
  87. package/lib/{utils-CUPhpw9A.mjs → utils-BMmwLB0f.mjs} +2 -2
  88. package/lib/{utils-ECAwvaPQ.mjs → utils-C-Lo_JC6.mjs} +41 -41
  89. package/lib/{utils-DeWqNWV8.mjs → utils-D31VFael.mjs} +1 -1
  90. package/lib/{vision-guard-_XwsL1DF.mjs → vision-guard-BlAAqGUC.mjs} +1 -1
  91. package/lib/{xai-oauth-CXhnz0gO.mjs → xai-oauth-QPyScm4X.mjs} +1 -1
  92. package/lib/{xiaomi-psoUItal.mjs → xiaomi-CLzjxHAi.mjs} +1 -1
  93. package/lib/{zai-ApJ8HBgT.mjs → zai-wDp0g7VC.mjs} +1 -1
  94. package/lib/{zip-Kd602XNe.mjs → zip-CNtIFfyV.mjs} +7 -7
  95. package/package.json +3 -1
  96. package/presets/package.json +1 -1
package/lib/client.js CHANGED
@@ -8,6 +8,7 @@ window.__ModuleLoader__.load({
8
8
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
10
  let _deepseek_ai_dsh_client_connection_client = require("@deepseek-ai/dsh-client-connection/client");
11
+ let react_dom = require("react-dom");
11
12
  //#region src/config/fields.ts
12
13
  /** Field group ids -> locale key of the group heading. */
13
14
  const TOOL_PLUS_GROUP_LABELS = {
@@ -94,6 +95,15 @@ window.__ModuleLoader__.load({
94
95
  group: "behavior",
95
96
  tool: "bash"
96
97
  },
98
+ {
99
+ name: "webCards",
100
+ kind: "boolean",
101
+ default: true,
102
+ labelKey: "webCards",
103
+ hintKey: "webCardsHint",
104
+ group: "behavior",
105
+ tool: "bash"
106
+ },
97
107
  {
98
108
  name: "autoBackgroundMs",
99
109
  kind: "number",
@@ -1136,7 +1146,7 @@ window.__ModuleLoader__.load({
1136
1146
  return SCHEMA_DEFAULTS[field];
1137
1147
  }
1138
1148
  /** Card chrome CSS, keyed by `data-plugin-css` and injected once (design-token driven). */
1139
- const CSS$1 = `
1149
+ const CSS$2 = `
1140
1150
  .tp-card{list-style:none;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-3);transition:border-color .16s,background .16s;margin:0 0 8px}
1141
1151
  .tp-card:hover{border-color:var(--dsw-alias-label-dimmed)}
1142
1152
  .tp-card.tp-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
@@ -1175,16 +1185,16 @@ window.__ModuleLoader__.load({
1175
1185
  }
1176
1186
  `;
1177
1187
  /** Inject the card stylesheet once per page; the loader removes plugin-owned style tags on unload. */
1178
- let cssInjected$1 = false;
1188
+ let cssInjected$2 = false;
1179
1189
  function injectCardCss() {
1180
- if (cssInjected$1 || typeof document === "undefined") return;
1181
- cssInjected$1 = true;
1190
+ if (cssInjected$2 || typeof document === "undefined") return;
1191
+ cssInjected$2 = true;
1182
1192
  const id = "tool-plus-card";
1183
1193
  if (document.querySelector("style[data-plugin-css=\"tool-plus-card\"]") !== null) return;
1184
1194
  const tag = document.createElement("style");
1185
1195
  tag.dataset.plugin = "@xiaoso/dsh-tool-plus";
1186
1196
  tag.dataset.pluginCss = id;
1187
- tag.textContent = CSS$1;
1197
+ tag.textContent = CSS$2;
1188
1198
  document.head.appendChild(tag);
1189
1199
  }
1190
1200
  /** Text of a staged (or resolved) value for the number controls. */
@@ -1710,13 +1720,13 @@ window.__ModuleLoader__.load({
1710
1720
  return (c === "x" ? r : r & 3 | 8).toString(16);
1711
1721
  });
1712
1722
  }
1713
- function isRecord(value) {
1723
+ function isRecord$4(value) {
1714
1724
  return typeof value === "object" && value !== null && !Array.isArray(value);
1715
1725
  }
1716
1726
  function parseConnectionResponse(value) {
1717
- if (!isRecord(value) || value.type !== "server-response" || typeof value.rpcId !== "string") throw new TypeError("connection: invalid server-response envelope");
1727
+ if (!isRecord$4(value) || value.type !== "server-response" || typeof value.rpcId !== "string") throw new TypeError("connection: invalid server-response envelope");
1718
1728
  const result = value.result;
1719
- if (!isRecord(result)) throw new TypeError("connection: invalid server-response result");
1729
+ if (!isRecord$4(result)) throw new TypeError("connection: invalid server-response result");
1720
1730
  if (result.ok === true) return {
1721
1731
  rpcId: value.rpcId,
1722
1732
  result: {
@@ -1724,9 +1734,9 @@ window.__ModuleLoader__.load({
1724
1734
  value: result.value
1725
1735
  }
1726
1736
  };
1727
- if (result.ok !== false || !isRecord(result.error)) throw new TypeError("connection: invalid server-response result");
1737
+ if (result.ok !== false || !isRecord$4(result.error)) throw new TypeError("connection: invalid server-response result");
1728
1738
  const error = result.error;
1729
- if (typeof error.code !== "string" || typeof error.message !== "string" || !isRecord(error.details)) throw new TypeError("connection: invalid server-response failure");
1739
+ if (typeof error.code !== "string" || typeof error.message !== "string" || !isRecord$4(error.details)) throw new TypeError("connection: invalid server-response failure");
1730
1740
  return {
1731
1741
  rpcId: value.rpcId,
1732
1742
  result: {
@@ -2131,7 +2141,7 @@ window.__ModuleLoader__.load({
2131
2141
  * off under prefers-reduced-motion (CSS here, matchMedia for the
2132
2142
  * JS-driven underline).
2133
2143
  */
2134
- const CSS = `
2144
+ const CSS$1 = `
2135
2145
  .tps-section{box-sizing:border-box;display:flex;flex-direction:column;gap:16px;padding-top:2px}
2136
2146
  .tps-heading{margin:0;font-size:18px;font-weight:600;line-height:26px;color:var(--dsw-alias-label-primary);animation:tps-rise .38s cubic-bezier(.22,1,.36,1) both}
2137
2147
  .tps-intro{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary);max-width:64ch;animation:tps-rise .38s cubic-bezier(.22,1,.36,1) .05s both}
@@ -2176,16 +2186,16 @@ window.__ModuleLoader__.load({
2176
2186
  }
2177
2187
  `;
2178
2188
  /** Inject the section stylesheet once per page. */
2179
- let cssInjected = false;
2189
+ let cssInjected$1 = false;
2180
2190
  function injectSectionCss() {
2181
- if (cssInjected || typeof document === "undefined") return;
2182
- cssInjected = true;
2191
+ if (cssInjected$1 || typeof document === "undefined") return;
2192
+ cssInjected$1 = true;
2183
2193
  const id = "tool-plus-section";
2184
2194
  if (document.querySelector("style[data-plugin-css=\"tool-plus-section\"]") !== null) return;
2185
2195
  const tag = document.createElement("style");
2186
2196
  tag.dataset.plugin = "@xiaoso/dsh-tool-plus";
2187
2197
  tag.dataset.pluginCss = id;
2188
- tag.textContent = CSS;
2198
+ tag.textContent = CSS$1;
2189
2199
  document.head.appendChild(tag);
2190
2200
  }
2191
2201
  /** Tool tabs with their owned fields, derived from the single-source table. */
@@ -2621,7 +2631,7 @@ window.__ModuleLoader__.load({
2621
2631
  //#endregion
2622
2632
  //#region src/client/locales.ts
2623
2633
  /** Simplified Chinese copy. */
2624
- const zh = {
2634
+ const zh$1 = {
2625
2635
  title: "Bash(bash-plus)",
2626
2636
  description: "持久化 shell 工具:超时、后台化、命令拦截与运行环境。",
2627
2637
  nav: "Tool Plus",
@@ -2774,6 +2784,8 @@ window.__ModuleLoader__.load({
2774
2784
  rmSafeReasonRuntimeNotEffective: "运行时未生效",
2775
2785
  useShellCommandWrapper: "用 bash -c 包裹命令",
2776
2786
  useShellCommandWrapperHint: "把每条命令用 bash -c 包裹,以获得完整的 bash 环境。",
2787
+ webCards: "Web 工具卡片",
2788
+ webCardsHint: "在对话里用本插件的卡片展示 write / edit / read / grep / glob / ast_grep / ast_edit / bash 的调用结果;关闭后回到官方行。",
2777
2789
  outputTruncateStrategy: "截断单位",
2778
2790
  outputTruncateStrategyHint: "后台任务完成消息达到阈值时的截断单位:按字节或按行。切换后下方显示对应单位的配置。",
2779
2791
  outputTruncateTriggerBytes: "字节触发阈值",
@@ -2811,7 +2823,7 @@ window.__ModuleLoader__.load({
2811
2823
  appliesTo: "保存后对新调用生效。"
2812
2824
  };
2813
2825
  /** English copy. */
2814
- const en = {
2826
+ const en$1 = {
2815
2827
  title: "Bash (bash-plus)",
2816
2828
  description: "Persistent shell tool: timeouts, backgrounding, interception, and environment.",
2817
2829
  nav: "Tool Plus",
@@ -2964,6 +2976,8 @@ window.__ModuleLoader__.load({
2964
2976
  rmSafeReasonRuntimeNotEffective: "not effective at runtime",
2965
2977
  useShellCommandWrapper: "Wrap commands in bash -c",
2966
2978
  useShellCommandWrapperHint: "Wrap every command in bash -c for a full bash environment.",
2979
+ webCards: "Web tool cards",
2980
+ webCardsHint: "Show this plugin's cards for write / edit / read / grep / glob / ast_grep / ast_edit / bash calls in chat; off falls back to the shipped rows.",
2967
2981
  outputTruncateStrategy: "Truncate unit",
2968
2982
  outputTruncateStrategyHint: "Unit used when a background completion message crosses its threshold: bytes or lines. Switching shows the matching unit's settings below.",
2969
2983
  outputTruncateTriggerBytes: "Byte trigger threshold",
@@ -3001,6 +3015,2177 @@ window.__ModuleLoader__.load({
3001
3015
  appliesTo: "Values apply to new calls."
3002
3016
  };
3003
3017
  //#endregion
3018
+ //#region src/web/contract.ts
3019
+ /**
3020
+ * Web tool-card contract: the payload this plugin's Host half projects into
3021
+ * `output.presentationMeta` and its browser half narrows back before drawing.
3022
+ *
3023
+ * Dependency-free by design (no React, no cordis, no Node built-ins): the Node
3024
+ * half imports it to build metadata, the browser half imports it to validate
3025
+ * it, and because it carries no React import it can never drag the browser
3026
+ * bundle into the Node build.
3027
+ *
3028
+ * `edit` / `read` / `search` mirror the exact shapes the shipped
3029
+ * `@deepseek-ai/dsh-client-ui-tool` card models accept — a `kind` tag rides on
3030
+ * top, and those models ignore unknown keys — so the same payload stays
3031
+ * readable if this plugin ever stops shadowing those rows. `write`,
3032
+ * `ast_edit` and `terminal` are this plugin's own shapes: the shipped rows have
3033
+ * no equivalent (their write card is a diff card; ours is a content preview,
3034
+ * matching the OMP card the model already sees).
3035
+ *
3036
+ * Everything here is defensive: a card never throws. The browser half turns any
3037
+ * malformed, oversized or foreign payload into `null`, and the row falls back
3038
+ * to the generic shell.
3039
+ *
3040
+ * @module @xiaoso/dsh-tool-plus/web/contract
3041
+ */
3042
+ /**
3043
+ * Cell-shadowing rank of every card entry this plugin registers.
3044
+ *
3045
+ * The shipped `tool.call.toolview` entries all register at the default
3046
+ * priority 0, and the lowest live entry of a cell renders. A large negative
3047
+ * constant keeps this plugin first even if a future release starts ranking its
3048
+ * own rows below zero — the gap is deliberate, not a tuned value.
3049
+ */
3050
+ const CARD_TAKEOVER_PRIORITY = -1e3;
3051
+ /**
3052
+ * Wire tool names whose `tool.call.toolview` cell this plugin renders.
3053
+ *
3054
+ * `ast_grep` / `ast_edit` are this plugin's own tool names (no shipped entry
3055
+ * claims those keys), so registering them is purely additive; the other six
3056
+ * shadow a shipped row.
3057
+ */
3058
+ const CARD_TOOL_KEYS = [
3059
+ "bash",
3060
+ "read",
3061
+ "write",
3062
+ "edit",
3063
+ "grep",
3064
+ "glob",
3065
+ "ast_grep",
3066
+ "ast_edit"
3067
+ ];
3068
+ /** UTF-8 byte length of `text`, without allocating an encoder. */
3069
+ function utf8ByteLength(text) {
3070
+ let bytes = 0;
3071
+ for (let index = 0; index < text.length; index += 1) {
3072
+ const code = text.charCodeAt(index);
3073
+ if (code < 128) bytes += 1;
3074
+ else if (code < 2048) bytes += 2;
3075
+ else if (code >= 55296 && code <= 56319) {
3076
+ bytes += 4;
3077
+ index += 1;
3078
+ } else bytes += 3;
3079
+ }
3080
+ return bytes;
3081
+ }
3082
+ /** UTF-8 byte length of a value's JSON form; the unit every cap above uses. */
3083
+ function jsonByteLength(value) {
3084
+ const json = JSON.stringify(value);
3085
+ return json === void 0 ? 0 : utf8ByteLength(json);
3086
+ }
3087
+ /** Whether `value` is a plain object (never an array or `null`). */
3088
+ function isRecord$3(value) {
3089
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3090
+ }
3091
+ /** Non-blank string, or `null`. */
3092
+ function nonBlankString(value) {
3093
+ return typeof value === "string" && value.trim() !== "" ? value : null;
3094
+ }
3095
+ /** 1-based integer, or `null` (the shipped card models' own line rule). */
3096
+ function positiveInteger(value) {
3097
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : null;
3098
+ }
3099
+ /** Non-negative integer, or `null`. */
3100
+ function nonNegativeInteger(value) {
3101
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
3102
+ }
3103
+ /** Optional grammar hint: a non-empty string stays, anything else drops. */
3104
+ function lang(value) {
3105
+ return typeof value === "string" && value !== "" ? { lang: value } : {};
3106
+ }
3107
+ /**
3108
+ * Narrow opaque metadata to a {@link WriteCardMeta}.
3109
+ * @param meta - the result node's `meta` field.
3110
+ * @returns the narrowed metadata, or `null` when it is not a usable write card.
3111
+ */
3112
+ function narrowWriteCardMeta(meta) {
3113
+ if (!isRecord$3(meta) || meta.kind !== "write") return null;
3114
+ const path = nonBlankString(meta.path);
3115
+ if (path === null) return null;
3116
+ return {
3117
+ kind: "write",
3118
+ path,
3119
+ ...lang(meta.lang),
3120
+ ...meta.madeExecutable === true ? { madeExecutable: true } : {}
3121
+ };
3122
+ }
3123
+ /**
3124
+ * Narrow opaque metadata to an {@link EditCardMeta}.
3125
+ *
3126
+ * Mirrors the shipped diff card's own validation (a hunk needs a non-blank
3127
+ * `path`, an explicit `null` or string `oldText`, and a string `newText`) and
3128
+ * additionally rejects a payload past {@link EDIT_META_MAX_BYTES}.
3129
+ * @param meta - the result node's `meta` field.
3130
+ * @returns the narrowed metadata, or `null` when it is not a usable diff card.
3131
+ */
3132
+ function narrowEditCardMeta(meta) {
3133
+ if (!isRecord$3(meta) || meta.kind !== "edit") return null;
3134
+ if (jsonByteLength(meta) > 131072) return null;
3135
+ const raw = meta.diffs;
3136
+ if (!Array.isArray(raw) || raw.length === 0) return null;
3137
+ const diffs = [];
3138
+ for (const hunk of raw) {
3139
+ if (!isRecord$3(hunk)) return null;
3140
+ const path = nonBlankString(hunk.path);
3141
+ if (path === null) return null;
3142
+ const { oldText, newText } = hunk;
3143
+ if (oldText !== null && typeof oldText !== "string") return null;
3144
+ if (typeof newText !== "string") return null;
3145
+ diffs.push({
3146
+ path,
3147
+ oldText,
3148
+ newText
3149
+ });
3150
+ }
3151
+ return {
3152
+ kind: "edit",
3153
+ diffs
3154
+ };
3155
+ }
3156
+ /**
3157
+ * Narrow opaque metadata to an {@link AstEditCardMeta}.
3158
+ * @param meta - the result node's `meta` field.
3159
+ * @returns the narrowed metadata, or `null` when it is not a usable preview.
3160
+ */
3161
+ function narrowAstEditCardMeta(meta) {
3162
+ if (!isRecord$3(meta) || meta.kind !== "ast_edit") return null;
3163
+ if (jsonByteLength(meta) > 65536) return null;
3164
+ if (typeof meta.preview !== "string") return null;
3165
+ const replacements = nonNegativeInteger(meta.replacements);
3166
+ if (replacements === null || typeof meta.applied !== "boolean") return null;
3167
+ if (!Array.isArray(meta.files)) return null;
3168
+ const files = [];
3169
+ for (const file of meta.files) {
3170
+ if (!isRecord$3(file)) return null;
3171
+ const path = nonBlankString(file.path);
3172
+ const count = nonNegativeInteger(file.count);
3173
+ if (path === null || count === null) return null;
3174
+ files.push({
3175
+ path,
3176
+ count
3177
+ });
3178
+ }
3179
+ return {
3180
+ kind: "ast_edit",
3181
+ preview: meta.preview,
3182
+ files,
3183
+ replacements,
3184
+ applied: meta.applied
3185
+ };
3186
+ }
3187
+ /**
3188
+ * Narrow opaque metadata to a {@link ReadCardMeta}.
3189
+ *
3190
+ * Mirrors the shipped read card's own validation: a 1-based `offset`, lines
3191
+ * strictly increasing in `number` and never past `totalLines`. A window whose
3192
+ * lines were all elided therefore narrows to a card with an empty body, not to
3193
+ * a malformed one.
3194
+ * @param meta - the result node's `meta` field.
3195
+ * @returns the narrowed metadata, or `null` when it is not a usable read card.
3196
+ */
3197
+ function narrowReadCardMeta(meta) {
3198
+ if (!isRecord$3(meta) || meta.kind !== "read") return null;
3199
+ if (jsonByteLength(meta) > 262144) return null;
3200
+ const path = nonBlankString(meta.path);
3201
+ const offset = positiveInteger(meta.offset);
3202
+ const totalLines = nonNegativeInteger(meta.totalLines);
3203
+ if (path === null || offset === null || totalLines === null) return null;
3204
+ if (!Array.isArray(meta.lines)) return null;
3205
+ const lines = [];
3206
+ let previous = offset - 1;
3207
+ for (const line of meta.lines) {
3208
+ if (!isRecord$3(line)) return null;
3209
+ const number = positiveInteger(line.number);
3210
+ if (number === null || number <= previous || number > totalLines) return null;
3211
+ if (typeof line.text !== "string") return null;
3212
+ previous = number;
3213
+ lines.push({
3214
+ number,
3215
+ text: line.text
3216
+ });
3217
+ }
3218
+ return {
3219
+ kind: "read",
3220
+ path,
3221
+ offset,
3222
+ lines,
3223
+ totalLines,
3224
+ ...lang(meta.lang)
3225
+ };
3226
+ }
3227
+ /** Narrow one grouped-matches file entry, or `null` when malformed. */
3228
+ function narrowSearchFile(file) {
3229
+ if (!isRecord$3(file)) return null;
3230
+ const path = nonBlankString(file.path);
3231
+ if (path === null || !Array.isArray(file.matches)) return null;
3232
+ const matches = [];
3233
+ for (const match of file.matches) {
3234
+ if (!isRecord$3(match)) return null;
3235
+ const lineNumber = positiveInteger(match.lineNumber);
3236
+ if (lineNumber === null || typeof match.line !== "string") return null;
3237
+ matches.push({
3238
+ lineNumber,
3239
+ line: match.line
3240
+ });
3241
+ }
3242
+ return {
3243
+ path,
3244
+ matches
3245
+ };
3246
+ }
3247
+ /**
3248
+ * Narrow opaque metadata to a {@link SearchCardMeta}.
3249
+ * @param meta - the result node's `meta` field.
3250
+ * @returns the narrowed metadata, or `null` when it is not a usable search card.
3251
+ */
3252
+ function narrowSearchCardMeta(meta) {
3253
+ if (!isRecord$3(meta) || meta.kind !== "search") return null;
3254
+ if (jsonByteLength(meta) > 65536) return null;
3255
+ if (typeof meta.truncated !== "boolean") return null;
3256
+ const total = nonNegativeInteger(meta.total);
3257
+ if (total === null) return null;
3258
+ if (meta.shape === "matches") {
3259
+ if (!Array.isArray(meta.files)) return null;
3260
+ const files = [];
3261
+ for (const file of meta.files) {
3262
+ const narrowed = narrowSearchFile(file);
3263
+ if (narrowed === null) return null;
3264
+ files.push(narrowed);
3265
+ }
3266
+ return {
3267
+ kind: "search",
3268
+ shape: "matches",
3269
+ files,
3270
+ truncated: meta.truncated,
3271
+ total
3272
+ };
3273
+ }
3274
+ if (meta.shape !== "paths" || !Array.isArray(meta.paths)) return null;
3275
+ const paths = [];
3276
+ for (const path of meta.paths) {
3277
+ if (typeof path !== "string") return null;
3278
+ paths.push(path);
3279
+ }
3280
+ return {
3281
+ kind: "search",
3282
+ shape: "paths",
3283
+ paths,
3284
+ truncated: meta.truncated,
3285
+ total
3286
+ };
3287
+ }
3288
+ /**
3289
+ * Narrow opaque metadata to a {@link TerminalCardMeta}.
3290
+ * @param meta - the result node's `meta` field.
3291
+ * @returns the narrowed metadata, or `null` when it is not a usable terminal card.
3292
+ */
3293
+ function narrowTerminalCardMeta(meta) {
3294
+ if (!isRecord$3(meta) || meta.kind !== "terminal") return null;
3295
+ if (meta.mode === "background") {
3296
+ const jobId = nonBlankString(meta.jobId);
3297
+ return jobId === null ? null : {
3298
+ kind: "terminal",
3299
+ mode: "background",
3300
+ jobId
3301
+ };
3302
+ }
3303
+ if (meta.mode !== "foreground") return null;
3304
+ const { exitCode, timedOut, aborted, workingDir } = meta;
3305
+ if (exitCode !== null && (typeof exitCode !== "number" || !Number.isInteger(exitCode))) return null;
3306
+ if (typeof timedOut !== "boolean" || typeof aborted !== "boolean") return null;
3307
+ if (workingDir !== void 0 && typeof workingDir !== "string") return null;
3308
+ return {
3309
+ kind: "terminal",
3310
+ mode: "foreground",
3311
+ exitCode,
3312
+ timedOut,
3313
+ aborted,
3314
+ ...typeof workingDir === "string" && workingDir !== "" ? { workingDir } : {}
3315
+ };
3316
+ }
3317
+ //#endregion
3318
+ //#region src/web/client/labels.ts
3319
+ /**
3320
+ * Card copy for the browser tool views: every string the six row components
3321
+ * and the shared shell draw, in its own locale namespace so it can never
3322
+ * collide with the settings card's `tool-plus` bundle (the slots registry
3323
+ * merges namespaces, and re-declaring one would be a compile error).
3324
+ *
3325
+ * zh is the source of truth for the key set; both dictionaries are
3326
+ * `Record<ToolCardLocaleKey, string>`, so a missing or extra key in either is
3327
+ * a compile error. Template parameters use the host's `{name}` placeholder
3328
+ * convention (the seat interpolates the params record into the string).
3329
+ *
3330
+ * The wording mirrors the shipped conversation namespace for the same
3331
+ * primitives (diff / read / search / terminal chrome) so a takeover does not
3332
+ * change how the cards read; tool titles and the generic-row copy are ours.
3333
+ * @module @xiaoso/dsh-tool-plus/web/client/labels
3334
+ */
3335
+ /** Locale namespace of the tool-card bundle (registered by `registerToolCards`). */
3336
+ const CARD_LOCALE_NS = "tool-plus-cards";
3337
+ /** The bilingual bundle handed to `ctx.locale.register(CARD_LOCALE_NS, …)`. */
3338
+ const cardLocales = {
3339
+ zh: {
3340
+ copy: "复制",
3341
+ copied: "已复制",
3342
+ collapse: "收起",
3343
+ expand: "展开",
3344
+ collapseAria: "收起",
3345
+ expandAria: "展开其余 {count} 行",
3346
+ expandRest: "… 其余 {count} 行",
3347
+ "files.one": "{count} 个文件",
3348
+ "files.other": "{count} 个文件",
3349
+ running: "运行中",
3350
+ done: "完成",
3351
+ failed: "失败",
3352
+ cancelled: "已取消",
3353
+ timedOut: "已超时",
3354
+ noOutput: "无输出",
3355
+ "title.bash": "Bash",
3356
+ "title.read": "读取",
3357
+ "title.write": "写入",
3358
+ "title.edit": "编辑",
3359
+ "title.grep": "Grep",
3360
+ "title.glob": "Glob",
3361
+ "title.astGrep": "AST Grep",
3362
+ "title.astEdit": "AST Edit",
3363
+ "write.lines": "{count} 行",
3364
+ "write.madeExecutable": "已赋予可执行权限",
3365
+ "astEdit.replacements": "{count} 处替换",
3366
+ "astEdit.files": "{count} 个文件",
3367
+ "astEdit.parseErrors": "解析错误",
3368
+ "astEdit.rules": "规则",
3369
+ "astEdit.rulesRest": "… 其余 {count} 条",
3370
+ "read.window": "显示 {shown} / {total} 行",
3371
+ "read.collapseAria": "收起内容",
3372
+ "read.expandAria": "展开其余 {count} 行",
3373
+ "read.expandRest": "… 其余 {count} 行",
3374
+ "search.paths": "{shown} 个路径",
3375
+ "search.paths.truncated": "显示 {shown} / 共 {total} 个路径",
3376
+ "search.matches": "{shown} 处匹配 · {files} 个文件",
3377
+ "search.matches.truncated": "显示 {shown} / 共 {total} 处匹配 · {files} 个文件",
3378
+ "search.noResults": "无结果",
3379
+ "search.scope": "范围 {path}",
3380
+ "search.collapseAria": "收起结果",
3381
+ "search.expandAria": "展开其余 {count} 行结果",
3382
+ "search.expandRest": "… 其余 {count} 行",
3383
+ "terminal.signal": "信号 {signal}",
3384
+ "terminal.exitCode": "退出码 {code}",
3385
+ "terminal.running": "运行中",
3386
+ "terminal.failed": "失败",
3387
+ "terminal.done": "已完成",
3388
+ "terminal.noOutput": "无输出",
3389
+ "bash.background": "后台任务 {jobId}",
3390
+ "image.loading": "加载中",
3391
+ "image.loadFailed": "加载失败",
3392
+ "image.open": "查看原图",
3393
+ "image.openNamed": "查看原图:{name}",
3394
+ "image.dialog": "原图预览",
3395
+ "image.close": "关闭",
3396
+ "image.unnamed": "图片",
3397
+ "image.caption": "{name} · {width}×{height} · {size}",
3398
+ "generic.params": "{count} 个参数",
3399
+ "generic.noDetail": "没有更多可展示的细节",
3400
+ inspect: "查看"
3401
+ },
3402
+ en: {
3403
+ copy: "Copy",
3404
+ copied: "Copied",
3405
+ collapse: "Collapse",
3406
+ expand: "Expand",
3407
+ collapseAria: "Collapse",
3408
+ expandAria: "Expand {count} more lines",
3409
+ expandRest: "… {count} more lines",
3410
+ "files.one": "{count} file",
3411
+ "files.other": "{count} files",
3412
+ running: "Running",
3413
+ done: "Done",
3414
+ failed: "Failed",
3415
+ cancelled: "Cancelled",
3416
+ timedOut: "Timed out",
3417
+ noOutput: "No output",
3418
+ "title.bash": "Bash",
3419
+ "title.read": "Read",
3420
+ "title.write": "Write",
3421
+ "title.edit": "Edit",
3422
+ "title.grep": "Grep",
3423
+ "title.glob": "Glob",
3424
+ "title.astGrep": "AST Grep",
3425
+ "title.astEdit": "AST Edit",
3426
+ "write.lines": "{count} lines",
3427
+ "write.madeExecutable": "Made executable",
3428
+ "astEdit.replacements": "{count} replacements",
3429
+ "astEdit.files": "{count} files",
3430
+ "astEdit.parseErrors": "Parse errors",
3431
+ "astEdit.rules": "Rules",
3432
+ "astEdit.rulesRest": "… {count} more",
3433
+ "read.window": "Showing {shown} of {total} lines",
3434
+ "read.collapseAria": "Collapse content",
3435
+ "read.expandAria": "Expand {count} more lines",
3436
+ "read.expandRest": "… {count} more lines",
3437
+ "search.paths": "{shown} paths",
3438
+ "search.paths.truncated": "Showing {shown} of {total} paths",
3439
+ "search.matches": "{shown} matches · {files} files",
3440
+ "search.matches.truncated": "Showing {shown} of {total} matches · {files} files",
3441
+ "search.noResults": "No results",
3442
+ "search.scope": "in {path}",
3443
+ "search.collapseAria": "Collapse results",
3444
+ "search.expandAria": "Expand {count} more result lines",
3445
+ "search.expandRest": "… {count} more lines",
3446
+ "terminal.signal": "signal {signal}",
3447
+ "terminal.exitCode": "exit code {code}",
3448
+ "terminal.running": "Running",
3449
+ "terminal.failed": "Failed",
3450
+ "terminal.done": "Done",
3451
+ "terminal.noOutput": "No output",
3452
+ "bash.background": "Background job {jobId}",
3453
+ "image.loading": "Loading",
3454
+ "image.loadFailed": "Load failed",
3455
+ "image.open": "View original",
3456
+ "image.openNamed": "View original: {name}",
3457
+ "image.dialog": "Original image",
3458
+ "image.close": "Close",
3459
+ "image.unnamed": "Image",
3460
+ "image.caption": "{name} · {width}×{height} · {size}",
3461
+ "generic.params": "{count} params",
3462
+ "generic.noDetail": "No further detail",
3463
+ inspect: "Inspect"
3464
+ }
3465
+ };
3466
+ //#endregion
3467
+ //#region src/web/client/row-utils.ts
3468
+ /** Run state word the shell exposes to assistive technology; `ok` says nothing. */
3469
+ const STATE_STATUS_KEY = {
3470
+ running: "running",
3471
+ ok: null,
3472
+ error: "failed",
3473
+ warning: "failed"
3474
+ };
3475
+ /** Parsed-argument cache: the node is immutable, so one parse serves every render. */
3476
+ const parsedArgs = /* @__PURE__ */ new WeakMap();
3477
+ /** Whether a block is the settled half of a call pair. */
3478
+ function isSettled(block) {
3479
+ return block.kind === "tool-result";
3480
+ }
3481
+ /**
3482
+ * The call head paired with a block: a running call is its own head; a settled
3483
+ * result carries the head backfilled from the in-window `tool/call`, which is
3484
+ * `null` when window truncation left that event outside.
3485
+ */
3486
+ function callHead(block) {
3487
+ if (!isSettled(block)) return block;
3488
+ const call = block.call;
3489
+ return typeof call === "object" && call !== null ? call : null;
3490
+ }
3491
+ /** Whether `value` is a plain object, the only argument shape the cards accept. */
3492
+ function isRecord$2(value) {
3493
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3494
+ }
3495
+ /**
3496
+ * Parse the call's raw argument JSON.
3497
+ *
3498
+ * The parsed object is memoized per node (the node is immutable), so repeated
3499
+ * renders of one card pay for a single parse.
3500
+ * @param block - the frozen running-or-settled call node.
3501
+ * @returns the argument object, or `null` when the head is missing or its JSON
3502
+ * is not an object.
3503
+ */
3504
+ function parseCardArgs(block) {
3505
+ const cacheable = typeof block === "object" && block !== null ? block : null;
3506
+ if (cacheable !== null) {
3507
+ const hit = parsedArgs.get(cacheable);
3508
+ if (hit !== void 0 || parsedArgs.has(cacheable)) return hit ?? null;
3509
+ }
3510
+ const raw = callHead(block)?.argsRaw;
3511
+ let parsed = null;
3512
+ if (typeof raw === "string") try {
3513
+ const value = JSON.parse(raw);
3514
+ if (isRecord$2(value)) parsed = value;
3515
+ } catch {
3516
+ parsed = null;
3517
+ }
3518
+ if (cacheable !== null) parsedArgs.set(cacheable, parsed);
3519
+ return parsed;
3520
+ }
3521
+ /**
3522
+ * Run state of one call.
3523
+ *
3524
+ * A settled `isError` result is an error; a foreground command that timed out
3525
+ * or was cancelled is amber rather than green (the shipped terminal model
3526
+ * reads only the trailing exit marker, which is exactly what the card owner
3527
+ * was told not to trust). A non-zero exit stays `ok`: bash reports a failing
3528
+ * command as result data, and the card's own status pill carries the red.
3529
+ * @param block - the frozen running-or-settled call node.
3530
+ * @returns the card run state.
3531
+ */
3532
+ function cardState(block) {
3533
+ if (!isSettled(block)) return "running";
3534
+ if (block.isError === true) return "error";
3535
+ const terminal = narrowTerminalCardMeta(cardMeta(block));
3536
+ if (terminal !== null && terminal.mode === "foreground" && (terminal.timedOut || terminal.aborted)) return "warning";
3537
+ return "ok";
3538
+ }
3539
+ /** Map a card run state onto the primitive's dot semantics. */
3540
+ function dotState(state) {
3541
+ switch (state) {
3542
+ case "running": return "ongoing";
3543
+ case "error": return "error";
3544
+ case "warning": return "warning";
3545
+ default: return "done";
3546
+ }
3547
+ }
3548
+ /**
3549
+ * The assistive-technology status word for a run state, or `null` when the
3550
+ * state is already carried by the row's content.
3551
+ */
3552
+ function stateStatus(state, t) {
3553
+ const key = STATE_STATUS_KEY[state];
3554
+ return key === null ? null : t(key);
3555
+ }
3556
+ /**
3557
+ * Flatten a settled result's text blocks.
3558
+ * @param block - the frozen call node.
3559
+ * @returns the joined text blocks (empty for a running call or non-text output).
3560
+ */
3561
+ function resultText(block) {
3562
+ if (!isSettled(block) || !Array.isArray(block.content)) return "";
3563
+ const parts = [];
3564
+ for (const part of block.content) {
3565
+ if (!isRecord$2(part)) continue;
3566
+ if (part.type === "text" && typeof part.text === "string") parts.push(part.text);
3567
+ }
3568
+ return parts.join("\n");
3569
+ }
3570
+ /**
3571
+ * The opaque metadata a call's result carried, exactly as the transport
3572
+ * delivered it. Callers must run it through a `narrow*` guard before reading.
3573
+ * @param block - the frozen call node.
3574
+ * @returns the raw metadata, or `undefined` while the call is running.
3575
+ */
3576
+ function cardMeta(block) {
3577
+ return isSettled(block) ? block.meta : void 0;
3578
+ }
3579
+ /**
3580
+ * Whether the call was dispatched by another tool (a Code Dispatch child).
3581
+ * The card rows fall back to the generic shell for those, mirroring the
3582
+ * shipped rows: a child diff/read/search inside a parent card would nest a
3583
+ * surface inside the parent's own body.
3584
+ */
3585
+ function isSubCall(block) {
3586
+ return typeof block.parentCallId === "string";
3587
+ }
3588
+ /** Longest summary the shell renders before it is clipped for the row. */
3589
+ const FIRST_LINE_MAX = 200;
3590
+ /**
3591
+ * The first non-empty line of a text, trimmed — the collapsed summary of an
3592
+ * error, and the closing line of the generic shell's body.
3593
+ * @param text - the source text.
3594
+ * @returns the first line, clipped to a row-sized maximum (never throws).
3595
+ */
3596
+ function firstLine(text) {
3597
+ if (typeof text !== "string") return "";
3598
+ const line = text.trim().split("\n", 1)[0]?.trim() ?? "";
3599
+ return line.length > FIRST_LINE_MAX ? `${line.slice(0, 199)}…` : line;
3600
+ }
3601
+ /**
3602
+ * A string argument by name.
3603
+ * @param args - parsed arguments, or `null`.
3604
+ * @param name - the argument key.
3605
+ * @returns its value when it is a non-empty string, else `null`.
3606
+ */
3607
+ function argText(args, name) {
3608
+ const value = args?.[name];
3609
+ return typeof value === "string" && value !== "" ? value : null;
3610
+ }
3611
+ /**
3612
+ * Shorten an absolute path against the session workspace so a summary stays
3613
+ * readable in a narrow row.
3614
+ * @param path - the path to display.
3615
+ * @param cwd - the session workspace root, when known.
3616
+ * @returns the path relative to `cwd`, or `path` unchanged.
3617
+ */
3618
+ function displayPath(path, cwd) {
3619
+ if (cwd === void 0 || cwd === "") return path;
3620
+ const root = cwd.endsWith("/") || cwd.endsWith("\\") ? cwd : `${cwd}/`;
3621
+ const normalized = path.replace(/\\/g, "/");
3622
+ const prefix = root.replace(/\\/g, "/");
3623
+ if (!normalized.startsWith(prefix)) return path;
3624
+ const rest = normalized.slice(prefix.length);
3625
+ return rest === "" ? path : rest;
3626
+ }
3627
+ /**
3628
+ * The collapsed summary for a call whose card could not be built: how many
3629
+ * arguments the model sent, so the row is not blank.
3630
+ * @param args - parsed arguments, or `null`.
3631
+ * @param t - the card translate seat.
3632
+ * @returns the localized parameter count.
3633
+ */
3634
+ function argsSummary(args, t) {
3635
+ const count = args === null ? 0 : Object.keys(args).length;
3636
+ return count === 0 ? t("generic.noDetail") : t("generic.params", { count });
3637
+ }
3638
+ /** Card title key of every wire tool name this plugin renders. */
3639
+ const TITLE_KEYS = {
3640
+ bash: "title.bash",
3641
+ read: "title.read",
3642
+ write: "title.write",
3643
+ edit: "title.edit",
3644
+ grep: "title.grep",
3645
+ glob: "title.glob",
3646
+ ast_grep: "title.astGrep",
3647
+ ast_edit: "title.astEdit"
3648
+ };
3649
+ /**
3650
+ * The card title for a wire tool name.
3651
+ *
3652
+ * The same row component can be registered under several keys (one search row
3653
+ * serves `grep`, `glob`, and `ast_grep`), and a window-truncated call has no
3654
+ * name at all, so the caller names the key it was registered for as a fallback.
3655
+ * @param toolName - the node's wire tool name (empty when truncated).
3656
+ * @param t - the card translate seat.
3657
+ * @param fallback - title key to use when the name is unknown.
3658
+ * @returns the localized title.
3659
+ */
3660
+ function cardTitle(toolName, t, fallback) {
3661
+ return t((typeof toolName === "string" ? TITLE_KEYS[toolName] : void 0) ?? fallback);
3662
+ }
3663
+ /**
3664
+ * Count a preview's content lines, ignoring the single trailing newline that
3665
+ * terminates the last line.
3666
+ * @param text - the preview text.
3667
+ * @returns its line count (0 for empty text).
3668
+ */
3669
+ function lineCount(text) {
3670
+ if (typeof text !== "string" || text === "") return 0;
3671
+ return (text.endsWith("\n") ? text.slice(0, -1) : text).split("\n").length;
3672
+ }
3673
+ /** `[timed out after 30000ms]` — `@deepseek-ai/dsh-shell/render`'s own literal. */
3674
+ const TIMEOUT_MARKER = /^\[timed out after [^\]\n]*\]$/;
3675
+ /** `[killed by signal: SIGTERM]`. */
3676
+ const SIGNAL_MARKER = /^\[killed by signal: ([^\]\n]+)\]$/;
3677
+ /** `[exit code: 1]`, or `[exit code: null]` for an unknown status. */
3678
+ const EXIT_MARKER = /^\[exit code: (\d+|null)\]$/;
3679
+ /**
3680
+ * Split a settled shell result into its output and the status markers the
3681
+ * renderer appended (timeout, terminating signal, exit code — in that order,
3682
+ * so every marker is at the end and any run of them is stripped).
3683
+ *
3684
+ * The Host metadata already carries the run state; parsing is what keeps the
3685
+ * markers out of the terminal card's output pane, and it is the only source
3686
+ * for a signal name the projection does not carry.
3687
+ * @param text - the result text.
3688
+ * @returns the marker-free output and the facts the markers named.
3689
+ */
3690
+ function parseShellStatus(text) {
3691
+ let rest = typeof text === "string" ? text : "";
3692
+ let exitCode;
3693
+ let signal;
3694
+ let timedOut = false;
3695
+ for (;;) {
3696
+ const trimmed = rest.endsWith("\n") ? rest.slice(0, -1) : rest;
3697
+ const breakAt = trimmed.lastIndexOf("\n");
3698
+ const last = breakAt === -1 ? trimmed : trimmed.slice(breakAt + 1);
3699
+ const exit = EXIT_MARKER.exec(last);
3700
+ if (exit !== null) {
3701
+ exitCode = exit[1] === "null" ? null : Number(exit[1]);
3702
+ rest = breakAt === -1 ? "" : trimmed.slice(0, breakAt);
3703
+ continue;
3704
+ }
3705
+ const killed = SIGNAL_MARKER.exec(last);
3706
+ if (killed !== null) {
3707
+ signal = killed[1];
3708
+ rest = breakAt === -1 ? "" : trimmed.slice(0, breakAt);
3709
+ continue;
3710
+ }
3711
+ if (TIMEOUT_MARKER.test(last)) {
3712
+ timedOut = true;
3713
+ rest = breakAt === -1 ? "" : trimmed.slice(0, breakAt);
3714
+ continue;
3715
+ }
3716
+ break;
3717
+ }
3718
+ return {
3719
+ output: rest,
3720
+ exitCode,
3721
+ signal,
3722
+ timedOut
3723
+ };
3724
+ }
3725
+ /** Bind the card dictionary to the diff primitive's chrome labels. */
3726
+ function diffBlockLabels(t) {
3727
+ return {
3728
+ copy: t("copy"),
3729
+ copied: t("copied"),
3730
+ collapseAria: t("collapseAria"),
3731
+ expandAria: (count) => t("expandAria", { count }),
3732
+ collapse: t("collapse"),
3733
+ expand: (count) => t("expandRest", { count }),
3734
+ files: (count) => t(count === 1 ? "files.one" : "files.other", { count })
3735
+ };
3736
+ }
3737
+ /** Bind the card dictionary to the read primitive's chrome labels. */
3738
+ function readBlockLabels(t) {
3739
+ return {
3740
+ window: (shown, total) => t("read.window", {
3741
+ shown,
3742
+ total
3743
+ }),
3744
+ copy: t("copy"),
3745
+ copied: t("copied"),
3746
+ collapseAria: t("read.collapseAria"),
3747
+ expandAria: (count) => t("read.expandAria", { count }),
3748
+ collapse: t("collapse"),
3749
+ expand: (count) => t("read.expandRest", { count })
3750
+ };
3751
+ }
3752
+ /** Bind the card dictionary to the search primitive's chrome labels. */
3753
+ function searchBlockLabels(t) {
3754
+ return {
3755
+ pathsSummary: (shown, total, truncated) => t(truncated ? "search.paths.truncated" : "search.paths", {
3756
+ shown,
3757
+ total
3758
+ }),
3759
+ matchesSummary: (shown, total, files, truncated) => t(truncated ? "search.matches.truncated" : "search.matches", {
3760
+ shown,
3761
+ total,
3762
+ files
3763
+ }),
3764
+ copy: t("copy"),
3765
+ copied: t("copied"),
3766
+ noResults: t("search.noResults"),
3767
+ collapseAria: t("search.collapseAria"),
3768
+ expandAria: (count) => t("search.expandAria", { count }),
3769
+ collapse: t("collapse"),
3770
+ expand: (count) => t("search.expandRest", { count })
3771
+ };
3772
+ }
3773
+ /** Bind the card dictionary to the terminal primitive's chrome labels. */
3774
+ function terminalBlockLabels(t) {
3775
+ return {
3776
+ signal: (signal) => t("terminal.signal", { signal }),
3777
+ exitCode: (exitCode) => t("terminal.exitCode", { code: exitCode }),
3778
+ running: t("terminal.running"),
3779
+ failed: t("terminal.failed"),
3780
+ done: t("terminal.done"),
3781
+ copy: t("copy"),
3782
+ copied: t("copied"),
3783
+ noOutput: t("terminal.noOutput"),
3784
+ collapseAria: t("collapseAria"),
3785
+ collapse: t("collapse"),
3786
+ expandAria: (hidden) => t("expandAria", { count: hidden }),
3787
+ expand: (hidden) => t("expandRest", { count: hidden })
3788
+ };
3789
+ }
3790
+ //#endregion
3791
+ //#region src/web/client/ToolCardShell.tsx
3792
+ /**
3793
+ * The shared card shell every row renders through.
3794
+ *
3795
+ * The shipped `ToolRow` is not reusable from a plugin (ui-tool is not part of
3796
+ * the client platform baseline), so this is our own: one 24px header line
3797
+ * (state dot + title + separator + ellipsized summary + optional `+N -M`
3798
+ * suffix + clickable path), a keyboard-reachable disclosure, and a body that
3799
+ * is either the card a row built or the flattened result text.
3800
+ *
3801
+ * Two properties matter more than the visuals:
3802
+ * - **Never throw.** Every value is coerced defensively, and {@link CardBoundary}
3803
+ * turns a crash inside a card into the same generic shell, because an
3804
+ * exception reaching the slot retires the cell and abdicates the call to the
3805
+ * shipped row.
3806
+ * - **Theme tokens only.** The sheet below reads `--dsw-alias-*` (plus the two
3807
+ * published content-font vars) and hard-codes no colour, so light and dark
3808
+ * both come out right.
3809
+ * @module @xiaoso/dsh-tool-plus/web/client/ToolCardShell
3810
+ */
3811
+ /** Chrome CSS, keyed by `data-plugin-css` and injected once per page. */
3812
+ const CSS = `
3813
+ .twc-root{display:flex;flex-direction:column;min-width:0}
3814
+ .twc-row{position:relative;min-width:0}
3815
+ .twc-leading{flex-shrink:0}
3816
+ .twc-chevron{color:var(--dsw-alias-label-secondary)}
3817
+ .twc-title{font-weight:400}
3818
+ .twc-sep{flex:none;width:2px;height:2px;border-radius:1px;margin:0 8px;background:var(--dsw-alias-label-caption)}
3819
+ .twc-summary{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;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)}
3820
+ .twc-summarySuffix{flex:none;margin-left:4px;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)}
3821
+ .twc-errorSummary{color:var(--dsw-alias-state-error-primary)}
3822
+ .twc-fileLink{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin:0;padding:0;border:none;background:none;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-decoration-thickness:1px;text-underline-offset:3px;cursor:pointer;border-radius:4px}
3823
+ .twc-fileLink:hover{color:var(--dsw-alias-label-primary);text-decoration-color:currentColor}
3824
+ .twc-fileLink:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
3825
+ .twc-bodyWrap{display:flex;flex-direction:column;min-width:0}
3826
+ .twc-body{margin:4px 0 4px 4px;min-width:0}
3827
+ .twc-text{margin:4px 0 4px 4px;padding:10px 14px;max-height:260px;overflow:auto;border-radius:12px;background:var(--dsw-alias-markdown-code-block);color:var(--dsw-alias-label-secondary);font-family:var(--ds-font-family-code);font-size:12px;line-height:18px;white-space:pre-wrap;word-break:break-word}
3828
+ .twc-note{margin:4px 0 4px 4px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
3829
+ .twc-media{display:flex;flex-direction:column;align-items:flex-start;gap:8px;margin:4px 0 4px 4px;min-width:0}
3830
+ /* Image gallery: the wrapping group the message gallery uses, so a card's
3831
+ images and a message's images wrap the same way. */
3832
+ .twc-imgGallery{display:flex;flex-wrap:wrap;gap:10px;max-width:100%;min-width:0}
3833
+ /* Thumbnail frame (figma message-image rule): a 16px-radius tile that crops
3834
+ with object-fit cover, sized inline for a lone image and 64×64 in a gallery. */
3835
+ .twc-imgFrame{display:grid;flex:0 0 auto;place-items:center;box-sizing:border-box;min-width:44px;min-height:44px;padding:0;overflow:hidden;border:0.5px solid var(--dsw-alias-border-l2-darkmode-thin);border-radius:16px;background:var(--dsw-alias-interactive-bg-hover);cursor:zoom-in}
3836
+ .twc-imgFrame[data-variant='tile']{width:64px;height:64px;min-width:64px;min-height:64px}
3837
+ .twc-imgFrame:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
3838
+ .twc-mediaImg{display:block;width:100%;height:100%;object-fit:cover}
3839
+ .twc-imgLoading{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
3840
+ /* Failed load: the frame itself becomes the retry control (the official
3841
+ behaviour), so a transient failure costs one click instead of a reload. It
3842
+ carries the frame's own box model — border-box, same 44px floor — so the
3843
+ reserved box survives loading → loaded → failed unchanged (no jump). */
3844
+ .twc-imgError{box-sizing:border-box;min-width:44px;min-height:44px;max-width:240px;padding:10px 12px;overflow:hidden;border:0.5px solid var(--dsw-alias-border-l2-darkmode-thin);border-radius:10px;background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-label-tertiary);font:inherit;font-size:12px;line-height:18px;text-align:left;cursor:pointer}
3845
+ .twc-imgError[data-variant='tile']{width:64px;height:64px;min-width:64px;min-height:64px;padding:4px;border-radius:16px}
3846
+ .twc-imgError:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
3847
+ /* Original-image lightbox: a body portal, a mask on its own layer (blurring
3848
+ the backdrop itself would blur the preview and the close control), and the
3849
+ preview stacked with its caption so the two always read as one object. */
3850
+ .twc-lightbox{position:fixed;inset:0;z-index:1000;display:grid;place-items:center;padding:40px}
3851
+ .twc-lightboxMask{position:absolute;inset:0;background:var(--dsw-alias-bg-mask-1);backdrop-filter:var(--dsw-mask-blur)}
3852
+ .twc-lightboxStack{position:relative;display:flex;flex-direction:column;align-items:center;gap:10px;max-height:calc(100vh - 80px);min-width:0}
3853
+ .twc-lightboxImg{max-width:min(100%,1600px);max-height:calc(100vh - 130px);object-fit:contain;border-radius:12px;background:var(--dsw-specific-input-major);box-shadow:var(--dsw-shadow-lv3)}
3854
+ .twc-lightboxClose{position:fixed;top:20px;right:20px;z-index:1;display:grid;place-items:center;width:36px;height:36px;border:0.5px solid var(--dsw-alias-border-l2-darkmode-thin);border-radius:999px;background:var(--dsw-specific-input-major);color:var(--dsw-alias-label-primary);cursor:pointer}
3855
+ .twc-lightboxClose:hover{background:var(--dsw-alias-interactive-bg-hover-solid)}
3856
+ .twc-lightboxClose:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
3857
+ /* The caption carries the reference's own numbers — name, intrinsic size,
3858
+ byte length — so the enlarged view states what the file actually is. It sits
3859
+ in the stack under the image: fixed to the viewport it read as a separate
3860
+ object from a centred image, and a tall image could squeeze it out. */
3861
+ .twc-lightboxCaption{margin:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:5px 14px;border:0.5px solid var(--dsw-alias-border-l2-darkmode-thin);border-radius:999px;background:var(--dsw-specific-input-major);color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
3862
+ /* Motion is feedback, never decoration: the hover lift, the skeleton pulse,
3863
+ and the lightbox entrance all sit behind the user's motion preference. A
3864
+ reduced-motion reader keeps every state change, just without the glide. */
3865
+ .twc-imgFrame:hover{box-shadow:var(--dsw-shadow-lv1-blur)}
3866
+ @media (prefers-reduced-motion: no-preference){
3867
+ .twc-imgFrame{transition:transform 160ms ease,box-shadow 160ms ease}
3868
+ .twc-imgFrame:hover{transform:scale(1.01)}
3869
+ .twc-imgFrame:active{transform:scale(0.99)}
3870
+ .twc-imgLoading{animation:twc-img-pulse 1.6s ease-in-out infinite}
3871
+ .twc-lightbox{animation:twc-lightbox-in 160ms ease-out}
3872
+ .twc-lightboxImg{animation:twc-lightbox-img-in 180ms ease-out}
3873
+ @keyframes twc-img-pulse{0%,100%{opacity:.55}50%{opacity:1}}
3874
+ @keyframes twc-lightbox-in{from{opacity:0}to{opacity:1}}
3875
+ @keyframes twc-lightbox-img-in{from{opacity:0;transform:scale(.98)}to{opacity:1;transform:scale(1)}}
3876
+ }
3877
+ .twc-inspect{display:inline-flex;align-self:flex-start;align-items:center;gap:4px;margin:4px 0 2px 4px;padding:2px 8px;border:0.5px solid var(--dsw-alias-border-l3);border-radius:999px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;line-height:16px;cursor:pointer;opacity:0;transition:opacity 100ms ease}
3878
+ .twc-root:hover .twc-inspect,.twc-inspect:focus-visible{opacity:1}
3879
+ .twc-inspect:hover{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-label-primary)}
3880
+ .twc-visuallyHidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
3881
+ .twc-fallbackDot{flex:none;width:8px;height:8px;border-radius:999px;background:var(--dsw-alias-state-error-primary)}
3882
+ @media (prefers-reduced-motion: reduce){.twc-inspect{transition:none}}
3883
+ `;
3884
+ /** Inject the card stylesheet once per page; the loader drops plugin style tags on unload. */
3885
+ let cssInjected = false;
3886
+ function injectCss() {
3887
+ if (cssInjected) return;
3888
+ if (typeof document === "undefined") return;
3889
+ cssInjected = true;
3890
+ const id = "tool-plus-cards";
3891
+ if (document.querySelector("style[data-plugin-css=\"tool-plus-cards\"]") !== null) return;
3892
+ const tag = document.createElement("style");
3893
+ tag.dataset.plugin = "@xiaoso/dsh-tool-plus";
3894
+ tag.dataset.pluginCss = id;
3895
+ tag.textContent = CSS;
3896
+ document.head.appendChild(tag);
3897
+ }
3898
+ /** Whether a React node would draw anything. */
3899
+ function hasContent(node) {
3900
+ return node !== void 0 && node !== null && node !== false;
3901
+ }
3902
+ /**
3903
+ * Render the shared card shell.
3904
+ * @param props - see {@link ToolCardShellProps}.
3905
+ * @returns the card row and, when expanded, its body.
3906
+ */
3907
+ function ToolCardShell(props) {
3908
+ injectCss();
3909
+ const { t, state, title, children, filePath, openFile, inspect } = props;
3910
+ const [expanded, setExpanded] = (0, react.useState)(props.defaultOpen === true);
3911
+ const bodyCard = hasContent(children) ? children : null;
3912
+ const bodyText = typeof props.text === "string" && props.text !== "" ? props.text : null;
3913
+ const expandable = bodyCard !== null || bodyText !== null;
3914
+ const open = expanded && expandable;
3915
+ const status = stateStatus(state, t);
3916
+ const summaryText = typeof props.summary === "string" ? props.summary : "";
3917
+ const suffix = typeof props.summarySuffix === "string" && props.summarySuffix !== "" ? props.summarySuffix : null;
3918
+ const fileLink = filePath !== void 0 && filePath !== null && filePath !== "" && openFile !== void 0;
3919
+ const errorTone = props.summaryTone === "error";
3920
+ const toggle = () => {
3921
+ setExpanded((value) => !value);
3922
+ };
3923
+ const openPath = (event) => {
3924
+ event.stopPropagation();
3925
+ if (!fileLink || openFile === void 0) return;
3926
+ const line = props.filePathLine;
3927
+ if (typeof line === "number") openFile(filePath, { line });
3928
+ else openFile(filePath);
3929
+ };
3930
+ const openPathKeyDown = (event) => {
3931
+ if (event.key === "Enter" || event.key === " ") event.stopPropagation();
3932
+ };
3933
+ const collapsed = summaryText === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3934
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3935
+ className: "twc-sep",
3936
+ "aria-hidden": true
3937
+ }),
3938
+ fileLink ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3939
+ type: "button",
3940
+ className: "twc-fileLink",
3941
+ onClick: openPath,
3942
+ onKeyDown: openPathKeyDown,
3943
+ children: summaryText
3944
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3945
+ className: errorTone ? "twc-summary twc-errorSummary" : "twc-summary",
3946
+ children: summaryText
3947
+ }),
3948
+ suffix !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3949
+ className: "twc-summarySuffix",
3950
+ children: suffix
3951
+ })
3952
+ ] });
3953
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3954
+ className: "twc-root",
3955
+ "data-state": state,
3956
+ "data-tool": title,
3957
+ children: [status !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3958
+ className: "twc-visuallyHidden",
3959
+ children: status
3960
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
3961
+ rowClassName: "twc-row",
3962
+ leadingClassName: "twc-leading",
3963
+ titleClassName: "twc-title",
3964
+ chevronClassName: "twc-chevron",
3965
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: dotState(state) }),
3966
+ title,
3967
+ open,
3968
+ expandable,
3969
+ expandOnRowClick: true,
3970
+ keepContentWhenOpen: true,
3971
+ onToggle: toggle,
3972
+ collapsedContent: collapsed,
3973
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3974
+ className: "twc-bodyWrap",
3975
+ children: [bodyCard ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
3976
+ className: "twc-text",
3977
+ children: bodyText
3978
+ }), inspect !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3979
+ type: "button",
3980
+ className: "twc-inspect",
3981
+ onClick: inspect,
3982
+ children: t("inspect")
3983
+ })]
3984
+ })
3985
+ })]
3986
+ });
3987
+ }
3988
+ /**
3989
+ * The generic card: every row's degradation path.
3990
+ *
3991
+ * A card is only drawn once its metadata passed a `narrow*` guard, so anything
3992
+ * else — a running call, a window-truncated call, a Code Dispatch child, a
3993
+ * failure, a payload from another plugin version — lands here with the call's
3994
+ * own title, run state, argument count, and result text.
3995
+ * @param props - see {@link GenericCardProps}.
3996
+ * @returns the generic shell.
3997
+ */
3998
+ function GenericCard({ t, title, block, args, inspect }) {
3999
+ const state = cardState(block);
4000
+ const output = resultText(block);
4001
+ const failure = state === "error" ? firstLine(output) : "";
4002
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
4003
+ t,
4004
+ state,
4005
+ title,
4006
+ summary: failure === "" ? argsSummary(args, t) : failure,
4007
+ summaryTone: failure === "" ? "default" : "error",
4008
+ text: output,
4009
+ inspect
4010
+ });
4011
+ }
4012
+ /** Best-effort message for a thrown value; never throws itself. */
4013
+ function failureText(error) {
4014
+ try {
4015
+ if (error instanceof Error && typeof error.message === "string" && error.message !== "") return error.message;
4016
+ if (typeof error === "string" && error !== "") return error;
4017
+ } catch {}
4018
+ return "Unknown card error";
4019
+ }
4020
+ /**
4021
+ * Contain a card's render failure: the fallback is a generic shell (never the
4022
+ * card's own chrome), so one malformed payload cannot take the call's cell —
4023
+ * or the whole slot — down with it.
4024
+ */
4025
+ var CardBoundary = class extends react.Component {
4026
+ state = { failure: null };
4027
+ /** React's error-boundary hook: record the failure and render the fallback. */
4028
+ static getDerivedStateFromError(error) {
4029
+ return { failure: failureText(error) };
4030
+ }
4031
+ /** Report the failure without rethrowing. */
4032
+ componentDidCatch(error) {
4033
+ console.error("[tool-plus] a web tool card failed to render; showing the generic card instead.", error);
4034
+ }
4035
+ render() {
4036
+ const { failure } = this.state;
4037
+ if (failure === null) return this.props.children;
4038
+ const { t, title, block, summary } = this.props;
4039
+ let text = null;
4040
+ try {
4041
+ const output = block === void 0 ? "" : resultText(block);
4042
+ text = output === "" ? null : output;
4043
+ } catch {
4044
+ text = null;
4045
+ }
4046
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
4047
+ t,
4048
+ state: "error",
4049
+ title,
4050
+ summary: failure === "" ? summary ?? null : firstLine(failure),
4051
+ summaryTone: "error",
4052
+ text
4053
+ });
4054
+ }
4055
+ };
4056
+ //#endregion
4057
+ //#region src/web/client/rows/command-card-facts.ts
4058
+ /**
4059
+ * Collapsed-fact builders for the two command-shaped cards: the command a
4060
+ * `bash` description hides, and the rewrite rule an `ast_edit` preview only
4061
+ * shows once expanded.
4062
+ *
4063
+ * Both cards collapse to one head line. `bash`'s head used to be the model's
4064
+ * own `description` — a sentence about intent, never the command that ran — so
4065
+ * the only way to learn what executed was to expand the card. `ast_edit`'s head
4066
+ * names the file and the replacement count, and its body previews the *result*:
4067
+ * the rule that produced that result lives only in the call's `ops` argument,
4068
+ * which no card showed at all. Each builder recovers one fact from the call's
4069
+ * own arguments, so nothing is inferred from output, and a call head that fell
4070
+ * outside the session window degrades to the facts the card already holds.
4071
+ * @module @xiaoso/dsh-tool-plus/web/client/rows/command-card-facts
4072
+ */
4073
+ /** Whether `value` is a plain object — the only op shape the engine sends. */
4074
+ function isRecord$1(value) {
4075
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4076
+ }
4077
+ /**
4078
+ * The `bash` card's collapsed summary: what ran, placed after the sentence
4079
+ * saying why.
4080
+ *
4081
+ * The command is the fact the head must carry, so the description can never be
4082
+ * the whole summary — it is a prefix at most. Only its first line is used: a
4083
+ * summary is one row high, and the rest of a multi-line command belongs to the
4084
+ * expanded body, which draws the whole thing.
4085
+ * @param description - the model's one-line intent, or `null` when absent.
4086
+ * @param command - the command actually executed (never empty).
4087
+ * @param outcome - the interruption word (`timedOut` / `cancelled`), or `null`.
4088
+ * @returns the outcome alone when the run was interrupted; otherwise the
4089
+ * description joined to the command's first line, or that line alone.
4090
+ */
4091
+ function bashCardSummary(description, command, outcome) {
4092
+ if (outcome !== null && outcome !== "") return outcome;
4093
+ const intent = description === null ? "" : description.trim();
4094
+ const line = firstLine(command);
4095
+ return intent === "" ? line : `${intent} · ${line}`;
4096
+ }
4097
+ /**
4098
+ * The `ast_edit` card's rule line: the first usable `ops` entry, written the
4099
+ * way the tool's own `ops` argument writes it (`pat → out`).
4100
+ *
4101
+ * Only the first rule is stated, with the count of the entries behind it: the
4102
+ * card exists to preview one change, and a call may carry many rules whose
4103
+ * patterns are whole AST fragments — listing them would bury the preview. The
4104
+ * remaining count is the array tail, not the number of *usable* entries, so it
4105
+ * always describes the call as it was made.
4106
+ * @param args - the call's parsed arguments, or `null` when its head is gone.
4107
+ * @param t - this plugin's card translate seat.
4108
+ * @returns the rule line, or `null` when the call carries no usable rule.
4109
+ */
4110
+ function astEditRuleLine(args, t) {
4111
+ const ops = args?.ops;
4112
+ if (!Array.isArray(ops)) return null;
4113
+ for (let index = 0; index < ops.length; index += 1) {
4114
+ const op = ops[index];
4115
+ if (!isRecord$1(op)) continue;
4116
+ const pat = argText(op, "pat");
4117
+ if (pat === null) continue;
4118
+ const replacement = op.out;
4119
+ if (typeof replacement !== "string") continue;
4120
+ const rest = ops.length - 1;
4121
+ const rule = `${t("astEdit.rules")}: ${pat} →${replacement === "" ? "" : ` ${replacement}`}`;
4122
+ return rest === 0 ? rule : `${rule} · ${t("astEdit.rulesRest", { count: rest })}`;
4123
+ }
4124
+ return null;
4125
+ }
4126
+ //#endregion
4127
+ //#region src/web/client/rows/AstEditRow.tsx
4128
+ /**
4129
+ * `ast_edit` row: the engine's own change preview.
4130
+ *
4131
+ * The AST engine already renders a grouped preview (file header, replacement
4132
+ * count, the first line of each change) and hands it to the Host as
4133
+ * `displayContent`; that text is what this card shows, because the exact
4134
+ * before/after hunks are dropped inside the ported tool and the write happens
4135
+ * in the native engine. No diff is computed here, and no `±` totals are shown.
4136
+ * @module @xiaoso/dsh-tool-plus/web/client/rows/AstEditRow
4137
+ */
4138
+ /** The engine reports parse failures in its result text, not in the metadata. */
4139
+ const PARSE_ERROR = /parse error/i;
4140
+ /**
4141
+ * Render an `ast_edit` call as a change preview.
4142
+ * @param props - the slot's owner props and this plugin's translate seat.
4143
+ * @returns the preview card, or the generic shell when there is no preview.
4144
+ */
4145
+ function AstEditRow(props) {
4146
+ const { t, block } = props;
4147
+ const title = cardTitle(props.toolName, t, "title.astEdit");
4148
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBoundary, {
4149
+ t,
4150
+ title,
4151
+ block,
4152
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AstEditCard, {
4153
+ ...props,
4154
+ title
4155
+ })
4156
+ });
4157
+ }
4158
+ /**
4159
+ * The card itself, inside the boundary.
4160
+ * @param props - row props plus the resolved title.
4161
+ * @returns the preview card.
4162
+ */
4163
+ function AstEditCard(props) {
4164
+ const { t, block, cwd, inspect, openFile, title } = props;
4165
+ const args = parseCardArgs(block);
4166
+ const meta = narrowAstEditCardMeta(cardMeta(block));
4167
+ if (isSubCall(block) || block.isError === true || meta === null || meta.preview === "") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenericCard, {
4168
+ t,
4169
+ title,
4170
+ block,
4171
+ args,
4172
+ inspect
4173
+ });
4174
+ const files = meta.files.length;
4175
+ const single = files === 1 ? meta.files[0]?.path : void 0;
4176
+ const output = resultText(block);
4177
+ const parseErrors = PARSE_ERROR.test(output);
4178
+ const rule = astEditRuleLine(args, t);
4179
+ const summary = single !== void 0 && single !== "" ? displayPath(single, cwd) : t("astEdit.files", { count: files });
4180
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
4181
+ t,
4182
+ state: cardState(block),
4183
+ title,
4184
+ summary,
4185
+ summarySuffix: `${t("astEdit.replacements", { count: meta.replacements })} · ${t("astEdit.files", { count: files })}`,
4186
+ filePath: single,
4187
+ openFile,
4188
+ inspect,
4189
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4190
+ className: "twc-body",
4191
+ children: [
4192
+ rule !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4193
+ className: "twc-note",
4194
+ children: rule
4195
+ }),
4196
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.CodeBlock, {
4197
+ code: meta.preview,
4198
+ copyLabel: t("copy"),
4199
+ copiedLabel: t("copied")
4200
+ }),
4201
+ output !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4202
+ className: "twc-note",
4203
+ children: parseErrors ? `${t("astEdit.parseErrors")}: ${output}` : output
4204
+ })
4205
+ ]
4206
+ })
4207
+ });
4208
+ }
4209
+ //#endregion
4210
+ //#region src/web/client/rows/FileMutationRow.tsx
4211
+ /**
4212
+ * `edit` row: a diff card.
4213
+ *
4214
+ * Two sources, in order: the Host projection (`meta.diffs`, built from the
4215
+ * engine's own unified diff — the change is already computed and needs no
4216
+ * extra I/O), then the replace-form arguments while the call is still running
4217
+ * (or when the Host could not project a diff). Every other edit mode (patch,
4218
+ * hashline, apply_patch) has no diff until it settles, so it falls back to the
4219
+ * generic shell rather than guessing from `patch`/`input` text.
4220
+ * @module @xiaoso/dsh-tool-plus/web/client/rows/FileMutationRow
4221
+ */
4222
+ /**
4223
+ * Render an `edit` call as a diff card.
4224
+ * @param props - the slot's owner props and this plugin's translate seat.
4225
+ * @returns the diff card, or the generic shell for a mode with no diff yet.
4226
+ */
4227
+ function FileMutationRow(props) {
4228
+ const { t, block } = props;
4229
+ const title = cardTitle(props.toolName, t, "title.edit");
4230
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBoundary, {
4231
+ t,
4232
+ title,
4233
+ block,
4234
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileMutationCard, {
4235
+ ...props,
4236
+ title
4237
+ })
4238
+ });
4239
+ }
4240
+ /**
4241
+ * The card itself, inside the boundary.
4242
+ * @param props - row props plus the resolved title.
4243
+ * @returns the diff card.
4244
+ */
4245
+ function FileMutationCard(props) {
4246
+ const { t, block, cwd, inspect, openFile, title } = props;
4247
+ const args = parseCardArgs(block);
4248
+ const meta = narrowEditCardMeta(cardMeta(block));
4249
+ const replaceForm = replaceHunks(args);
4250
+ if (isSubCall(block) || block.isError === true || meta === null && replaceForm === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenericCard, {
4251
+ t,
4252
+ title,
4253
+ block,
4254
+ args,
4255
+ inspect
4256
+ });
4257
+ const diffs = meta === null ? replaceForm ?? [] : meta.diffs;
4258
+ const paths = [...new Set(diffs.map((hunk) => hunk.path))];
4259
+ const single = paths.length === 1 ? paths[0] : void 0;
4260
+ const totals = (0, _deepseek_ai_dsh_client_ui_primitives.diffTotals)(diffs);
4261
+ const summary = single === void 0 ? t("files.other", { count: paths.length }) : displayPath(single, cwd);
4262
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
4263
+ t,
4264
+ state: cardState(block),
4265
+ title,
4266
+ summary,
4267
+ summarySuffix: `+${totals.added} -${totals.removed}`,
4268
+ filePath: single,
4269
+ openFile,
4270
+ inspect,
4271
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DiffBlock, {
4272
+ diffs,
4273
+ labels: diffBlockLabels(t),
4274
+ className: "twc-body"
4275
+ })
4276
+ });
4277
+ }
4278
+ /**
4279
+ * The replace form's diff, straight from the call's arguments: one hunk for
4280
+ * `old_string` → `new_string`. The empty `new_string` is a real value (it
4281
+ * deletes), so only the path and `old_string` are required to be non-empty.
4282
+ * @param args - the call's parsed arguments, or `null`.
4283
+ * @returns one hunk, or `null` when the call is not the replace form.
4284
+ */
4285
+ function replaceHunks(args) {
4286
+ const path = argText(args, "file_path");
4287
+ const oldText = argText(args, "old_string");
4288
+ const newText = args?.new_string;
4289
+ if (path === null || oldText === null || typeof newText !== "string") return null;
4290
+ return [{
4291
+ path,
4292
+ oldText,
4293
+ newText
4294
+ }];
4295
+ }
4296
+ //#endregion
4297
+ //#region src/web/client/rows/image-card.ts
4298
+ /** Whether `value` is a plain object. */
4299
+ function isRecord(value) {
4300
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4301
+ }
4302
+ /** A finite number — a `NaN` dimension would only break layout later. */
4303
+ function isFiniteNumber(value) {
4304
+ return typeof value === "number" && Number.isFinite(value);
4305
+ }
4306
+ /**
4307
+ * Display box for a lone image — DSH's own rule, copied from
4308
+ * `ui-attachment/src/MessageImage.tsx::singleFit` so a tool-card image and a
4309
+ * message image of the same file occupy the same box: long edge 240px, the
4310
+ * rendered aspect ratio clamped to [0.25, 4] (the clamped overflow is cropped
4311
+ * by `object-fit: cover`), never upscaled past the natural size, and the crop
4312
+ * anchored to the top of very tall images / the left of very wide ones, where
4313
+ * the informative content usually starts.
4314
+ *
4315
+ * The clamp is deliberately not strict: an exactly 4:1 image stays centred.
4316
+ * @param size - the reference's intrinsic dimensions, in pixels.
4317
+ * @returns the rendered box and the crop anchor.
4318
+ */
4319
+ function imageFitBox(size) {
4320
+ const natural = size.width / size.height;
4321
+ const ratio = Math.min(4, Math.max(.25, natural));
4322
+ const box = ratio >= 1 ? {
4323
+ width: 240,
4324
+ height: 240 / ratio
4325
+ } : {
4326
+ width: 240 * ratio,
4327
+ height: 240
4328
+ };
4329
+ const scale = Math.min(1, size.width / box.width, size.height / box.height);
4330
+ return {
4331
+ width: Math.max(1, Math.round(box.width * scale)),
4332
+ height: Math.max(1, Math.round(box.height * scale)),
4333
+ objectPosition: natural < .25 ? "center top" : natural > 4 ? "left center" : "center"
4334
+ };
4335
+ }
4336
+ /**
4337
+ * The call's arguments.
4338
+ *
4339
+ * A settled node carries the paired call head under `call`, and that is the
4340
+ * only source: the node-level `argsRaw` belongs to a *running* call, so reading
4341
+ * it here would let a truncated settled call (head outside the window) render a
4342
+ * card whose argument summary is empty. `parseCardArgs` narrows the same field
4343
+ * the same way.
4344
+ * @param block - the settled call node.
4345
+ * @returns the parsed argument object, or `null`.
4346
+ */
4347
+ function callArgs(block) {
4348
+ const raw = block.call?.argsRaw;
4349
+ if (typeof raw !== "string") return null;
4350
+ try {
4351
+ const value = JSON.parse(raw);
4352
+ return isRecord(value) ? value : null;
4353
+ } catch {
4354
+ return null;
4355
+ }
4356
+ }
4357
+ /**
4358
+ * Validate one wire attachment object as a durable image reference.
4359
+ *
4360
+ * The store's `ImageAttachmentRef` brands `attachmentId` and `mediaType`; the
4361
+ * result's JSON carries plain strings. This is the single boundary where a
4362
+ * validated wire object becomes that typed reference, field by field — and the
4363
+ * copy keeps the optional fields (`name`, `originalDimensions`) the wire may
4364
+ * carry.
4365
+ * @param value - the image block's `attachment` field.
4366
+ * @returns the typed reference, or `null` when any required field is wrong.
4367
+ */
4368
+ function typedAttachment(value) {
4369
+ if (!isRecord(value)) return null;
4370
+ if (typeof value.attachmentId !== "string") return null;
4371
+ if (typeof value.mediaType !== "string") return null;
4372
+ if (!isFiniteNumber(value.bytes)) return null;
4373
+ if (!isFiniteNumber(value.width)) return null;
4374
+ if (!isFiniteNumber(value.height)) return null;
4375
+ if (value.width <= 0 || value.height <= 0) return null;
4376
+ return { ...value };
4377
+ }
4378
+ /**
4379
+ * Derive the read card's image material from a settled result.
4380
+ *
4381
+ * Declines — rather than guessing — whenever the result would not be fully
4382
+ * representable: a running call, a failed call, a result whose content carries
4383
+ * a block that is neither text nor image, an image whose reference is
4384
+ * malformed, a text-only result (the text card owns those), or a call whose
4385
+ * arguments name no path.
4386
+ * @param block - the frozen call node.
4387
+ * @param cwd - the session workspace root, for shortening the label.
4388
+ * @returns the material, or `null` to leave the row on its existing path.
4389
+ */
4390
+ function imageCardMaterial(block, cwd) {
4391
+ if (!isSettled(block) || block.isError === true) return null;
4392
+ const content = block.content;
4393
+ if (!Array.isArray(content)) return null;
4394
+ const images = [];
4395
+ for (const blockContent of content) {
4396
+ if (!isRecord(blockContent)) return null;
4397
+ if (blockContent.type === "text") continue;
4398
+ if (blockContent.type !== "image") return null;
4399
+ const image = typedAttachment(blockContent.attachment);
4400
+ if (image === null) return null;
4401
+ images.push(image);
4402
+ }
4403
+ if (images.length === 0) return null;
4404
+ const path = callArgs(block)?.path;
4405
+ if (typeof path !== "string" || path.trim() === "") return null;
4406
+ return {
4407
+ label: displayPath(path, cwd),
4408
+ path,
4409
+ images
4410
+ };
4411
+ }
4412
+ //#endregion
4413
+ //#region src/web/client/rows/read-card-facts.ts
4414
+ /**
4415
+ * Collapsed-summary facts for the `read` card.
4416
+ *
4417
+ * The Host projection strips the inline selector off the path
4418
+ * (`stripReadSelector` in `web/host/read.ts`) because the card's *link* must
4419
+ * point at a real file, and it keeps only the file's own window. The collapsed
4420
+ * head is then the one place where the question the model actually asked is
4421
+ * still readable — and the projected path cannot answer it: a windowed read of
4422
+ * `package.json:22-25,31-34` and a whole-file read of `package.json` project to
4423
+ * the very same string, so the card hides the difference.
4424
+ *
4425
+ * The summary is therefore built from the call's own argument, selector and
4426
+ * all, with the projected path kept as the fallback for a result whose call
4427
+ * head fell outside the session window (a truncated window carries no
4428
+ * `tool/call` event, so no argument survives). That fallback states a fact the
4429
+ * card already holds; nothing is invented.
4430
+ * @module @xiaoso/dsh-tool-plus/web/client/rows/read-card-facts
4431
+ */
4432
+ /**
4433
+ * The `read` card's collapsed summary: the path the model asked for, keeping
4434
+ * whatever inline selector it carried (`:22-25`, `:raw`, `:conflicts`, `:28-`,
4435
+ * a sqlite row, an archive member) and shortened against the workspace root.
4436
+ *
4437
+ * The selector is deliberately not validated against the read grammar: the head
4438
+ * reports the call verbatim, and a selector this plugin does not recognize is
4439
+ * still what the model typed — substituting the projection's stripped path
4440
+ * would erase the evidence that two calls differ.
4441
+ * @param args - the call's parsed arguments, or `null` when its head is gone.
4442
+ * @param metaPath - the Host-projected path (already selector-free).
4443
+ * @param cwd - the session workspace root, when known.
4444
+ * @returns the workspace-shortened argument path, or the projected path.
4445
+ */
4446
+ function readCardSummary(args, metaPath, cwd) {
4447
+ return displayPath(argText(args, "path") ?? metaPath, cwd);
4448
+ }
4449
+ //#endregion
4450
+ //#region src/web/client/rows/ReadRow.tsx
4451
+ /**
4452
+ * `read` row: a line-numbered window view, or the image the read returned.
4453
+ *
4454
+ * Two mutually exclusive bodies hang off one call:
4455
+ * - The Host projected a readable window (`meta`) → the line-numbered text card.
4456
+ * - It did not (an image, a directory, a sqlite or archive target, an internal
4457
+ * URL, a multi-range `:raw` read) → this row tries the image branch, which
4458
+ * draws the result's image blocks through the session-authorized loader the
4459
+ * slot hands every tool view. Anything else keeps the generic shell and its
4460
+ * result text.
4461
+ *
4462
+ * The image branch reimplements the shipped message-image interaction — the
4463
+ * `singleFit` box, 64px gallery tiles, a retry control on a failed load, and a
4464
+ * body-portal lightbox for the original — so a tool-card image and a message
4465
+ * image of the same file behave identically. It lives here rather than in
4466
+ * `image-card.ts` because that module stays pure: this file owns the loader
4467
+ * calls, their cancellation, and the DOM.
4468
+ * @module @xiaoso/dsh-tool-plus/web/client/rows/ReadRow
4469
+ */
4470
+ /**
4471
+ * Render a `read` call as a line-numbered window or an image.
4472
+ * @param props - the slot's owner props and this plugin's translate seat.
4473
+ * @returns the read card, or the generic shell when nothing else fits.
4474
+ */
4475
+ function ReadRow(props) {
4476
+ const { t, block } = props;
4477
+ const title = cardTitle(props.toolName, t, "title.read");
4478
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBoundary, {
4479
+ t,
4480
+ title,
4481
+ block,
4482
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReadCard, {
4483
+ ...props,
4484
+ title
4485
+ })
4486
+ });
4487
+ }
4488
+ /**
4489
+ * The card itself, inside the boundary.
4490
+ * @param props - row props plus the resolved title.
4491
+ * @returns the read card.
4492
+ */
4493
+ function ReadCard(props) {
4494
+ const { t, block, cwd, inspect, openFile, title } = props;
4495
+ const args = parseCardArgs(block);
4496
+ const meta = narrowReadCardMeta(cardMeta(block));
4497
+ const material = (0, react.useMemo)(() => imageCardMaterial(block, cwd), [block, cwd]);
4498
+ if (isSubCall(block) || block.isError === true) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenericCard, {
4499
+ t,
4500
+ title,
4501
+ block,
4502
+ args,
4503
+ inspect
4504
+ });
4505
+ if (meta === null) {
4506
+ if (material === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenericCard, {
4507
+ t,
4508
+ title,
4509
+ block,
4510
+ args,
4511
+ inspect
4512
+ });
4513
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageReadCard, {
4514
+ t,
4515
+ block,
4516
+ material,
4517
+ title,
4518
+ inspect,
4519
+ openFile,
4520
+ loadImage: props.loadImage
4521
+ });
4522
+ }
4523
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
4524
+ t,
4525
+ state: cardState(block),
4526
+ title,
4527
+ summary: readCardSummary(args, meta.path, cwd),
4528
+ filePath: meta.path,
4529
+ filePathLine: meta.offset,
4530
+ openFile,
4531
+ inspect,
4532
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ReadBlock, {
4533
+ label: displayPath(meta.path, cwd),
4534
+ lines: meta.lines,
4535
+ totalLines: meta.totalLines,
4536
+ lang: meta.lang,
4537
+ labels: readBlockLabels(t),
4538
+ className: "twc-body"
4539
+ })
4540
+ });
4541
+ }
4542
+ /**
4543
+ * Ask the loader's synchronous cache for an already-known URL.
4544
+ * @param loader - the host's image loader.
4545
+ * @param attachment - the reference to resolve.
4546
+ * @returns the cached URL, or `null` when there is none.
4547
+ */
4548
+ function peekUrl(loader, attachment) {
4549
+ if (loader?.peek === void 0) return null;
4550
+ try {
4551
+ const url = loader.peek(attachment);
4552
+ return typeof url === "string" && url !== "" ? url : null;
4553
+ } catch {
4554
+ return null;
4555
+ }
4556
+ }
4557
+ /** Whether two per-image states are the same, so state identity can be kept. */
4558
+ function sameImageState(left, right) {
4559
+ return left.url === right.url && left.failed === right.failed;
4560
+ }
4561
+ /**
4562
+ * The image body: one framed thumbnail per reference over the result's own
4563
+ * envelope text.
4564
+ *
4565
+ * A rejected load or a missing loader leaves that image's frame as a retry
4566
+ * control and the text in place: an image card that cannot show its picture
4567
+ * must still show that the call returned one.
4568
+ * @param props - see {@link ImageReadCardProps}.
4569
+ * @returns the image card.
4570
+ */
4571
+ function ImageReadCard({ t, block, material, title, inspect, openFile, loadImage }) {
4572
+ const images = material.images;
4573
+ const text = resultText(block);
4574
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ToolCardShell, {
4575
+ t,
4576
+ state: cardState(block),
4577
+ title,
4578
+ summary: material.label,
4579
+ filePath: material.path,
4580
+ openFile,
4581
+ inspect,
4582
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4583
+ className: "twc-media",
4584
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4585
+ className: "twc-imgGallery",
4586
+ children: images.map((image, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardImage, {
4587
+ t,
4588
+ image,
4589
+ single: images.length === 1,
4590
+ loadImage
4591
+ }, `${image.attachmentId}:${index}`))
4592
+ })
4593
+ }), text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4594
+ className: "twc-note",
4595
+ children: text
4596
+ })]
4597
+ });
4598
+ }
4599
+ /**
4600
+ * One image: a framed thumbnail that opens the original.
4601
+ *
4602
+ * The load runs under a liveness guard and is re-armed by {@link CardImage}'s
4603
+ * retry counter, so a failed load costs one click rather than a page reload.
4604
+ * @param props - see {@link CardImageProps}.
4605
+ * @returns the frame, or the retry control when the load failed.
4606
+ */
4607
+ function CardImage({ t, image, single, loadImage }) {
4608
+ const [state, setState] = (0, react.useState)(() => ({
4609
+ url: peekUrl(loadImage, image),
4610
+ failed: false
4611
+ }));
4612
+ const [attempt, setAttempt] = (0, react.useState)(0);
4613
+ const [open, setOpen] = (0, react.useState)(false);
4614
+ const close = (0, react.useCallback)(() => {
4615
+ setOpen(false);
4616
+ }, []);
4617
+ const retry = (0, react.useCallback)(() => {
4618
+ setAttempt((value) => value + 1);
4619
+ }, []);
4620
+ const frameRef = (0, react.useRef)(null);
4621
+ const focusOpener = (0, react.useCallback)(() => {
4622
+ frameRef.current?.focus();
4623
+ }, []);
4624
+ (0, react.useEffect)(() => {
4625
+ let live = true;
4626
+ setState((previous) => previous.failed ? {
4627
+ url: peekUrl(loadImage, image),
4628
+ failed: false
4629
+ } : previous);
4630
+ const load = async () => {
4631
+ if (loadImage === void 0) {
4632
+ if (live) setState((previous) => previous.failed ? previous : {
4633
+ url: null,
4634
+ failed: true
4635
+ });
4636
+ return;
4637
+ }
4638
+ try {
4639
+ const url = await loadImage(image);
4640
+ const next = typeof url === "string" && url !== "" ? {
4641
+ url,
4642
+ failed: false
4643
+ } : {
4644
+ url: null,
4645
+ failed: true
4646
+ };
4647
+ if (live) setState((previous) => sameImageState(previous, next) ? previous : next);
4648
+ } catch {
4649
+ if (live) setState((previous) => previous.failed ? previous : {
4650
+ url: null,
4651
+ failed: true
4652
+ });
4653
+ }
4654
+ };
4655
+ load();
4656
+ return () => {
4657
+ live = false;
4658
+ };
4659
+ }, [
4660
+ image,
4661
+ loadImage,
4662
+ attempt
4663
+ ]);
4664
+ const variant = single ? "single" : "tile";
4665
+ const name = image.name ?? t("image.unnamed");
4666
+ const fit = (0, react.useMemo)(() => imageFitBox(image), [image]);
4667
+ if (state.failed) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4668
+ type: "button",
4669
+ className: "twc-imgError",
4670
+ "data-variant": variant,
4671
+ style: single ? {
4672
+ width: fit.width,
4673
+ height: fit.height
4674
+ } : void 0,
4675
+ title: t("image.loadFailed"),
4676
+ "aria-label": t("image.loadFailed"),
4677
+ onClick: retry,
4678
+ children: t("image.loadFailed")
4679
+ });
4680
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4681
+ ref: frameRef,
4682
+ type: "button",
4683
+ className: "twc-imgFrame",
4684
+ "data-variant": variant,
4685
+ style: single ? {
4686
+ width: fit.width,
4687
+ height: fit.height
4688
+ } : void 0,
4689
+ title: t("image.open"),
4690
+ "aria-label": t("image.openNamed", { name }),
4691
+ onClick: () => {
4692
+ if (state.url !== null) setOpen(true);
4693
+ },
4694
+ children: state.url === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4695
+ className: "twc-imgLoading",
4696
+ children: t("image.loading")
4697
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
4698
+ className: "twc-mediaImg",
4699
+ src: state.url,
4700
+ alt: name,
4701
+ style: single ? { objectPosition: fit.objectPosition } : void 0
4702
+ })
4703
+ }), open && state.url !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardLightbox, {
4704
+ t,
4705
+ src: state.url,
4706
+ image,
4707
+ name,
4708
+ onClose: close,
4709
+ focusOpener
4710
+ })] });
4711
+ }
4712
+ /**
4713
+ * The document-level original-image preview.
4714
+ *
4715
+ * Closes on Escape, a mask press, or the close control, and restores focus to
4716
+ * the opener on unmount. Rendered through a body portal: an opener inside a
4717
+ * transformed or filtered ancestor would otherwise trap the fixed backdrop in
4718
+ * that ancestor's box instead of covering the viewport.
4719
+ * @param props - see {@link CardLightboxProps}.
4720
+ * @returns the modal preview.
4721
+ */
4722
+ function CardLightbox({ t, src, image, name, onClose, focusOpener }) {
4723
+ const closeRef = (0, react.useRef)(null);
4724
+ (0, react.useEffect)(() => {
4725
+ closeRef.current?.focus();
4726
+ const onKeyDown = (event) => {
4727
+ if (event.key === "Escape") onClose();
4728
+ };
4729
+ window.addEventListener("keydown", onKeyDown);
4730
+ return () => {
4731
+ window.removeEventListener("keydown", onKeyDown);
4732
+ focusOpener();
4733
+ };
4734
+ }, [onClose, focusOpener]);
4735
+ const caption = t("image.caption", {
4736
+ name,
4737
+ width: image.width,
4738
+ height: image.height,
4739
+ size: (0, _deepseek_ai_dsh_client_ui_primitives.fileSizeText)(image.bytes)
4740
+ });
4741
+ return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4742
+ className: "twc-lightbox",
4743
+ role: "dialog",
4744
+ "aria-modal": "true",
4745
+ "aria-label": t("image.dialog"),
4746
+ children: [
4747
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4748
+ className: "twc-lightboxMask",
4749
+ "aria-hidden": "true",
4750
+ onMouseDown: (event) => {
4751
+ event.preventDefault();
4752
+ onClose();
4753
+ }
4754
+ }),
4755
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4756
+ className: "twc-lightboxStack",
4757
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
4758
+ className: "twc-lightboxImg",
4759
+ src,
4760
+ alt: name
4761
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4762
+ className: "twc-lightboxCaption",
4763
+ children: caption
4764
+ })]
4765
+ }),
4766
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4767
+ ref: closeRef,
4768
+ type: "button",
4769
+ className: "twc-lightboxClose",
4770
+ "aria-label": t("image.close"),
4771
+ onClick: onClose,
4772
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 16 })
4773
+ })
4774
+ ]
4775
+ }), document.body);
4776
+ }
4777
+ //#endregion
4778
+ //#region src/web/client/rows/search-card-facts.ts
4779
+ /** Separator between the suffix's facts; the shell adds the summary's own. */
4780
+ const FACT_SEPARATOR = " · ";
4781
+ /**
4782
+ * A target may be a `;`-delimited list of paths. Shortening the whole list in
4783
+ * one pass would cut the workspace prefix off its first entry only — the rest
4784
+ * stay absolute, and the same scope then reads as two unrelated places — so
4785
+ * each entry is shortened on its own and the list is rejoined.
4786
+ * @param scope - the call's own target argument.
4787
+ * @param cwd - the session workspace root, when known.
4788
+ * @returns the workspace-shortened target.
4789
+ */
4790
+ function displayScope(scope, cwd) {
4791
+ if (!scope.includes(";")) return displayPath(scope, cwd);
4792
+ return scope.split(";").map((entry) => displayPath(entry.trim(), cwd)).join("; ");
4793
+ }
4794
+ /**
4795
+ * The search card's collapsed trailing fragment: the scope the call searched,
4796
+ * then what the search found.
4797
+ * @param args - the call's parsed arguments, or `null` when its head is gone.
4798
+ * @param meta - the Host-projected search metadata (already narrowed).
4799
+ * @param cwd - the session workspace root, for shortening an absolute scope.
4800
+ * @param t - the card translate seat.
4801
+ * @returns the joined fragment, or `null` when there is no fact to add.
4802
+ */
4803
+ function searchCardSuffix(args, meta, cwd, t) {
4804
+ const facts = [];
4805
+ if (meta.shape === "matches") {
4806
+ const scope = argText(args, "path");
4807
+ if (scope !== null) facts.push(t("search.scope", { path: displayScope(scope, cwd) }));
4808
+ }
4809
+ if (meta.total === 0) facts.push(t("search.noResults"));
4810
+ else if (meta.shape === "matches") {
4811
+ const shown = meta.files.reduce((count, file) => count + file.matches.length, 0);
4812
+ facts.push(searchBlockLabels(t).matchesSummary(shown, meta.total, meta.files.length, meta.truncated));
4813
+ } else facts.push(searchBlockLabels(t).pathsSummary(meta.paths.length, meta.total, meta.truncated));
4814
+ return facts.length === 0 ? null : `· ${facts.join(FACT_SEPARATOR)}`;
4815
+ }
4816
+ //#endregion
4817
+ //#region src/web/client/rows/SearchRow.tsx
4818
+ /**
4819
+ * `grep` / `glob` / `ast_grep` row: a search result card.
4820
+ *
4821
+ * One component serves all three keys because the Host projects one
4822
+ * `shape`-discriminated metadata form for them: grouped matches for the two
4823
+ * grep engines, a flat path list for glob. The title follows the call's own
4824
+ * wire name, so the same component reads as Grep, Glob, or AST Grep.
4825
+ * @module @xiaoso/dsh-tool-plus/web/client/rows/SearchRow
4826
+ */
4827
+ /**
4828
+ * Render a search call as a result card.
4829
+ * @param props - the slot's owner props and this plugin's translate seat.
4830
+ * @returns the search card, or the generic shell with the result text.
4831
+ */
4832
+ function SearchRow(props) {
4833
+ const { t, block } = props;
4834
+ const title = cardTitle(props.toolName, t, "title.grep");
4835
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBoundary, {
4836
+ t,
4837
+ title,
4838
+ block,
4839
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchCard, {
4840
+ ...props,
4841
+ title
4842
+ })
4843
+ });
4844
+ }
4845
+ /**
4846
+ * The card itself, inside the boundary.
4847
+ * @param props - row props plus the resolved title.
4848
+ * @returns the search card.
4849
+ */
4850
+ function SearchCard(props) {
4851
+ const { t, block, cwd, inspect, title } = props;
4852
+ const args = parseCardArgs(block);
4853
+ const meta = narrowSearchCardMeta(cardMeta(block));
4854
+ if (isSubCall(block) || block.isError === true || meta === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenericCard, {
4855
+ t,
4856
+ title,
4857
+ block,
4858
+ args,
4859
+ inspect
4860
+ });
4861
+ const subject = argText(args, "pattern") ?? argText(args, "pat") ?? argText(args, "path");
4862
+ const labels = searchBlockLabels(t);
4863
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
4864
+ t,
4865
+ state: cardState(block),
4866
+ title,
4867
+ summary: subject ?? argsSummary(args, t),
4868
+ summarySuffix: searchCardSuffix(args, meta, cwd, t),
4869
+ inspect,
4870
+ children: meta.shape === "matches" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SearchBlock, {
4871
+ kind: "matches",
4872
+ files: meta.files,
4873
+ truncated: meta.truncated,
4874
+ total: meta.total,
4875
+ labels,
4876
+ className: "twc-body"
4877
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SearchBlock, {
4878
+ kind: "paths",
4879
+ paths: meta.paths,
4880
+ truncated: meta.truncated,
4881
+ total: meta.total,
4882
+ labels,
4883
+ className: "twc-body"
4884
+ })
4885
+ });
4886
+ }
4887
+ //#endregion
4888
+ //#region src/web/client/rows/TerminalRow.tsx
4889
+ /**
4890
+ * `bash` row: a terminal card for foreground runs, a generic row for jobs.
4891
+ *
4892
+ * The card's command comes from the arguments, its working directory from the
4893
+ * arguments falling back to the Host projection and then the session root, and
4894
+ * its exit status from the Host projection — the trailing `[exit code: N]`
4895
+ * marker is only parsed to keep it out of the output pane (and to recover the
4896
+ * signal name the projection does not carry). A call the Host handed to a
4897
+ * background job shows the job id instead: its result is a hand-off notice,
4898
+ * not a finished command.
4899
+ * @module @xiaoso/dsh-tool-plus/web/client/rows/TerminalRow
4900
+ */
4901
+ /**
4902
+ * Render a `bash` call as a terminal card.
4903
+ * @param props - the slot's owner props and this plugin's translate seat.
4904
+ * @returns the terminal card, the job hand-off row, or the generic shell.
4905
+ */
4906
+ function TerminalRow(props) {
4907
+ const { t, block } = props;
4908
+ const title = cardTitle(props.toolName, t, "title.bash");
4909
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBoundary, {
4910
+ t,
4911
+ title,
4912
+ block,
4913
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TerminalCard, {
4914
+ ...props,
4915
+ title
4916
+ })
4917
+ });
4918
+ }
4919
+ /**
4920
+ * The card itself, inside the boundary.
4921
+ * @param props - row props plus the resolved title.
4922
+ * @returns the terminal card or the generic shell.
4923
+ */
4924
+ function TerminalCard(props) {
4925
+ const { t, block, cwd: sessionCwd, home, inspect, title } = props;
4926
+ const args = parseCardArgs(block);
4927
+ const meta = narrowTerminalCardMeta(cardMeta(block));
4928
+ const command = argText(args, "command");
4929
+ const state = cardState(block);
4930
+ if (block.isError === true || command === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenericCard, {
4931
+ t,
4932
+ title,
4933
+ block,
4934
+ args,
4935
+ inspect
4936
+ });
4937
+ if (meta !== null && meta.mode === "background") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
4938
+ t,
4939
+ state,
4940
+ title,
4941
+ summary: t("bash.background", { jobId: meta.jobId }),
4942
+ text: resultText(block),
4943
+ inspect
4944
+ });
4945
+ const foreground = meta !== null && meta.mode === "foreground" ? meta : null;
4946
+ const status = parseShellStatus(resultText(block));
4947
+ const running = state === "running";
4948
+ const timedOut = status.timedOut || foreground?.timedOut === true;
4949
+ const aborted = foreground?.aborted === true;
4950
+ const interrupted = timedOut || aborted;
4951
+ const exitCode = running || interrupted ? void 0 : typeof foreground?.exitCode === "number" ? foreground.exitCode : typeof status.exitCode === "number" ? status.exitCode : void 0;
4952
+ const workdir = argText(args, "workdir") ?? foreground?.workingDir ?? sessionCwd;
4953
+ const summary = bashCardSummary(argText(args, "description"), command, interrupted ? timedOut ? t("timedOut") : t("cancelled") : null);
4954
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
4955
+ t,
4956
+ state,
4957
+ title,
4958
+ summary,
4959
+ inspect,
4960
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.TerminalBlock, {
4961
+ command,
4962
+ cwd: workdir,
4963
+ home,
4964
+ output: status.output,
4965
+ exitCode,
4966
+ signal: status.signal,
4967
+ running,
4968
+ labels: terminalBlockLabels(t),
4969
+ className: "twc-body"
4970
+ })
4971
+ });
4972
+ }
4973
+ //#endregion
4974
+ //#region src/web/client/rows/WriteRow.tsx
4975
+ /**
4976
+ * `write` row: a content preview card.
4977
+ *
4978
+ * The engine's write card is a preview of what was written, not a diff, and
4979
+ * the content is in the arguments — so the card is complete while the call is
4980
+ * still running, and the line count is counted on the client exactly as the
4981
+ * engine's own header does. The language hint comes from the Host projection
4982
+ * (`meta.lang`), which is the only part the arguments cannot supply.
4983
+ * @module @xiaoso/dsh-tool-plus/web/client/rows/WriteRow
4984
+ */
4985
+ /**
4986
+ * Render a `write` call as a content preview.
4987
+ * @param props - the slot's owner props and this plugin's translate seat.
4988
+ * @returns the card, or the generic shell when the arguments are unusable.
4989
+ */
4990
+ function WriteRow(props) {
4991
+ const { t, block } = props;
4992
+ const title = cardTitle(props.toolName, t, "title.write");
4993
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBoundary, {
4994
+ t,
4995
+ title,
4996
+ block,
4997
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WriteCard, {
4998
+ ...props,
4999
+ title
5000
+ })
5001
+ });
5002
+ }
5003
+ /**
5004
+ * The card itself, inside the boundary.
5005
+ * @param props - row props plus the resolved title.
5006
+ * @returns the preview card.
5007
+ */
5008
+ function WriteCard(props) {
5009
+ const { t, block, cwd, inspect, openFile, title } = props;
5010
+ const args = parseCardArgs(block);
5011
+ const meta = narrowWriteCardMeta(cardMeta(block));
5012
+ const content = argText(args, "content");
5013
+ const path = meta?.path ?? argText(args, "path");
5014
+ if (isSubCall(block) || block.isError === true || content === null || path === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenericCard, {
5015
+ t,
5016
+ title,
5017
+ block,
5018
+ args,
5019
+ inspect
5020
+ });
5021
+ const lines = t("write.lines", { count: lineCount(content) });
5022
+ const skip = meta?.madeExecutable === true ? ` · ${t("write.madeExecutable")}` : "";
5023
+ const output = resultText(block);
5024
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
5025
+ t,
5026
+ state: cardState(block),
5027
+ title,
5028
+ summary: displayPath(path, cwd),
5029
+ summarySuffix: lines + skip,
5030
+ filePath: path,
5031
+ openFile,
5032
+ inspect,
5033
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5034
+ className: "twc-body",
5035
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.CodeBlock, {
5036
+ code: content,
5037
+ lang: meta?.lang,
5038
+ lineNumbers: true,
5039
+ copyLabel: t("copy"),
5040
+ copiedLabel: t("copied")
5041
+ }), output !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5042
+ className: "twc-note",
5043
+ children: output
5044
+ })]
5045
+ })
5046
+ });
5047
+ }
5048
+ //#endregion
5049
+ //#region src/web/client/registerToolCards.ts
5050
+ /** The shipped atomic tool-view slot every card registers into. */
5051
+ const TOOLVIEW_SLOT = "tool.call.toolview";
5052
+ /** Settings namespace of this plugin (the browser half spells the Host's). */
5053
+ const SETTINGS_NS = "tool-plus";
5054
+ /** Every row component this module owns, for the winner self-check. */
5055
+ const OWN_COMPONENTS = /* @__PURE__ */ new Set([
5056
+ WriteRow,
5057
+ FileMutationRow,
5058
+ AstEditRow,
5059
+ ReadRow,
5060
+ SearchRow,
5061
+ TerminalRow
5062
+ ]);
5063
+ /**
5064
+ * Mount the tool cards.
5065
+ *
5066
+ * Registers the card dictionaries and one entry per tool key, then keeps the
5067
+ * registration in step with the `webCards` switch. Everything
5068
+ * installed here rides the caller's fiber, so plugin unload removes it all.
5069
+ * @param ctx - the browser plugin context.
5070
+ */
5071
+ function registerToolCards(ctx) {
5072
+ ctx.effect(() => ctx.locale.register(CARD_LOCALE_NS, cardLocales), "tool-plus: card dictionaries");
5073
+ let disposeEntries;
5074
+ const installEntries = () => {
5075
+ const disposers = [
5076
+ ctx.slots.register({
5077
+ name: TOOLVIEW_SLOT,
5078
+ key: "write",
5079
+ priority: CARD_TAKEOVER_PRIORITY,
5080
+ locale: CARD_LOCALE_NS
5081
+ }, WriteRow),
5082
+ ctx.slots.register({
5083
+ name: TOOLVIEW_SLOT,
5084
+ key: "edit",
5085
+ priority: CARD_TAKEOVER_PRIORITY,
5086
+ locale: CARD_LOCALE_NS
5087
+ }, FileMutationRow),
5088
+ ctx.slots.register({
5089
+ name: TOOLVIEW_SLOT,
5090
+ key: "ast_edit",
5091
+ priority: CARD_TAKEOVER_PRIORITY,
5092
+ locale: CARD_LOCALE_NS
5093
+ }, AstEditRow),
5094
+ ctx.slots.register({
5095
+ name: TOOLVIEW_SLOT,
5096
+ key: "read",
5097
+ priority: CARD_TAKEOVER_PRIORITY,
5098
+ locale: CARD_LOCALE_NS
5099
+ }, ReadRow),
5100
+ ctx.slots.register({
5101
+ name: TOOLVIEW_SLOT,
5102
+ key: "grep",
5103
+ priority: CARD_TAKEOVER_PRIORITY,
5104
+ locale: CARD_LOCALE_NS
5105
+ }, SearchRow),
5106
+ ctx.slots.register({
5107
+ name: TOOLVIEW_SLOT,
5108
+ key: "glob",
5109
+ priority: CARD_TAKEOVER_PRIORITY,
5110
+ locale: CARD_LOCALE_NS
5111
+ }, SearchRow),
5112
+ ctx.slots.register({
5113
+ name: TOOLVIEW_SLOT,
5114
+ key: "ast_grep",
5115
+ priority: CARD_TAKEOVER_PRIORITY,
5116
+ locale: CARD_LOCALE_NS
5117
+ }, SearchRow),
5118
+ ctx.slots.register({
5119
+ name: TOOLVIEW_SLOT,
5120
+ key: "bash",
5121
+ priority: CARD_TAKEOVER_PRIORITY,
5122
+ locale: CARD_LOCALE_NS
5123
+ }, TerminalRow)
5124
+ ];
5125
+ if (!winnersAreOurs(ctx)) console.error(`[tool-plus] web tool cards did not win ${TOOLVIEW_SLOT} at priority ${String(CARD_TAKEOVER_PRIORITY)} for: ${lostCardKeys(ctx).join(", ")}. Keeping our entries; another component renders those cells.`);
5126
+ return disposers;
5127
+ };
5128
+ const install = () => {
5129
+ if (disposeEntries !== void 0) return;
5130
+ disposeEntries = ctx.slots.inject(TOOLVIEW_SLOT, () => installEntries());
5131
+ };
5132
+ const uninstall = () => {
5133
+ disposeEntries?.();
5134
+ disposeEntries = void 0;
5135
+ };
5136
+ const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NS });
5137
+ ctx.effect(() => {
5138
+ const sync = () => {
5139
+ if (cardsEnabled(scope.getSnapshot().value)) install();
5140
+ else uninstall();
5141
+ };
5142
+ const stop = scope.subscribe(sync);
5143
+ sync();
5144
+ return stop;
5145
+ }, "tool-plus: web card switch");
5146
+ }
5147
+ /**
5148
+ * Whether the cards should be mounted for one settings document.
5149
+ *
5150
+ * An absent field (first load, namespace not exposed to this client, plugin not
5151
+ * yet described) means on: the shipped rows are the fallback, not the target.
5152
+ * @param value - the namespace's last accepted section, when there is one.
5153
+ * @returns `true` unless the field is explicitly `false`.
5154
+ */
5155
+ function cardsEnabled(value) {
5156
+ return value?.webCards !== false;
5157
+ }
5158
+ /**
5159
+ * Read the current cell winners back and report whether every one of them is a
5160
+ * row this module registered.
5161
+ *
5162
+ * Called immediately after registering, while the ledger is synchronously
5163
+ * consistent: a cell whose winner is some other component means our entry did
5164
+ * not take the cell — something outranks it, or a boundary retired it.
5165
+ * @param ctx - the browser plugin context owning this registration.
5166
+ * @returns `true` when every card key resolves to one of this module's rows.
5167
+ */
5168
+ function winnersAreOurs(ctx) {
5169
+ return lostCardKeys(ctx).length === 0;
5170
+ }
5171
+ /**
5172
+ * The card keys whose current cell winner is not one of this module's rows.
5173
+ *
5174
+ * The takeover assumption is unverified host behaviour, so a lost cell is
5175
+ * reported by name rather than swallowed.
5176
+ * @param ctx - the browser plugin context owning this registration.
5177
+ * @returns the lost keys, in contract order (empty when every cell is ours).
5178
+ */
5179
+ function lostCardKeys(ctx) {
5180
+ const winners = ctx.slots.entriesOfSlot(TOOLVIEW_SLOT);
5181
+ const lost = [];
5182
+ for (const key of CARD_TOOL_KEYS) {
5183
+ const winner = winners.find((entry) => entry.options.key === key);
5184
+ if (winner === void 0 || !OWN_COMPONENTS.has(winner.component)) lost.push(key);
5185
+ }
5186
+ return lost;
5187
+ }
5188
+ //#endregion
3004
5189
  //#region src/client/index.ts
3005
5190
  /**
3006
5191
  * Settings namespace of this plugin, spelled here because a client package
@@ -3022,8 +5207,8 @@ window.__ModuleLoader__.load({
3022
5207
  */
3023
5208
  function apply(ctx) {
3024
5209
  ctx.effect(() => ctx.locale.register(BASH_PLUS_LOCALE_NS, {
3025
- zh,
3026
- en
5210
+ zh: zh$1,
5211
+ en: en$1
3027
5212
  }), "tool-plus: card dictionaries");
3028
5213
  const scope = ctx.settingsScope.bind({ namespace: BASH_PLUS_CLIENT_NS });
3029
5214
  ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
@@ -3032,6 +5217,7 @@ window.__ModuleLoader__.load({
3032
5217
  locale: BASH_PLUS_LOCALE_NS,
3033
5218
  inject: () => ({ scope })
3034
5219
  }, BashPlusCard));
5220
+ registerToolCards(ctx);
3035
5221
  const sectionScope = ctx.settingsScope.bind({ namespace: BASH_PLUS_CLIENT_NS });
3036
5222
  ctx.slots.inject("settings.section", () => ctx.slots.register({
3037
5223
  name: "settings.section",