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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1720,13 +1720,13 @@ window.__ModuleLoader__.load({
1720
1720
  return (c === "x" ? r : r & 3 | 8).toString(16);
1721
1721
  });
1722
1722
  }
1723
- function isRecord$3(value) {
1723
+ function isRecord$4(value) {
1724
1724
  return typeof value === "object" && value !== null && !Array.isArray(value);
1725
1725
  }
1726
1726
  function parseConnectionResponse(value) {
1727
- if (!isRecord$3(value) || value.type !== "server-response" || typeof value.rpcId !== "string") throw new TypeError("connection: invalid server-response envelope");
1727
+ if (!isRecord$4(value) || value.type !== "server-response" || typeof value.rpcId !== "string") throw new TypeError("connection: invalid server-response envelope");
1728
1728
  const result = value.result;
1729
- if (!isRecord$3(result)) throw new TypeError("connection: invalid server-response result");
1729
+ if (!isRecord$4(result)) throw new TypeError("connection: invalid server-response result");
1730
1730
  if (result.ok === true) return {
1731
1731
  rpcId: value.rpcId,
1732
1732
  result: {
@@ -1734,9 +1734,9 @@ window.__ModuleLoader__.load({
1734
1734
  value: result.value
1735
1735
  }
1736
1736
  };
1737
- if (result.ok !== false || !isRecord$3(result.error)) throw new TypeError("connection: invalid server-response result");
1737
+ if (result.ok !== false || !isRecord$4(result.error)) throw new TypeError("connection: invalid server-response result");
1738
1738
  const error = result.error;
1739
- if (typeof error.code !== "string" || typeof error.message !== "string" || !isRecord$3(error.details)) throw new TypeError("connection: invalid server-response failure");
1739
+ if (typeof error.code !== "string" || typeof error.message !== "string" || !isRecord$4(error.details)) throw new TypeError("connection: invalid server-response failure");
1740
1740
  return {
1741
1741
  rpcId: value.rpcId,
1742
1742
  result: {
@@ -3085,7 +3085,7 @@ window.__ModuleLoader__.load({
3085
3085
  return json === void 0 ? 0 : utf8ByteLength(json);
3086
3086
  }
3087
3087
  /** Whether `value` is a plain object (never an array or `null`). */
3088
- function isRecord$2(value) {
3088
+ function isRecord$3(value) {
3089
3089
  return typeof value === "object" && value !== null && !Array.isArray(value);
3090
3090
  }
3091
3091
  /** Non-blank string, or `null`. */
@@ -3110,7 +3110,7 @@ window.__ModuleLoader__.load({
3110
3110
  * @returns the narrowed metadata, or `null` when it is not a usable write card.
3111
3111
  */
3112
3112
  function narrowWriteCardMeta(meta) {
3113
- if (!isRecord$2(meta) || meta.kind !== "write") return null;
3113
+ if (!isRecord$3(meta) || meta.kind !== "write") return null;
3114
3114
  const path = nonBlankString(meta.path);
3115
3115
  if (path === null) return null;
3116
3116
  return {
@@ -3130,13 +3130,13 @@ window.__ModuleLoader__.load({
3130
3130
  * @returns the narrowed metadata, or `null` when it is not a usable diff card.
3131
3131
  */
3132
3132
  function narrowEditCardMeta(meta) {
3133
- if (!isRecord$2(meta) || meta.kind !== "edit") return null;
3133
+ if (!isRecord$3(meta) || meta.kind !== "edit") return null;
3134
3134
  if (jsonByteLength(meta) > 131072) return null;
3135
3135
  const raw = meta.diffs;
3136
3136
  if (!Array.isArray(raw) || raw.length === 0) return null;
3137
3137
  const diffs = [];
3138
3138
  for (const hunk of raw) {
3139
- if (!isRecord$2(hunk)) return null;
3139
+ if (!isRecord$3(hunk)) return null;
3140
3140
  const path = nonBlankString(hunk.path);
3141
3141
  if (path === null) return null;
3142
3142
  const { oldText, newText } = hunk;
@@ -3159,7 +3159,7 @@ window.__ModuleLoader__.load({
3159
3159
  * @returns the narrowed metadata, or `null` when it is not a usable preview.
3160
3160
  */
3161
3161
  function narrowAstEditCardMeta(meta) {
3162
- if (!isRecord$2(meta) || meta.kind !== "ast_edit") return null;
3162
+ if (!isRecord$3(meta) || meta.kind !== "ast_edit") return null;
3163
3163
  if (jsonByteLength(meta) > 65536) return null;
3164
3164
  if (typeof meta.preview !== "string") return null;
3165
3165
  const replacements = nonNegativeInteger(meta.replacements);
@@ -3167,7 +3167,7 @@ window.__ModuleLoader__.load({
3167
3167
  if (!Array.isArray(meta.files)) return null;
3168
3168
  const files = [];
3169
3169
  for (const file of meta.files) {
3170
- if (!isRecord$2(file)) return null;
3170
+ if (!isRecord$3(file)) return null;
3171
3171
  const path = nonBlankString(file.path);
3172
3172
  const count = nonNegativeInteger(file.count);
3173
3173
  if (path === null || count === null) return null;
@@ -3195,7 +3195,7 @@ window.__ModuleLoader__.load({
3195
3195
  * @returns the narrowed metadata, or `null` when it is not a usable read card.
3196
3196
  */
3197
3197
  function narrowReadCardMeta(meta) {
3198
- if (!isRecord$2(meta) || meta.kind !== "read") return null;
3198
+ if (!isRecord$3(meta) || meta.kind !== "read") return null;
3199
3199
  if (jsonByteLength(meta) > 262144) return null;
3200
3200
  const path = nonBlankString(meta.path);
3201
3201
  const offset = positiveInteger(meta.offset);
@@ -3205,7 +3205,7 @@ window.__ModuleLoader__.load({
3205
3205
  const lines = [];
3206
3206
  let previous = offset - 1;
3207
3207
  for (const line of meta.lines) {
3208
- if (!isRecord$2(line)) return null;
3208
+ if (!isRecord$3(line)) return null;
3209
3209
  const number = positiveInteger(line.number);
3210
3210
  if (number === null || number <= previous || number > totalLines) return null;
3211
3211
  if (typeof line.text !== "string") return null;
@@ -3226,12 +3226,12 @@ window.__ModuleLoader__.load({
3226
3226
  }
3227
3227
  /** Narrow one grouped-matches file entry, or `null` when malformed. */
3228
3228
  function narrowSearchFile(file) {
3229
- if (!isRecord$2(file)) return null;
3229
+ if (!isRecord$3(file)) return null;
3230
3230
  const path = nonBlankString(file.path);
3231
3231
  if (path === null || !Array.isArray(file.matches)) return null;
3232
3232
  const matches = [];
3233
3233
  for (const match of file.matches) {
3234
- if (!isRecord$2(match)) return null;
3234
+ if (!isRecord$3(match)) return null;
3235
3235
  const lineNumber = positiveInteger(match.lineNumber);
3236
3236
  if (lineNumber === null || typeof match.line !== "string") return null;
3237
3237
  matches.push({
@@ -3250,7 +3250,7 @@ window.__ModuleLoader__.load({
3250
3250
  * @returns the narrowed metadata, or `null` when it is not a usable search card.
3251
3251
  */
3252
3252
  function narrowSearchCardMeta(meta) {
3253
- if (!isRecord$2(meta) || meta.kind !== "search") return null;
3253
+ if (!isRecord$3(meta) || meta.kind !== "search") return null;
3254
3254
  if (jsonByteLength(meta) > 65536) return null;
3255
3255
  if (typeof meta.truncated !== "boolean") return null;
3256
3256
  const total = nonNegativeInteger(meta.total);
@@ -3291,7 +3291,7 @@ window.__ModuleLoader__.load({
3291
3291
  * @returns the narrowed metadata, or `null` when it is not a usable terminal card.
3292
3292
  */
3293
3293
  function narrowTerminalCardMeta(meta) {
3294
- if (!isRecord$2(meta) || meta.kind !== "terminal") return null;
3294
+ if (!isRecord$3(meta) || meta.kind !== "terminal") return null;
3295
3295
  if (meta.mode === "background") {
3296
3296
  const jobId = nonBlankString(meta.jobId);
3297
3297
  return jobId === null ? null : {
@@ -3365,6 +3365,8 @@ window.__ModuleLoader__.load({
3365
3365
  "astEdit.replacements": "{count} 处替换",
3366
3366
  "astEdit.files": "{count} 个文件",
3367
3367
  "astEdit.parseErrors": "解析错误",
3368
+ "astEdit.rules": "规则",
3369
+ "astEdit.rulesRest": "… 其余 {count} 条",
3368
3370
  "read.window": "显示 {shown} / {total} 行",
3369
3371
  "read.collapseAria": "收起内容",
3370
3372
  "read.expandAria": "展开其余 {count} 行",
@@ -3374,6 +3376,7 @@ window.__ModuleLoader__.load({
3374
3376
  "search.matches": "{shown} 处匹配 · {files} 个文件",
3375
3377
  "search.matches.truncated": "显示 {shown} / 共 {total} 处匹配 · {files} 个文件",
3376
3378
  "search.noResults": "无结果",
3379
+ "search.scope": "范围 {path}",
3377
3380
  "search.collapseAria": "收起结果",
3378
3381
  "search.expandAria": "展开其余 {count} 行结果",
3379
3382
  "search.expandRest": "… 其余 {count} 行",
@@ -3425,6 +3428,8 @@ window.__ModuleLoader__.load({
3425
3428
  "astEdit.replacements": "{count} replacements",
3426
3429
  "astEdit.files": "{count} files",
3427
3430
  "astEdit.parseErrors": "Parse errors",
3431
+ "astEdit.rules": "Rules",
3432
+ "astEdit.rulesRest": "… {count} more",
3428
3433
  "read.window": "Showing {shown} of {total} lines",
3429
3434
  "read.collapseAria": "Collapse content",
3430
3435
  "read.expandAria": "Expand {count} more lines",
@@ -3434,6 +3439,7 @@ window.__ModuleLoader__.load({
3434
3439
  "search.matches": "{shown} matches · {files} files",
3435
3440
  "search.matches.truncated": "Showing {shown} of {total} matches · {files} files",
3436
3441
  "search.noResults": "No results",
3442
+ "search.scope": "in {path}",
3437
3443
  "search.collapseAria": "Collapse results",
3438
3444
  "search.expandAria": "Expand {count} more result lines",
3439
3445
  "search.expandRest": "… {count} more lines",
@@ -3483,7 +3489,7 @@ window.__ModuleLoader__.load({
3483
3489
  return typeof call === "object" && call !== null ? call : null;
3484
3490
  }
3485
3491
  /** Whether `value` is a plain object, the only argument shape the cards accept. */
3486
- function isRecord$1(value) {
3492
+ function isRecord$2(value) {
3487
3493
  return typeof value === "object" && value !== null && !Array.isArray(value);
3488
3494
  }
3489
3495
  /**
@@ -3505,7 +3511,7 @@ window.__ModuleLoader__.load({
3505
3511
  let parsed = null;
3506
3512
  if (typeof raw === "string") try {
3507
3513
  const value = JSON.parse(raw);
3508
- if (isRecord$1(value)) parsed = value;
3514
+ if (isRecord$2(value)) parsed = value;
3509
3515
  } catch {
3510
3516
  parsed = null;
3511
3517
  }
@@ -3556,7 +3562,7 @@ window.__ModuleLoader__.load({
3556
3562
  if (!isSettled(block) || !Array.isArray(block.content)) return "";
3557
3563
  const parts = [];
3558
3564
  for (const part of block.content) {
3559
- if (!isRecord$1(part)) continue;
3565
+ if (!isRecord$2(part)) continue;
3560
3566
  if (part.type === "text" && typeof part.text === "string") parts.push(part.text);
3561
3567
  }
3562
3568
  return parts.join("\n");
@@ -3810,9 +3816,8 @@ window.__ModuleLoader__.load({
3810
3816
  .twc-chevron{color:var(--dsw-alias-label-secondary)}
3811
3817
  .twc-title{font-weight:400}
3812
3818
  .twc-sep{flex:none;width:2px;height:2px;border-radius:1px;margin:0 8px;background:var(--dsw-alias-label-caption)}
3813
- .twc-summary{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary)}
3819
+ .twc-summary{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary)}
3814
3820
  .twc-summarySuffix{flex:none;margin-left:4px;white-space:nowrap;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary)}
3815
- .twc-diffStat{margin-left:10px;font-family:var(--ds-font-family-code);font-size:calc(var(--dsh-content-font-size-secondary,13px) - 2px);color:var(--dsw-alias-label-caption)}
3816
3821
  .twc-errorSummary{color:var(--dsw-alias-state-error-primary)}
3817
3822
  .twc-fileLink{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin:0;padding:0;border:none;background:none;font:inherit;text-align:left;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-secondary);text-decoration:underline dotted;text-decoration-color:var(--dsw-alias-label-tertiary);text-decoration-thickness:1px;text-underline-offset:3px;cursor:pointer;border-radius:4px}
3818
3823
  .twc-fileLink:hover{color:var(--dsw-alias-label-primary);text-decoration-color:currentColor}
@@ -4049,6 +4054,76 @@ window.__ModuleLoader__.load({
4049
4054
  }
4050
4055
  };
4051
4056
  //#endregion
4057
+ //#region src/web/client/rows/command-card-facts.ts
4058
+ /**
4059
+ * Collapsed-fact builders for the two command-shaped cards: the command a
4060
+ * `bash` description hides, and the rewrite rule an `ast_edit` preview only
4061
+ * shows once expanded.
4062
+ *
4063
+ * Both cards collapse to one head line. `bash`'s head used to be the model's
4064
+ * own `description` — a sentence about intent, never the command that ran — so
4065
+ * the only way to learn what executed was to expand the card. `ast_edit`'s head
4066
+ * names the file and the replacement count, and its body previews the *result*:
4067
+ * the rule that produced that result lives only in the call's `ops` argument,
4068
+ * which no card showed at all. Each builder recovers one fact from the call's
4069
+ * own arguments, so nothing is inferred from output, and a call head that fell
4070
+ * outside the session window degrades to the facts the card already holds.
4071
+ * @module @xiaoso/dsh-tool-plus/web/client/rows/command-card-facts
4072
+ */
4073
+ /** Whether `value` is a plain object — the only op shape the engine sends. */
4074
+ function isRecord$1(value) {
4075
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4076
+ }
4077
+ /**
4078
+ * The `bash` card's collapsed summary: what ran, placed after the sentence
4079
+ * saying why.
4080
+ *
4081
+ * The command is the fact the head must carry, so the description can never be
4082
+ * the whole summary — it is a prefix at most. Only its first line is used: a
4083
+ * summary is one row high, and the rest of a multi-line command belongs to the
4084
+ * expanded body, which draws the whole thing.
4085
+ * @param description - the model's one-line intent, or `null` when absent.
4086
+ * @param command - the command actually executed (never empty).
4087
+ * @param outcome - the interruption word (`timedOut` / `cancelled`), or `null`.
4088
+ * @returns the outcome alone when the run was interrupted; otherwise the
4089
+ * description joined to the command's first line, or that line alone.
4090
+ */
4091
+ function bashCardSummary(description, command, outcome) {
4092
+ if (outcome !== null && outcome !== "") return outcome;
4093
+ const intent = description === null ? "" : description.trim();
4094
+ const line = firstLine(command);
4095
+ return intent === "" ? line : `${intent} · ${line}`;
4096
+ }
4097
+ /**
4098
+ * The `ast_edit` card's rule line: the first usable `ops` entry, written the
4099
+ * way the tool's own `ops` argument writes it (`pat → out`).
4100
+ *
4101
+ * Only the first rule is stated, with the count of the entries behind it: the
4102
+ * card exists to preview one change, and a call may carry many rules whose
4103
+ * patterns are whole AST fragments — listing them would bury the preview. The
4104
+ * remaining count is the array tail, not the number of *usable* entries, so it
4105
+ * always describes the call as it was made.
4106
+ * @param args - the call's parsed arguments, or `null` when its head is gone.
4107
+ * @param t - this plugin's card translate seat.
4108
+ * @returns the rule line, or `null` when the call carries no usable rule.
4109
+ */
4110
+ function astEditRuleLine(args, t) {
4111
+ const ops = args?.ops;
4112
+ if (!Array.isArray(ops)) return null;
4113
+ for (let index = 0; index < ops.length; index += 1) {
4114
+ const op = ops[index];
4115
+ if (!isRecord$1(op)) continue;
4116
+ const pat = argText(op, "pat");
4117
+ if (pat === null) continue;
4118
+ const replacement = op.out;
4119
+ if (typeof replacement !== "string") continue;
4120
+ const rest = ops.length - 1;
4121
+ const rule = `${t("astEdit.rules")}: ${pat} →${replacement === "" ? "" : ` ${replacement}`}`;
4122
+ return rest === 0 ? rule : `${rule} · ${t("astEdit.rulesRest", { count: rest })}`;
4123
+ }
4124
+ return null;
4125
+ }
4126
+ //#endregion
4052
4127
  //#region src/web/client/rows/AstEditRow.tsx
4053
4128
  /**
4054
4129
  * `ast_edit` row: the engine's own change preview.
@@ -4100,6 +4175,7 @@ window.__ModuleLoader__.load({
4100
4175
  const single = files === 1 ? meta.files[0]?.path : void 0;
4101
4176
  const output = resultText(block);
4102
4177
  const parseErrors = PARSE_ERROR.test(output);
4178
+ const rule = astEditRuleLine(args, t);
4103
4179
  const summary = single !== void 0 && single !== "" ? displayPath(single, cwd) : t("astEdit.files", { count: files });
4104
4180
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
4105
4181
  t,
@@ -4112,14 +4188,21 @@ window.__ModuleLoader__.load({
4112
4188
  inspect,
4113
4189
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4114
4190
  className: "twc-body",
4115
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.CodeBlock, {
4116
- code: meta.preview,
4117
- copyLabel: t("copy"),
4118
- copiedLabel: t("copied")
4119
- }), output !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4120
- className: "twc-note",
4121
- children: parseErrors ? `${t("astEdit.parseErrors")}: ${output}` : output
4122
- })]
4191
+ children: [
4192
+ rule !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4193
+ className: "twc-note",
4194
+ children: rule
4195
+ }),
4196
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.CodeBlock, {
4197
+ code: meta.preview,
4198
+ copyLabel: t("copy"),
4199
+ copiedLabel: t("copied")
4200
+ }),
4201
+ output !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4202
+ className: "twc-note",
4203
+ children: parseErrors ? `${t("astEdit.parseErrors")}: ${output}` : output
4204
+ })
4205
+ ]
4123
4206
  })
4124
4207
  });
4125
4208
  }
@@ -4327,6 +4410,43 @@ window.__ModuleLoader__.load({
4327
4410
  };
4328
4411
  }
4329
4412
  //#endregion
4413
+ //#region src/web/client/rows/read-card-facts.ts
4414
+ /**
4415
+ * Collapsed-summary facts for the `read` card.
4416
+ *
4417
+ * The Host projection strips the inline selector off the path
4418
+ * (`stripReadSelector` in `web/host/read.ts`) because the card's *link* must
4419
+ * point at a real file, and it keeps only the file's own window. The collapsed
4420
+ * head is then the one place where the question the model actually asked is
4421
+ * still readable — and the projected path cannot answer it: a windowed read of
4422
+ * `package.json:22-25,31-34` and a whole-file read of `package.json` project to
4423
+ * the very same string, so the card hides the difference.
4424
+ *
4425
+ * The summary is therefore built from the call's own argument, selector and
4426
+ * all, with the projected path kept as the fallback for a result whose call
4427
+ * head fell outside the session window (a truncated window carries no
4428
+ * `tool/call` event, so no argument survives). That fallback states a fact the
4429
+ * card already holds; nothing is invented.
4430
+ * @module @xiaoso/dsh-tool-plus/web/client/rows/read-card-facts
4431
+ */
4432
+ /**
4433
+ * The `read` card's collapsed summary: the path the model asked for, keeping
4434
+ * whatever inline selector it carried (`:22-25`, `:raw`, `:conflicts`, `:28-`,
4435
+ * a sqlite row, an archive member) and shortened against the workspace root.
4436
+ *
4437
+ * The selector is deliberately not validated against the read grammar: the head
4438
+ * reports the call verbatim, and a selector this plugin does not recognize is
4439
+ * still what the model typed — substituting the projection's stripped path
4440
+ * would erase the evidence that two calls differ.
4441
+ * @param args - the call's parsed arguments, or `null` when its head is gone.
4442
+ * @param metaPath - the Host-projected path (already selector-free).
4443
+ * @param cwd - the session workspace root, when known.
4444
+ * @returns the workspace-shortened argument path, or the projected path.
4445
+ */
4446
+ function readCardSummary(args, metaPath, cwd) {
4447
+ return displayPath(argText(args, "path") ?? metaPath, cwd);
4448
+ }
4449
+ //#endregion
4330
4450
  //#region src/web/client/rows/ReadRow.tsx
4331
4451
  /**
4332
4452
  * `read` row: a line-numbered window view, or the image the read returned.
@@ -4404,7 +4524,7 @@ window.__ModuleLoader__.load({
4404
4524
  t,
4405
4525
  state: cardState(block),
4406
4526
  title,
4407
- summary: displayPath(meta.path, cwd),
4527
+ summary: readCardSummary(args, meta.path, cwd),
4408
4528
  filePath: meta.path,
4409
4529
  filePathLine: meta.offset,
4410
4530
  openFile,
@@ -4655,6 +4775,45 @@ window.__ModuleLoader__.load({
4655
4775
  }), document.body);
4656
4776
  }
4657
4777
  //#endregion
4778
+ //#region src/web/client/rows/search-card-facts.ts
4779
+ /** Separator between the suffix's facts; the shell adds the summary's own. */
4780
+ const FACT_SEPARATOR = " · ";
4781
+ /**
4782
+ * A target may be a `;`-delimited list of paths. Shortening the whole list in
4783
+ * one pass would cut the workspace prefix off its first entry only — the rest
4784
+ * stay absolute, and the same scope then reads as two unrelated places — so
4785
+ * each entry is shortened on its own and the list is rejoined.
4786
+ * @param scope - the call's own target argument.
4787
+ * @param cwd - the session workspace root, when known.
4788
+ * @returns the workspace-shortened target.
4789
+ */
4790
+ function displayScope(scope, cwd) {
4791
+ if (!scope.includes(";")) return displayPath(scope, cwd);
4792
+ return scope.split(";").map((entry) => displayPath(entry.trim(), cwd)).join("; ");
4793
+ }
4794
+ /**
4795
+ * The search card's collapsed trailing fragment: the scope the call searched,
4796
+ * then what the search found.
4797
+ * @param args - the call's parsed arguments, or `null` when its head is gone.
4798
+ * @param meta - the Host-projected search metadata (already narrowed).
4799
+ * @param cwd - the session workspace root, for shortening an absolute scope.
4800
+ * @param t - the card translate seat.
4801
+ * @returns the joined fragment, or `null` when there is no fact to add.
4802
+ */
4803
+ function searchCardSuffix(args, meta, cwd, t) {
4804
+ const facts = [];
4805
+ if (meta.shape === "matches") {
4806
+ const scope = argText(args, "path");
4807
+ if (scope !== null) facts.push(t("search.scope", { path: displayScope(scope, cwd) }));
4808
+ }
4809
+ if (meta.total === 0) facts.push(t("search.noResults"));
4810
+ else if (meta.shape === "matches") {
4811
+ const shown = meta.files.reduce((count, file) => count + file.matches.length, 0);
4812
+ facts.push(searchBlockLabels(t).matchesSummary(shown, meta.total, meta.files.length, meta.truncated));
4813
+ } else facts.push(searchBlockLabels(t).pathsSummary(meta.paths.length, meta.total, meta.truncated));
4814
+ return facts.length === 0 ? null : `· ${facts.join(FACT_SEPARATOR)}`;
4815
+ }
4816
+ //#endregion
4658
4817
  //#region src/web/client/rows/SearchRow.tsx
4659
4818
  /**
4660
4819
  * `grep` / `glob` / `ast_grep` row: a search result card.
@@ -4689,7 +4848,7 @@ window.__ModuleLoader__.load({
4689
4848
  * @returns the search card.
4690
4849
  */
4691
4850
  function SearchCard(props) {
4692
- const { t, block, inspect, title } = props;
4851
+ const { t, block, cwd, inspect, title } = props;
4693
4852
  const args = parseCardArgs(block);
4694
4853
  const meta = narrowSearchCardMeta(cardMeta(block));
4695
4854
  if (isSubCall(block) || block.isError === true || meta === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenericCard, {
@@ -4706,6 +4865,7 @@ window.__ModuleLoader__.load({
4706
4865
  state: cardState(block),
4707
4866
  title,
4708
4867
  summary: subject ?? argsSummary(args, t),
4868
+ summarySuffix: searchCardSuffix(args, meta, cwd, t),
4709
4869
  inspect,
4710
4870
  children: meta.shape === "matches" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SearchBlock, {
4711
4871
  kind: "matches",
@@ -4790,7 +4950,7 @@ window.__ModuleLoader__.load({
4790
4950
  const interrupted = timedOut || aborted;
4791
4951
  const exitCode = running || interrupted ? void 0 : typeof foreground?.exitCode === "number" ? foreground.exitCode : typeof status.exitCode === "number" ? status.exitCode : void 0;
4792
4952
  const workdir = argText(args, "workdir") ?? foreground?.workingDir ?? sessionCwd;
4793
- const summary = interrupted ? timedOut ? t("timedOut") : t("cancelled") : argText(args, "description") ?? firstLine(command);
4953
+ const summary = bashCardSummary(argText(args, "description"), command, interrupted ? timedOut ? t("timedOut") : t("cancelled") : null);
4794
4954
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
4795
4955
  t,
4796
4956
  state,
@@ -17,7 +17,7 @@
17
17
  /** Locale namespace of the tool-card bundle (registered by `registerToolCards`). */
18
18
  export declare const CARD_LOCALE_NS = "tool-plus-cards";
19
19
  /** Every copy key of the tool-card namespace. */
20
- export type ToolCardLocaleKey = 'copy' | 'copied' | 'collapse' | 'expand' | 'collapseAria' | 'expandAria' | 'expandRest' | 'files.one' | 'files.other' | 'running' | 'done' | 'failed' | 'cancelled' | 'timedOut' | 'noOutput' | 'title.bash' | 'title.read' | 'title.write' | 'title.edit' | 'title.grep' | 'title.glob' | 'title.astGrep' | 'title.astEdit' | 'write.lines' | 'write.madeExecutable' | 'astEdit.replacements' | 'astEdit.files' | 'astEdit.parseErrors' | 'read.window' | 'read.collapseAria' | 'read.expandAria' | 'read.expandRest' | 'search.paths' | 'search.paths.truncated' | 'search.matches' | 'search.matches.truncated' | 'search.noResults' | 'search.collapseAria' | 'search.expandAria' | 'search.expandRest' | 'terminal.signal' | 'terminal.exitCode' | 'terminal.running' | 'terminal.failed' | 'terminal.done' | 'terminal.noOutput' | 'bash.background' | 'image.loading' | 'image.loadFailed' | 'image.open' | 'image.openNamed' | 'image.dialog' | 'image.close' | 'image.unnamed' | 'image.caption' | 'generic.params' | 'generic.noDetail' | 'inspect';
20
+ export type ToolCardLocaleKey = 'copy' | 'copied' | 'collapse' | 'expand' | 'collapseAria' | 'expandAria' | 'expandRest' | 'files.one' | 'files.other' | 'running' | 'done' | 'failed' | 'cancelled' | 'timedOut' | 'noOutput' | 'title.bash' | 'title.read' | 'title.write' | 'title.edit' | 'title.grep' | 'title.glob' | 'title.astGrep' | 'title.astEdit' | 'write.lines' | 'write.madeExecutable' | 'astEdit.replacements' | 'astEdit.files' | 'astEdit.parseErrors' | 'astEdit.rules' | 'astEdit.rulesRest' | 'read.window' | 'read.collapseAria' | 'read.expandAria' | 'read.expandRest' | 'search.paths' | 'search.paths.truncated' | 'search.matches' | 'search.matches.truncated' | 'search.noResults' | 'search.scope' | 'search.collapseAria' | 'search.expandAria' | 'search.expandRest' | 'terminal.signal' | 'terminal.exitCode' | 'terminal.running' | 'terminal.failed' | 'terminal.done' | 'terminal.noOutput' | 'bash.background' | 'image.loading' | 'image.loadFailed' | 'image.open' | 'image.openNamed' | 'image.dialog' | 'image.close' | 'image.unnamed' | 'image.caption' | 'generic.params' | 'generic.noDetail' | 'inspect';
21
21
  /** Simplified Chinese copy (the source dictionary). */
22
22
  export declare const zh: Record<ToolCardLocaleKey, string>;
23
23
  /** English dictionary, checked complete against the zh key set. */
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Collapsed-fact builders for the two command-shaped cards: the command a
3
+ * `bash` description hides, and the rewrite rule an `ast_edit` preview only
4
+ * shows once expanded.
5
+ *
6
+ * Both cards collapse to one head line. `bash`'s head used to be the model's
7
+ * own `description` — a sentence about intent, never the command that ran — so
8
+ * the only way to learn what executed was to expand the card. `ast_edit`'s head
9
+ * names the file and the replacement count, and its body previews the *result*:
10
+ * the rule that produced that result lives only in the call's `ops` argument,
11
+ * which no card showed at all. Each builder recovers one fact from the call's
12
+ * own arguments, so nothing is inferred from output, and a call head that fell
13
+ * outside the session window degrades to the facts the card already holds.
14
+ * @module @xiaoso/dsh-tool-plus/web/client/rows/command-card-facts
15
+ */
16
+ import { type CardTranslate } from '../row-utils.ts';
17
+ /**
18
+ * The `bash` card's collapsed summary: what ran, placed after the sentence
19
+ * saying why.
20
+ *
21
+ * The command is the fact the head must carry, so the description can never be
22
+ * the whole summary — it is a prefix at most. Only its first line is used: a
23
+ * summary is one row high, and the rest of a multi-line command belongs to the
24
+ * expanded body, which draws the whole thing.
25
+ * @param description - the model's one-line intent, or `null` when absent.
26
+ * @param command - the command actually executed (never empty).
27
+ * @param outcome - the interruption word (`timedOut` / `cancelled`), or `null`.
28
+ * @returns the outcome alone when the run was interrupted; otherwise the
29
+ * description joined to the command's first line, or that line alone.
30
+ */
31
+ export declare function bashCardSummary(description: string | null, command: string, outcome: string | null): string;
32
+ /**
33
+ * The `ast_edit` card's rule line: the first usable `ops` entry, written the
34
+ * way the tool's own `ops` argument writes it (`pat → out`).
35
+ *
36
+ * Only the first rule is stated, with the count of the entries behind it: the
37
+ * card exists to preview one change, and a call may carry many rules whose
38
+ * patterns are whole AST fragments — listing them would bury the preview. The
39
+ * remaining count is the array tail, not the number of *usable* entries, so it
40
+ * always describes the call as it was made.
41
+ * @param args - the call's parsed arguments, or `null` when its head is gone.
42
+ * @param t - this plugin's card translate seat.
43
+ * @returns the rule line, or `null` when the call carries no usable rule.
44
+ */
45
+ export declare function astEditRuleLine(args: Record<string, unknown> | null, t: CardTranslate): string | null;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Collapsed-summary facts for the `read` card.
3
+ *
4
+ * The Host projection strips the inline selector off the path
5
+ * (`stripReadSelector` in `web/host/read.ts`) because the card's *link* must
6
+ * point at a real file, and it keeps only the file's own window. The collapsed
7
+ * head is then the one place where the question the model actually asked is
8
+ * still readable — and the projected path cannot answer it: a windowed read of
9
+ * `package.json:22-25,31-34` and a whole-file read of `package.json` project to
10
+ * the very same string, so the card hides the difference.
11
+ *
12
+ * The summary is therefore built from the call's own argument, selector and
13
+ * all, with the projected path kept as the fallback for a result whose call
14
+ * head fell outside the session window (a truncated window carries no
15
+ * `tool/call` event, so no argument survives). That fallback states a fact the
16
+ * card already holds; nothing is invented.
17
+ * @module @xiaoso/dsh-tool-plus/web/client/rows/read-card-facts
18
+ */
19
+ /**
20
+ * The `read` card's collapsed summary: the path the model asked for, keeping
21
+ * whatever inline selector it carried (`:22-25`, `:raw`, `:conflicts`, `:28-`,
22
+ * a sqlite row, an archive member) and shortened against the workspace root.
23
+ *
24
+ * The selector is deliberately not validated against the read grammar: the head
25
+ * reports the call verbatim, and a selector this plugin does not recognize is
26
+ * still what the model typed — substituting the projection's stripped path
27
+ * would erase the evidence that two calls differ.
28
+ * @param args - the call's parsed arguments, or `null` when its head is gone.
29
+ * @param metaPath - the Host-projected path (already selector-free).
30
+ * @param cwd - the session workspace root, when known.
31
+ * @returns the workspace-shortened argument path, or the projected path.
32
+ */
33
+ export declare function readCardSummary(args: Record<string, unknown> | null, metaPath: string, cwd: string | undefined): string;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Collapsed-summary facts for the `grep` / `ast_grep` / `glob` cards.
3
+ *
4
+ * The row's summary is the search's *subject* — `pattern` for grep, `pat` for
5
+ * ast_grep, the path pattern for glob — so the one question the collapsed head
6
+ * could not answer is "which tree was searched?", and the counts sat in the
7
+ * folded body. That reads worst on an empty result: a bare `Grep · zzz` looks
8
+ * like a bad pattern rather than like a search of the wrong directory.
9
+ *
10
+ * Two facts therefore ride the head as a trailing fragment, after the summary
11
+ * and outside its ellipsis, because they are the parts a narrow row must never
12
+ * clip:
13
+ * - the scope, taken from the call's own `path` argument. Only `matches`
14
+ * shapes take it: a glob's `path` argument *is* its pattern, which the
15
+ * summary already shows, so repeating it as a scope would claim a second
16
+ * fact out of one argument. A call without `path` searched the workspace
17
+ * root — naming no scope is honest, inventing one is not;
18
+ * - the count, or "no results" instead of a zero count (a zero count is what
19
+ * the empty-state failure reads as, not what it says).
20
+ *
21
+ * The counting labels are the {@link searchBlockLabels} adapter the block body
22
+ * already uses, so the head and the body say the same thing in the same words,
23
+ * including the "showing N of M" wording of a capped payload.
24
+ * @module @xiaoso/dsh-tool-plus/web/client/rows/search-card-facts
25
+ */
26
+ import type { SearchCardMeta } from '../../contract.ts';
27
+ import type { CardTranslate } from '../row-utils.ts';
28
+ /**
29
+ * The search card's collapsed trailing fragment: the scope the call searched,
30
+ * then what the search found.
31
+ * @param args - the call's parsed arguments, or `null` when its head is gone.
32
+ * @param meta - the Host-projected search metadata (already narrowed).
33
+ * @param cwd - the session workspace root, for shortening an absolute scope.
34
+ * @param t - the card translate seat.
35
+ * @returns the joined fragment, or `null` when there is no fact to add.
36
+ */
37
+ export declare function searchCardSuffix(args: Record<string, unknown> | null, meta: SearchCardMeta, cwd: string | undefined, t: CardTranslate): string | null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xiaoso/dsh-tool-plus",
3
3
  "description": "Oh My Pi bash runtime ported as a deepseek-harness tool plugin: persistent shell session, output minimizer, managed background jobs, command interception",
4
- "version": "0.1.8-beta.2",
4
+ "version": "0.1.8-beta.3",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "deepseek-harness",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaoso/dsh-tool-plus-presets",
3
- "version": "0.1.8-beta.2",
3
+ "version": "0.1.8-beta.3",
4
4
  "description": "Agent-preset installer for dsh-tool-plus: installs the standard / PTC enhanced preset templates into a DeepSeek Harness home.",
5
5
  "license": "MIT",
6
6
  "keywords": [