@deepseek-ai/dsh-client-ui-tool 0.1.1-rc.2 → 0.1.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +868 -418
  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 +7 -31
  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 +22 -5
  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.
@@ -182,14 +209,14 @@ window.__ModuleLoader__.load({
182
209
  const done = "kind" in block;
183
210
  const argsRaw = (done ? block.call?.argsRaw : block.argsRaw) ?? "";
184
211
  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;
212
+ const base = argsRaw === "" ? block.callId : abbreviateHomePath(relativizeToCwd(deriveSummary(variant, argsRaw), cwd), home);
213
+ const toolTitleKey = TOOL_TITLE_KEYS[toolName];
214
+ const summary = variant === "others" && toolName !== "" && toolTitleKey === void 0 ? `${toolName} · ${base}` : base;
188
215
  const output = done ? resultText(block) || null : null;
189
216
  const errorSummary = state === "error" && output !== null ? firstLine(output) : null;
190
217
  return {
191
218
  variant,
192
- title: toolTitle ?? VARIANT_TITLES[variant],
219
+ titleKey: toolTitleKey ?? VARIANT_TITLE_KEYS[variant],
193
220
  summary,
194
221
  filePath: deriveFilePath(variant, argsRaw),
195
222
  body: deriveBody(variant, argsRaw),
@@ -199,72 +226,129 @@ window.__ModuleLoader__.load({
199
226
  };
200
227
  }
201
228
  //#endregion
202
- //#region lib/types/client/tool/models/read-card-model.js
229
+ //#region lib/types/client/tool/models/raw-tool-call.js
230
+ const parsedCalls = /* @__PURE__ */ new WeakMap();
203
231
  /**
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
232
+ * Parse the call head paired with one immutable Tool block.
233
+ * @param block - running or settled Tool block.
234
+ * @returns the Tool name and object arguments, or null when the call head or valid JSON object is unavailable.
217
235
  */
236
+ function parsedToolCall(block) {
237
+ const cached = parsedCalls.get(block);
238
+ if (cached !== void 0 || parsedCalls.has(block)) return cached ?? null;
239
+ const call = "kind" in block ? block.call : block;
240
+ if (call === null) {
241
+ parsedCalls.set(block, null);
242
+ return null;
243
+ }
244
+ let value;
245
+ try {
246
+ value = JSON.parse(call.argsRaw);
247
+ } catch {
248
+ parsedCalls.set(block, null);
249
+ return null;
250
+ }
251
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
252
+ parsedCalls.set(block, null);
253
+ return null;
254
+ }
255
+ const parsed = {
256
+ name: call.name,
257
+ args: value
258
+ };
259
+ parsedCalls.set(block, parsed);
260
+ return parsed;
261
+ }
218
262
  /**
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.
263
+ * Read the exact single text block consumed by first-party card derivations.
264
+ * @param block - settled Tool result.
265
+ * @returns its text, or undefined for any other content layout.
266
+ */
267
+ function singleResultText(block) {
268
+ if (block.content.length !== 1) return void 0;
269
+ const only = block.content[0];
270
+ return only?.type === "text" ? only.text : void 0;
271
+ }
272
+ /**
273
+ * Validate the optional escalation pair shared by first-party shell and file
274
+ * mutation tools.
275
+ * @param args - parsed open-root Tool arguments.
276
+ * @returns whether the declared escalation fields form a valid pair.
277
+ */
278
+ function validEscalationFields(args) {
279
+ const permission = args.sandbox_permissions;
280
+ const justification = args.justification;
281
+ if (permission === void 0 && justification === void 0) return true;
282
+ if (permission !== "workspace-write" && permission !== "danger-full-access") return false;
283
+ return typeof justification === "string" && justification.trim() !== "";
284
+ }
285
+ //#endregion
286
+ //#region lib/types/client/tool/models/read-card-model.js
287
+ function validReadCall(block) {
288
+ const call = parsedToolCall(block);
289
+ if (call?.name !== "read") return false;
290
+ const { file_path: path, offset, limit } = call.args;
291
+ if (typeof path !== "string" || path.trim() === "") return false;
292
+ if (offset !== void 0 && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 1)) return false;
293
+ if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1)) return false;
294
+ return true;
295
+ }
296
+ function readMeta(meta) {
297
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return null;
298
+ const { path, offset, lines, totalLines, lang } = meta;
299
+ if (typeof path !== "string" || typeof offset !== "number" || !Number.isInteger(offset) || offset < 1) return null;
300
+ if (typeof totalLines !== "number" || !Number.isInteger(totalLines) || totalLines < 0 || !Array.isArray(lines)) return null;
301
+ if (lang !== void 0 && typeof lang !== "string") return null;
302
+ const narrowed = [];
303
+ let previous = offset - 1;
304
+ for (const line of lines) {
305
+ if (typeof line !== "object" || line === null || Array.isArray(line)) return null;
306
+ const { number, text } = line;
307
+ if (typeof number !== "number" || !Number.isInteger(number) || number < 1 || number <= previous) return null;
308
+ if (number > totalLines || typeof text !== "string") return null;
309
+ previous = number;
310
+ narrowed.push({
311
+ number,
312
+ text
313
+ });
314
+ }
315
+ return {
316
+ path,
317
+ offset,
318
+ lines: narrowed,
319
+ totalLines,
320
+ ...lang === void 0 ? {} : { lang }
321
+ };
322
+ }
323
+ /**
324
+ * Derive a settled root read card after validating its persisted metadata and
325
+ * model-facing read envelope.
326
+ * @param block - running or settled Tool block.
238
327
  * @param sessionCwd - the session workspace root; a workspace-rooted absolute
239
328
  * path label displays relative to it. Absent leaves the path as authored.
240
329
  * @param home - host account home; a leftover POSIX home path displays as `~`.
241
330
  * @returns the read-card props, or null for the generic path.
242
331
  */
243
332
  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
- }));
333
+ if (block.parentCallId !== void 0 || !("kind" in block) || block.isError) return null;
334
+ if (!validReadCall(block)) return null;
335
+ const meta = readMeta(block.meta);
336
+ if (meta === null) return null;
337
+ const text = singleResultText(block);
338
+ if (text === void 0) return null;
339
+ if (/^<path>[^\n]*<\/path>\n<type>file<\/type>\n<content>\n([\s\S]*)\n<\/content>$/u.exec(text)?.[1] === void 0) return null;
251
340
  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
341
+ label: abbreviateHomePath(relativizeToCwd(meta.path, sessionCwd), home),
342
+ lines: meta.lines,
343
+ totalLines: meta.totalLines,
344
+ lang: meta.lang
256
345
  };
257
346
  }
258
347
  //#endregion
259
348
  //#region lib/types/client/tool/models/diff-card-model.js
260
349
  /**
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.
350
+ * Narrow opaque result metadata's `diffs` to well-formed hunks.
351
+ * @param diffs - the metadata field to validate.
268
352
  * @returns the validated hunks, or null when the payload is not usable.
269
353
  */
270
354
  function narrowDiffs(diffs) {
@@ -284,110 +368,182 @@ window.__ModuleLoader__.load({
284
368
  }
285
369
  return out;
286
370
  }
371
+ function intendedDiff(block) {
372
+ const parsed = parsedToolCall(block);
373
+ if (parsed === null) return null;
374
+ if (parsed.name === "str_replace_editor") {
375
+ const { command, path, file_text: fileText, old_str: oldText, new_str: newText } = parsed.args;
376
+ if (typeof path !== "string" || path.trim() === "") return null;
377
+ if (command === "create") {
378
+ if (fileText !== void 0 && typeof fileText !== "string") return null;
379
+ return {
380
+ tool: "str_replace_editor",
381
+ diff: {
382
+ path,
383
+ oldText: null,
384
+ newText: fileText ?? ""
385
+ }
386
+ };
387
+ }
388
+ if (command === "str_replace") {
389
+ if (oldText !== void 0 && typeof oldText !== "string") return null;
390
+ if (newText !== void 0 && typeof newText !== "string") return null;
391
+ return {
392
+ tool: "str_replace_editor",
393
+ diff: {
394
+ path,
395
+ oldText: oldText ?? null,
396
+ newText: newText ?? ""
397
+ }
398
+ };
399
+ }
400
+ return null;
401
+ }
402
+ const { file_path: path } = parsed.args;
403
+ if (typeof path !== "string" || path.trim() === "") return null;
404
+ if (!validEscalationFields(parsed.args)) return null;
405
+ if (parsed.name === "write") {
406
+ const { content } = parsed.args;
407
+ return typeof content === "string" ? {
408
+ tool: "write",
409
+ diff: {
410
+ path,
411
+ oldText: null,
412
+ newText: content
413
+ }
414
+ } : null;
415
+ }
416
+ if (parsed.name !== "edit") return null;
417
+ const { old_string: oldText, new_string: newText, replace_all: replaceAll } = parsed.args;
418
+ if (typeof oldText !== "string" || typeof newText !== "string") return null;
419
+ if (replaceAll !== void 0 && typeof replaceAll !== "boolean") return null;
420
+ return {
421
+ tool: "edit",
422
+ diff: {
423
+ path,
424
+ oldText: oldText || null,
425
+ newText
426
+ }
427
+ };
428
+ }
429
+ function appliedDiffs(meta) {
430
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return null;
431
+ const diffs = meta.diffs;
432
+ if (!Array.isArray(diffs)) return null;
433
+ if (diffs.length === 0) return "empty";
434
+ return narrowDiffs(diffs);
435
+ }
287
436
  /**
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.
437
+ * Derive running diffs for root write/edit and `str_replace_editor`
438
+ * create/replace calls, plus applied settled diffs for root write/edit calls.
439
+ * A successful write with valid empty metadata uses its argument-derived
440
+ * whole-file diff, matching create and identical-overwrite presentation;
441
+ * `str_replace_editor` settles through Generic because it has no result view.
442
+ * @param block - running or settled Tool block.
307
443
  * @returns the diff-card props, or null for the generic path.
308
444
  */
309
445
  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 } };
446
+ if (block.parentCallId !== void 0) return null;
447
+ const intended = intendedDiff(block);
448
+ if (intended === null) return null;
449
+ if (!("kind" in block)) return { card: { diffs: [intended.diff] } };
450
+ if (intended.tool === "str_replace_editor") return null;
451
+ if (block.isError) return null;
452
+ const applied = appliedDiffs(block.meta);
453
+ if (applied === null || applied === "empty") return intended.tool === "write" ? { card: { diffs: [intended.diff] } } : null;
454
+ return { card: { diffs: applied } };
318
455
  }
319
456
  //#endregion
320
457
  //#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"));
458
+ function validSearchCall(block) {
459
+ const call = parsedToolCall(block);
460
+ if (call === null) return null;
461
+ const { pattern, path } = call.args;
462
+ if (typeof pattern !== "string") return null;
463
+ if (call.name === "grep" && pattern === "") return null;
464
+ if (call.name === "glob" && pattern.trim() === "") return null;
465
+ if (call.name !== "grep" && call.name !== "glob") return null;
466
+ if (path !== void 0 && (typeof path !== "string" || path.trim() === "")) return null;
467
+ if (call.name === "grep") {
468
+ const { include } = call.args;
469
+ if (include !== void 0 && (typeof include !== "string" || !validInclude(include))) return null;
470
+ }
471
+ return call.name;
472
+ }
473
+ function validInclude(include) {
474
+ if (include.trim() === "" || include.startsWith("!")) return false;
475
+ let braceDepth = 0;
476
+ for (const character of include) if (character === "{") braceDepth += 1;
477
+ else if (character === "}") braceDepth = Math.max(0, braceDepth - 1);
478
+ else if (character === "," && braceDepth === 0) return false;
479
+ return true;
480
+ }
481
+ function searchFiles(value) {
482
+ if (!Array.isArray(value)) return null;
483
+ const files = [];
484
+ for (const file of value) {
485
+ if (typeof file !== "object" || file === null || Array.isArray(file)) return null;
486
+ const { path, matches } = file;
487
+ if (typeof path !== "string" || !Array.isArray(matches)) return null;
488
+ const narrowed = [];
489
+ for (const match of matches) {
490
+ if (typeof match !== "object" || match === null || Array.isArray(match)) return null;
491
+ const { lineNumber, line } = match;
492
+ if (typeof lineNumber !== "number" || !Number.isInteger(lineNumber) || lineNumber < 1) return null;
493
+ if (typeof line !== "string") return null;
494
+ narrowed.push({
495
+ lineNumber,
496
+ line
497
+ });
498
+ }
499
+ files.push({
500
+ path,
501
+ matches: narrowed
502
+ });
503
+ }
504
+ return files;
333
505
  }
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
506
  function flattenContent(content) {
344
507
  const text = content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
345
508
  return text === "" ? void 0 : text;
346
509
  }
347
510
  /**
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.
511
+ * Derive a settled root grep/glob card from persisted metadata.
512
+ * @param block - running or settled Tool block.
513
+ * @returns search-card props, or null for the generic path.
361
514
  */
362
515
  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;
516
+ if (block.parentCallId !== void 0 || !("kind" in block) || block.isError) return null;
517
+ const tool = validSearchCall(block);
518
+ if (tool === null) return null;
519
+ if (typeof block.meta !== "object" || block.meta === null || Array.isArray(block.meta)) return null;
520
+ const meta = block.meta;
521
+ if (typeof meta.truncated !== "boolean") return null;
522
+ if (typeof meta.total !== "number" || !Number.isInteger(meta.total) || meta.total < 0) return null;
366
523
  const common = {
367
- truncated: result.truncated,
368
- total: result.total
524
+ truncated: meta.truncated,
525
+ total: meta.total
369
526
  };
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,
527
+ const recovery = meta.truncated ? flattenContent(block.content) : void 0;
528
+ if (tool === "grep") {
529
+ if (meta.shape !== "matches") return null;
530
+ const files = searchFiles(meta.files);
531
+ return files === null ? null : {
375
532
  recovery,
376
533
  card: {
377
534
  kind: "matches",
378
- files: result.files,
535
+ files,
379
536
  ...common
380
537
  }
381
538
  };
382
539
  }
383
- if (result.shape !== "paths") return null;
384
- if (!Array.isArray(result.paths) || !result.paths.every((path) => typeof path === "string")) return null;
540
+ if (meta.shape !== "paths" || !Array.isArray(meta.paths)) return null;
541
+ if (!meta.paths.every((path) => typeof path === "string")) return null;
385
542
  return {
386
- title: result.title,
387
543
  recovery,
388
544
  card: {
389
545
  kind: "paths",
390
- paths: result.paths,
546
+ paths: [...meta.paths],
391
547
  ...common
392
548
  }
393
549
  };
@@ -395,16 +551,6 @@ window.__ModuleLoader__.load({
395
551
  //#endregion
396
552
  //#region lib/types/client/tool/models/terminal-card-model.js
397
553
  /**
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
554
  * Build the TerminalBlock display copy from the conversation locale seat —
409
555
  * the one place the primitive's label surface pairs with this package's
410
556
  * dictionary, shared by every terminal render site (chat row, bash row,
@@ -429,6 +575,29 @@ window.__ModuleLoader__.load({
429
575
  };
430
576
  }
431
577
  /**
578
+ * Resolve locale-owned `terminal_send` copy while preserving Tool-authored
579
+ * shell commands and descriptions verbatim.
580
+ * @param model - locale-neutral terminal card data.
581
+ * @param t - the render site's conversation locale seat.
582
+ * @returns terminal props and description ready for rendering.
583
+ */
584
+ function localizeTerminalCardModel(model, t) {
585
+ if (model.copy.kind === "shell") return {
586
+ card: {
587
+ command: model.copy.command,
588
+ ...model.card
589
+ },
590
+ description: model.copy.description
591
+ };
592
+ return {
593
+ card: {
594
+ command: model.copy.text === "" ? t("terminal.sendInput") : model.copy.text,
595
+ ...model.card
596
+ },
597
+ description: t("terminal.session", { sessionId: model.copy.sessionId })
598
+ };
599
+ }
600
+ /**
432
601
  * True when a settled terminal card reports a failing exit — a non-zero code
433
602
  * or a terminating signal. The bash tool settles a failing command as a
434
603
  * completed call (`isError` stays false: the exit status is result data), so
@@ -442,21 +611,18 @@ window.__ModuleLoader__.load({
442
611
  return running !== true && (exitCode !== void 0 && exitCode !== 0 || signal !== void 0);
443
612
  }
444
613
  /**
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.
614
+ * Resolve a shell call's workdir for display: an absolute path is used as-is,
615
+ * a relative one joins under the session workspace, and an omitted one is the
616
+ * session workspace. Without a session cwd, a relative path stays as authored
617
+ * and an omitted one stays absent.
618
+ * @param workdir - the raw call's workdir, if any.
453
619
  * @param sessionCwd - the session workspace root, if the caller knows it.
454
620
  * @returns the working directory for the prompt label, or undefined.
455
621
  */
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));
622
+ function resolveTerminalCwd(workdir, sessionCwd) {
623
+ if (workdir === void 0 || workdir === "") return sessionCwd;
624
+ if (sessionCwd === void 0 || sessionCwd === "") return normalizeSegments(workdir);
625
+ return normalizeSegments(resolveWorkspacePath(sessionCwd, workdir));
460
626
  }
461
627
  /**
462
628
  * Collapse `.` and `..` segments so the prompt label names the directory the
@@ -508,104 +674,195 @@ window.__ModuleLoader__.load({
508
674
  }
509
675
  return kept.join(separator);
510
676
  }
677
+ function shellCall(name, args) {
678
+ if (name !== "bash" && name !== "pwsh") return null;
679
+ const { command, description, timeoutMs, workdir, run_in_background: background } = args;
680
+ if (typeof command !== "string" || command.trim() === "") return null;
681
+ if (timeoutMs !== void 0 && (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0)) return null;
682
+ if (workdir !== void 0 && typeof workdir !== "string") return null;
683
+ if (background !== void 0 && typeof background !== "boolean") return null;
684
+ if (!validEscalationFields(args)) return null;
685
+ if (description === void 0) return {
686
+ kind: "shell",
687
+ command,
688
+ description: void 0,
689
+ workdir: void 0,
690
+ persistent: true,
691
+ background: false
692
+ };
693
+ if (typeof description !== "string" || description.trim() === "") return null;
694
+ return {
695
+ kind: "shell",
696
+ command,
697
+ description,
698
+ workdir,
699
+ persistent: false,
700
+ background: background === true
701
+ };
702
+ }
511
703
  /**
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.
704
+ * Identify a settled root call from the persistent Bash or PowerShell tool.
705
+ * Its result stays on the generic input/output path because the persistent
706
+ * shell can report resets and partial output without one process exit status.
707
+ * @param block - running or settled Tool block.
708
+ * @returns whether the block is a settled persistent-shell call.
709
+ */
710
+ function isSettledPersistentShellCall(block) {
711
+ if (!("kind" in block) || block.parentCallId !== void 0) return false;
712
+ const parsed = parsedToolCall(block);
713
+ if (parsed === null) return false;
714
+ return shellCall(parsed.name, parsed.args)?.persistent === true;
715
+ }
716
+ function terminalSendCall(name, args) {
717
+ if (name !== "terminal_send") return null;
718
+ const { sessionId, text, submit, run_in_background: background } = args;
719
+ if (typeof sessionId !== "string" || sessionId === "" || typeof text !== "string") return null;
720
+ if (submit !== void 0 && typeof submit !== "boolean") return null;
721
+ if (background !== void 0 && typeof background !== "boolean") return null;
722
+ return {
723
+ kind: "terminal-send",
724
+ text,
725
+ sessionId,
726
+ background: background === true
727
+ };
728
+ }
729
+ /**
730
+ * Parse the marker literals owned by `@deepseek-ai/dsh-shell/render` without
731
+ * importing that Host-only package into the Client dependency graph.
732
+ * @param text - rendered shell result text.
733
+ * @returns output with a trailing exit-code or signal marker extracted.
734
+ */
735
+ function parseExitStatus(text) {
736
+ const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text);
737
+ if (signal?.[1] !== void 0) return {
738
+ output: text.slice(0, signal.index),
739
+ signal: signal[1]
740
+ };
741
+ const exit = /\n\[exit code: (\d+)\]$/.exec(text);
742
+ if (exit?.[1] !== void 0) return {
743
+ output: text.slice(0, exit.index),
744
+ exitCode: Number(exit[1])
745
+ };
746
+ return {
747
+ output: text,
748
+ exitCode: 0
749
+ };
750
+ }
751
+ /**
752
+ * Derive terminal props for supported root shell and terminal-send calls.
753
+ * Standard shell results parse their final status marker; persistent shell
754
+ * results, background calls, errors, malformed input, or child dispatches use
755
+ * the generic path. {@link isSettledPersistentShellCall} lets that generic
756
+ * persistent result remain expandable without inventing one process status.
757
+ * @param block - running or settled Tool block.
758
+ * @param sessionCwd - session workspace root used to resolve workdir.
759
+ * @returns locale-neutral terminal-card data, or null for the generic path.
536
760
  */
537
761
  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,
762
+ if (block.parentCallId !== void 0) return null;
763
+ const parsed = parsedToolCall(block);
764
+ if (parsed === null) return null;
765
+ const call = shellCall(parsed.name, parsed.args) ?? terminalSendCall(parsed.name, parsed.args);
766
+ if (call === null || call.background) return null;
767
+ const copy = call.kind === "shell" ? {
768
+ kind: "shell",
769
+ command: call.command,
770
+ description: call.description
771
+ } : {
772
+ kind: "terminal-send",
773
+ text: call.text,
774
+ sessionId: call.sessionId
775
+ };
776
+ const cwd = resolveTerminalCwd(call.kind === "shell" ? call.workdir : void 0, sessionCwd);
777
+ if (!("kind" in block)) return {
778
+ copy,
541
779
  card: {
542
- command: call.title,
543
- cwd: resolveTerminalCwd(call.cwd, sessionCwd),
780
+ cwd,
544
781
  output: void 0,
545
782
  exitCode: void 0,
546
783
  signal: void 0,
547
784
  running: true
548
785
  }
549
786
  };
550
- const result = block.resultView?.card === "terminal" ? block.resultView : null;
551
- if (result === null) return null;
787
+ if (block.isError || call.kind === "shell" && call.persistent) return null;
788
+ const output = singleResultText(block);
789
+ if (output === void 0) return null;
790
+ const status = call.kind === "terminal-send" ? { output } : parseExitStatus(output);
552
791
  return {
553
- description: call?.description,
792
+ copy,
554
793
  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,
794
+ cwd,
795
+ output: status.output,
796
+ exitCode: status.exitCode,
797
+ signal: status.signal,
560
798
  running: false
561
799
  }
562
800
  };
563
801
  }
564
802
  //#endregion
565
803
  //#region lib/types/client/tool/models/web-card-model.js
804
+ function validWebCall(block) {
805
+ const call = parsedToolCall(block);
806
+ if (call === null) return null;
807
+ if (call.name === "web_search") {
808
+ const { queries } = call.args;
809
+ if (!Array.isArray(queries) || queries.length === 0) return null;
810
+ return queries.every((query) => typeof query === "string" && query.trim() !== "") ? call.name : null;
811
+ }
812
+ if (call.name === "web_fetch") {
813
+ const { url } = call.args;
814
+ return typeof url === "string" && url.trim() !== "" ? call.name : null;
815
+ }
816
+ return null;
817
+ }
818
+ function webSources(value) {
819
+ if (!Array.isArray(value)) return null;
820
+ const sources = [];
821
+ for (const source of value) {
822
+ if (typeof source !== "object" || source === null || Array.isArray(source)) return null;
823
+ const { url, title, snippet, publishedAt } = source;
824
+ if (typeof url !== "string") return null;
825
+ if (title !== void 0 && typeof title !== "string") return null;
826
+ if (snippet !== void 0 && typeof snippet !== "string") return null;
827
+ if (publishedAt !== void 0 && typeof publishedAt !== "string") return null;
828
+ sources.push({
829
+ url,
830
+ ...title === void 0 ? {} : { title },
831
+ ...snippet === void 0 ? {} : { snippet },
832
+ ...publishedAt === void 0 ? {} : { publishedAt }
833
+ });
834
+ }
835
+ return sources;
836
+ }
566
837
  /**
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.
838
+ * Derive a settled root web-search or web-fetch card from persisted metadata.
839
+ * @param block - running or settled Tool block.
840
+ * @returns web-card props, or null for the generic path.
586
841
  */
587
842
  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 {
843
+ if (block.parentCallId !== void 0 || !("kind" in block) || block.isError) return null;
844
+ const tool = validWebCall(block);
845
+ if (tool === null || typeof block.meta !== "object" || block.meta === null || Array.isArray(block.meta)) return null;
846
+ const meta = block.meta;
847
+ if (typeof meta.truncated !== "boolean") return null;
848
+ if (tool === "web_search") {
849
+ const sources = webSources(meta.sources);
850
+ if (sources === null || meta.answer !== void 0 && typeof meta.answer !== "string") return null;
851
+ return {
852
+ kind: "search",
853
+ answer: meta.answer,
854
+ sources,
855
+ truncated: meta.truncated
856
+ };
857
+ }
858
+ if (typeof meta.url !== "string") return null;
859
+ if (typeof meta.statusCode !== "number" || !Number.isInteger(meta.statusCode)) return null;
860
+ return {
603
861
  kind: "fetch",
604
- url: result.url,
605
- statusCode: result.statusCode,
606
- truncated: result.truncated
862
+ url: meta.url,
863
+ statusCode: meta.statusCode,
864
+ truncated: meta.truncated
607
865
  };
608
- return null;
609
866
  }
610
867
  //#endregion
611
868
  //#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
@@ -623,8 +880,162 @@ window.__ModuleLoader__.load({
623
880
  return n;
624
881
  }
625
882
  //#endregion
883
+ //#region lib/types/client/tool/models/primitive-labels.js
884
+ /** Localized copy adapters for Cordis-free UI primitives used by Tool cards. */
885
+ /**
886
+ * Build localized Markdown chrome labels.
887
+ * @param t - Conversation locale seat.
888
+ * @returns Markdown chrome labels.
889
+ */
890
+ function markdownLabels(t) {
891
+ return {
892
+ code: {
893
+ copyLabel: t("copy"),
894
+ copiedLabel: t("copied")
895
+ },
896
+ footnotes: t("markdown.footnotes")
897
+ };
898
+ }
899
+ /**
900
+ * Build localized diff-card chrome labels.
901
+ * @param t - Conversation locale seat.
902
+ * @returns Diff-card chrome labels.
903
+ */
904
+ function diffBlockLabels(t) {
905
+ return {
906
+ copy: t("copy"),
907
+ copied: t("copied"),
908
+ collapseAria: t("diff.collapseAria"),
909
+ expandAria: (count) => t("diff.expandAria", { count }),
910
+ collapse: t("collapse"),
911
+ expand: (count) => t("diff.expandRest", { count }),
912
+ files: (count) => t(count === 1 ? "diff.files.one" : "diff.files.other", { count })
913
+ };
914
+ }
915
+ /**
916
+ * Build localized read-card chrome labels.
917
+ * @param t - Conversation locale seat.
918
+ * @returns Read-card chrome labels.
919
+ */
920
+ function readBlockLabels(t) {
921
+ return {
922
+ window: (shown, total) => t("read.window", {
923
+ shown,
924
+ total
925
+ }),
926
+ copy: t("copy"),
927
+ copied: t("copied"),
928
+ collapseAria: t("read.collapseAria"),
929
+ expandAria: (count) => t("read.expandAria", { count }),
930
+ collapse: t("collapse"),
931
+ expand: (count) => t("read.expandRest", { count })
932
+ };
933
+ }
934
+ /**
935
+ * Build localized search-card chrome labels.
936
+ * @param t - Conversation locale seat.
937
+ * @returns Search-card chrome labels.
938
+ */
939
+ function searchBlockLabels(t) {
940
+ return {
941
+ pathsSummary: (shown, total, truncated) => t(truncated ? "search.paths.truncated" : "search.paths", {
942
+ shown,
943
+ total
944
+ }),
945
+ matchesSummary: (shown, total, files, truncated) => t(truncated ? "search.matches.truncated" : "search.matches", {
946
+ shown,
947
+ total,
948
+ files
949
+ }),
950
+ copy: t("copy"),
951
+ copied: t("copied"),
952
+ noResults: t("search.noResults"),
953
+ collapseAria: t("search.collapseAria"),
954
+ expandAria: (count) => t("search.expandAria", { count }),
955
+ collapse: t("collapse"),
956
+ expand: (count) => t("search.expandRest", { count })
957
+ };
958
+ }
959
+ /**
960
+ * Build localized web-card chrome labels.
961
+ * @param t - Conversation locale seat.
962
+ * @returns Web-card chrome labels.
963
+ */
964
+ function webBlockLabels(t) {
965
+ return {
966
+ noResults: t("web.noResults"),
967
+ sourcesTruncated: t("web.sourcesTruncated"),
968
+ http: t("web.http"),
969
+ contentTruncated: t("web.contentTruncated"),
970
+ markdown: markdownLabels(t)
971
+ };
972
+ }
973
+ //#endregion
974
+ //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-tool/src/client/tool/components/AskQuestionCard.module.css.mjs
975
+ 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))}";
976
+ const tagId$4 = "@deepseek-ai/dsh-client-ui-tool/AskQuestionCard.module.css";
977
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
978
+ const tag = document.createElement("style");
979
+ tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-tool";
980
+ tag.dataset.pluginCss = tagId$4;
981
+ tag.textContent = css$4;
982
+ document.head.appendChild(tag);
983
+ }
984
+ var AskQuestionCard_module_css_default = {
985
+ "answer": "fsXYAq_answer",
986
+ "answerLine": "fsXYAq_answerLine",
987
+ "card": "fsXYAq_card",
988
+ "item": "fsXYAq_item",
989
+ "question": "fsXYAq_question",
990
+ "questionList": "fsXYAq_questionList",
991
+ "skipped": "fsXYAq_skipped",
992
+ "unansweredQuestion": "fsXYAq_unansweredQuestion",
993
+ "verdict": "fsXYAq_verdict"
994
+ };
995
+ //#endregion
996
+ //#region lib/types/client/tool/components/AskQuestionCard.js
997
+ /**
998
+ * Render a validated ask-user transcript from plain card data.
999
+ * @param props - Localized transcript card data.
1000
+ * @returns the readable answered or unanswered question list.
1001
+ */
1002
+ function AskQuestionCard({ card }) {
1003
+ if (card.kind === "unanswered") return (0, react_jsx_runtime.jsxs)("div", {
1004
+ className: AskQuestionCard_module_css_default.card,
1005
+ children: [(0, react_jsx_runtime.jsx)("p", {
1006
+ className: AskQuestionCard_module_css_default.verdict,
1007
+ children: card.verdict
1008
+ }), (0, react_jsx_runtime.jsx)("ul", {
1009
+ className: AskQuestionCard_module_css_default.questionList,
1010
+ children: card.questions.map((question) => (0, react_jsx_runtime.jsx)("li", {
1011
+ className: AskQuestionCard_module_css_default.unansweredQuestion,
1012
+ children: question.question
1013
+ }, question.id))
1014
+ })]
1015
+ });
1016
+ return (0, react_jsx_runtime.jsx)("dl", {
1017
+ className: AskQuestionCard_module_css_default.card,
1018
+ children: card.questions.map((question) => (0, react_jsx_runtime.jsxs)("div", {
1019
+ className: AskQuestionCard_module_css_default.item,
1020
+ children: [(0, react_jsx_runtime.jsx)("dt", {
1021
+ className: AskQuestionCard_module_css_default.question,
1022
+ children: question.question
1023
+ }), (0, react_jsx_runtime.jsx)("dd", {
1024
+ className: AskQuestionCard_module_css_default.answer,
1025
+ children: question.answers.length === 0 ? (0, react_jsx_runtime.jsx)("span", {
1026
+ className: AskQuestionCard_module_css_default.skipped,
1027
+ children: card.skippedLabel
1028
+ }) : question.answers.map((answer, index) => (0, react_jsx_runtime.jsx)("span", {
1029
+ className: AskQuestionCard_module_css_default.answerLine,
1030
+ children: answer
1031
+ }, `${question.id}-${String(index)}`))
1032
+ })]
1033
+ }, question.id))
1034
+ });
1035
+ }
1036
+ //#endregion
626
1037
  //#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}";
1038
+ 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
1039
  const tagId$3 = "@deepseek-ai/dsh-client-ui-tool/ToolRow.module.css";
629
1040
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
630
1041
  const tag = document.createElement("style");
@@ -639,6 +1050,7 @@ window.__ModuleLoader__.load({
639
1050
  "chevron": "o3BgMG_chevron",
640
1051
  "codeBody": "o3BgMG_codeBody",
641
1052
  "diffBody": "o3BgMG_diffBody",
1053
+ "diffStat": "o3BgMG_diffStat",
642
1054
  "dsh-tool-row-sweep": "o3BgMG_dsh-tool-row-sweep",
643
1055
  "errorSummary": "o3BgMG_errorSummary",
644
1056
  "fileLink": "o3BgMG_fileLink",
@@ -664,9 +1076,6 @@ window.__ModuleLoader__.load({
664
1076
  };
665
1077
  //#endregion
666
1078
  //#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
1079
  function leadingFor$1(state, icon) {
671
1080
  switch (state) {
672
1081
  case "error": return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "error" });
@@ -686,20 +1095,31 @@ window.__ModuleLoader__.load({
686
1095
  default: return null;
687
1096
  }
688
1097
  }
689
- function ToolRow({ t, variant, toolName, icon, title, summary, summarySuffix, body, output, errorSummary, terminal, diff, read, search, web, state, filePath, onOpenFile, inspect }) {
1098
+ function ToolRow({ t, variant, toolName, icon, title, summary, summarySuffix, body, output, askQuestion, errorSummary, terminal, diff, read, search, web, state, filePath, onOpenFile, inspect }) {
690
1099
  const [expanded, setExpanded] = (0, react.useState)(false);
691
- const terminalBody = terminal ?? null;
1100
+ const terminalLabels = (0, react.useMemo)(() => terminalBlockLabels(t), [t]);
1101
+ const diffLabels = (0, react.useMemo)(() => diffBlockLabels(t), [t]);
1102
+ const readLabels = (0, react.useMemo)(() => readBlockLabels(t), [t]);
1103
+ const searchLabels = (0, react.useMemo)(() => searchBlockLabels(t), [t]);
1104
+ const webLabels = (0, react.useMemo)(() => webBlockLabels(t), [t]);
1105
+ const terminalBody = terminal === void 0 || terminal === null ? null : localizeTerminalCardModel(terminal, t);
692
1106
  const diffBody = diff ?? null;
693
1107
  const readBody = read ?? null;
694
1108
  const searchBody = search ?? null;
695
1109
  const webBody = web ?? null;
1110
+ const askQuestionBody = askQuestion ?? null;
696
1111
  const outputText = output ?? null;
697
- const expandable = body !== null || outputText !== null || (terminalBody ?? diffBody ?? readBody ?? searchBody ?? webBody) !== null;
1112
+ const expandable = body !== null || outputText !== null || (askQuestionBody ?? terminalBody ?? diffBody ?? readBody ?? searchBody ?? webBody) !== null;
698
1113
  const open = expanded && expandable;
699
1114
  const status = stateStatus$1(state, t);
700
1115
  const failureLine = state === "error" ? errorSummary ?? null : null;
701
- const summaryText = failureLine ?? summary;
702
- const suffix = failureLine === null ? summarySuffix ?? null : null;
1116
+ const summaryText = failureLine ?? terminalBody?.description ?? summary;
1117
+ const diffStat = (0, react.useMemo)(() => {
1118
+ if (diffBody === null) return null;
1119
+ const { added, removed } = (0, _deepseek_ai_dsh_client_ui_primitives.diffTotals)(diffBody.card.diffs);
1120
+ return `+${added} -${removed}`;
1121
+ }, [diffBody]);
1122
+ const suffix = failureLine === null ? summarySuffix ?? diffStat : null;
703
1123
  const fileLink = filePath !== void 0 && onOpenFile !== void 0 && failureLine === null;
704
1124
  const toggleExpand = () => {
705
1125
  setExpanded((v) => !v);
@@ -748,27 +1168,30 @@ window.__ModuleLoader__.load({
748
1168
  children: summaryText
749
1169
  }),
750
1170
  suffix !== null && (0, react_jsx_runtime.jsx)("span", {
751
- className: ToolRow_module_css_default.summarySuffix,
1171
+ className: clsx(ToolRow_module_css_default.summarySuffix, suffix === diffStat && ToolRow_module_css_default.diffStat),
752
1172
  children: suffix
753
1173
  })
754
1174
  ] }),
755
1175
  children: (0, react_jsx_runtime.jsxs)("div", {
756
1176
  className: ToolRow_module_css_default.bodyWrap,
757
- children: [terminalBody !== null ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.TerminalBlock, {
1177
+ 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
1178
  ...terminalBody.card,
759
1179
  maxLines: Infinity,
760
- labels: terminalBlockLabels(t),
1180
+ labels: terminalLabels,
761
1181
  className: ToolRow_module_css_default.terminalBody
762
1182
  }) : diffBody !== null ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DiffBlock, {
763
1183
  ...diffBody.card,
1184
+ labels: diffLabels,
764
1185
  maxLines: 8,
765
1186
  className: ToolRow_module_css_default.diffBody
766
1187
  }) : readBody !== null ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ReadBlock, {
767
1188
  ...readBody,
1189
+ labels: readLabels,
768
1190
  maxLines: 8,
769
1191
  className: ToolRow_module_css_default.readBody
770
1192
  }) : 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
1193
  ...searchBody.card,
1194
+ labels: searchLabels,
772
1195
  maxLines: 8,
773
1196
  className: ToolRow_module_css_default.searchBody
774
1197
  }), searchBody.recovery !== void 0 && (0, react_jsx_runtime.jsx)("div", {
@@ -776,6 +1199,7 @@ window.__ModuleLoader__.load({
776
1199
  children: searchBody.recovery
777
1200
  })] }) : webBody !== null ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.WebBlock, {
778
1201
  ...webBody,
1202
+ labels: webLabels,
779
1203
  className: ToolRow_module_css_default.webBody
780
1204
  }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [variant === "code" && body !== null && (0, react_jsx_runtime.jsx)("div", {
781
1205
  className: ToolRow_module_css_default.bodyScroll,
@@ -793,7 +1217,7 @@ window.__ModuleLoader__.load({
793
1217
  className: ToolRow_module_css_default.ioSection,
794
1218
  children: [(0, react_jsx_runtime.jsx)("span", {
795
1219
  className: ToolRow_module_css_default.ioLabel,
796
- children: "IN"
1220
+ children: t("row.input")
797
1221
  }), (0, react_jsx_runtime.jsx)("span", {
798
1222
  className: ToolRow_module_css_default.ioText,
799
1223
  children: cardBody
@@ -807,7 +1231,7 @@ window.__ModuleLoader__.load({
807
1231
  className: ToolRow_module_css_default.ioSection,
808
1232
  children: [(0, react_jsx_runtime.jsx)("span", {
809
1233
  className: ToolRow_module_css_default.ioLabel,
810
- children: "OUT"
1234
+ children: t("row.output")
811
1235
  }), (0, react_jsx_runtime.jsx)("span", {
812
1236
  className: ToolRow_module_css_default.ioText,
813
1237
  "data-error": state === "error" || void 0,
@@ -819,7 +1243,7 @@ window.__ModuleLoader__.load({
819
1243
  type: "button",
820
1244
  className: ToolRow_module_css_default.inspectButton,
821
1245
  onClick: inspect,
822
- children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconInspectOutline12, {}), "Inspect"]
1246
+ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconInspectOutline12, {}), t("row.inspect")]
823
1247
  })]
824
1248
  })
825
1249
  })]
@@ -851,8 +1275,8 @@ window.__ModuleLoader__.load({
851
1275
  variant: model.variant,
852
1276
  toolName,
853
1277
  icon: VARIANT_ICONS[model.variant],
854
- title: model.title,
855
- summary: terminal?.description ?? search?.title ?? model.summary,
1278
+ title: t(model.titleKey),
1279
+ summary: model.summary,
856
1280
  body: singleFile ? null : model.body,
857
1281
  output: model.output,
858
1282
  errorSummary: model.errorSummary,
@@ -958,8 +1382,8 @@ window.__ModuleLoader__.load({
958
1382
  * @param props - whole-Tool owner data and the Tool-owned child-slot share.
959
1383
  * @returns the Tool call tree.
960
1384
  */
961
- function ToolCallTree({ renderSlot, node, selectedCallId, cwd, openFile, inspectCall, useHostDescription, t }) {
962
- const home = useHostDescription((description) => description?.home);
1385
+ function ToolCallTree({ renderSlot, node, selectedCallId, cwd, openFile, inspectCall, useHostInfo, t }) {
1386
+ const home = useHostInfo((info) => info.home);
963
1387
  const block = node.data.root;
964
1388
  return (0, react_jsx_runtime.jsx)(ToolCallBranch, {
965
1389
  renderSlot,
@@ -996,35 +1420,41 @@ window.__ModuleLoader__.load({
996
1420
  //#region lib/types/client/tool/ToolDetails.js
997
1421
  /** Card-aware output body for the selected Tool call in details. */
998
1422
  /**
999
- * Render the selected Tool call's structured output when its presentation
1000
- * intent is known, otherwise preserve the flattened result text.
1423
+ * Render the selected Tool call's structured output when its raw fields form a
1424
+ * supported root card, otherwise preserve the flattened result text.
1001
1425
  * @param props - selected call slice, workspace root, host home, and locale seat.
1002
1426
  * @returns the details output body.
1003
1427
  */
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
- })] });
1428
+ function ToolDetails({ block, cwd, useHostInfo, t }) {
1429
+ const home = useHostInfo((info) => info.home);
1430
+ const terminalModel = terminalCardModel(block, cwd);
1431
+ if (terminalModel !== null) {
1432
+ const terminal = localizeTerminalCardModel(terminalModel, t);
1433
+ return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [terminal.description !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
1434
+ className: ToolDetails_module_css_default.description,
1435
+ children: terminal.description
1436
+ }) : null, (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.TerminalBlock, {
1437
+ ...terminal.card,
1438
+ labels: terminalBlockLabels(t),
1439
+ className: ToolDetails_module_css_default.cardBody
1440
+ })] });
1441
+ }
1015
1442
  const read = readCardModel(block, cwd, home);
1016
1443
  if (read !== null) return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ReadBlock, {
1017
1444
  ...read,
1445
+ labels: readBlockLabels(t),
1018
1446
  className: ToolDetails_module_css_default.read
1019
1447
  });
1020
1448
  const diff = diffCardModel(block);
1021
1449
  if (diff !== null) return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DiffBlock, {
1022
1450
  ...diff.card,
1451
+ labels: diffBlockLabels(t),
1023
1452
  className: ToolDetails_module_css_default.cardBody
1024
1453
  });
1025
1454
  const search = searchCardModel(block);
1026
1455
  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
1456
  ...search.card,
1457
+ labels: searchBlockLabels(t),
1028
1458
  className: ToolDetails_module_css_default.cardBody
1029
1459
  }), search.recovery !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
1030
1460
  className: ToolDetails_module_css_default.recovery,
@@ -1035,6 +1465,7 @@ window.__ModuleLoader__.load({
1035
1465
  const body = "kind" in block ? resultText(block) : "";
1036
1466
  return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.WebBlock, {
1037
1467
  ...web,
1468
+ labels: webBlockLabels(t),
1038
1469
  className: ToolDetails_module_css_default.web
1039
1470
  }), body !== "" ? (0, react_jsx_runtime.jsx)("pre", {
1040
1471
  className: ToolDetails_module_css_default.code,
@@ -1057,40 +1488,133 @@ window.__ModuleLoader__.load({
1057
1488
  const CONVERSATION_NS = "conversation";
1058
1489
  //#endregion
1059
1490
  //#region lib/types/client/tool/toolviews/ask-question-row.js
1060
- function isAnswer(value) {
1061
- return typeof value === "object" && value !== null;
1491
+ function isRecord(value) {
1492
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1062
1493
  }
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;
1494
+ function parseJson(text) {
1067
1495
  try {
1068
- parsed = JSON.parse(text);
1496
+ return JSON.parse(text);
1069
1497
  } catch {
1070
- return null;
1498
+ return;
1071
1499
  }
1072
- if (typeof parsed !== "object" || parsed === null) return null;
1500
+ }
1501
+ /** Answer records from the result JSON; null when the result is malformed. */
1502
+ function answerEntries(text) {
1503
+ const parsed = parseJson(text);
1504
+ if (!isRecord(parsed)) return null;
1505
+ const answers = parsed.answers;
1506
+ if (!Array.isArray(answers) || !answers.every(isRecord)) return null;
1507
+ const entries = [];
1508
+ for (const answer of answers) {
1509
+ 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;
1510
+ entries.push({
1511
+ id: answer.id,
1512
+ selected: answer.selected,
1513
+ ...answer.custom === void 0 ? {} : { custom: answer.custom }
1514
+ });
1515
+ }
1516
+ return entries;
1517
+ }
1518
+ /** Questions from call JSON; null when pairing with answers would be ambiguous. */
1519
+ function questionEntries(argsRaw) {
1520
+ const parsed = parseJson(argsRaw);
1521
+ if (!isRecord(parsed) || !Array.isArray(parsed.questions) || parsed.questions.length === 0) return null;
1522
+ const questions = [];
1523
+ const ids = /* @__PURE__ */ new Set();
1524
+ for (const question of parsed.questions) {
1525
+ if (!isRecord(question) || typeof question.id !== "string" || typeof question.question !== "string" || ids.has(question.id)) return null;
1526
+ ids.add(question.id);
1527
+ questions.push({
1528
+ id: question.id,
1529
+ question: question.question
1530
+ });
1531
+ }
1532
+ return questions;
1533
+ }
1534
+ /** Pair questions with result entries by their echoed stable ids. */
1535
+ function pairAnswers(argsRaw, answers) {
1536
+ const questions = questionEntries(argsRaw);
1537
+ if (questions === null || questions.length !== answers.length) return null;
1538
+ const byId = /* @__PURE__ */ new Map();
1539
+ for (const answer of answers) {
1540
+ if (byId.has(answer.id)) return null;
1541
+ byId.set(answer.id, answer);
1542
+ }
1543
+ const paired = [];
1544
+ for (const question of questions) {
1545
+ const answer = byId.get(question.id);
1546
+ if (answer === void 0) return null;
1547
+ paired.push({
1548
+ ...question,
1549
+ answers: [...answer.selected, ...answer.custom === void 0 || answer.custom === "" ? [] : [answer.custom]]
1550
+ });
1551
+ }
1552
+ return paired;
1553
+ }
1554
+ /** Answer summary plus structured transcript content from the two wire JSON documents. */
1555
+ function answeredPresentation(argsRaw, text, t) {
1556
+ const answers = answerEntries(text);
1557
+ if (answers === null) return null;
1558
+ const answered = answers.filter((answer) => answer.selected.length > 0 || (answer.custom ?? "") !== "").length;
1559
+ return {
1560
+ summary: t("ask.answered", {
1561
+ answered,
1562
+ total: answers.length
1563
+ }),
1564
+ questions: pairAnswers(argsRaw, answers)
1565
+ };
1566
+ }
1567
+ /** Best-effort answered-count summary when strict transcript pairing fails. */
1568
+ function answeredSummary(text, t) {
1569
+ const parsed = parseJson(text);
1570
+ if (!isRecord(parsed)) return null;
1073
1571
  const answers = parsed.answers;
1074
- if (!Array.isArray(answers) || !answers.every(isAnswer)) return null;
1572
+ if (!Array.isArray(answers) || !answers.every(isRecord)) return null;
1075
1573
  const answered = answers.filter((a) => Array.isArray(a.selected) && a.selected.length > 0 || typeof a.custom === "string" && a.custom !== "").length;
1076
1574
  return t("ask.answered", {
1077
1575
  answered,
1078
1576
  total: answers.length
1079
1577
  });
1080
1578
  }
1081
- /** One-line question-interaction row (the whole row toggles the call's
1082
- * Input/Output sections, ToolRow's unified expand). */
1579
+ /** Summarizes a pending, answered, cancelled, or interrupted question set. */
1083
1580
  function AskQuestionRow({ toolName, block, inspect, t }) {
1084
1581
  const model = toolRowModel(toolName, block);
1085
1582
  const code = "kind" in block ? block.error?.code : void 0;
1583
+ const argsRaw = ("kind" in block ? block.call?.argsRaw : block.argsRaw) ?? "";
1086
1584
  let summary = model.summary;
1087
1585
  let state = model.state;
1088
- if (code === "ASK_CANCELLED") summary = t("ask.cancelled");
1089
- else if (code === "ASK_ABORTED") {
1586
+ let transcript = null;
1587
+ if (code === "ASK_CANCELLED") {
1588
+ summary = t("ask.cancelled");
1589
+ state = "ok";
1590
+ const questions = questionEntries(argsRaw);
1591
+ if (questions !== null) transcript = {
1592
+ kind: "unanswered",
1593
+ questions,
1594
+ verdict: t("ask.cancelledDetail")
1595
+ };
1596
+ } else if (code === "ASK_ABORTED") {
1090
1597
  summary = t("ask.interrupted");
1091
1598
  state = "stopped";
1599
+ const questions = questionEntries(argsRaw);
1600
+ if (questions !== null) transcript = {
1601
+ kind: "unanswered",
1602
+ questions,
1603
+ verdict: t("ask.interruptedDetail")
1604
+ };
1092
1605
  } 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;
1606
+ else if ("kind" in block && model.state === "ok") {
1607
+ const text = singleResultText(block);
1608
+ if (text !== void 0) {
1609
+ const presentation = answeredPresentation(argsRaw, text, t);
1610
+ summary = presentation?.summary ?? answeredSummary(text, t) ?? model.summary;
1611
+ if (presentation?.questions !== null && presentation?.questions !== void 0) transcript = {
1612
+ kind: "answered",
1613
+ questions: presentation.questions,
1614
+ skippedLabel: t("ask.skipped")
1615
+ };
1616
+ }
1617
+ }
1094
1618
  return (0, react_jsx_runtime.jsx)(ToolRow, {
1095
1619
  t,
1096
1620
  variant: model.variant,
@@ -1098,23 +1622,17 @@ window.__ModuleLoader__.load({
1098
1622
  icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconQuestionOutline14, {}),
1099
1623
  title: t("ask.rowTitle"),
1100
1624
  summary,
1101
- body: model.body,
1102
- output: model.output,
1625
+ body: transcript === null ? model.body : null,
1626
+ output: transcript === null ? model.output : null,
1627
+ askQuestion: transcript,
1103
1628
  state,
1104
1629
  inspect
1105
1630
  });
1106
1631
  }
1107
- /**
1108
- * The ask-question row as a plain registrant plugin following the chat
1109
- * toolview declaration across independent activation and reload lifetimes.
1110
- */
1632
+ /** Registers the ask-user-question conversation row. */
1111
1633
  const askQuestionToolview = {
1112
1634
  name: "ask-question-toolview",
1113
1635
  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
1636
  apply(ctx) {
1119
1637
  ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
1120
1638
  name: "tool.call.toolview",
@@ -1125,7 +1643,7 @@ window.__ModuleLoader__.load({
1125
1643
  };
1126
1644
  //#endregion
1127
1645
  //#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}";
1646
+ 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
1647
  const tagId = "@deepseek-ai/dsh-client-ui-tool/bash-sample.module.css";
1130
1648
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1131
1649
  const tag = document.createElement("style");
@@ -1174,19 +1692,16 @@ window.__ModuleLoader__.load({
1174
1692
  default: return null;
1175
1693
  }
1176
1694
  }
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
- */
1695
+ /** Renders expandable Bash output with an accessible lifecycle label. */
1182
1696
  function BashRow({ toolName, block, sessionId, useSessions, inspect, t }) {
1183
1697
  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;
1698
+ const terminalModel = terminalCardModel(block, useSessions((list) => list.byId[sessionId]?.cwd));
1699
+ const terminal = terminalModel === null ? null : localizeTerminalCardModel(terminalModel, t);
1700
+ const state = model.state === "ok" && terminalModel !== null && terminalFailed(terminalModel) ? "error" : model.state;
1186
1701
  const status = stateStatus(state, t);
1187
1702
  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;
1703
+ const genericBody = terminal === null && (model.state === "error" || isSettledPersistentShellCall(block)) && (model.body !== null || model.output !== null);
1704
+ const expandable = terminal !== null || genericBody;
1190
1705
  const open = expanded && expandable;
1191
1706
  const failureLine = model.state === "error" ? model.errorSummary : null;
1192
1707
  const toggleExpand = () => {
@@ -1225,7 +1740,7 @@ window.__ModuleLoader__.load({
1225
1740
  }),
1226
1741
  (0, react_jsx_runtime.jsx)("span", {
1227
1742
  className: bash_sample_module_css_default.title,
1228
- children: model.title
1743
+ children: t(model.titleKey)
1229
1744
  }),
1230
1745
  (0, react_jsx_runtime.jsx)("span", {
1231
1746
  className: bash_sample_module_css_default.sep,
@@ -1250,7 +1765,7 @@ window.__ModuleLoader__.load({
1250
1765
  className: bash_sample_module_css_default.ioSection,
1251
1766
  children: [(0, react_jsx_runtime.jsx)("span", {
1252
1767
  className: bash_sample_module_css_default.ioLabel,
1253
- children: "IN"
1768
+ children: t("row.input")
1254
1769
  }), (0, react_jsx_runtime.jsx)("span", {
1255
1770
  className: bash_sample_module_css_default.ioText,
1256
1771
  children: model.body
@@ -1264,10 +1779,10 @@ window.__ModuleLoader__.load({
1264
1779
  className: bash_sample_module_css_default.ioSection,
1265
1780
  children: [(0, react_jsx_runtime.jsx)("span", {
1266
1781
  className: bash_sample_module_css_default.ioLabel,
1267
- children: "OUT"
1782
+ children: t("row.output")
1268
1783
  }), (0, react_jsx_runtime.jsx)("span", {
1269
1784
  className: bash_sample_module_css_default.ioText,
1270
- "data-error": true,
1785
+ "data-error": state === "error" || void 0,
1271
1786
  children: model.output
1272
1787
  })]
1273
1788
  })
@@ -1276,22 +1791,15 @@ window.__ModuleLoader__.load({
1276
1791
  type: "button",
1277
1792
  className: bash_sample_module_css_default.inspectButton,
1278
1793
  onClick: inspect,
1279
- children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconInspectOutline12, {}), "Inspect"]
1794
+ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconInspectOutline12, {}), t("row.inspect")]
1280
1795
  })]
1281
1796
  })]
1282
1797
  });
1283
1798
  }
1284
- /**
1285
- * The sample as a plain registrant plugin. Slot injection follows the chat
1286
- * toolview declaration across independent activation and reload lifetimes.
1287
- */
1799
+ /** Registers the standalone Bash conversation-row sample. */
1288
1800
  const bashToolviewSample = {
1289
1801
  name: "bash-toolview-sample",
1290
1802
  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
1803
  apply(ctx) {
1296
1804
  ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
1297
1805
  name: "tool.call.toolview",
@@ -1303,13 +1811,7 @@ window.__ModuleLoader__.load({
1303
1811
  //#endregion
1304
1812
  //#region lib/types/client/tool/toolviews/file-mutation-row.js
1305
1813
  /**
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.
1814
+ * Lets users expand an applied file diff and open the reported path.
1313
1815
  */
1314
1816
  function FileMutationRow({ toolName, block, cwd, home, openFile, inspect, t }) {
1315
1817
  const model = toolRowModel(toolName, block, cwd, home);
@@ -1319,7 +1821,7 @@ window.__ModuleLoader__.load({
1319
1821
  variant: model.variant,
1320
1822
  toolName,
1321
1823
  icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, { size: 14 }),
1322
- title: model.title,
1824
+ title: t(model.titleKey),
1323
1825
  summary: model.summary,
1324
1826
  body: null,
1325
1827
  output: model.output,
@@ -1331,18 +1833,10 @@ window.__ModuleLoader__.load({
1331
1833
  inspect
1332
1834
  });
1333
1835
  }
1334
- /**
1335
- * The file-mutation rows as a plain registrant plugin following the chat
1336
- * toolview declaration across independent activation and reload lifetimes.
1337
- */
1836
+ /** Registers the edit and write conversation rows. */
1338
1837
  const fileMutationToolview = {
1339
1838
  name: "file-mutation-toolview",
1340
1839
  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
1840
  apply(ctx) {
1347
1841
  ctx.slots.inject("tool.call.toolview", function* () {
1348
1842
  yield ctx.slots.register({
@@ -1361,9 +1855,7 @@ window.__ModuleLoader__.load({
1361
1855
  //#endregion
1362
1856
  //#region lib/types/client/tool/toolviews/read-row.js
1363
1857
  /**
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.
1858
+ * Lets users expand a completed read result and open its reported path.
1367
1859
  */
1368
1860
  function ReadRow({ toolName, block, cwd, home, openFile, inspect, t }) {
1369
1861
  const model = toolRowModel(toolName, block, cwd, home);
@@ -1373,7 +1865,7 @@ window.__ModuleLoader__.load({
1373
1865
  variant: model.variant,
1374
1866
  toolName,
1375
1867
  icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBrowseOutline16, { size: 14 }),
1376
- title: model.title,
1868
+ title: t(model.titleKey),
1377
1869
  summary: model.summary,
1378
1870
  body: null,
1379
1871
  output: model.output,
@@ -1385,17 +1877,10 @@ window.__ModuleLoader__.load({
1385
1877
  inspect
1386
1878
  });
1387
1879
  }
1388
- /**
1389
- * The read row as a plain registrant plugin following the atomic Tool-view
1390
- * declaration across independent activation and reload lifetimes.
1391
- */
1880
+ /** Registers the read tool's conversation row. */
1392
1881
  const readToolview = {
1393
1882
  name: "read-toolview",
1394
1883
  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
1884
  apply(ctx) {
1400
1885
  ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
1401
1886
  name: "tool.call.toolview",
@@ -1406,18 +1891,11 @@ window.__ModuleLoader__.load({
1406
1891
  };
1407
1892
  //#endregion
1408
1893
  //#region lib/types/client/tool/toolviews/search-row.js
1409
- const SEARCH_TITLES = {
1410
- grep: "Grep",
1411
- glob: "Glob"
1894
+ const SEARCH_TITLE_KEYS = {
1895
+ grep: "tool.title.grep",
1896
+ glob: "tool.title.glob"
1412
1897
  };
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
- */
1898
+ /** Lets users expand grep or glob results and recover capped searches. */
1421
1899
  function SearchRow({ toolName, block, inspect, t }) {
1422
1900
  const model = toolRowModel(toolName, block);
1423
1901
  const search = searchCardModel(block);
@@ -1426,8 +1904,8 @@ window.__ModuleLoader__.load({
1426
1904
  variant: model.variant,
1427
1905
  toolName,
1428
1906
  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,
1907
+ title: t(toolName === "grep" ? SEARCH_TITLE_KEYS.grep : toolName === "glob" ? SEARCH_TITLE_KEYS.glob : model.titleKey),
1908
+ summary: model.summary,
1431
1909
  body: null,
1432
1910
  output: model.output,
1433
1911
  errorSummary: model.errorSummary,
@@ -1436,19 +1914,10 @@ window.__ModuleLoader__.load({
1436
1914
  inspect
1437
1915
  });
1438
1916
  }
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
- */
1917
+ /** Registers the grep and glob conversation rows. */
1444
1918
  const searchToolview = {
1445
1919
  name: "search-toolview",
1446
1920
  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
1921
  apply(ctx) {
1453
1922
  ctx.slots.inject("tool.call.toolview", function* () {
1454
1923
  yield ctx.slots.register({
@@ -1525,10 +1994,7 @@ window.__ModuleLoader__.load({
1525
1994
  extra: activeExtra
1526
1995
  };
1527
1996
  }
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. */
1997
+ /** Summarizes a plan update without presenting a cancelled call as completed. */
1532
1998
  function TodoRow({ toolName, block, inspect, t }) {
1533
1999
  const model = toolRowModel(toolName, block);
1534
2000
  const summary = summarize(("kind" in block ? block.call?.argsRaw : block.argsRaw) ?? "", t) ?? {
@@ -1550,17 +2016,10 @@ window.__ModuleLoader__.load({
1550
2016
  inspect
1551
2017
  });
1552
2018
  }
1553
- /**
1554
- * The todo row as a plain registrant plugin following the atomic Tool-view
1555
- * declaration across independent activation and reload lifetimes.
1556
- */
2019
+ /** Registers the todo conversation row. */
1557
2020
  const todoToolview = {
1558
2021
  name: "todo-toolview",
1559
2022
  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
2023
  apply(ctx) {
1565
2024
  ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
1566
2025
  name: "tool.call.toolview",
@@ -1571,16 +2030,11 @@ window.__ModuleLoader__.load({
1571
2030
  };
1572
2031
  //#endregion
1573
2032
  //#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"
2033
+ const WEB_TITLE_KEYS = {
2034
+ web_search: "tool.title.webSearch",
2035
+ web_fetch: "tool.title.webFetch"
1578
2036
  };
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
- */
2037
+ /** Lets users expand a completed web search or fetch result. */
1584
2038
  function WebRow({ toolName, block, inspect, t }) {
1585
2039
  const model = toolRowModel(toolName, block);
1586
2040
  const web = webCardModel(block);
@@ -1590,7 +2044,7 @@ window.__ModuleLoader__.load({
1590
2044
  variant: model.variant,
1591
2045
  toolName,
1592
2046
  icon,
1593
- title: WEB_TITLES[toolName] ?? model.title,
2047
+ title: t(toolName === "web_search" ? WEB_TITLE_KEYS.web_search : toolName === "web_fetch" ? WEB_TITLE_KEYS.web_fetch : model.titleKey),
1594
2048
  summary: model.summary,
1595
2049
  body: null,
1596
2050
  output: model.output,
@@ -1600,17 +2054,10 @@ window.__ModuleLoader__.load({
1600
2054
  inspect
1601
2055
  });
1602
2056
  }
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
- */
2057
+ /** Registers the web search and fetch conversation rows. */
1607
2058
  const webToolview = {
1608
2059
  name: "web-toolview",
1609
2060
  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
2061
  apply(ctx) {
1615
2062
  ctx.slots.inject("tool.call.toolview", function* () {
1616
2063
  yield ctx.slots.register({
@@ -1628,15 +2075,18 @@ window.__ModuleLoader__.load({
1628
2075
  };
1629
2076
  //#endregion
1630
2077
  //#region lib/types/client/apply.js
1631
- /** Required services: the slot registry and the Host description used for POSIX `~`. */
1632
- const inject = ["slots", "connection"];
2078
+ /** Required services: the slot registry and the Remote face carrying the Host home used for POSIX `~`. */
2079
+ const inject = ["slots", "remote"];
1633
2080
  /**
1634
2081
  * Mount the whole-Tool renderers and built-in atomic Tool registrations.
1635
2082
  * @param ctx - Client root context.
1636
2083
  */
1637
2084
  function apply(ctx) {
1638
- const connection = ctx.get("connection");
1639
- const toolInject = () => ({ hooks: { hostDescription: connection.hostDescription } });
2085
+ const hostInfo = {
2086
+ getSnapshot: () => ctx.remote.$host,
2087
+ subscribe: (listener) => ctx.on("connection/reset", listener)
2088
+ };
2089
+ const toolInject = () => ({ hooks: { hostInfo } });
1640
2090
  ctx.slots.inject("conversation.chat.node", () => ctx.slots.register({
1641
2091
  name: "conversation.chat.node",
1642
2092
  key: "tool-call",