@deepseek-ai/dsh-client-ui-tool 0.1.1-rc.2 → 0.1.2-alpha.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 (28) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +79 -17
  3. package/README.zh.md +80 -18
  4. package/lib/client.js +898 -432
  5. package/lib/types/client/apply.d.ts +3 -2
  6. package/lib/types/client/contract/slots.d.ts +13 -8
  7. package/lib/types/client/index.d.ts +1 -1
  8. package/lib/types/client/tool/ToolCallTree.d.ts +1 -1
  9. package/lib/types/client/tool/ToolDetails.d.ts +3 -3
  10. package/lib/types/client/tool/components/AskQuestionCard.d.ts +11 -0
  11. package/lib/types/client/tool/components/ToolRow.d.ts +10 -34
  12. package/lib/types/client/tool/models/ask-question-card-model.d.ts +22 -0
  13. package/lib/types/client/tool/models/diff-card-model.d.ts +7 -28
  14. package/lib/types/client/tool/models/primitive-labels.d.ts +36 -0
  15. package/lib/types/client/tool/models/raw-tool-call.d.ts +27 -0
  16. package/lib/types/client/tool/models/read-card-model.d.ts +4 -19
  17. package/lib/types/client/tool/models/search-card-model.d.ts +9 -75
  18. package/lib/types/client/tool/models/terminal-card-model.d.ts +46 -36
  19. package/lib/types/client/tool/models/tool-call-model.d.ts +31 -7
  20. package/lib/types/client/tool/models/web-card-model.d.ts +9 -34
  21. package/lib/types/client/tool/toolviews/ask-question-row.d.ts +2 -11
  22. package/lib/types/client/tool/toolviews/bash-sample.d.ts +2 -14
  23. package/lib/types/client/tool/toolviews/file-mutation-row.d.ts +2 -17
  24. package/lib/types/client/tool/toolviews/read-row.d.ts +2 -12
  25. package/lib/types/client/tool/toolviews/search-row.d.ts +2 -19
  26. package/lib/types/client/tool/toolviews/todo-row.d.ts +2 -13
  27. package/lib/types/client/tool/toolviews/web-row.d.ts +2 -14
  28. package/package.json +17 -19
package/lib/client.js CHANGED
@@ -7,25 +7,52 @@ window.__ModuleLoader__.load({
7
7
  let react_jsx_runtime = require("react/jsx-runtime");
8
8
  let react = require("react");
9
9
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
- let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
11
- //#region lib/types/client/tool/models/tool-call-model.js
10
+ //#region ../../util/workspace-path/src/index.ts
11
+ /**
12
+ * Browser-safe Workspace path and display helpers.
13
+ * @module @deepseek-ai/dsh-util-workspace-path
14
+ */
15
+ /** Whether a path uses a Windows drive or UNC prefix. */
16
+ function isWindowsStylePath(value) {
17
+ return /^[A-Za-z]:[/\\]/.test(value) || value.startsWith("\\\\");
18
+ }
12
19
  /**
13
- * Pure row-model derivation for tool summary rows: variant classification,
14
- * one-line summary, expanded-body text, and flattened result output from the
15
- * frozen call slice. Input material comes from the call ARGUMENTS; output and
16
- * error material from the settled result node. A call whose render intent is
17
- * a terminal card gets its expanded body from the views instead, through
18
- * `terminalCardModel` in terminal-card-model.ts.
20
+ * Resolve a Workspace-relative path into the Host-facing spelling used by path operations.
21
+ * @param cwd - Session Workspace root, when known.
22
+ * @param path - Absolute or Workspace-relative path.
23
+ * @returns an absolute path when a Workspace root is available, otherwise the original path.
19
24
  */
20
- /** Figma row titles per variant (design literals, not translatable copy). */
21
- const VARIANT_TITLES = {
22
- search: "Search",
23
- read: "Read",
24
- bash: "Bash",
25
- write: "Write",
26
- edit: "Edit",
27
- code: "Code",
28
- others: "Tool call"
25
+ function resolveWorkspacePath(cwd, path) {
26
+ if (path.startsWith("/") || isWindowsStylePath(path)) return path;
27
+ if (cwd === void 0 || cwd === "") return path;
28
+ return `${cwd.replace(/[/\\]+$/, "")}/${path.replace(/^[/\\]+/, "")}`;
29
+ }
30
+ /**
31
+ * Abbreviate a POSIX home directory for display.
32
+ * @param path - Absolute or already-short display path.
33
+ * @param home - Host account home; absent skips abbreviation.
34
+ * @returns `~` or `~/…` for the POSIX home and its descendants, otherwise `path`.
35
+ */
36
+ function abbreviateHomePath(path, home) {
37
+ if (home === void 0 || home === "") return path;
38
+ if (isWindowsStylePath(path) || isWindowsStylePath(home)) return path;
39
+ const root = home.replace(/\/+$/, "");
40
+ if (root === "" || root === "/") return path;
41
+ if (path.replace(/\/+$/, "") === root) return "~";
42
+ if (path.startsWith(`${root}/`)) return `~${path.slice(root.length)}`;
43
+ return path;
44
+ }
45
+ //#endregion
46
+ //#region lib/types/client/tool/models/tool-call-model.js
47
+ /** Locale key per generic row variant. */
48
+ const VARIANT_TITLE_KEYS = {
49
+ search: "tool.title.search",
50
+ read: "tool.title.read",
51
+ bash: "tool.title.bash",
52
+ write: "tool.title.write",
53
+ edit: "tool.title.edit",
54
+ code: "tool.title.code",
55
+ others: "tool.title.generic"
29
56
  };
30
57
  /**
31
58
  * Known tool name -> variant.
@@ -54,13 +81,13 @@ window.__ModuleLoader__.load({
54
81
  cordis_undefine: "others"
55
82
  };
56
83
  /** Tool-owned titles that refine a generic row variant without replacing it. */
57
- const TOOL_TITLES = {
58
- cordis_package_inspect: "Inspect",
59
- cordis_runtime_inspect: "Inspect",
60
- cordis_run: "Run Cordis Plugin",
61
- cordis_stop: "Stop Cordis Plugin",
62
- cordis_undefine: "Remove Cordis Plugin",
63
- pwsh: "Pwsh"
84
+ const TOOL_TITLE_KEYS = {
85
+ cordis_package_inspect: "tool.title.inspect",
86
+ cordis_runtime_inspect: "tool.title.inspect",
87
+ cordis_run: "tool.title.runCordis",
88
+ cordis_stop: "tool.title.stopCordis",
89
+ cordis_undefine: "tool.title.removeCordis",
90
+ pwsh: "tool.title.pwsh"
64
91
  };
65
92
  /**
66
93
  * Classify a tool name into its row variant.
@@ -159,7 +186,13 @@ window.__ModuleLoader__.load({
159
186
  const picked = pickString(parsed, FILE_PATH_KEYS);
160
187
  return picked === void 0 ? void 0 : firstLine(picked);
161
188
  }
162
- function deriveBody(variant, argsRaw) {
189
+ /**
190
+ * Format one argument payload when its generic input body becomes visible.
191
+ * @param variant - row presentation selected for the Tool name.
192
+ * @param argsRaw - original argument JSON or incomplete raw text.
193
+ * @returns display body, or null for empty input.
194
+ */
195
+ function formatToolBody(variant, argsRaw) {
163
196
  if (argsRaw === "") return null;
164
197
  const parsed = parseArgs(argsRaw);
165
198
  if (parsed === void 0) return argsRaw;
@@ -182,89 +215,147 @@ window.__ModuleLoader__.load({
182
215
  const done = "kind" in block;
183
216
  const argsRaw = (done ? block.call?.argsRaw : block.argsRaw) ?? "";
184
217
  const state = !done ? "running" : block.error?.code === "interrupted" ? "stopped" : block.isError ? "error" : "ok";
185
- const base = argsRaw === "" ? block.callId : (0, _deepseek_ai_dsh_client_runtime_client.abbreviateHomePath)(relativizeToCwd(deriveSummary(variant, argsRaw), cwd), home);
186
- const toolTitle = TOOL_TITLES[toolName];
187
- const summary = variant === "others" && toolName !== "" && toolTitle === void 0 ? `${toolName} · ${base}` : base;
218
+ const base = argsRaw === "" ? block.callId : abbreviateHomePath(relativizeToCwd(deriveSummary(variant, argsRaw), cwd), home);
219
+ const toolTitleKey = TOOL_TITLE_KEYS[toolName];
220
+ const summary = variant === "others" && toolName !== "" && toolTitleKey === void 0 ? `${toolName} · ${base}` : base;
188
221
  const output = done ? resultText(block) || null : null;
189
222
  const errorSummary = state === "error" && output !== null ? firstLine(output) : null;
223
+ const bodyRaw = argsRaw === "" ? null : argsRaw;
190
224
  return {
191
225
  variant,
192
- title: toolTitle ?? VARIANT_TITLES[variant],
226
+ titleKey: toolTitleKey ?? VARIANT_TITLE_KEYS[variant],
193
227
  summary,
194
228
  filePath: deriveFilePath(variant, argsRaw),
195
- body: deriveBody(variant, argsRaw),
229
+ bodyRaw,
196
230
  output,
197
231
  errorSummary,
198
232
  state
199
233
  };
200
234
  }
201
235
  //#endregion
202
- //#region lib/types/client/tool/models/read-card-model.js
236
+ //#region lib/types/client/tool/models/raw-tool-call.js
237
+ const parsedCalls = /* @__PURE__ */ new WeakMap();
203
238
  /**
204
- * Pure derivation of the read-card props from a frozen call slice: the
205
- * `card:'read'` render intent the read tool declares arrives on the snapshot as
206
- * the settled result node's `resultView`, and this is the one place that turns
207
- * it into what {@link ReadBlock} draws. Both conversation render sites (the chat
208
- * tool row's resident body and the details panel's Output section) call this, so
209
- * the path, lines, total, and language they show are derived once.
210
- *
211
- * The read card is result-side only ([read card note](../../../../../../.agents/notes/implemented/feature/2026-07-30-web-read-card.md)):
212
- * a call carries no file content until `execute` returns, so the pending call
213
- * stays a generic card (`kind: 'read'`). A running read therefore has no read
214
- * card, and this returns null for it — the row keeps its args-derived summary
215
- * until the result arrives.
216
- * @module
239
+ * Parse the call head paired with one immutable Tool block.
240
+ * @param block - running or settled Tool block.
241
+ * @returns the Tool name and object arguments, or null when the call head or valid JSON object is unavailable.
217
242
  */
243
+ function parsedToolCall(block) {
244
+ const cached = parsedCalls.get(block);
245
+ if (cached !== void 0 || parsedCalls.has(block)) return cached ?? null;
246
+ const call = "kind" in block ? block.call : block;
247
+ if (call === null) {
248
+ parsedCalls.set(block, null);
249
+ return null;
250
+ }
251
+ let value;
252
+ try {
253
+ value = JSON.parse(call.argsRaw);
254
+ } catch {
255
+ parsedCalls.set(block, null);
256
+ return null;
257
+ }
258
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
259
+ parsedCalls.set(block, null);
260
+ return null;
261
+ }
262
+ const parsed = {
263
+ name: call.name,
264
+ args: value
265
+ };
266
+ parsedCalls.set(block, parsed);
267
+ return parsed;
268
+ }
218
269
  /**
219
- * Derive the read-card props for a tool call, or null when this call is not a
220
- * read card and belongs on the generic path.
221
- *
222
- * The read card is result-side only, so only a settled call whose result view
223
- * declares `card:'read'` produces one. Every other case is null — the
224
- * documented generic-card default:
225
- *
226
- * - A running call: it has no result view yet, and a read carries no content at
227
- * call time.
228
- * - A settled call whose result view is not a read card — including a `card`
229
- * value this UI version does not know, which arrives over the wire and cannot
230
- * be trusted to be one of the compiled variants, and the read tool's own
231
- * generic fallback for an error result or a non-envelope body.
232
- *
233
- * The label is the read view's `title` when the tool supplied one (the
234
- * presentation contract's replacement-title rule), otherwise the file path
235
- * shortened the same way the row summary is: workspace-relative first, then
236
- * POSIX `~` for a leftover host-home path.
237
- * @param block - RunningToolCall or ToolResultNode off the snapshot caches.
270
+ * Read the exact single text block consumed by first-party card derivations.
271
+ * @param block - settled Tool result.
272
+ * @returns its text, or undefined for any other content layout.
273
+ */
274
+ function singleResultText(block) {
275
+ if (block.content.length !== 1) return void 0;
276
+ const only = block.content[0];
277
+ return only?.type === "text" ? only.text : void 0;
278
+ }
279
+ /**
280
+ * Validate the optional escalation pair shared by first-party shell and file
281
+ * mutation tools.
282
+ * @param args - parsed open-root Tool arguments.
283
+ * @returns whether the declared escalation fields form a valid pair.
284
+ */
285
+ function validEscalationFields(args) {
286
+ const permission = args.sandbox_permissions;
287
+ const justification = args.justification;
288
+ if (permission === void 0 && justification === void 0) return true;
289
+ if (permission !== "workspace-write" && permission !== "danger-full-access") return false;
290
+ return typeof justification === "string" && justification.trim() !== "";
291
+ }
292
+ //#endregion
293
+ //#region lib/types/client/tool/models/read-card-model.js
294
+ function validReadCall(block) {
295
+ const call = parsedToolCall(block);
296
+ if (call?.name !== "read") return false;
297
+ const { file_path: path, offset, limit } = call.args;
298
+ if (typeof path !== "string" || path.trim() === "") return false;
299
+ if (offset !== void 0 && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 1)) return false;
300
+ if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1)) return false;
301
+ return true;
302
+ }
303
+ function readMeta(meta) {
304
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return null;
305
+ const { path, offset, lines, totalLines, lang } = meta;
306
+ if (typeof path !== "string" || typeof offset !== "number" || !Number.isInteger(offset) || offset < 1) return null;
307
+ if (typeof totalLines !== "number" || !Number.isInteger(totalLines) || totalLines < 0 || !Array.isArray(lines)) return null;
308
+ if (lang !== void 0 && typeof lang !== "string") return null;
309
+ const narrowed = [];
310
+ let previous = offset - 1;
311
+ for (const line of lines) {
312
+ if (typeof line !== "object" || line === null || Array.isArray(line)) return null;
313
+ const { number, text } = line;
314
+ if (typeof number !== "number" || !Number.isInteger(number) || number < 1 || number <= previous) return null;
315
+ if (number > totalLines || typeof text !== "string") return null;
316
+ previous = number;
317
+ narrowed.push({
318
+ number,
319
+ text
320
+ });
321
+ }
322
+ return {
323
+ path,
324
+ offset,
325
+ lines: narrowed,
326
+ totalLines,
327
+ ...lang === void 0 ? {} : { lang }
328
+ };
329
+ }
330
+ /**
331
+ * Derive a settled root read card after validating its persisted metadata and
332
+ * model-facing read envelope.
333
+ * @param block - running or settled Tool block.
238
334
  * @param sessionCwd - the session workspace root; a workspace-rooted absolute
239
335
  * path label displays relative to it. Absent leaves the path as authored.
240
336
  * @param home - host account home; a leftover POSIX home path displays as `~`.
241
337
  * @returns the read-card props, or null for the generic path.
242
338
  */
243
339
  function readCardModel(block, sessionCwd, home) {
244
- if (!("kind" in block)) return null;
245
- const result = block.resultView?.card === "read" ? block.resultView : null;
246
- if (result === null) return null;
247
- const lines = result.lines.map((line) => ({
248
- number: line.number,
249
- text: line.text
250
- }));
340
+ if (block.parentCallId !== void 0 || !("kind" in block) || block.isError) return null;
341
+ if (!validReadCall(block)) return null;
342
+ const meta = readMeta(block.meta);
343
+ if (meta === null) return null;
344
+ const text = singleResultText(block);
345
+ if (text === void 0) return null;
346
+ if (/^<path>[^\n]*<\/path>\n<type>file<\/type>\n<content>\n([\s\S]*)\n<\/content>$/u.exec(text)?.[1] === void 0) return null;
251
347
  return {
252
- label: result.title ?? (0, _deepseek_ai_dsh_client_runtime_client.abbreviateHomePath)(relativizeToCwd(result.path, sessionCwd), home),
253
- lines,
254
- totalLines: result.totalLines,
255
- lang: result.lang
348
+ label: abbreviateHomePath(relativizeToCwd(meta.path, sessionCwd), home),
349
+ lines: meta.lines,
350
+ totalLines: meta.totalLines,
351
+ lang: meta.lang
256
352
  };
257
353
  }
258
354
  //#endregion
259
355
  //#region lib/types/client/tool/models/diff-card-model.js
260
356
  /**
261
- * Narrow a wire `card:'diff'` view's `diffs` to well-formed hunks. The event
262
- * view crosses the wire and `toolEventViewSchema` validates only the `card`
263
- * string, so a version mismatch or an anomalous plugin can deliver a `diff` card
264
- * whose `diffs` is absent, not an array, or carries malformed hunks. Returning
265
- * null for any of those routes the block to the generic path instead of letting
266
- * DiffBlock's `for...of`/`split` throw and crash the row or the details panel.
267
- * @param diffs - the view's `diffs` field, unverified.
357
+ * Narrow opaque result metadata's `diffs` to well-formed hunks.
358
+ * @param diffs - the metadata field to validate.
268
359
  * @returns the validated hunks, or null when the payload is not usable.
269
360
  */
270
361
  function narrowDiffs(diffs) {
@@ -284,110 +375,182 @@ window.__ModuleLoader__.load({
284
375
  }
285
376
  return out;
286
377
  }
378
+ function intendedDiff(block) {
379
+ const parsed = parsedToolCall(block);
380
+ if (parsed === null) return null;
381
+ if (parsed.name === "str_replace_editor") {
382
+ const { command, path, file_text: fileText, old_str: oldText, new_str: newText } = parsed.args;
383
+ if (typeof path !== "string" || path.trim() === "") return null;
384
+ if (command === "create") {
385
+ if (fileText !== void 0 && typeof fileText !== "string") return null;
386
+ return {
387
+ tool: "str_replace_editor",
388
+ diff: {
389
+ path,
390
+ oldText: null,
391
+ newText: fileText ?? ""
392
+ }
393
+ };
394
+ }
395
+ if (command === "str_replace") {
396
+ if (oldText !== void 0 && typeof oldText !== "string") return null;
397
+ if (newText !== void 0 && typeof newText !== "string") return null;
398
+ return {
399
+ tool: "str_replace_editor",
400
+ diff: {
401
+ path,
402
+ oldText: oldText ?? null,
403
+ newText: newText ?? ""
404
+ }
405
+ };
406
+ }
407
+ return null;
408
+ }
409
+ const { file_path: path } = parsed.args;
410
+ if (typeof path !== "string" || path.trim() === "") return null;
411
+ if (!validEscalationFields(parsed.args)) return null;
412
+ if (parsed.name === "write") {
413
+ const { content } = parsed.args;
414
+ return typeof content === "string" ? {
415
+ tool: "write",
416
+ diff: {
417
+ path,
418
+ oldText: null,
419
+ newText: content
420
+ }
421
+ } : null;
422
+ }
423
+ if (parsed.name !== "edit") return null;
424
+ const { old_string: oldText, new_string: newText, replace_all: replaceAll } = parsed.args;
425
+ if (typeof oldText !== "string" || typeof newText !== "string") return null;
426
+ if (replaceAll !== void 0 && typeof replaceAll !== "boolean") return null;
427
+ return {
428
+ tool: "edit",
429
+ diff: {
430
+ path,
431
+ oldText: oldText || null,
432
+ newText
433
+ }
434
+ };
435
+ }
436
+ function appliedDiffs(meta) {
437
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return null;
438
+ const diffs = meta.diffs;
439
+ if (!Array.isArray(diffs)) return null;
440
+ if (diffs.length === 0) return "empty";
441
+ return narrowDiffs(diffs);
442
+ }
287
443
  /**
288
- * Derive the diff-card props for a tool call, or null when this call is not a
289
- * diff card and belongs on the generic path.
290
- *
291
- * The result side is authoritative once the call settles: the write/edit tools
292
- * return the applied contextual hunks there (an edit's real before/after, a
293
- * create's whole-file diff), which replace the call-time diff derived from the
294
- * arguments alone. While the call is still running only the call side exists,
295
- * so a running write/edit shows its intended change. Null is the documented
296
- * generic-card default and covers every non-diff card — including a `card`
297
- * value this UI version does not know, which arrives over the wire and cannot
298
- * be trusted to be one of the compiled variants — and a settled call whose
299
- * result view is generic (how write/edit keep their execution errors on the
300
- * generic path).
301
- *
302
- * This derivation consumes only `diffs`; the render intent's `title` field is
303
- * deliberately dropped. The row supplies its own title (`Edit`/`Write · path`
304
- * from the args), which outranks the view's `title`. A tool that names its own
305
- * diff header therefore does not surface that text on the Web row.
306
- * @param block - RunningToolCall or ToolResultNode off the snapshot caches.
444
+ * Derive running diffs for root write/edit and `str_replace_editor`
445
+ * create/replace calls, plus applied settled diffs for root write/edit calls.
446
+ * A successful write with valid empty metadata uses its argument-derived
447
+ * whole-file diff, matching create and identical-overwrite presentation;
448
+ * `str_replace_editor` settles through Generic because it has no result view.
449
+ * @param block - running or settled Tool block.
307
450
  * @returns the diff-card props, or null for the generic path.
308
451
  */
309
452
  function diffCardModel(block) {
310
- if (!("kind" in block)) {
311
- const call = block.callView?.card === "diff" ? block.callView : null;
312
- const diffs = call === null ? null : narrowDiffs(call.diffs);
313
- return diffs === null ? null : { card: { diffs } };
314
- }
315
- const result = block.resultView?.card === "diff" ? block.resultView : null;
316
- const diffs = result === null ? null : narrowDiffs(result.diffs);
317
- return diffs === null ? null : { card: { diffs } };
453
+ if (block.parentCallId !== void 0) return null;
454
+ const intended = intendedDiff(block);
455
+ if (intended === null) return null;
456
+ if (!("kind" in block)) return { card: { diffs: [intended.diff] } };
457
+ if (intended.tool === "str_replace_editor") return null;
458
+ if (block.isError) return null;
459
+ const applied = appliedDiffs(block.meta);
460
+ if (applied === null || applied === "empty") return intended.tool === "write" ? { card: { diffs: [intended.diff] } } : null;
461
+ return { card: { diffs: applied } };
318
462
  }
319
463
  //#endregion
320
464
  //#region lib/types/client/tool/models/search-card-model.js
321
- /**
322
- * Whether every file group in a matches view is structurally valid: the wire
323
- * frame carries `shape` and `card` as strings the host schema checks, but not the
324
- * grouped `files` fields, so a version mismatch or loose producer could deliver
325
- * `shape: 'matches'` with a missing or malformed `files`. Rendering that would
326
- * crash {@link SearchBlock} at `.reduce`/`.map`; invalid fields select the
327
- * generic path instead.
328
- * @param files - the candidate `files` field off the untrusted result view.
329
- * @returns whether `files` is a valid {@link SearchFileGroup} array.
330
- */
331
- function isValidFiles(files) {
332
- return Array.isArray(files) && files.every((file) => typeof file === "object" && file !== null && typeof file.path === "string" && Array.isArray(file.matches) && file.matches.every((match) => typeof match === "object" && match !== null && typeof match.lineNumber === "number" && typeof match.line === "string"));
465
+ function validSearchCall(block) {
466
+ const call = parsedToolCall(block);
467
+ if (call === null) return null;
468
+ const { pattern, path } = call.args;
469
+ if (typeof pattern !== "string") return null;
470
+ if (call.name === "grep" && pattern === "") return null;
471
+ if (call.name === "glob" && pattern.trim() === "") return null;
472
+ if (call.name !== "grep" && call.name !== "glob") return null;
473
+ if (path !== void 0 && (typeof path !== "string" || path.trim() === "")) return null;
474
+ if (call.name === "grep") {
475
+ const { include } = call.args;
476
+ if (include !== void 0 && (typeof include !== "string" || !validInclude(include))) return null;
477
+ }
478
+ return call.name;
479
+ }
480
+ function validInclude(include) {
481
+ if (include.trim() === "" || include.startsWith("!")) return false;
482
+ let braceDepth = 0;
483
+ for (const character of include) if (character === "{") braceDepth += 1;
484
+ else if (character === "}") braceDepth = Math.max(0, braceDepth - 1);
485
+ else if (character === "," && braceDepth === 0) return false;
486
+ return true;
487
+ }
488
+ function searchFiles(value) {
489
+ if (!Array.isArray(value)) return null;
490
+ const files = [];
491
+ for (const file of value) {
492
+ if (typeof file !== "object" || file === null || Array.isArray(file)) return null;
493
+ const { path, matches } = file;
494
+ if (typeof path !== "string" || !Array.isArray(matches)) return null;
495
+ const narrowed = [];
496
+ for (const match of matches) {
497
+ if (typeof match !== "object" || match === null || Array.isArray(match)) return null;
498
+ const { lineNumber, line } = match;
499
+ if (typeof lineNumber !== "number" || !Number.isInteger(lineNumber) || lineNumber < 1) return null;
500
+ if (typeof line !== "string") return null;
501
+ narrowed.push({
502
+ lineNumber,
503
+ line
504
+ });
505
+ }
506
+ files.push({
507
+ path,
508
+ matches: narrowed
509
+ });
510
+ }
511
+ return files;
333
512
  }
334
- /**
335
- * Flatten a settled tool result's content blocks to their text, joined by
336
- * newlines. The search view carries no result text — a UI without a card falls
337
- * back to the raw `tool/result` content — so the truncation recovery footer is
338
- * read from the block's own content here. Non-text blocks (a search result
339
- * carries none) are skipped.
340
- * @param content - the result node's content blocks.
341
- * @returns the joined text, or undefined when empty.
342
- */
343
513
  function flattenContent(content) {
344
514
  const text = content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
345
515
  return text === "" ? void 0 : text;
346
516
  }
347
517
  /**
348
- * Derive the search-card props for a tool call, or null when this call is not a
349
- * search card and belongs on the generic path.
350
- *
351
- * Only the result side matters: the search card carries no call-time state, so
352
- * a still-running call (no result view) is null, as is a settled call whose
353
- * result view is not a search card — including a `card` value this UI version
354
- * does not know, which arrives over the wire and cannot be trusted to be one of
355
- * the compiled variants, a `card: 'search'` view whose `shape` is neither
356
- * `matches` nor `paths` (equally untrusted wire data), and a generic result a
357
- * `grep`/`glob` failure or nested `run_code` dispatch produces (its text keeps
358
- * the generic path).
359
- * @param block - RunningToolCall or ToolResultNode off the snapshot caches.
360
- * @returns the search-card props, or null for the generic path.
518
+ * Derive a settled root grep/glob card from persisted metadata.
519
+ * @param block - running or settled Tool block.
520
+ * @returns search-card props, or null for the generic path.
361
521
  */
362
522
  function searchCardModel(block) {
363
- if (!("kind" in block)) return null;
364
- const result = block.resultView?.card === "search" ? block.resultView : null;
365
- if (result === null) return null;
523
+ if (block.parentCallId !== void 0 || !("kind" in block) || block.isError) return null;
524
+ const tool = validSearchCall(block);
525
+ if (tool === null) return null;
526
+ if (typeof block.meta !== "object" || block.meta === null || Array.isArray(block.meta)) return null;
527
+ const meta = block.meta;
528
+ if (typeof meta.truncated !== "boolean") return null;
529
+ if (typeof meta.total !== "number" || !Number.isInteger(meta.total) || meta.total < 0) return null;
366
530
  const common = {
367
- truncated: result.truncated,
368
- total: result.total
531
+ truncated: meta.truncated,
532
+ total: meta.total
369
533
  };
370
- const recovery = result.truncated ? flattenContent(block.content) : void 0;
371
- if (result.shape === "matches") {
372
- if (!isValidFiles(result.files)) return null;
373
- return {
374
- title: result.title,
534
+ const recovery = meta.truncated ? flattenContent(block.content) : void 0;
535
+ if (tool === "grep") {
536
+ if (meta.shape !== "matches") return null;
537
+ const files = searchFiles(meta.files);
538
+ return files === null ? null : {
375
539
  recovery,
376
540
  card: {
377
541
  kind: "matches",
378
- files: result.files,
542
+ files,
379
543
  ...common
380
544
  }
381
545
  };
382
546
  }
383
- if (result.shape !== "paths") return null;
384
- if (!Array.isArray(result.paths) || !result.paths.every((path) => typeof path === "string")) return null;
547
+ if (meta.shape !== "paths" || !Array.isArray(meta.paths)) return null;
548
+ if (!meta.paths.every((path) => typeof path === "string")) return null;
385
549
  return {
386
- title: result.title,
387
550
  recovery,
388
551
  card: {
389
552
  kind: "paths",
390
- paths: result.paths,
553
+ paths: [...meta.paths],
391
554
  ...common
392
555
  }
393
556
  };
@@ -395,16 +558,6 @@ window.__ModuleLoader__.load({
395
558
  //#endregion
396
559
  //#region lib/types/client/tool/models/terminal-card-model.js
397
560
  /**
398
- * Pure derivation of the terminal-card props from a frozen call slice: the
399
- * `card:'terminal'` render intent the shell tools declare arrives on the
400
- * snapshot as `callView`/`resultView`, and this is the one place that turns
401
- * that pair into what {@link TerminalBlock} draws. Both conversation render
402
- * sites (the chat tool row's expanded body and the details panel's Output
403
- * section) call this, so the command, cwd, output and exit status they show
404
- * are derived once.
405
- * @module
406
- */
407
- /**
408
561
  * Build the TerminalBlock display copy from the conversation locale seat —
409
562
  * the one place the primitive's label surface pairs with this package's
410
563
  * dictionary, shared by every terminal render site (chat row, bash row,
@@ -429,6 +582,29 @@ window.__ModuleLoader__.load({
429
582
  };
430
583
  }
431
584
  /**
585
+ * Resolve locale-owned `terminal_send` copy while preserving Tool-authored
586
+ * shell commands and descriptions verbatim.
587
+ * @param model - locale-neutral terminal card data.
588
+ * @param t - the render site's conversation locale seat.
589
+ * @returns terminal props and description ready for rendering.
590
+ */
591
+ function localizeTerminalCardModel(model, t) {
592
+ if (model.copy.kind === "shell") return {
593
+ card: {
594
+ command: model.copy.command,
595
+ ...model.card
596
+ },
597
+ description: model.copy.description
598
+ };
599
+ return {
600
+ card: {
601
+ command: model.copy.text === "" ? t("terminal.sendInput") : model.copy.text,
602
+ ...model.card
603
+ },
604
+ description: t("terminal.session", { sessionId: model.copy.sessionId })
605
+ };
606
+ }
607
+ /**
432
608
  * True when a settled terminal card reports a failing exit — a non-zero code
433
609
  * or a terminating signal. The bash tool settles a failing command as a
434
610
  * completed call (`isError` stays false: the exit status is result data), so
@@ -442,21 +618,18 @@ window.__ModuleLoader__.load({
442
618
  return running !== true && (exitCode !== void 0 && exitCode !== 0 || signal !== void 0);
443
619
  }
444
620
  /**
445
- * Resolve a terminal view's working directory the way the render-intent
446
- * contract assigns to the UI bridge: an absolute path is used as-is, a relative
447
- * one joins under the session workspace, and an omitted one IS the session
448
- * workspace. A pure presenter cannot see the session cwd, which is why this
449
- * resolution belongs here rather than in the tool. Without a session cwd there
450
- * is nothing to resolve against, so a relative path stays as authored and an
451
- * omitted one stays absent (the prompt row then draws a bare `$`).
452
- * @param viewCwd - the cwd the terminal call view carries, if any.
621
+ * Resolve a shell call's workdir for display: an absolute path is used as-is,
622
+ * a relative one joins under the session workspace, and an omitted one is the
623
+ * session workspace. Without a session cwd, a relative path stays as authored
624
+ * and an omitted one stays absent.
625
+ * @param workdir - the raw call's workdir, if any.
453
626
  * @param sessionCwd - the session workspace root, if the caller knows it.
454
627
  * @returns the working directory for the prompt label, or undefined.
455
628
  */
456
- function resolveTerminalCwd(viewCwd, sessionCwd) {
457
- if (viewCwd === void 0 || viewCwd === "") return sessionCwd;
458
- if (sessionCwd === void 0 || sessionCwd === "") return normalizeSegments(viewCwd);
459
- return normalizeSegments((0, _deepseek_ai_dsh_client_runtime_client.resolveWorkspacePath)(sessionCwd, viewCwd));
629
+ function resolveTerminalCwd(workdir, sessionCwd) {
630
+ if (workdir === void 0 || workdir === "") return sessionCwd;
631
+ if (sessionCwd === void 0 || sessionCwd === "") return normalizeSegments(workdir);
632
+ return normalizeSegments(resolveWorkspacePath(sessionCwd, workdir));
460
633
  }
461
634
  /**
462
635
  * Collapse `.` and `..` segments so the prompt label names the directory the
@@ -508,104 +681,195 @@ window.__ModuleLoader__.load({
508
681
  }
509
682
  return kept.join(separator);
510
683
  }
684
+ function shellCall(name, args) {
685
+ if (name !== "bash" && name !== "pwsh") return null;
686
+ const { command, description, timeoutMs, workdir, run_in_background: background } = args;
687
+ if (typeof command !== "string" || command.trim() === "") return null;
688
+ if (timeoutMs !== void 0 && (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0)) return null;
689
+ if (workdir !== void 0 && typeof workdir !== "string") return null;
690
+ if (background !== void 0 && typeof background !== "boolean") return null;
691
+ if (!validEscalationFields(args)) return null;
692
+ if (description === void 0) return {
693
+ kind: "shell",
694
+ command,
695
+ description: void 0,
696
+ workdir: void 0,
697
+ persistent: true,
698
+ background: false
699
+ };
700
+ if (typeof description !== "string" || description.trim() === "") return null;
701
+ return {
702
+ kind: "shell",
703
+ command,
704
+ description,
705
+ workdir,
706
+ persistent: false,
707
+ background: background === true
708
+ };
709
+ }
511
710
  /**
512
- * Derive the terminal-card props for a tool call, or null when this call is
513
- * not a terminal card and belongs on the generic path.
514
- *
515
- * The call side supplies the command and its working directory; the result
516
- * side supplies the captured output and exit status. Three cases produce
517
- * null, all of them the documented generic-card default:
518
- *
519
- * - Neither side declares `card:'terminal'` — including a `card` value this
520
- * UI version does not know, which arrives over the wire and therefore
521
- * cannot be trusted to be one of the compiled variants.
522
- * - A settled call whose result view is not a terminal card: the result
523
- * presentation decides how the settled call renders, and the bash tool
524
- * returns a generic fenced card for an execution error or a background
525
- * start, whose text and error styling the generic path preserves.
526
- *
527
- * Window truncation can drop the call head from a settled result (see
528
- * `ToolResultNode.call`/`callView` in dsh-client-runtime), leaving a terminal
529
- * result with no call side. That still renders: the command falls back to the
530
- * result view's replacement title, then to an empty command (the prompt line
531
- * draws bare), and the prompt shows no cwd.
532
- * @param block - RunningToolCall or ToolResultNode off the snapshot caches.
533
- * @param sessionCwd - the session workspace root, which resolves an omitted or
534
- * relative view cwd (see {@link resolveTerminalCwd}); absent leaves both unresolved.
535
- * @returns the terminal-card props, or null for the generic path.
711
+ * Identify a settled root call from the persistent Bash or PowerShell tool.
712
+ * Its result stays on the generic input/output path because the persistent
713
+ * shell can report resets and partial output without one process exit status.
714
+ * @param block - running or settled Tool block.
715
+ * @returns whether the block is a settled persistent-shell call.
716
+ */
717
+ function isSettledPersistentShellCall(block) {
718
+ if (!("kind" in block) || block.parentCallId !== void 0) return false;
719
+ const parsed = parsedToolCall(block);
720
+ if (parsed === null) return false;
721
+ return shellCall(parsed.name, parsed.args)?.persistent === true;
722
+ }
723
+ function terminalSendCall(name, args) {
724
+ if (name !== "terminal_send") return null;
725
+ const { sessionId, text, submit, run_in_background: background } = args;
726
+ if (typeof sessionId !== "string" || sessionId === "" || typeof text !== "string") return null;
727
+ if (submit !== void 0 && typeof submit !== "boolean") return null;
728
+ if (background !== void 0 && typeof background !== "boolean") return null;
729
+ return {
730
+ kind: "terminal-send",
731
+ text,
732
+ sessionId,
733
+ background: background === true
734
+ };
735
+ }
736
+ /**
737
+ * Parse the marker literals owned by `@deepseek-ai/dsh-shell/render` without
738
+ * importing that Host-only package into the Client dependency graph.
739
+ * @param text - rendered shell result text.
740
+ * @returns output with a trailing exit-code or signal marker extracted.
741
+ */
742
+ function parseExitStatus(text) {
743
+ const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text);
744
+ if (signal?.[1] !== void 0) return {
745
+ output: text.slice(0, signal.index),
746
+ signal: signal[1]
747
+ };
748
+ const exit = /\n\[exit code: (\d+)\]$/.exec(text);
749
+ if (exit?.[1] !== void 0) return {
750
+ output: text.slice(0, exit.index),
751
+ exitCode: Number(exit[1])
752
+ };
753
+ return {
754
+ output: text,
755
+ exitCode: 0
756
+ };
757
+ }
758
+ /**
759
+ * Derive terminal props for supported root shell and terminal-send calls.
760
+ * Standard shell results parse their final status marker; persistent shell
761
+ * results, background calls, errors, malformed input, or child dispatches use
762
+ * the generic path. {@link isSettledPersistentShellCall} lets that generic
763
+ * persistent result remain expandable without inventing one process status.
764
+ * @param block - running or settled Tool block.
765
+ * @param sessionCwd - session workspace root used to resolve workdir.
766
+ * @returns locale-neutral terminal-card data, or null for the generic path.
536
767
  */
537
768
  function terminalCardModel(block, sessionCwd) {
538
- const call = block.callView?.card === "terminal" ? block.callView : null;
539
- if (!("kind" in block)) return call === null ? null : {
540
- description: call.description,
769
+ if (block.parentCallId !== void 0) return null;
770
+ const parsed = parsedToolCall(block);
771
+ if (parsed === null) return null;
772
+ const call = shellCall(parsed.name, parsed.args) ?? terminalSendCall(parsed.name, parsed.args);
773
+ if (call === null || call.background) return null;
774
+ const copy = call.kind === "shell" ? {
775
+ kind: "shell",
776
+ command: call.command,
777
+ description: call.description
778
+ } : {
779
+ kind: "terminal-send",
780
+ text: call.text,
781
+ sessionId: call.sessionId
782
+ };
783
+ const cwd = resolveTerminalCwd(call.kind === "shell" ? call.workdir : void 0, sessionCwd);
784
+ if (!("kind" in block)) return {
785
+ copy,
541
786
  card: {
542
- command: call.title,
543
- cwd: resolveTerminalCwd(call.cwd, sessionCwd),
787
+ cwd,
544
788
  output: void 0,
545
789
  exitCode: void 0,
546
790
  signal: void 0,
547
791
  running: true
548
792
  }
549
793
  };
550
- const result = block.resultView?.card === "terminal" ? block.resultView : null;
551
- if (result === null) return null;
794
+ if (block.isError || call.kind === "shell" && call.persistent) return null;
795
+ const output = singleResultText(block);
796
+ if (output === void 0) return null;
797
+ const status = call.kind === "terminal-send" ? { output } : parseExitStatus(output);
552
798
  return {
553
- description: call?.description,
799
+ copy,
554
800
  card: {
555
- command: result.title ?? call?.title ?? "",
556
- cwd: call === null ? void 0 : resolveTerminalCwd(call.cwd, sessionCwd),
557
- output: result.output,
558
- exitCode: result.exitCode,
559
- signal: result.signal,
801
+ cwd,
802
+ output: status.output,
803
+ exitCode: status.exitCode,
804
+ signal: status.signal,
560
805
  running: false
561
806
  }
562
807
  };
563
808
  }
564
809
  //#endregion
565
810
  //#region lib/types/client/tool/models/web-card-model.js
811
+ function validWebCall(block) {
812
+ const call = parsedToolCall(block);
813
+ if (call === null) return null;
814
+ if (call.name === "web_search") {
815
+ const { queries } = call.args;
816
+ if (!Array.isArray(queries) || queries.length === 0) return null;
817
+ return queries.every((query) => typeof query === "string" && query.trim() !== "") ? call.name : null;
818
+ }
819
+ if (call.name === "web_fetch") {
820
+ const { url } = call.args;
821
+ return typeof url === "string" && url.trim() !== "" ? call.name : null;
822
+ }
823
+ return null;
824
+ }
825
+ function webSources(value) {
826
+ if (!Array.isArray(value)) return null;
827
+ const sources = [];
828
+ for (const source of value) {
829
+ if (typeof source !== "object" || source === null || Array.isArray(source)) return null;
830
+ const { url, title, snippet, publishedAt } = source;
831
+ if (typeof url !== "string") return null;
832
+ if (title !== void 0 && typeof title !== "string") return null;
833
+ if (snippet !== void 0 && typeof snippet !== "string") return null;
834
+ if (publishedAt !== void 0 && typeof publishedAt !== "string") return null;
835
+ sources.push({
836
+ url,
837
+ ...title === void 0 ? {} : { title },
838
+ ...snippet === void 0 ? {} : { snippet },
839
+ ...publishedAt === void 0 ? {} : { publishedAt }
840
+ });
841
+ }
842
+ return sources;
843
+ }
566
844
  /**
567
- * Derive the web-card props for a tool call, or null when this call is not a
568
- * web card and belongs on the generic path.
569
- *
570
- * The result side supplies the whole card: the sources and answer for a
571
- * `search`, the URL and status for a `fetch`. Cases producing null, all of
572
- * them the documented generic-card default:
573
- *
574
- * - A running call (no `resultView` yet): the web tools keep a generic pending
575
- * card, so nothing web-shaped exists until the call settles.
576
- * - A settled call whose result view is not a web card — including a `card`
577
- * value this UI version does not know, which arrives over the wire and so
578
- * cannot be trusted to be one of the compiled variants, and a generic result
579
- * view (a web tool's error path returns the generic card, whose text the
580
- * generic path preserves).
581
- * - A web card whose `kind` this UI version does not know (a newer host's
582
- * value): the wire cannot be trusted to be `search` or `fetch`, so it takes
583
- * the generic path rather than rendering as a malformed fetch.
584
- * @param block - RunningToolCall or ToolResultNode off the snapshot caches.
585
- * @returns the web-card props, or null for the generic path.
845
+ * Derive a settled root web-search or web-fetch card from persisted metadata.
846
+ * @param block - running or settled Tool block.
847
+ * @returns web-card props, or null for the generic path.
586
848
  */
587
849
  function webCardModel(block) {
588
- if (!("kind" in block)) return null;
589
- const result = block.resultView;
590
- if (result?.card !== "web") return null;
591
- if (result.kind === "search") return {
592
- kind: "search",
593
- answer: result.answer,
594
- sources: result.sources.map((source) => ({
595
- url: source.url,
596
- title: source.title,
597
- snippet: source.snippet,
598
- publishedAt: source.publishedAt
599
- })),
600
- truncated: result.truncated
601
- };
602
- if (result.kind === "fetch") return {
850
+ if (block.parentCallId !== void 0 || !("kind" in block) || block.isError) return null;
851
+ const tool = validWebCall(block);
852
+ if (tool === null || typeof block.meta !== "object" || block.meta === null || Array.isArray(block.meta)) return null;
853
+ const meta = block.meta;
854
+ if (typeof meta.truncated !== "boolean") return null;
855
+ if (tool === "web_search") {
856
+ const sources = webSources(meta.sources);
857
+ if (sources === null || meta.answer !== void 0 && typeof meta.answer !== "string") return null;
858
+ return {
859
+ kind: "search",
860
+ answer: meta.answer,
861
+ sources,
862
+ truncated: meta.truncated
863
+ };
864
+ }
865
+ if (typeof meta.url !== "string") return null;
866
+ if (typeof meta.statusCode !== "number" || !Number.isInteger(meta.statusCode)) return null;
867
+ return {
603
868
  kind: "fetch",
604
- url: result.url,
605
- statusCode: result.statusCode,
606
- truncated: result.truncated
869
+ url: meta.url,
870
+ statusCode: meta.statusCode,
871
+ truncated: meta.truncated
607
872
  };
608
- return null;
609
873
  }
610
874
  //#endregion
611
875
  //#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
@@ -623,8 +887,162 @@ window.__ModuleLoader__.load({
623
887
  return n;
624
888
  }
625
889
  //#endregion
890
+ //#region lib/types/client/tool/models/primitive-labels.js
891
+ /** Localized copy adapters for Cordis-free UI primitives used by Tool cards. */
892
+ /**
893
+ * Build localized Markdown chrome labels.
894
+ * @param t - Conversation locale seat.
895
+ * @returns Markdown chrome labels.
896
+ */
897
+ function markdownLabels(t) {
898
+ return {
899
+ code: {
900
+ copyLabel: t("copy"),
901
+ copiedLabel: t("copied")
902
+ },
903
+ footnotes: t("markdown.footnotes")
904
+ };
905
+ }
906
+ /**
907
+ * Build localized diff-card chrome labels.
908
+ * @param t - Conversation locale seat.
909
+ * @returns Diff-card chrome labels.
910
+ */
911
+ function diffBlockLabels(t) {
912
+ return {
913
+ copy: t("copy"),
914
+ copied: t("copied"),
915
+ collapseAria: t("diff.collapseAria"),
916
+ expandAria: (count) => t("diff.expandAria", { count }),
917
+ collapse: t("collapse"),
918
+ expand: (count) => t("diff.expandRest", { count }),
919
+ files: (count) => t(count === 1 ? "diff.files.one" : "diff.files.other", { count })
920
+ };
921
+ }
922
+ /**
923
+ * Build localized read-card chrome labels.
924
+ * @param t - Conversation locale seat.
925
+ * @returns Read-card chrome labels.
926
+ */
927
+ function readBlockLabels(t) {
928
+ return {
929
+ window: (shown, total) => t("read.window", {
930
+ shown,
931
+ total
932
+ }),
933
+ copy: t("copy"),
934
+ copied: t("copied"),
935
+ collapseAria: t("read.collapseAria"),
936
+ expandAria: (count) => t("read.expandAria", { count }),
937
+ collapse: t("collapse"),
938
+ expand: (count) => t("read.expandRest", { count })
939
+ };
940
+ }
941
+ /**
942
+ * Build localized search-card chrome labels.
943
+ * @param t - Conversation locale seat.
944
+ * @returns Search-card chrome labels.
945
+ */
946
+ function searchBlockLabels(t) {
947
+ return {
948
+ pathsSummary: (shown, total, truncated) => t(truncated ? "search.paths.truncated" : "search.paths", {
949
+ shown,
950
+ total
951
+ }),
952
+ matchesSummary: (shown, total, files, truncated) => t(truncated ? "search.matches.truncated" : "search.matches", {
953
+ shown,
954
+ total,
955
+ files
956
+ }),
957
+ copy: t("copy"),
958
+ copied: t("copied"),
959
+ noResults: t("search.noResults"),
960
+ collapseAria: t("search.collapseAria"),
961
+ expandAria: (count) => t("search.expandAria", { count }),
962
+ collapse: t("collapse"),
963
+ expand: (count) => t("search.expandRest", { count })
964
+ };
965
+ }
966
+ /**
967
+ * Build localized web-card chrome labels.
968
+ * @param t - Conversation locale seat.
969
+ * @returns Web-card chrome labels.
970
+ */
971
+ function webBlockLabels(t) {
972
+ return {
973
+ noResults: t("web.noResults"),
974
+ sourcesTruncated: t("web.sourcesTruncated"),
975
+ http: t("web.http"),
976
+ contentTruncated: t("web.contentTruncated"),
977
+ markdown: markdownLabels(t)
978
+ };
979
+ }
980
+ //#endregion
981
+ //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-tool/src/client/tool/components/AskQuestionCard.module.css.mjs
982
+ const css$4 = ".fsXYAq_card{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);border-radius:12px;flex-direction:column;gap:16px;max-height:360px;margin:4px 0 4px 4px;padding:16px 20px;display:flex;overflow-y:auto}.fsXYAq_item{flex-direction:column;gap:2px;min-width:0;display:flex}.fsXYAq_question,.fsXYAq_answer{white-space:pre-wrap;overflow-wrap:anywhere;font-size:var(--dsh-content-font-size,14px);line-height:calc(24px + var(--dsh-content-font-delta,0px));margin:0}.fsXYAq_question{color:var(--dsw-alias-label-tertiary)}.fsXYAq_answer{color:var(--dsw-alias-label-primary)}.fsXYAq_answerLine{display:block}.fsXYAq_skipped{color:var(--dsw-alias-label-tertiary)}.fsXYAq_verdict{color:var(--dsw-alias-label-primary);font-size:var(--dsh-content-font-size,14px);line-height:calc(24px + var(--dsh-content-font-delta,0px));margin:0}.fsXYAq_questionList{flex-direction:column;gap:8px;margin:0;padding-left:20px;display:flex}.fsXYAq_unansweredQuestion{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;overflow-wrap:anywhere;font-size:var(--dsh-content-font-size,14px);line-height:calc(24px + var(--dsh-content-font-delta,0px))}";
983
+ const tagId$4 = "@deepseek-ai/dsh-client-ui-tool/AskQuestionCard.module.css";
984
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
985
+ const tag = document.createElement("style");
986
+ tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-tool";
987
+ tag.dataset.pluginCss = tagId$4;
988
+ tag.textContent = css$4;
989
+ document.head.appendChild(tag);
990
+ }
991
+ var AskQuestionCard_module_css_default = {
992
+ "answer": "fsXYAq_answer",
993
+ "answerLine": "fsXYAq_answerLine",
994
+ "card": "fsXYAq_card",
995
+ "item": "fsXYAq_item",
996
+ "question": "fsXYAq_question",
997
+ "questionList": "fsXYAq_questionList",
998
+ "skipped": "fsXYAq_skipped",
999
+ "unansweredQuestion": "fsXYAq_unansweredQuestion",
1000
+ "verdict": "fsXYAq_verdict"
1001
+ };
1002
+ //#endregion
1003
+ //#region lib/types/client/tool/components/AskQuestionCard.js
1004
+ /**
1005
+ * Render a validated ask-user transcript from plain card data.
1006
+ * @param props - Localized transcript card data.
1007
+ * @returns the readable answered or unanswered question list.
1008
+ */
1009
+ function AskQuestionCard({ card }) {
1010
+ if (card.kind === "unanswered") return (0, react_jsx_runtime.jsxs)("div", {
1011
+ className: AskQuestionCard_module_css_default.card,
1012
+ children: [(0, react_jsx_runtime.jsx)("p", {
1013
+ className: AskQuestionCard_module_css_default.verdict,
1014
+ children: card.verdict
1015
+ }), (0, react_jsx_runtime.jsx)("ul", {
1016
+ className: AskQuestionCard_module_css_default.questionList,
1017
+ children: card.questions.map((question) => (0, react_jsx_runtime.jsx)("li", {
1018
+ className: AskQuestionCard_module_css_default.unansweredQuestion,
1019
+ children: question.question
1020
+ }, question.id))
1021
+ })]
1022
+ });
1023
+ return (0, react_jsx_runtime.jsx)("dl", {
1024
+ className: AskQuestionCard_module_css_default.card,
1025
+ children: card.questions.map((question) => (0, react_jsx_runtime.jsxs)("div", {
1026
+ className: AskQuestionCard_module_css_default.item,
1027
+ children: [(0, react_jsx_runtime.jsx)("dt", {
1028
+ className: AskQuestionCard_module_css_default.question,
1029
+ children: question.question
1030
+ }), (0, react_jsx_runtime.jsx)("dd", {
1031
+ className: AskQuestionCard_module_css_default.answer,
1032
+ children: question.answers.length === 0 ? (0, react_jsx_runtime.jsx)("span", {
1033
+ className: AskQuestionCard_module_css_default.skipped,
1034
+ children: card.skippedLabel
1035
+ }) : question.answers.map((answer, index) => (0, react_jsx_runtime.jsx)("span", {
1036
+ className: AskQuestionCard_module_css_default.answerLine,
1037
+ children: answer
1038
+ }, `${question.id}-${String(index)}`))
1039
+ })]
1040
+ }, question.id))
1041
+ });
1042
+ }
1043
+ //#endregion
626
1044
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css.mjs
627
- const css$3 = ".o3BgMG_root{flex-direction:column;display:flex}.o3BgMG_row{position:relative;overflow:hidden}.o3BgMG_root[data-state=running] .o3BgMG_row:after{content:\"\";background:linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);pointer-events:none;width:300px;animation:2.6s ease-out infinite o3BgMG_dsh-tool-row-sweep;position:absolute;top:0;bottom:0;left:0}@keyframes o3BgMG_dsh-tool-row-sweep{0%{left:-300px}90%,to{left:100%}}.o3BgMG_leading{flex-shrink:0}.o3BgMG_root[data-tool^=cordis_] .o3BgMG_leading,.o3BgMG_root[data-tool^=cordis_] .o3BgMG_title{color:var(--dsw-alias-state-business-primary)}.o3BgMG_root[data-tool^=cordis_] .o3BgMG_title{font-weight:500}.o3BgMG_root[data-tool^=cordis_] .o3BgMG_sep{background:var(--dsw-alias-state-business-primary)}.o3BgMG_chevron{color:var(--dsw-alias-label-secondary)}.o3BgMG_title{font-weight:400}.o3BgMG_sep{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.o3BgMG_summary{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-tertiary);flex:auto;font-size:14px;line-height:24px;overflow:hidden}.o3BgMG_summarySuffix{white-space:nowrap;color:var(--dsw-alias-label-tertiary);flex:none;margin-left:4px;font-size:14px;line-height:24px}.o3BgMG_fileLink{text-overflow:ellipsis;white-space:nowrap;min-width:0;font:inherit;text-align:left;color:var(--dsw-alias-label-secondary);text-decoration:underline;text-decoration-color:var(--dsw-alias-label-quaternary);text-underline-offset:3px;cursor:pointer;background:0 0;border:none;flex:auto;margin:0;padding:0;font-size:14px;line-height:24px;overflow:hidden}.o3BgMG_fileLink:hover{color:var(--dsw-alias-label-primary);text-decoration-color:currentColor}.o3BgMG_errorSummary{color:var(--dsw-alias-state-error-primary)}.o3BgMG_bodyWrap{flex-direction:column;display:flex}.o3BgMG_inspectButton{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-secondary);cursor:pointer;opacity:0;border-radius:999px;align-self:flex-start;align-items:center;gap:4px;margin:4px 0 2px 4px;padding:2px 8px;font-size:11px;line-height:16px;transition:opacity .1s;display:inline-flex}.o3BgMG_root:hover .o3BgMG_inspectButton,.o3BgMG_inspectButton:focus-visible{opacity:1}.o3BgMG_inspectButton:hover{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-label-primary)}.o3BgMG_bodyScroll{max-height:260px;overflow-y:auto}.o3BgMG_ioCard{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-markdown-code-block);font:var(--dsw-font-markdown-code-block-small);border-radius:12px;flex-direction:column;margin:4px 0 4px 4px;display:flex}.o3BgMG_ioSection{grid-template-columns:max-content 1fr;align-items:baseline;column-gap:14px;max-height:150px;padding:12px 16px;display:grid;overflow-y:auto}.o3BgMG_ioSection::-webkit-scrollbar-thumb{background-clip:padding-box;border:2px solid #0000;border-radius:6px}.o3BgMG_ioSection::-webkit-scrollbar-track{margin:6px 0}.o3BgMG_ioLabel{color:var(--dsw-alias-label-caption);align-self:start;position:sticky;top:0}.o3BgMG_ioDivider{background:var(--dsw-alias-border-l2);flex:none;height:1px}.o3BgMG_ioText{white-space:pre-wrap;word-break:break-word;min-width:0;color:var(--dsw-alias-label-secondary)}.o3BgMG_ioText[data-error]{color:var(--dsw-alias-state-error-primary)}.o3BgMG_codeBody,.o3BgMG_terminalBody,.o3BgMG_diffBody,.o3BgMG_readBody,.o3BgMG_searchBody,.o3BgMG_webBody{margin:4px 0 4px 4px}.o3BgMG_searchRecovery{white-space:pre-wrap;overflow-wrap:anywhere;font:var(--dsw-font-xs-13);color:var(--dsw-alias-label-tertiary);margin:4px 0 4px 4px}.o3BgMG_codeBody{--dsl-code-block-content-font:var(--dsw-font-markdown-code-block-small)}.o3BgMG_terminalBody{--dsl-terminal-font:var(--dsw-font-markdown-code-block-small);--dsl-terminal-line-height:18px;--dsl-terminal-output-max-height:224px;border:1px solid var(--dsw-alias-border-l1)}.o3BgMG_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}";
1045
+ const css$3 = ".o3BgMG_root{flex-direction:column;display:flex}.o3BgMG_row{position:relative;overflow:hidden}.o3BgMG_root[data-state=running] .o3BgMG_row:after{content:\"\";background:linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);pointer-events:none;width:300px;animation:2.6s ease-out infinite o3BgMG_dsh-tool-row-sweep;position:absolute;top:0;bottom:0;left:0}@keyframes o3BgMG_dsh-tool-row-sweep{0%{left:-300px}90%,to{left:100%}}.o3BgMG_leading{flex-shrink:0}.o3BgMG_root[data-tool^=cordis_] .o3BgMG_leading,.o3BgMG_root[data-tool^=cordis_] .o3BgMG_title{color:var(--dsw-alias-state-business-primary)}.o3BgMG_root[data-tool^=cordis_] .o3BgMG_title{font-weight:500}.o3BgMG_root[data-tool^=cordis_] .o3BgMG_sep{background:var(--dsw-alias-state-business-primary)}.o3BgMG_chevron{color:var(--dsw-alias-label-secondary)}.o3BgMG_title{font-weight:400}.o3BgMG_sep{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.o3BgMG_summary{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);flex:auto;overflow:hidden}.o3BgMG_summarySuffix{white-space:nowrap;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);flex:none;margin-left:4px}.o3BgMG_diffStat{font-family:var(--ds-font-family-code);font-size:calc(var(--dsh-content-font-size-secondary,13px) - 2px);color:var(--dsw-alias-label-caption);margin-left:10px;transform:translateY(.5px)}.o3BgMG_fileLink{text-overflow:ellipsis;white-space:nowrap;min-width:0;font:inherit;text-align:left;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-secondary);text-decoration:underline dotted;text-decoration-color:var(--dsw-alias-label-tertiary);text-underline-offset:3px;cursor:pointer;background:0 0;border:none;flex:0 auto;margin:0;padding:0;text-decoration-thickness:1px;overflow:hidden}.o3BgMG_fileLink:hover{color:var(--dsw-alias-label-primary);text-decoration-color:currentColor}.o3BgMG_errorSummary{color:var(--dsw-alias-state-error-primary)}.o3BgMG_bodyWrap{flex-direction:column;display:flex}.o3BgMG_inspectButton{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-secondary);cursor:pointer;opacity:0;border-radius:999px;align-self:flex-start;align-items:center;gap:4px;margin:4px 0 2px 4px;padding:2px 8px;font-size:11px;line-height:16px;transition:opacity .1s;display:inline-flex}.o3BgMG_root:hover .o3BgMG_inspectButton,.o3BgMG_inspectButton:focus-visible{opacity:1}.o3BgMG_inspectButton:hover{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-label-primary)}.o3BgMG_bodyScroll{max-height:260px;overflow-y:auto}.o3BgMG_ioCard{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-markdown-code-block);font:var(--dsw-font-markdown-code-block-small);border-radius:12px;flex-direction:column;margin:4px 0 4px 4px;display:flex}.o3BgMG_ioSection{grid-template-columns:max-content 1fr;align-items:baseline;column-gap:14px;max-height:150px;padding:12px 16px;display:grid;overflow-y:auto}.o3BgMG_ioSection::-webkit-scrollbar-thumb{background-clip:padding-box;border:2px solid #0000;border-radius:6px}.o3BgMG_ioSection::-webkit-scrollbar-track{margin:6px 0}.o3BgMG_ioLabel{color:var(--dsw-alias-label-caption);align-self:start;position:sticky;top:0}.o3BgMG_ioDivider{background:var(--dsw-alias-border-l2);flex:none;height:1px}.o3BgMG_ioText{white-space:pre-wrap;word-break:break-word;min-width:0;color:var(--dsw-alias-label-secondary)}.o3BgMG_ioText[data-error]{color:var(--dsw-alias-state-error-primary)}.o3BgMG_codeBody,.o3BgMG_terminalBody,.o3BgMG_diffBody,.o3BgMG_readBody,.o3BgMG_searchBody,.o3BgMG_webBody{margin:4px 0 4px 4px}.o3BgMG_searchRecovery{white-space:pre-wrap;overflow-wrap:anywhere;font:var(--dsw-font-xs-13);color:var(--dsw-alias-label-tertiary);margin:4px 0 4px 4px}.o3BgMG_codeBody{--dsl-code-block-content-font:var(--dsw-font-markdown-code-block-small)}.o3BgMG_terminalBody{--dsl-terminal-font:var(--dsw-font-markdown-code-block-small);--dsl-terminal-line-height:18px;--dsl-terminal-output-max-height:224px;border:1px solid var(--dsw-alias-border-l1)}.o3BgMG_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}";
628
1046
  const tagId$3 = "@deepseek-ai/dsh-client-ui-tool/ToolRow.module.css";
629
1047
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
630
1048
  const tag = document.createElement("style");
@@ -639,6 +1057,7 @@ window.__ModuleLoader__.load({
639
1057
  "chevron": "o3BgMG_chevron",
640
1058
  "codeBody": "o3BgMG_codeBody",
641
1059
  "diffBody": "o3BgMG_diffBody",
1060
+ "diffStat": "o3BgMG_diffStat",
642
1061
  "dsh-tool-row-sweep": "o3BgMG_dsh-tool-row-sweep",
643
1062
  "errorSummary": "o3BgMG_errorSummary",
644
1063
  "fileLink": "o3BgMG_fileLink",
@@ -664,9 +1083,6 @@ window.__ModuleLoader__.load({
664
1083
  };
665
1084
  //#endregion
666
1085
  //#region lib/types/client/tool/components/ToolRow.js
667
- /** Leading-slot state substitution: the tool icon yields to the terminal state
668
- * semantic (error = red, interrupted = amber halo). Running keeps the icon —
669
- * the row sweep (CSS on data-state) carries the in-flight signal. */
670
1086
  function leadingFor$1(state, icon) {
671
1087
  switch (state) {
672
1088
  case "error": return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "error" });
@@ -686,20 +1102,38 @@ window.__ModuleLoader__.load({
686
1102
  default: return null;
687
1103
  }
688
1104
  }
689
- function ToolRow({ t, variant, toolName, icon, title, summary, summarySuffix, body, output, errorSummary, terminal, diff, read, search, web, state, filePath, onOpenFile, inspect }) {
1105
+ function ToolRow({ t, variant, toolName, icon, title, summary, summarySuffix, bodyRaw, output, askQuestion, errorSummary, terminal, diff, read, search, web, state, filePath, onOpenFile, inspect }) {
690
1106
  const [expanded, setExpanded] = (0, react.useState)(false);
691
- const terminalBody = terminal ?? null;
1107
+ const terminalLabels = (0, react.useMemo)(() => terminalBlockLabels(t), [t]);
1108
+ const diffLabels = (0, react.useMemo)(() => diffBlockLabels(t), [t]);
1109
+ const readLabels = (0, react.useMemo)(() => readBlockLabels(t), [t]);
1110
+ const searchLabels = (0, react.useMemo)(() => searchBlockLabels(t), [t]);
1111
+ const webLabels = (0, react.useMemo)(() => webBlockLabels(t), [t]);
1112
+ const terminalBody = terminal === void 0 || terminal === null ? null : localizeTerminalCardModel(terminal, t);
692
1113
  const diffBody = diff ?? null;
693
1114
  const readBody = read ?? null;
694
1115
  const searchBody = search ?? null;
695
1116
  const webBody = web ?? null;
1117
+ const askQuestionBody = askQuestion ?? null;
696
1118
  const outputText = output ?? null;
697
- const expandable = body !== null || outputText !== null || (terminalBody ?? diffBody ?? readBody ?? searchBody ?? webBody) !== null;
1119
+ const card = askQuestionBody ?? terminalBody ?? diffBody ?? readBody ?? searchBody ?? webBody;
1120
+ const expandable = bodyRaw != null || outputText !== null || card !== null;
698
1121
  const open = expanded && expandable;
1122
+ const bodyText = (0, react.useMemo)(() => open && card === null && bodyRaw != null ? formatToolBody(variant, bodyRaw) : null, [
1123
+ bodyRaw,
1124
+ card,
1125
+ open,
1126
+ variant
1127
+ ]);
699
1128
  const status = stateStatus$1(state, t);
700
1129
  const failureLine = state === "error" ? errorSummary ?? null : null;
701
- const summaryText = failureLine ?? summary;
702
- const suffix = failureLine === null ? summarySuffix ?? null : null;
1130
+ const summaryText = failureLine ?? terminalBody?.description ?? summary;
1131
+ const diffStat = (0, react.useMemo)(() => {
1132
+ if (diffBody === null) return null;
1133
+ const { added, removed } = (0, _deepseek_ai_dsh_client_ui_primitives.diffTotals)(diffBody.card.diffs);
1134
+ return `+${added} -${removed}`;
1135
+ }, [diffBody]);
1136
+ const suffix = failureLine === null ? summarySuffix ?? diffStat : null;
703
1137
  const fileLink = filePath !== void 0 && onOpenFile !== void 0 && failureLine === null;
704
1138
  const toggleExpand = () => {
705
1139
  setExpanded((v) => !v);
@@ -711,7 +1145,7 @@ window.__ModuleLoader__.load({
711
1145
  const fileLinkKeyDown = (event) => {
712
1146
  if (event.key === "Enter" || event.key === " ") event.stopPropagation();
713
1147
  };
714
- const cardBody = variant === "code" ? null : body;
1148
+ const cardBody = variant === "code" ? null : bodyText;
715
1149
  return (0, react_jsx_runtime.jsxs)("div", {
716
1150
  className: ToolRow_module_css_default.root,
717
1151
  "data-variant": variant,
@@ -748,27 +1182,30 @@ window.__ModuleLoader__.load({
748
1182
  children: summaryText
749
1183
  }),
750
1184
  suffix !== null && (0, react_jsx_runtime.jsx)("span", {
751
- className: ToolRow_module_css_default.summarySuffix,
1185
+ className: clsx(ToolRow_module_css_default.summarySuffix, suffix === diffStat && ToolRow_module_css_default.diffStat),
752
1186
  children: suffix
753
1187
  })
754
1188
  ] }),
755
1189
  children: (0, react_jsx_runtime.jsxs)("div", {
756
1190
  className: ToolRow_module_css_default.bodyWrap,
757
- children: [terminalBody !== null ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.TerminalBlock, {
1191
+ children: [askQuestionBody !== null ? (0, react_jsx_runtime.jsx)(AskQuestionCard, { card: askQuestionBody }) : terminalBody !== null ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.TerminalBlock, {
758
1192
  ...terminalBody.card,
759
1193
  maxLines: Infinity,
760
- labels: terminalBlockLabels(t),
1194
+ labels: terminalLabels,
761
1195
  className: ToolRow_module_css_default.terminalBody
762
1196
  }) : diffBody !== null ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DiffBlock, {
763
1197
  ...diffBody.card,
1198
+ labels: diffLabels,
764
1199
  maxLines: 8,
765
1200
  className: ToolRow_module_css_default.diffBody
766
1201
  }) : readBody !== null ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ReadBlock, {
767
1202
  ...readBody,
1203
+ labels: readLabels,
768
1204
  maxLines: 8,
769
1205
  className: ToolRow_module_css_default.readBody
770
1206
  }) : searchBody !== null ? (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SearchBlock, {
771
1207
  ...searchBody.card,
1208
+ labels: searchLabels,
772
1209
  maxLines: 8,
773
1210
  className: ToolRow_module_css_default.searchBody
774
1211
  }), searchBody.recovery !== void 0 && (0, react_jsx_runtime.jsx)("div", {
@@ -776,11 +1213,12 @@ window.__ModuleLoader__.load({
776
1213
  children: searchBody.recovery
777
1214
  })] }) : webBody !== null ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.WebBlock, {
778
1215
  ...webBody,
1216
+ labels: webLabels,
779
1217
  className: ToolRow_module_css_default.webBody
780
- }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [variant === "code" && body !== null && (0, react_jsx_runtime.jsx)("div", {
1218
+ }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [variant === "code" && bodyText !== null && (0, react_jsx_runtime.jsx)("div", {
781
1219
  className: ToolRow_module_css_default.bodyScroll,
782
1220
  children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.CodeBlock, {
783
- code: body,
1221
+ code: bodyText,
784
1222
  lang: "typescript",
785
1223
  copyLabel: t("copy"),
786
1224
  copiedLabel: t("copied"),
@@ -793,7 +1231,7 @@ window.__ModuleLoader__.load({
793
1231
  className: ToolRow_module_css_default.ioSection,
794
1232
  children: [(0, react_jsx_runtime.jsx)("span", {
795
1233
  className: ToolRow_module_css_default.ioLabel,
796
- children: "IN"
1234
+ children: t("row.input")
797
1235
  }), (0, react_jsx_runtime.jsx)("span", {
798
1236
  className: ToolRow_module_css_default.ioText,
799
1237
  children: cardBody
@@ -807,7 +1245,7 @@ window.__ModuleLoader__.load({
807
1245
  className: ToolRow_module_css_default.ioSection,
808
1246
  children: [(0, react_jsx_runtime.jsx)("span", {
809
1247
  className: ToolRow_module_css_default.ioLabel,
810
- children: "OUT"
1248
+ children: t("row.output")
811
1249
  }), (0, react_jsx_runtime.jsx)("span", {
812
1250
  className: ToolRow_module_css_default.ioText,
813
1251
  "data-error": state === "error" || void 0,
@@ -819,7 +1257,7 @@ window.__ModuleLoader__.load({
819
1257
  type: "button",
820
1258
  className: ToolRow_module_css_default.inspectButton,
821
1259
  onClick: inspect,
822
- children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconInspectOutline12, {}), "Inspect"]
1260
+ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconInspectOutline12, {}), t("row.inspect")]
823
1261
  })]
824
1262
  })
825
1263
  })]
@@ -851,9 +1289,9 @@ window.__ModuleLoader__.load({
851
1289
  variant: model.variant,
852
1290
  toolName,
853
1291
  icon: VARIANT_ICONS[model.variant],
854
- title: model.title,
855
- summary: terminal?.description ?? search?.title ?? model.summary,
856
- body: singleFile ? null : model.body,
1292
+ title: t(model.titleKey),
1293
+ summary: model.summary,
1294
+ bodyRaw: singleFile ? null : model.bodyRaw,
857
1295
  output: model.output,
858
1296
  errorSummary: model.errorSummary,
859
1297
  terminal,
@@ -958,8 +1396,8 @@ window.__ModuleLoader__.load({
958
1396
  * @param props - whole-Tool owner data and the Tool-owned child-slot share.
959
1397
  * @returns the Tool call tree.
960
1398
  */
961
- function ToolCallTree({ renderSlot, node, selectedCallId, cwd, openFile, inspectCall, useHostDescription, t }) {
962
- const home = useHostDescription((description) => description?.home);
1399
+ function ToolCallTree({ renderSlot, node, selectedCallId, cwd, openFile, inspectCall, useHostInfo, t }) {
1400
+ const home = useHostInfo((info) => info.home);
963
1401
  const block = node.data.root;
964
1402
  return (0, react_jsx_runtime.jsx)(ToolCallBranch, {
965
1403
  renderSlot,
@@ -996,35 +1434,41 @@ window.__ModuleLoader__.load({
996
1434
  //#region lib/types/client/tool/ToolDetails.js
997
1435
  /** Card-aware output body for the selected Tool call in details. */
998
1436
  /**
999
- * Render the selected Tool call's structured output when its presentation
1000
- * intent is known, otherwise preserve the flattened result text.
1437
+ * Render the selected Tool call's structured output when its raw fields form a
1438
+ * supported root card, otherwise preserve the flattened result text.
1001
1439
  * @param props - selected call slice, workspace root, host home, and locale seat.
1002
1440
  * @returns the details output body.
1003
1441
  */
1004
- function ToolDetails({ block, cwd, useHostDescription, t }) {
1005
- const home = useHostDescription((description) => description?.home);
1006
- const terminal = terminalCardModel(block, cwd);
1007
- if (terminal !== null) return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [terminal.description !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
1008
- className: ToolDetails_module_css_default.description,
1009
- children: terminal.description
1010
- }) : null, (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.TerminalBlock, {
1011
- ...terminal.card,
1012
- labels: terminalBlockLabels(t),
1013
- className: ToolDetails_module_css_default.cardBody
1014
- })] });
1442
+ function ToolDetails({ block, cwd, useHostInfo, t }) {
1443
+ const home = useHostInfo((info) => info.home);
1444
+ const terminalModel = terminalCardModel(block, cwd);
1445
+ if (terminalModel !== null) {
1446
+ const terminal = localizeTerminalCardModel(terminalModel, t);
1447
+ return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [terminal.description !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
1448
+ className: ToolDetails_module_css_default.description,
1449
+ children: terminal.description
1450
+ }) : null, (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.TerminalBlock, {
1451
+ ...terminal.card,
1452
+ labels: terminalBlockLabels(t),
1453
+ className: ToolDetails_module_css_default.cardBody
1454
+ })] });
1455
+ }
1015
1456
  const read = readCardModel(block, cwd, home);
1016
1457
  if (read !== null) return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ReadBlock, {
1017
1458
  ...read,
1459
+ labels: readBlockLabels(t),
1018
1460
  className: ToolDetails_module_css_default.read
1019
1461
  });
1020
1462
  const diff = diffCardModel(block);
1021
1463
  if (diff !== null) return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DiffBlock, {
1022
1464
  ...diff.card,
1465
+ labels: diffBlockLabels(t),
1023
1466
  className: ToolDetails_module_css_default.cardBody
1024
1467
  });
1025
1468
  const search = searchCardModel(block);
1026
1469
  if (search !== null) return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SearchBlock, {
1027
1470
  ...search.card,
1471
+ labels: searchBlockLabels(t),
1028
1472
  className: ToolDetails_module_css_default.cardBody
1029
1473
  }), search.recovery !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
1030
1474
  className: ToolDetails_module_css_default.recovery,
@@ -1035,6 +1479,7 @@ window.__ModuleLoader__.load({
1035
1479
  const body = "kind" in block ? resultText(block) : "";
1036
1480
  return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.WebBlock, {
1037
1481
  ...web,
1482
+ labels: webBlockLabels(t),
1038
1483
  className: ToolDetails_module_css_default.web
1039
1484
  }), body !== "" ? (0, react_jsx_runtime.jsx)("pre", {
1040
1485
  className: ToolDetails_module_css_default.code,
@@ -1057,40 +1502,133 @@ window.__ModuleLoader__.load({
1057
1502
  const CONVERSATION_NS = "conversation";
1058
1503
  //#endregion
1059
1504
  //#region lib/types/client/tool/toolviews/ask-question-row.js
1060
- function isAnswer(value) {
1061
- return typeof value === "object" && value !== null;
1505
+ function isRecord(value) {
1506
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1062
1507
  }
1063
- /** Answered-count summary from the result JSON (a skipped question has
1064
- * empty `selected` and no `custom`); null when answer fields are invalid. */
1065
- function answeredSummary(text, t) {
1066
- let parsed;
1508
+ function parseJson(text) {
1067
1509
  try {
1068
- parsed = JSON.parse(text);
1510
+ return JSON.parse(text);
1069
1511
  } catch {
1070
- return null;
1512
+ return;
1071
1513
  }
1072
- if (typeof parsed !== "object" || parsed === null) return null;
1514
+ }
1515
+ /** Answer records from the result JSON; null when the result is malformed. */
1516
+ function answerEntries(text) {
1517
+ const parsed = parseJson(text);
1518
+ if (!isRecord(parsed)) return null;
1073
1519
  const answers = parsed.answers;
1074
- if (!Array.isArray(answers) || !answers.every(isAnswer)) return null;
1520
+ if (!Array.isArray(answers) || !answers.every(isRecord)) return null;
1521
+ const entries = [];
1522
+ for (const answer of answers) {
1523
+ if (typeof answer.id !== "string" || !Array.isArray(answer.selected) || !answer.selected.every((item) => typeof item === "string") || answer.custom !== void 0 && typeof answer.custom !== "string") return null;
1524
+ entries.push({
1525
+ id: answer.id,
1526
+ selected: answer.selected,
1527
+ ...answer.custom === void 0 ? {} : { custom: answer.custom }
1528
+ });
1529
+ }
1530
+ return entries;
1531
+ }
1532
+ /** Questions from call JSON; null when pairing with answers would be ambiguous. */
1533
+ function questionEntries(argsRaw) {
1534
+ const parsed = parseJson(argsRaw);
1535
+ if (!isRecord(parsed) || !Array.isArray(parsed.questions) || parsed.questions.length === 0) return null;
1536
+ const questions = [];
1537
+ const ids = /* @__PURE__ */ new Set();
1538
+ for (const question of parsed.questions) {
1539
+ if (!isRecord(question) || typeof question.id !== "string" || typeof question.question !== "string" || ids.has(question.id)) return null;
1540
+ ids.add(question.id);
1541
+ questions.push({
1542
+ id: question.id,
1543
+ question: question.question
1544
+ });
1545
+ }
1546
+ return questions;
1547
+ }
1548
+ /** Pair questions with result entries by their echoed stable ids. */
1549
+ function pairAnswers(argsRaw, answers) {
1550
+ const questions = questionEntries(argsRaw);
1551
+ if (questions === null || questions.length !== answers.length) return null;
1552
+ const byId = /* @__PURE__ */ new Map();
1553
+ for (const answer of answers) {
1554
+ if (byId.has(answer.id)) return null;
1555
+ byId.set(answer.id, answer);
1556
+ }
1557
+ const paired = [];
1558
+ for (const question of questions) {
1559
+ const answer = byId.get(question.id);
1560
+ if (answer === void 0) return null;
1561
+ paired.push({
1562
+ ...question,
1563
+ answers: [...answer.selected, ...answer.custom === void 0 || answer.custom === "" ? [] : [answer.custom]]
1564
+ });
1565
+ }
1566
+ return paired;
1567
+ }
1568
+ /** Answer summary plus structured transcript content from the two wire JSON documents. */
1569
+ function answeredPresentation(argsRaw, text, t) {
1570
+ const answers = answerEntries(text);
1571
+ if (answers === null) return null;
1572
+ const answered = answers.filter((answer) => answer.selected.length > 0 || (answer.custom ?? "") !== "").length;
1573
+ return {
1574
+ summary: t("ask.answered", {
1575
+ answered,
1576
+ total: answers.length
1577
+ }),
1578
+ questions: pairAnswers(argsRaw, answers)
1579
+ };
1580
+ }
1581
+ /** Best-effort answered-count summary when strict transcript pairing fails. */
1582
+ function answeredSummary(text, t) {
1583
+ const parsed = parseJson(text);
1584
+ if (!isRecord(parsed)) return null;
1585
+ const answers = parsed.answers;
1586
+ if (!Array.isArray(answers) || !answers.every(isRecord)) return null;
1075
1587
  const answered = answers.filter((a) => Array.isArray(a.selected) && a.selected.length > 0 || typeof a.custom === "string" && a.custom !== "").length;
1076
1588
  return t("ask.answered", {
1077
1589
  answered,
1078
1590
  total: answers.length
1079
1591
  });
1080
1592
  }
1081
- /** One-line question-interaction row (the whole row toggles the call's
1082
- * Input/Output sections, ToolRow's unified expand). */
1593
+ /** Summarizes a pending, answered, cancelled, or interrupted question set. */
1083
1594
  function AskQuestionRow({ toolName, block, inspect, t }) {
1084
1595
  const model = toolRowModel(toolName, block);
1085
1596
  const code = "kind" in block ? block.error?.code : void 0;
1597
+ const argsRaw = ("kind" in block ? block.call?.argsRaw : block.argsRaw) ?? "";
1086
1598
  let summary = model.summary;
1087
1599
  let state = model.state;
1088
- if (code === "ASK_CANCELLED") summary = t("ask.cancelled");
1089
- else if (code === "ASK_ABORTED") {
1600
+ let transcript = null;
1601
+ if (code === "ASK_CANCELLED") {
1602
+ summary = t("ask.cancelled");
1603
+ state = "ok";
1604
+ const questions = questionEntries(argsRaw);
1605
+ if (questions !== null) transcript = {
1606
+ kind: "unanswered",
1607
+ questions,
1608
+ verdict: t("ask.cancelledDetail")
1609
+ };
1610
+ } else if (code === "ASK_ABORTED") {
1090
1611
  summary = t("ask.interrupted");
1091
1612
  state = "stopped";
1613
+ const questions = questionEntries(argsRaw);
1614
+ if (questions !== null) transcript = {
1615
+ kind: "unanswered",
1616
+ questions,
1617
+ verdict: t("ask.interruptedDetail")
1618
+ };
1092
1619
  } else if (model.state === "running") summary = t("ask.waiting");
1093
- else if ("kind" in block && model.state === "ok") summary = answeredSummary(block.content.filter((b) => b.type === "text").map((b) => b.text).join(""), t) ?? model.summary;
1620
+ else if ("kind" in block && model.state === "ok") {
1621
+ const text = singleResultText(block);
1622
+ if (text !== void 0) {
1623
+ const presentation = answeredPresentation(argsRaw, text, t);
1624
+ summary = presentation?.summary ?? answeredSummary(text, t) ?? model.summary;
1625
+ if (presentation?.questions !== null && presentation?.questions !== void 0) transcript = {
1626
+ kind: "answered",
1627
+ questions: presentation.questions,
1628
+ skippedLabel: t("ask.skipped")
1629
+ };
1630
+ }
1631
+ }
1094
1632
  return (0, react_jsx_runtime.jsx)(ToolRow, {
1095
1633
  t,
1096
1634
  variant: model.variant,
@@ -1098,23 +1636,17 @@ window.__ModuleLoader__.load({
1098
1636
  icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconQuestionOutline14, {}),
1099
1637
  title: t("ask.rowTitle"),
1100
1638
  summary,
1101
- body: model.body,
1102
- output: model.output,
1639
+ bodyRaw: transcript === null ? model.bodyRaw : null,
1640
+ output: transcript === null ? model.output : null,
1641
+ askQuestion: transcript,
1103
1642
  state,
1104
1643
  inspect
1105
1644
  });
1106
1645
  }
1107
- /**
1108
- * The ask-question row as a plain registrant plugin following the chat
1109
- * toolview declaration across independent activation and reload lifetimes.
1110
- */
1646
+ /** Registers the ask-user-question conversation row. */
1111
1647
  const askQuestionToolview = {
1112
1648
  name: "ask-question-toolview",
1113
1649
  inject: ["slots"],
1114
- /**
1115
- * Register the ask-question row into the Tool-owned keyed view slot.
1116
- * @param ctx - registrant context (disposal rides ctx.effect inside slots.register).
1117
- */
1118
1650
  apply(ctx) {
1119
1651
  ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
1120
1652
  name: "tool.call.toolview",
@@ -1125,7 +1657,7 @@ window.__ModuleLoader__.load({
1125
1657
  };
1126
1658
  //#endregion
1127
1659
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.module.css.mjs
1128
- const css = ".CY-8Ka_card{flex-direction:column;display:flex}.CY-8Ka_terminal{--dsl-terminal-font:var(--dsw-font-markdown-code-block-small);--dsl-terminal-line-height:18px;--dsl-terminal-output-max-height:224px;border:1px solid var(--dsw-alias-border-l1);margin:4px 0 4px 4px}.CY-8Ka_ioCard{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-markdown-code-block);font:var(--dsw-font-markdown-code-block-small);border-radius:12px;flex-direction:column;margin:4px 0 4px 4px;display:flex}.CY-8Ka_ioSection{grid-template-columns:max-content 1fr;align-items:baseline;column-gap:14px;max-height:150px;padding:12px 16px;display:grid;overflow-y:auto}.CY-8Ka_ioSection::-webkit-scrollbar-thumb{background-clip:padding-box;border:2px solid #0000;border-radius:6px}.CY-8Ka_ioSection::-webkit-scrollbar-track{margin:6px 0}.CY-8Ka_ioLabel{color:var(--dsw-alias-label-caption);align-self:start;position:sticky;top:0}.CY-8Ka_ioDivider{background:var(--dsw-alias-border-l2);flex:none;height:1px}.CY-8Ka_ioText{white-space:pre-wrap;word-break:break-word;min-width:0;color:var(--dsw-alias-label-secondary)}.CY-8Ka_ioText[data-error]{color:var(--dsw-alias-state-error-primary)}.CY-8Ka_root[data-expandable]{cursor:pointer}.CY-8Ka_root{align-items:center;min-width:0;height:24px;display:flex;position:relative;overflow:hidden}.CY-8Ka_root[data-state=running]:after{content:\"\";background:linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);pointer-events:none;width:300px;animation:2.6s ease-out infinite CY-8Ka_dsh-bash-row-sweep;position:absolute;top:0;bottom:0;left:0}@keyframes CY-8Ka_dsh-bash-row-sweep{0%{left:-300px}90%,to{left:100%}}.CY-8Ka_leading{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;margin-right:6px;display:inline-flex;position:relative}.CY-8Ka_chevron{color:var(--dsw-alias-label-secondary)}.CY-8Ka_iconIdle{opacity:1;transition:opacity .1s;display:inline-flex}.CY-8Ka_chevronHover{opacity:0;margin:auto;transition:opacity .1s;position:absolute;inset:0}.CY-8Ka_root:hover .CY-8Ka_iconIdle{opacity:0}.CY-8Ka_root:hover .CY-8Ka_chevronHover{opacity:1}.CY-8Ka_title{color:var(--dsw-alias-label-secondary);flex:none;font-size:14px;line-height:24px}.CY-8Ka_sep{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.CY-8Ka_summary{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-tertiary);flex:auto;font-size:14px;line-height:24px;overflow:hidden}.CY-8Ka_errorSummary{color:var(--dsw-alias-state-error-primary)}.CY-8Ka_bodyWrap{flex-direction:column;display:flex}.CY-8Ka_inspectButton{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-secondary);cursor:pointer;opacity:0;border-radius:999px;align-self:flex-start;align-items:center;gap:4px;margin:4px 0 2px 4px;padding:2px 8px;font-size:11px;line-height:16px;transition:opacity .1s;display:inline-flex}.CY-8Ka_card:hover .CY-8Ka_inspectButton,.CY-8Ka_inspectButton:focus-visible{opacity:1}.CY-8Ka_inspectButton:hover{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-label-primary)}.CY-8Ka_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}";
1660
+ const css = ".CY-8Ka_card{flex-direction:column;display:flex}.CY-8Ka_terminal{--dsl-terminal-font:var(--dsw-font-markdown-code-block-small);--dsl-terminal-line-height:18px;--dsl-terminal-output-max-height:224px;border:1px solid var(--dsw-alias-border-l1);margin:4px 0 4px 4px}.CY-8Ka_ioCard{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-markdown-code-block);font:var(--dsw-font-markdown-code-block-small);border-radius:12px;flex-direction:column;margin:4px 0 4px 4px;display:flex}.CY-8Ka_ioSection{grid-template-columns:max-content 1fr;align-items:baseline;column-gap:14px;max-height:150px;padding:12px 16px;display:grid;overflow-y:auto}.CY-8Ka_ioSection::-webkit-scrollbar-thumb{background-clip:padding-box;border:2px solid #0000;border-radius:6px}.CY-8Ka_ioSection::-webkit-scrollbar-track{margin:6px 0}.CY-8Ka_ioLabel{color:var(--dsw-alias-label-caption);align-self:start;position:sticky;top:0}.CY-8Ka_ioDivider{background:var(--dsw-alias-border-l2);flex:none;height:1px}.CY-8Ka_ioText{white-space:pre-wrap;word-break:break-word;min-width:0;color:var(--dsw-alias-label-secondary)}.CY-8Ka_ioText[data-error]{color:var(--dsw-alias-state-error-primary)}.CY-8Ka_root[data-expandable]{cursor:pointer}.CY-8Ka_root{height:calc(24px + var(--dsh-content-font-delta,0px));align-items:center;min-width:0;display:flex;position:relative;overflow:hidden}.CY-8Ka_root[data-state=running]:after{content:\"\";background:linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);pointer-events:none;width:300px;animation:2.6s ease-out infinite CY-8Ka_dsh-bash-row-sweep;position:absolute;top:0;bottom:0;left:0}@keyframes CY-8Ka_dsh-bash-row-sweep{0%{left:-300px}90%,to{left:100%}}.CY-8Ka_leading{width:calc(16px + var(--dsh-content-font-delta,0px));height:calc(16px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;margin-right:6px;display:inline-flex;position:relative}.CY-8Ka_leading svg:not([data-state]){width:calc(14px + var(--dsh-content-font-delta,0px));height:calc(14px + var(--dsh-content-font-delta,0px))}.CY-8Ka_chevron{color:var(--dsw-alias-label-secondary)}.CY-8Ka_iconIdle{opacity:1;transition:opacity .1s;display:inline-flex}.CY-8Ka_chevronHover{opacity:0;margin:auto;transition:opacity .1s;position:absolute;inset:0}.CY-8Ka_root:hover .CY-8Ka_iconIdle{opacity:0}.CY-8Ka_root:hover .CY-8Ka_chevronHover{opacity:1}.CY-8Ka_title{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);flex:none}.CY-8Ka_sep{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.CY-8Ka_summary{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);flex:auto;overflow:hidden}.CY-8Ka_errorSummary{color:var(--dsw-alias-state-error-primary)}.CY-8Ka_bodyWrap{flex-direction:column;display:flex}.CY-8Ka_inspectButton{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-secondary);cursor:pointer;opacity:0;border-radius:999px;align-self:flex-start;align-items:center;gap:4px;margin:4px 0 2px 4px;padding:2px 8px;font-size:11px;line-height:16px;transition:opacity .1s;display:inline-flex}.CY-8Ka_card:hover .CY-8Ka_inspectButton,.CY-8Ka_inspectButton:focus-visible{opacity:1}.CY-8Ka_inspectButton:hover{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-label-primary)}.CY-8Ka_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}";
1129
1661
  const tagId = "@deepseek-ai/dsh-client-ui-tool/bash-sample.module.css";
1130
1662
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1131
1663
  const tag = document.createElement("style");
@@ -1174,20 +1706,23 @@ window.__ModuleLoader__.load({
1174
1706
  default: return null;
1175
1707
  }
1176
1708
  }
1177
- /**
1178
- * Bash row: icon + Bash · {description} in the shared ToolRow chrome, the
1179
- * whole row toggling the command's terminal or generic error card (ToolRow's unified
1180
- * expand interaction, replicated locally per the registrant posture).
1181
- */
1709
+ /** Renders expandable Bash output with an accessible lifecycle label. */
1182
1710
  function BashRow({ toolName, block, sessionId, useSessions, inspect, t }) {
1183
1711
  const model = toolRowModel(toolName, block);
1184
- const terminal = terminalCardModel(block, useSessions((list) => list.byId[sessionId]?.cwd));
1185
- const state = model.state === "ok" && terminal !== null && terminalFailed(terminal) ? "error" : model.state;
1712
+ const terminalModel = terminalCardModel(block, useSessions((list) => list.byId[sessionId]?.cwd));
1713
+ const terminal = terminalModel === null ? null : localizeTerminalCardModel(terminalModel, t);
1714
+ const state = model.state === "ok" && terminalModel !== null && terminalFailed(terminalModel) ? "error" : model.state;
1186
1715
  const status = stateStatus(state, t);
1187
1716
  const [expanded, setExpanded] = (0, react.useState)(false);
1188
- const genericError = terminal === null && model.state === "error" && (model.body !== null || model.output !== null);
1189
- const expandable = terminal !== null || genericError;
1717
+ const genericBody = terminal === null && (model.state === "error" || isSettledPersistentShellCall(block)) && (model.bodyRaw !== null || model.output !== null);
1718
+ const expandable = terminal !== null || genericBody;
1190
1719
  const open = expanded && expandable;
1720
+ const body = (0, react.useMemo)(() => open && genericBody && model.bodyRaw !== null ? formatToolBody(model.variant, model.bodyRaw) : null, [
1721
+ genericBody,
1722
+ model.bodyRaw,
1723
+ model.variant,
1724
+ open
1725
+ ]);
1191
1726
  const failureLine = model.state === "error" ? model.errorSummary : null;
1192
1727
  const toggleExpand = () => {
1193
1728
  setExpanded((v) => !v);
@@ -1225,7 +1760,7 @@ window.__ModuleLoader__.load({
1225
1760
  }),
1226
1761
  (0, react_jsx_runtime.jsx)("span", {
1227
1762
  className: bash_sample_module_css_default.title,
1228
- children: model.title
1763
+ children: t(model.titleKey)
1229
1764
  }),
1230
1765
  (0, react_jsx_runtime.jsx)("span", {
1231
1766
  className: bash_sample_module_css_default.sep,
@@ -1246,17 +1781,17 @@ window.__ModuleLoader__.load({
1246
1781
  }) : (0, react_jsx_runtime.jsxs)("div", {
1247
1782
  className: bash_sample_module_css_default.ioCard,
1248
1783
  children: [
1249
- model.body !== null && (0, react_jsx_runtime.jsxs)("div", {
1784
+ body !== null && (0, react_jsx_runtime.jsxs)("div", {
1250
1785
  className: bash_sample_module_css_default.ioSection,
1251
1786
  children: [(0, react_jsx_runtime.jsx)("span", {
1252
1787
  className: bash_sample_module_css_default.ioLabel,
1253
- children: "IN"
1788
+ children: t("row.input")
1254
1789
  }), (0, react_jsx_runtime.jsx)("span", {
1255
1790
  className: bash_sample_module_css_default.ioText,
1256
- children: model.body
1791
+ children: body
1257
1792
  })]
1258
1793
  }),
1259
- model.body !== null && model.output !== null && (0, react_jsx_runtime.jsx)("span", {
1794
+ body !== null && model.output !== null && (0, react_jsx_runtime.jsx)("span", {
1260
1795
  className: bash_sample_module_css_default.ioDivider,
1261
1796
  "aria-hidden": true
1262
1797
  }),
@@ -1264,10 +1799,10 @@ window.__ModuleLoader__.load({
1264
1799
  className: bash_sample_module_css_default.ioSection,
1265
1800
  children: [(0, react_jsx_runtime.jsx)("span", {
1266
1801
  className: bash_sample_module_css_default.ioLabel,
1267
- children: "OUT"
1802
+ children: t("row.output")
1268
1803
  }), (0, react_jsx_runtime.jsx)("span", {
1269
1804
  className: bash_sample_module_css_default.ioText,
1270
- "data-error": true,
1805
+ "data-error": state === "error" || void 0,
1271
1806
  children: model.output
1272
1807
  })]
1273
1808
  })
@@ -1276,22 +1811,15 @@ window.__ModuleLoader__.load({
1276
1811
  type: "button",
1277
1812
  className: bash_sample_module_css_default.inspectButton,
1278
1813
  onClick: inspect,
1279
- children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconInspectOutline12, {}), "Inspect"]
1814
+ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconInspectOutline12, {}), t("row.inspect")]
1280
1815
  })]
1281
1816
  })]
1282
1817
  });
1283
1818
  }
1284
- /**
1285
- * The sample as a plain registrant plugin. Slot injection follows the chat
1286
- * toolview declaration across independent activation and reload lifetimes.
1287
- */
1819
+ /** Registers the standalone Bash conversation-row sample. */
1288
1820
  const bashToolviewSample = {
1289
1821
  name: "bash-toolview-sample",
1290
1822
  inject: ["slots"],
1291
- /**
1292
- * Register the bash row into the Tool-owned keyed view slot.
1293
- * @param ctx - registrant context (disposal rides ctx.effect inside slots.register).
1294
- */
1295
1823
  apply(ctx) {
1296
1824
  ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
1297
1825
  name: "tool.call.toolview",
@@ -1303,13 +1831,7 @@ window.__ModuleLoader__.load({
1303
1831
  //#endregion
1304
1832
  //#region lib/types/client/tool/toolviews/file-mutation-row.js
1305
1833
  /**
1306
- * File-mutation row: icon + {Edit,Write} · {path} in the shared ToolRow chrome,
1307
- * with the applied diff as the row's collapsed-by-default card body. The
1308
- * summary is a path link (a file tool's interaction); the host's `openFile`
1309
- * resolves it against the session cwd, so this passes the tool's own path
1310
- * verbatim. An errored mutation has no diff card, so ToolRow surfaces the
1311
- * model-facing error text through its Output section and its first line in the
1312
- * collapsed summary instead.
1834
+ * Lets users expand an applied file diff and open the reported path.
1313
1835
  */
1314
1836
  function FileMutationRow({ toolName, block, cwd, home, openFile, inspect, t }) {
1315
1837
  const model = toolRowModel(toolName, block, cwd, home);
@@ -1319,9 +1841,8 @@ window.__ModuleLoader__.load({
1319
1841
  variant: model.variant,
1320
1842
  toolName,
1321
1843
  icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, { size: 14 }),
1322
- title: model.title,
1844
+ title: t(model.titleKey),
1323
1845
  summary: model.summary,
1324
- body: null,
1325
1846
  output: model.output,
1326
1847
  errorSummary: model.errorSummary,
1327
1848
  diff,
@@ -1331,18 +1852,10 @@ window.__ModuleLoader__.load({
1331
1852
  inspect
1332
1853
  });
1333
1854
  }
1334
- /**
1335
- * The file-mutation rows as a plain registrant plugin following the chat
1336
- * toolview declaration across independent activation and reload lifetimes.
1337
- */
1855
+ /** Registers the edit and write conversation rows. */
1338
1856
  const fileMutationToolview = {
1339
1857
  name: "file-mutation-toolview",
1340
1858
  inject: ["slots"],
1341
- /**
1342
- * Register the file-mutation row into the Tool-owned keyed view slot
1343
- * under both mutation tool names.
1344
- * @param ctx - registrant context (disposal rides ctx.effect inside slots.register).
1345
- */
1346
1859
  apply(ctx) {
1347
1860
  ctx.slots.inject("tool.call.toolview", function* () {
1348
1861
  yield ctx.slots.register({
@@ -1361,9 +1874,7 @@ window.__ModuleLoader__.load({
1361
1874
  //#endregion
1362
1875
  //#region lib/types/client/tool/toolviews/read-row.js
1363
1876
  /**
1364
- * Read row: icon + Read · {path} in the shared ToolRow chrome, with the file's
1365
- * read card as the row's collapsed-by-default card body. The summary path is an
1366
- * openable host link when the row names a single file.
1877
+ * Lets users expand a completed read result and open its reported path.
1367
1878
  */
1368
1879
  function ReadRow({ toolName, block, cwd, home, openFile, inspect, t }) {
1369
1880
  const model = toolRowModel(toolName, block, cwd, home);
@@ -1373,9 +1884,8 @@ window.__ModuleLoader__.load({
1373
1884
  variant: model.variant,
1374
1885
  toolName,
1375
1886
  icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBrowseOutline16, { size: 14 }),
1376
- title: model.title,
1887
+ title: t(model.titleKey),
1377
1888
  summary: model.summary,
1378
- body: null,
1379
1889
  output: model.output,
1380
1890
  errorSummary: model.errorSummary,
1381
1891
  read,
@@ -1385,17 +1895,10 @@ window.__ModuleLoader__.load({
1385
1895
  inspect
1386
1896
  });
1387
1897
  }
1388
- /**
1389
- * The read row as a plain registrant plugin following the atomic Tool-view
1390
- * declaration across independent activation and reload lifetimes.
1391
- */
1898
+ /** Registers the read tool's conversation row. */
1392
1899
  const readToolview = {
1393
1900
  name: "read-toolview",
1394
1901
  inject: ["slots"],
1395
- /**
1396
- * Register the read row into the Tool-owned keyed view slot.
1397
- * @param ctx - registrant context (disposal rides ctx.effect inside slots.register).
1398
- */
1399
1902
  apply(ctx) {
1400
1903
  ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
1401
1904
  name: "tool.call.toolview",
@@ -1406,18 +1909,11 @@ window.__ModuleLoader__.load({
1406
1909
  };
1407
1910
  //#endregion
1408
1911
  //#region lib/types/client/tool/toolviews/search-row.js
1409
- const SEARCH_TITLES = {
1410
- grep: "Grep",
1411
- glob: "Glob"
1912
+ const SEARCH_TITLE_KEYS = {
1913
+ grep: "tool.title.grep",
1914
+ glob: "tool.title.glob"
1412
1915
  };
1413
- /**
1414
- * Search row: icon + Grep/Glob · {summary} in the shared ToolRow chrome, with the
1415
- * completed search's card as the row's collapsed-by-default card body (a capped
1416
- * search's recovery footer rides below it, inside ToolRow). Registered under
1417
- * both `grep` and `glob`; the derived model's `kind` decides the card shape. A
1418
- * settled call with no search card surfaces its model-facing text through
1419
- * ToolRow's Output section, since the keyed SearchRow owns this render slot.
1420
- */
1916
+ /** Lets users expand grep or glob results and recover capped searches. */
1421
1917
  function SearchRow({ toolName, block, inspect, t }) {
1422
1918
  const model = toolRowModel(toolName, block);
1423
1919
  const search = searchCardModel(block);
@@ -1426,9 +1922,8 @@ window.__ModuleLoader__.load({
1426
1922
  variant: model.variant,
1427
1923
  toolName,
1428
1924
  icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, { size: 14 }),
1429
- title: SEARCH_TITLES[toolName] ?? model.title,
1430
- summary: search?.title ?? model.summary,
1431
- body: null,
1925
+ title: t(toolName === "grep" ? SEARCH_TITLE_KEYS.grep : toolName === "glob" ? SEARCH_TITLE_KEYS.glob : model.titleKey),
1926
+ summary: model.summary,
1432
1927
  output: model.output,
1433
1928
  errorSummary: model.errorSummary,
1434
1929
  search,
@@ -1436,19 +1931,10 @@ window.__ModuleLoader__.load({
1436
1931
  inspect
1437
1932
  });
1438
1933
  }
1439
- /**
1440
- * The search view follows the atomic Tool-view declaration across activation
1441
- * and reload. One component registers under both keys because `grep` and
1442
- * `glob` are the same visual object discriminated by the result view's `kind`.
1443
- */
1934
+ /** Registers the grep and glob conversation rows. */
1444
1935
  const searchToolview = {
1445
1936
  name: "search-toolview",
1446
1937
  inject: ["slots"],
1447
- /**
1448
- * Register the search row into the Tool-owned keyed view slot under both
1449
- * the `grep` and `glob` tool names.
1450
- * @param ctx - registrant context (disposal rides ctx.effect inside slots.register).
1451
- */
1452
1938
  apply(ctx) {
1453
1939
  ctx.slots.inject("tool.call.toolview", function* () {
1454
1940
  yield ctx.slots.register({
@@ -1525,10 +2011,7 @@ window.__ModuleLoader__.load({
1525
2011
  extra: activeExtra
1526
2012
  };
1527
2013
  }
1528
- /** One-line plan update row (the whole row toggles the call's Input/Output
1529
- * sections, ToolRow's unified expand). Non-ok execution states keep the
1530
- * shared row's dot semantics — a cancelled call wrote no todo/write, so it
1531
- * must not read as a completed update. */
2014
+ /** Summarizes a plan update without presenting a cancelled call as completed. */
1532
2015
  function TodoRow({ toolName, block, inspect, t }) {
1533
2016
  const model = toolRowModel(toolName, block);
1534
2017
  const summary = summarize(("kind" in block ? block.call?.argsRaw : block.argsRaw) ?? "", t) ?? {
@@ -1543,24 +2026,17 @@ window.__ModuleLoader__.load({
1543
2026
  title: t("todo.rowTitle"),
1544
2027
  summary: summary.text,
1545
2028
  summarySuffix: summary.extra > 0 ? `+${summary.extra}` : null,
1546
- body: model.body,
2029
+ bodyRaw: model.bodyRaw,
1547
2030
  output: model.output,
1548
2031
  errorSummary: model.errorSummary,
1549
2032
  state: model.state,
1550
2033
  inspect
1551
2034
  });
1552
2035
  }
1553
- /**
1554
- * The todo row as a plain registrant plugin following the atomic Tool-view
1555
- * declaration across independent activation and reload lifetimes.
1556
- */
2036
+ /** Registers the todo conversation row. */
1557
2037
  const todoToolview = {
1558
2038
  name: "todo-toolview",
1559
2039
  inject: ["slots"],
1560
- /**
1561
- * Register the todo row into the Tool-owned keyed view slot.
1562
- * @param ctx - registrant context (disposal rides ctx.effect inside slots.register).
1563
- */
1564
2040
  apply(ctx) {
1565
2041
  ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
1566
2042
  name: "tool.call.toolview",
@@ -1571,16 +2047,11 @@ window.__ModuleLoader__.load({
1571
2047
  };
1572
2048
  //#endregion
1573
2049
  //#region lib/types/client/tool/toolviews/web-row.js
1574
- /** web_fetch reads one URL; web_search queries. Titles are figma literals. */
1575
- const WEB_TITLES = {
1576
- web_search: "Search",
1577
- web_fetch: "Fetch"
2050
+ const WEB_TITLE_KEYS = {
2051
+ web_search: "tool.title.webSearch",
2052
+ web_fetch: "tool.title.webFetch"
1578
2053
  };
1579
- /**
1580
- * Web row: icon + Search/Fetch · {summary} in the shared ToolRow chrome, with
1581
- * the completed retrieval's web card as the row's collapsed-by-default card
1582
- * body. The row discriminates on `toolName` only to pick its icon and title.
1583
- */
2054
+ /** Lets users expand a completed web search or fetch result. */
1584
2055
  function WebRow({ toolName, block, inspect, t }) {
1585
2056
  const model = toolRowModel(toolName, block);
1586
2057
  const web = webCardModel(block);
@@ -1590,9 +2061,8 @@ window.__ModuleLoader__.load({
1590
2061
  variant: model.variant,
1591
2062
  toolName,
1592
2063
  icon,
1593
- title: WEB_TITLES[toolName] ?? model.title,
2064
+ title: t(toolName === "web_search" ? WEB_TITLE_KEYS.web_search : toolName === "web_fetch" ? WEB_TITLE_KEYS.web_fetch : model.titleKey),
1594
2065
  summary: model.summary,
1595
- body: null,
1596
2066
  output: model.output,
1597
2067
  errorSummary: model.errorSummary,
1598
2068
  web,
@@ -1600,17 +2070,10 @@ window.__ModuleLoader__.load({
1600
2070
  inspect
1601
2071
  });
1602
2072
  }
1603
- /**
1604
- * The web rows follow the atomic Tool-view declaration across activation and
1605
- * reload. One WebRow component registers under both web tool names.
1606
- */
2073
+ /** Registers the web search and fetch conversation rows. */
1607
2074
  const webToolview = {
1608
2075
  name: "web-toolview",
1609
2076
  inject: ["slots"],
1610
- /**
1611
- * Register the web row under both web tool names' keyed toolview holes.
1612
- * @param ctx - registrant context (disposal rides ctx.effect inside slots.register).
1613
- */
1614
2077
  apply(ctx) {
1615
2078
  ctx.slots.inject("tool.call.toolview", function* () {
1616
2079
  yield ctx.slots.register({
@@ -1628,15 +2091,18 @@ window.__ModuleLoader__.load({
1628
2091
  };
1629
2092
  //#endregion
1630
2093
  //#region lib/types/client/apply.js
1631
- /** Required services: the slot registry and the Host description used for POSIX `~`. */
1632
- const inject = ["slots", "connection"];
2094
+ /** Required services: the slot registry and the Remote face carrying the Host home used for POSIX `~`. */
2095
+ const inject = ["slots", "remote"];
1633
2096
  /**
1634
2097
  * Mount the whole-Tool renderers and built-in atomic Tool registrations.
1635
2098
  * @param ctx - Client root context.
1636
2099
  */
1637
2100
  function apply(ctx) {
1638
- const connection = ctx.get("connection");
1639
- const toolInject = () => ({ hooks: { hostDescription: connection.hostDescription } });
2101
+ const hostInfo = {
2102
+ getSnapshot: () => ctx.remote.$host,
2103
+ subscribe: (listener) => ctx.on("connection/reset", listener)
2104
+ };
2105
+ const toolInject = () => ({ hooks: { hostInfo } });
1640
2106
  ctx.slots.inject("conversation.chat.node", () => ctx.slots.register({
1641
2107
  name: "conversation.chat.node",
1642
2108
  key: "tool-call",