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

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
@@ -12,6 +12,7 @@ window.__ModuleLoader__.load({
12
12
  //#region src/config/fields.ts
13
13
  /** Field group ids -> locale key of the group heading. */
14
14
  const TOOL_PLUS_GROUP_LABELS = {
15
+ cards: "groupCards",
15
16
  timing: "groupTiming",
16
17
  output: "groupOutput",
17
18
  truncation: "groupTruncation",
@@ -32,6 +33,15 @@ window.__ModuleLoader__.load({
32
33
  * single source; settings.ts reads from here.
33
34
  */
34
35
  const TOOL_PLUS_FIELDS = [
36
+ {
37
+ name: "webCards",
38
+ kind: "boolean",
39
+ default: true,
40
+ labelKey: "webCards",
41
+ hintKey: "webCardsHint",
42
+ group: "cards",
43
+ tool: "web"
44
+ },
35
45
  {
36
46
  name: "enableRunInBackground",
37
47
  kind: "boolean",
@@ -95,15 +105,6 @@ window.__ModuleLoader__.load({
95
105
  group: "behavior",
96
106
  tool: "bash"
97
107
  },
98
- {
99
- name: "webCards",
100
- kind: "boolean",
101
- default: true,
102
- labelKey: "webCards",
103
- hintKey: "webCardsHint",
104
- group: "behavior",
105
- tool: "bash"
106
- },
107
108
  {
108
109
  name: "autoBackgroundMs",
109
110
  kind: "number",
@@ -740,6 +741,11 @@ window.__ModuleLoader__.load({
740
741
  ];
741
742
  /** Tool tabs in nav order; empty `fields` = no configurable settings. */
742
743
  const TOOL_PLUS_TABS = [
744
+ {
745
+ id: "web",
746
+ labelKey: "tabWeb",
747
+ fields: TOOL_PLUS_FIELDS.filter((f) => f.tool === "web").map((f) => f.name)
748
+ },
743
749
  {
744
750
  id: "bash",
745
751
  labelKey: "tabBash",
@@ -2105,8 +2111,9 @@ window.__ModuleLoader__.load({
2105
2111
  /**
2106
2112
  * Tool Plus settings section — the plugin's own page in the Settings panel,
2107
2113
  * registered into the `settings.section` slot (nav id `tool-plus`). The page
2108
- * renders one tab per tool (Bash / Read / Write & Edit / Grep /
2109
- * Ast Edit / Read Image); switching a tab shows that tool's configurable
2114
+ * renders one tab per tool (Bash / Read / Write & Edit / Grep / Glob /
2115
+ * Ast Grep / Ast Edit) plus a leading `Web 卡片` tab for the plugin-wide
2116
+ * browser-card switch; switching a tab shows that tool's configurable
2110
2117
  * fields, while tools without global settings show a placeholder. Only the
2111
2118
  * active tool's panel is rendered; the shared staged form over the
2112
2119
  * `tool-plus` namespace lives at the page level, so drafts survive switching
@@ -2637,6 +2644,7 @@ window.__ModuleLoader__.load({
2637
2644
  nav: "Tool Plus",
2638
2645
  pageTitle: "Tool Plus 工具配置",
2639
2646
  pageDescription: "按工具切换,调整每个工具的全局配置;保存后对新调用生效。",
2647
+ tabWeb: "Web 卡片",
2640
2648
  tabBash: "Bash",
2641
2649
  tabRead: "Read",
2642
2650
  tabWriteEdit: "Write & Edit",
@@ -2645,6 +2653,7 @@ window.__ModuleLoader__.load({
2645
2653
  tabAstGrep: "Ast Grep",
2646
2654
  tabAstEdit: "Ast Edit",
2647
2655
  tabReadImage: "Read Image",
2656
+ groupCards: "卡片",
2648
2657
  groupTiming: "超时与后台",
2649
2658
  groupOutput: "输出",
2650
2659
  groupTruncation: "完成消息截断",
@@ -2829,6 +2838,7 @@ window.__ModuleLoader__.load({
2829
2838
  nav: "Tool Plus",
2830
2839
  pageTitle: "Tool Plus tool configuration",
2831
2840
  pageDescription: "Switch by tool to adjust its global settings; values apply …",
2841
+ tabWeb: "Web cards",
2832
2842
  tabBash: "Bash",
2833
2843
  tabRead: "Read",
2834
2844
  tabWriteEdit: "Write & Edit",
@@ -2837,6 +2847,7 @@ window.__ModuleLoader__.load({
2837
2847
  tabAstGrep: "Ast Grep",
2838
2848
  tabAstEdit: "Ast Edit",
2839
2849
  tabReadImage: "Read Image",
2850
+ groupCards: "Cards",
2840
2851
  groupTiming: "Timeouts & backgrounding",
2841
2852
  groupOutput: "Output",
2842
2853
  groupTruncation: "Completion truncation",
@@ -3349,6 +3360,7 @@ window.__ModuleLoader__.load({
3349
3360
  running: "运行中",
3350
3361
  done: "完成",
3351
3362
  failed: "失败",
3363
+ stopped: "已停止",
3352
3364
  cancelled: "已取消",
3353
3365
  timedOut: "已超时",
3354
3366
  noOutput: "无输出",
@@ -3412,6 +3424,7 @@ window.__ModuleLoader__.load({
3412
3424
  running: "Running",
3413
3425
  done: "Done",
3414
3426
  failed: "Failed",
3427
+ stopped: "Stopped",
3415
3428
  cancelled: "Cancelled",
3416
3429
  timedOut: "Timed out",
3417
3430
  noOutput: "No output",
@@ -3470,7 +3483,7 @@ window.__ModuleLoader__.load({
3470
3483
  running: "running",
3471
3484
  ok: null,
3472
3485
  error: "failed",
3473
- warning: "failed"
3486
+ warning: "stopped"
3474
3487
  };
3475
3488
  /** Parsed-argument cache: the node is immutable, so one parse serves every render. */
3476
3489
  const parsedArgs = /* @__PURE__ */ new WeakMap();
@@ -3493,6 +3506,21 @@ window.__ModuleLoader__.load({
3493
3506
  return typeof value === "object" && value !== null && !Array.isArray(value);
3494
3507
  }
3495
3508
  /**
3509
+ * The call's original argument text.
3510
+ *
3511
+ * A running call carries it directly; a settled result carries the head
3512
+ * backfilled from the in-window `tool/call`, which is absent when window
3513
+ * truncation left that event outside. The text outlives the parse: a call whose
3514
+ * arguments are still streaming is not valid JSON yet, and the shell states its
3515
+ * first line rather than pretending the call said nothing.
3516
+ * @param block - the frozen running-or-settled call node.
3517
+ * @returns the argument text, or `''` when no head survives.
3518
+ */
3519
+ function cardArgsRaw(block) {
3520
+ const raw = callHead(block)?.argsRaw;
3521
+ return typeof raw === "string" ? raw : "";
3522
+ }
3523
+ /**
3496
3524
  * Parse the call's raw argument JSON.
3497
3525
  *
3498
3526
  * The parsed object is memoized per node (the node is immutable), so repeated
@@ -3521,30 +3549,26 @@ window.__ModuleLoader__.load({
3521
3549
  /**
3522
3550
  * Run state of one call.
3523
3551
  *
3524
- * A settled `isError` result is an error; a foreground command that timed out
3525
- * or was cancelled is amber rather than green (the shipped terminal model
3526
- * reads only the trailing exit marker, which is exactly what the card owner
3527
- * was told not to trust). A non-zero exit stays `ok`: bash reports a failing
3528
- * command as result data, and the card's own status pill carries the red.
3552
+ * A turn stopped mid-call is projected as a settled `isError` block carrying
3553
+ * `error.code === 'interrupted'`; the shipped row model reads that before
3554
+ * `isError`, so a stopped call is amber and named "stopped" rather than failed.
3555
+ * Any other settled `isError` result is an error. A foreground command that
3556
+ * timed out or was cancelled is amber rather than green (the shipped terminal
3557
+ * model reads only the trailing exit marker, which is exactly what the card
3558
+ * owner was told not to trust). A non-zero exit stays `ok`: bash reports a
3559
+ * failing command as result data, and the card's own status pill carries the
3560
+ * red.
3529
3561
  * @param block - the frozen running-or-settled call node.
3530
3562
  * @returns the card run state.
3531
3563
  */
3532
3564
  function cardState(block) {
3533
3565
  if (!isSettled(block)) return "running";
3566
+ if (isRecord$2(block.error) && block.error.code === "interrupted") return "warning";
3534
3567
  if (block.isError === true) return "error";
3535
3568
  const terminal = narrowTerminalCardMeta(cardMeta(block));
3536
3569
  if (terminal !== null && terminal.mode === "foreground" && (terminal.timedOut || terminal.aborted)) return "warning";
3537
3570
  return "ok";
3538
3571
  }
3539
- /** Map a card run state onto the primitive's dot semantics. */
3540
- function dotState(state) {
3541
- switch (state) {
3542
- case "running": return "ongoing";
3543
- case "error": return "error";
3544
- case "warning": return "warning";
3545
- default: return "done";
3546
- }
3547
- }
3548
3572
  /**
3549
3573
  * The assistive-technology status word for a run state, or `null` when the
3550
3574
  * state is already carried by the row's content.
@@ -3788,15 +3812,339 @@ window.__ModuleLoader__.load({
3788
3812
  };
3789
3813
  }
3790
3814
  //#endregion
3815
+ //#region src/web/client/rows/card-leading.ts
3816
+ /**
3817
+ * Wire tool name to glyph family: the shipped name table for the six keys this
3818
+ * plugin shadows, plus its own two tools.
3819
+ */
3820
+ const GLYPH_BY_TOOL = {
3821
+ bash: "bash",
3822
+ read: "read",
3823
+ write: "edit",
3824
+ edit: "edit",
3825
+ grep: "search",
3826
+ glob: "search",
3827
+ ast_grep: "search",
3828
+ ast_edit: "edit"
3829
+ };
3830
+ /**
3831
+ * Glyph family a row leads with.
3832
+ * @param toolName - the call's wire tool name, when the node carries one.
3833
+ * @returns the family, or `others` (the shipped generic glyph) for an unknown name.
3834
+ */
3835
+ function leadingGlyph(toolName) {
3836
+ if (typeof toolName !== "string") return "others";
3837
+ return GLYPH_BY_TOOL[toolName] ?? "others";
3838
+ }
3839
+ /**
3840
+ * Whether a run state keeps the row's glyph or replaces it with a state dot.
3841
+ * @param state - the card's run state.
3842
+ * @returns the leading kind; `error` and `warning` are the two dot states.
3843
+ */
3844
+ function leadingKind(state) {
3845
+ switch (state) {
3846
+ case "error": return "error";
3847
+ case "warning": return "warning";
3848
+ default: return "glyph";
3849
+ }
3850
+ }
3851
+ //#endregion
3852
+ //#region src/web/client/rows/command-card-facts.ts
3853
+ /**
3854
+ * Collapsed-fact builders for the two command-shaped cards: the command a
3855
+ * `bash` description hides, and the rewrite rule an `ast_edit` preview only
3856
+ * shows once expanded.
3857
+ *
3858
+ * Both cards collapse to one head line. `bash`'s head used to be the model's
3859
+ * own `description` — a sentence about intent, never the command that ran — so
3860
+ * the only way to learn what executed was to expand the card. `ast_edit`'s head
3861
+ * names the file and the replacement count, and its body previews the *result*:
3862
+ * the rule that produced that result lives only in the call's `ops` argument,
3863
+ * which no card showed at all. Each builder recovers one fact from the call's
3864
+ * own arguments, so nothing is inferred from output, and a call head that fell
3865
+ * outside the session window degrades to the facts the card already holds.
3866
+ * @module @xiaoso/dsh-tool-plus/web/client/rows/command-card-facts
3867
+ */
3868
+ /** Whether `value` is a plain object — the only op shape the engine sends. */
3869
+ function isRecord$1(value) {
3870
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3871
+ }
3872
+ /**
3873
+ * The `bash` card's collapsed summary: what ran, placed after the sentence
3874
+ * saying why.
3875
+ *
3876
+ * The command is the fact the head must carry, so the description can never be
3877
+ * the whole summary — it is a prefix at most. Only its first line is used: a
3878
+ * summary is one row high, and the rest of a multi-line command belongs to the
3879
+ * expanded body, which draws the whole thing.
3880
+ * @param description - the model's one-line intent, or `null` when absent.
3881
+ * @param command - the command actually executed (never empty).
3882
+ * @param outcome - the interruption word (`timedOut` / `cancelled`), or `null`.
3883
+ * @returns the outcome alone when the run was interrupted; otherwise the
3884
+ * description joined to the command's first line, or that line alone.
3885
+ */
3886
+ function bashCardSummary(description, command, outcome) {
3887
+ if (outcome !== null && outcome !== "") return outcome;
3888
+ const intent = description === null ? "" : description.trim();
3889
+ const line = firstLine(command);
3890
+ return intent === "" ? line : `${intent} · ${line}`;
3891
+ }
3892
+ /**
3893
+ * The `ast_edit` card's rule line: the first usable `ops` entry, written the
3894
+ * way the tool's own `ops` argument writes it (`pat → out`).
3895
+ *
3896
+ * Only the first rule is stated, with the count of the entries behind it: the
3897
+ * card exists to preview one change, and a call may carry many rules whose
3898
+ * patterns are whole AST fragments — listing them would bury the preview. The
3899
+ * remaining count is the array tail, not the number of *usable* entries, so it
3900
+ * always describes the call as it was made.
3901
+ * @param args - the call's parsed arguments, or `null` when its head is gone.
3902
+ * @param t - this plugin's card translate seat.
3903
+ * @returns the rule line, or `null` when the call carries no usable rule.
3904
+ */
3905
+ function astEditRuleLine(args, t) {
3906
+ const ops = args?.ops;
3907
+ if (!Array.isArray(ops)) return null;
3908
+ for (let index = 0; index < ops.length; index += 1) {
3909
+ const op = ops[index];
3910
+ if (!isRecord$1(op)) continue;
3911
+ const pat = argText(op, "pat");
3912
+ if (pat === null) continue;
3913
+ const replacement = op.out;
3914
+ if (typeof replacement !== "string") continue;
3915
+ const rest = ops.length - 1;
3916
+ const rule = `${t("astEdit.rules")}: ${pat} →${replacement === "" ? "" : ` ${replacement}`}`;
3917
+ return rest === 0 ? rule : `${rule} · ${t("astEdit.rulesRest", { count: rest })}`;
3918
+ }
3919
+ return null;
3920
+ }
3921
+ //#endregion
3922
+ //#region src/web/client/rows/read-card-facts.ts
3923
+ /**
3924
+ * Collapsed-summary facts for the `read` card.
3925
+ *
3926
+ * The Host projection strips the inline selector off the path
3927
+ * (`stripReadSelector` in `web/host/read.ts`) because the card's *link* must
3928
+ * point at a real file, and it keeps only the file's own window. The collapsed
3929
+ * head is then the one place where the question the model actually asked is
3930
+ * still readable — and the projected path cannot answer it: a windowed read of
3931
+ * `package.json:22-25,31-34` and a whole-file read of `package.json` project to
3932
+ * the very same string, so the card hides the difference.
3933
+ *
3934
+ * The summary is therefore built from the call's own argument, selector and
3935
+ * all, with the projected path kept as the fallback for a result whose call
3936
+ * head fell outside the session window (a truncated window carries no
3937
+ * `tool/call` event, so no argument survives). That fallback states a fact the
3938
+ * card already holds; nothing is invented.
3939
+ * @module @xiaoso/dsh-tool-plus/web/client/rows/read-card-facts
3940
+ */
3941
+ /**
3942
+ * The `read` card's collapsed summary: the path the model asked for, keeping
3943
+ * whatever inline selector it carried (`:22-25`, `:raw`, `:conflicts`, `:28-`,
3944
+ * a sqlite row, an archive member) and shortened against the workspace root.
3945
+ *
3946
+ * The selector is deliberately not validated against the read grammar: the head
3947
+ * reports the call verbatim, and a selector this plugin does not recognize is
3948
+ * still what the model typed — substituting the projection's stripped path
3949
+ * would erase the evidence that two calls differ.
3950
+ * @param args - the call's parsed arguments, or `null` when its head is gone.
3951
+ * @param metaPath - the Host-projected path (already selector-free).
3952
+ * @param cwd - the session workspace root, when known.
3953
+ * @returns the workspace-shortened argument path, or the projected path.
3954
+ */
3955
+ function readCardSummary(args, metaPath, cwd) {
3956
+ return displayPath(argText(args, "path") ?? metaPath, cwd);
3957
+ }
3958
+ //#endregion
3959
+ //#region src/web/client/rows/fallback-card-facts.ts
3960
+ /**
3961
+ * Collapsed-summary facts for the generic card — every row's degradation path.
3962
+ *
3963
+ * A call lands on the generic shell when its own card cannot be built: it is
3964
+ * still running, its result carried no text, its metadata fell outside the
3965
+ * session window, it is a Code Dispatch child, or the Host projected an
3966
+ * interruption the card has no material for. In every one of those cases the
3967
+ * call's ARGUMENTS are the only material left, and the shipped generic row
3968
+ * answers them with the call's own fact: its variant's summary keys first, then
3969
+ * ANY string the payload carries, then the first line of the raw argument text
3970
+ * (`deriveSummary` in ui-tool's `tool-call-model.ts`). It never answers with an
3971
+ * argument count.
3972
+ *
3973
+ * That is the rule here. The count it replaces told the reader nothing: a
3974
+ * directory read, a `:raw` window, a running `grep` and a child call all
3975
+ * collapsed to "1 个参数" — the one thing a degraded row must not do, since the
3976
+ * head is then the only place the fact exists.
3977
+ *
3978
+ * The key preference follows THIS plugin's own tool schemas rather than the
3979
+ * shipped `SUMMARY_KEYS`: our tools replaced the built-in ones with the OMP
3980
+ * native parameter names (`read`/`write` take `path`, `ast_grep` takes `pat`,
3981
+ * `ast_edit` takes `paths`), and the shipped table's `file_path` is what the
3982
+ * built-in tools used. Both spellings are accepted, ours first, so a transcript
3983
+ * recorded before the takeover still states its path.
3984
+ * @module @xiaoso/dsh-tool-plus/web/client/rows/fallback-card-facts
3985
+ */
3986
+ /**
3987
+ * First entry of a path-list argument (`ast_edit.paths`), tolerating the
3988
+ * single-string form some calls use.
3989
+ * @param args - the call's parsed arguments, or `null`.
3990
+ * @param name - the argument's name.
3991
+ * @returns the first non-empty path, or `null`.
3992
+ */
3993
+ function firstPath(args, name) {
3994
+ const value = args?.[name];
3995
+ if (typeof value === "string" && value !== "") return value;
3996
+ if (!Array.isArray(value)) return null;
3997
+ for (const entry of value) if (typeof entry === "string" && entry !== "") return entry;
3998
+ return null;
3999
+ }
4000
+ /**
4001
+ * The first non-empty string the payload carries under one of `keys` — the
4002
+ * shipped row's own key-preference rule, first line only (a payload's own
4003
+ * newlines never reach a one-line head).
4004
+ * @param args - the call's parsed arguments, or `null`.
4005
+ * @param keys - argument names, most significant first.
4006
+ * @returns the value, or `null` when none of them is a non-empty string.
4007
+ */
4008
+ function pickKeys(args, keys) {
4009
+ for (const key of keys) {
4010
+ const value = argText(args, key);
4011
+ if (value !== null) return firstLine(value);
4012
+ }
4013
+ return null;
4014
+ }
4015
+ /**
4016
+ * The `queries` a search call may carry, joined one query per line — the one
4017
+ * special case the shipped row makes before its key table.
4018
+ * @param args - the call's parsed arguments, or `null`.
4019
+ * @returns the joined subject, or `null` when there is no usable query.
4020
+ */
4021
+ function queriesSubject(args) {
4022
+ const queries = args?.["queries"];
4023
+ if (!Array.isArray(queries)) return null;
4024
+ const parts = queries.filter((query) => typeof query === "string" && query !== "").map(firstLine);
4025
+ return parts.length === 0 ? null : parts.join(", ");
4026
+ }
4027
+ /**
4028
+ * The fact the call states for a key this plugin renders.
4029
+ * @param toolName - the call's wire tool name.
4030
+ * @param args - the call's parsed arguments, or `null`.
4031
+ * @param cwd - the session workspace root, for shortening an absolute path.
4032
+ * @returns the stated fact, or `null` when this key states nothing usable.
4033
+ */
4034
+ function statedFact(toolName, args, cwd) {
4035
+ switch (toolName) {
4036
+ case "read": {
4037
+ const asked = pickKeys(args, [
4038
+ "path",
4039
+ "file_path",
4040
+ "url"
4041
+ ]);
4042
+ return asked === null ? null : readCardSummary(args, asked, cwd);
4043
+ }
4044
+ case "bash": {
4045
+ const command = argText(args, "command");
4046
+ const description = argText(args, "description");
4047
+ if (command === null) return description;
4048
+ return bashCardSummary(description, command, null);
4049
+ }
4050
+ case "write": {
4051
+ const path = pickKeys(args, ["path", "file_path"]);
4052
+ return path === null ? null : displayPath(path, cwd);
4053
+ }
4054
+ case "edit": {
4055
+ const path = pickKeys(args, ["file_path", "path"]);
4056
+ return path === null ? null : displayPath(path, cwd);
4057
+ }
4058
+ case "ast_edit": {
4059
+ const path = firstPath(args, "paths");
4060
+ return path === null ? null : displayPath(path, cwd);
4061
+ }
4062
+ case "grep":
4063
+ case "glob":
4064
+ case "ast_grep": {
4065
+ const subject = queriesSubject(args) ?? pickKeys(args, [
4066
+ "query",
4067
+ "pattern",
4068
+ "pat",
4069
+ "url",
4070
+ "path"
4071
+ ]);
4072
+ return subject === null ? null : displayPath(subject, cwd);
4073
+ }
4074
+ default: return null;
4075
+ }
4076
+ }
4077
+ /**
4078
+ * The generic card's collapsed summary: the fact the call itself states.
4079
+ * @param toolName - the call's wire tool name (empty when truncated).
4080
+ * @param args - the call's parsed arguments, or `null` when its head is gone.
4081
+ * @param cwd - the session workspace root, for shortening an absolute path.
4082
+ * @param t - the card translate seat.
4083
+ * @param raw - the call's original argument text, for a payload that is not
4084
+ * valid JSON yet (a call still streaming its arguments) or whose only values
4085
+ * are non-strings.
4086
+ * @returns the collapsed summary text.
4087
+ */
4088
+ function fallbackCardSummary(toolName, args, cwd, t, raw = null) {
4089
+ const stated = statedFact(toolName, args, cwd);
4090
+ if (stated !== null) return stated;
4091
+ if (args !== null) {
4092
+ for (const value of Object.values(args)) if (typeof value === "string" && value !== "") return firstLine(value);
4093
+ }
4094
+ const text = raw === null || raw === "" ? null : firstLine(raw);
4095
+ if (text !== null && text !== "") return text;
4096
+ return argsSummary(args, t);
4097
+ }
4098
+ /**
4099
+ * Everything from the first colon is a read selector, not part of a filename.
4100
+ * The colon after a Windows drive letter is not one, and a URL is not a session
4101
+ * file at all.
4102
+ * @param value - the path argument, verbatim.
4103
+ * @returns the file the path names, or `null` when it names no openable file.
4104
+ */
4105
+ function stripSelector(value) {
4106
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value)) return null;
4107
+ const start = /^[a-zA-Z]:[\\/]/.test(value) ? 2 : 0;
4108
+ const colon = value.indexOf(":", start);
4109
+ const base = colon === -1 ? value : value.slice(0, colon);
4110
+ return base === "" ? null : base;
4111
+ }
4112
+ /**
4113
+ * The real file a degraded file-backed row may offer to open — `null` when the
4114
+ * call names no single file.
4115
+ *
4116
+ * The head states the call verbatim (selector included) while the link has to
4117
+ * name a real file: the same split the read card itself makes between its
4118
+ * summary and its link. Whether that file then previews is the side bar's
4119
+ * decision, never this module's.
4120
+ * @param toolName - the call's wire tool name.
4121
+ * @param args - the call's parsed arguments, or `null`.
4122
+ * @returns the file to open, or `null` when there is nothing to open.
4123
+ */
4124
+ function fallbackCardLink(toolName, args) {
4125
+ switch (toolName) {
4126
+ case "read": {
4127
+ const asked = pickKeys(args, ["path", "file_path"]);
4128
+ return asked === null ? null : stripSelector(asked);
4129
+ }
4130
+ case "write": return pickKeys(args, ["path", "file_path"]);
4131
+ case "edit": return pickKeys(args, ["file_path", "path"]);
4132
+ case "ast_edit": return firstPath(args, "paths");
4133
+ default: return null;
4134
+ }
4135
+ }
4136
+ //#endregion
3791
4137
  //#region src/web/client/ToolCardShell.tsx
3792
4138
  /**
3793
4139
  * The shared card shell every row renders through.
3794
4140
  *
3795
4141
  * The shipped `ToolRow` is not reusable from a plugin (ui-tool is not part of
3796
4142
  * the client platform baseline), so this is our own: one 24px header line
3797
- * (state dot + title + separator + ellipsized summary + optional `+N -M`
4143
+ * (tool glyph + title + separator + ellipsized summary + optional `+N -M`
3798
4144
  * suffix + clickable path), a keyboard-reachable disclosure, and a body that
3799
- * is either the card a row built or the flattened result text.
4145
+ * is either the card a row built or the flattened result text. The leading
4146
+ * glyph gives way to a state dot on a failed or interrupted call, and the row
4147
+ * sweeps while the call runs, as the shipped row does.
3800
4148
  *
3801
4149
  * Two properties matter more than the visuals:
3802
4150
  * - **Never throw.** Every value is coerced defensively, and {@link CardBoundary}
@@ -3808,11 +4156,45 @@ window.__ModuleLoader__.load({
3808
4156
  * both come out right.
3809
4157
  * @module @xiaoso/dsh-tool-plus/web/client/ToolCardShell
3810
4158
  */
4159
+ /** Leading glyphs (the shipped table): every glyph renders at 14 inside the 16px leading box. */
4160
+ const LEADING_GLYPHS = {
4161
+ bash: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconApiOutline14, { size: 14 }),
4162
+ read: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBrowseOutline16, { size: 14 }),
4163
+ edit: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, { size: 14 }),
4164
+ search: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, { size: 14 }),
4165
+ others: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSparkle16, { size: 14 })
4166
+ };
4167
+ /**
4168
+ * The leading mark of one row: the tool's glyph while the call is clean or
4169
+ * running, and the state dot a failure or an interrupt swaps in — the shipped
4170
+ * `leadingFor` rule, so a failed row gives up its identity for the state
4171
+ * exactly where the shipped row does.
4172
+ * @param state - the card's run state.
4173
+ * @param toolName - the call's wire tool name, when the node carries one.
4174
+ * @returns the node the leading box draws.
4175
+ */
4176
+ function leadingFor(state, toolName) {
4177
+ switch (leadingKind(state)) {
4178
+ case "error": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "error" });
4179
+ case "warning": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "warning" });
4180
+ default: return LEADING_GLYPHS[leadingGlyph(toolName)];
4181
+ }
4182
+ }
3811
4183
  /** Chrome CSS, keyed by `data-plugin-css` and injected once per page. */
3812
4184
  const CSS = `
3813
4185
  .twc-root{display:flex;flex-direction:column;min-width:0}
3814
- .twc-row{position:relative;min-width:0}
4186
+ /* The sweep's own anchor and clip, repeated from the shipped sheet so the band
4187
+ cannot escape the row if the shared row chrome ever drops them. */
4188
+ .twc-row{position:relative;min-width:0;overflow:hidden}
3815
4189
  .twc-leading{flex-shrink:0}
4190
+ /* Running sweep — the shipped row's own treatment (ui-tool/ToolRow.module.css),
4191
+ copied band for band: a 300px glare of the theme background glides from
4192
+ off-left to off-right, washing the glyph and the text toward the background
4193
+ as it passes. Same anchor as the shipped sheet puts it on: DisclosureRow's
4194
+ own row already carries the position:relative and the overflow:hidden that
4195
+ clips the band. */
4196
+ .twc-root[data-state='running'] .twc-row::after{content:'';position:absolute;top:0;bottom:0;left:0;width:300px;background:linear-gradient(90deg,transparent 0%,color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%,transparent 100%);animation:twc-row-sweep 2.6s ease-out infinite;pointer-events:none}
4197
+ @keyframes twc-row-sweep{0%{left:-300px}90%,100%{left:100%}}
3816
4198
  .twc-chevron{color:var(--dsw-alias-label-secondary)}
3817
4199
  .twc-title{font-weight:400}
3818
4200
  .twc-sep{flex:none;width:2px;height:2px;border-radius:1px;margin:0 8px;background:var(--dsw-alias-label-caption)}
@@ -3878,8 +4260,11 @@ window.__ModuleLoader__.load({
3878
4260
  .twc-root:hover .twc-inspect,.twc-inspect:focus-visible{opacity:1}
3879
4261
  .twc-inspect:hover{background:var(--dsw-alias-interactive-bg-hover-solid);color:var(--dsw-alias-label-primary)}
3880
4262
  .twc-visuallyHidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
3881
- .twc-fallbackDot{flex:none;width:8px;height:8px;border-radius:999px;background:var(--dsw-alias-state-error-primary)}
3882
- @media (prefers-reduced-motion: reduce){.twc-inspect{transition:none}}
4263
+ /* Motion is feedback, never decoration: the running sweep is the one animation
4264
+ this shell borrows from the shipped row, which runs it with no motion query
4265
+ at all. A reduced-motion reader loses only the glide — the state itself is
4266
+ still announced through the visually-hidden status word. */
4267
+ @media (prefers-reduced-motion: reduce){.twc-inspect{transition:none}.twc-root[data-state='running'] .twc-row::after{display:none}}
3883
4268
  `;
3884
4269
  /** Inject the card stylesheet once per page; the loader drops plugin style tags on unload. */
3885
4270
  let cssInjected = false;
@@ -3906,7 +4291,7 @@ window.__ModuleLoader__.load({
3906
4291
  */
3907
4292
  function ToolCardShell(props) {
3908
4293
  injectCss();
3909
- const { t, state, title, children, filePath, openFile, inspect } = props;
4294
+ const { t, state, title, children, filePath, openFile, inspect, tool } = props;
3910
4295
  const [expanded, setExpanded] = (0, react.useState)(props.defaultOpen === true);
3911
4296
  const bodyCard = hasContent(children) ? children : null;
3912
4297
  const bodyText = typeof props.text === "string" && props.text !== "" ? props.text : null;
@@ -3937,7 +4322,7 @@ window.__ModuleLoader__.load({
3937
4322
  }),
3938
4323
  fileLink ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3939
4324
  type: "button",
3940
- className: "twc-fileLink",
4325
+ className: errorTone ? "twc-fileLink twc-errorSummary" : "twc-fileLink",
3941
4326
  onClick: openPath,
3942
4327
  onKeyDown: openPathKeyDown,
3943
4328
  children: summaryText
@@ -3962,7 +4347,7 @@ window.__ModuleLoader__.load({
3962
4347
  leadingClassName: "twc-leading",
3963
4348
  titleClassName: "twc-title",
3964
4349
  chevronClassName: "twc-chevron",
3965
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: dotState(state) }),
4350
+ icon: leadingFor(state, tool),
3966
4351
  title,
3967
4352
  open,
3968
4353
  expandable,
@@ -3991,20 +4376,24 @@ window.__ModuleLoader__.load({
3991
4376
  * A card is only drawn once its metadata passed a `narrow*` guard, so anything
3992
4377
  * else — a running call, a window-truncated call, a Code Dispatch child, a
3993
4378
  * failure, a payload from another plugin version — lands here with the call's
3994
- * own title, run state, argument count, and result text.
4379
+ * own title, run state, the fact its arguments state, and result text.
3995
4380
  * @param props - see {@link GenericCardProps}.
3996
4381
  * @returns the generic shell.
3997
4382
  */
3998
- function GenericCard({ t, title, block, args, inspect }) {
4383
+ function GenericCard({ t, title, block, args, toolName, cwd, openFile, inspect }) {
3999
4384
  const state = cardState(block);
4000
4385
  const output = resultText(block);
4001
4386
  const failure = state === "error" ? firstLine(output) : "";
4387
+ const link = failure === "" ? fallbackCardLink(toolName, args) : null;
4002
4388
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
4003
4389
  t,
4004
4390
  state,
4005
4391
  title,
4006
- summary: failure === "" ? argsSummary(args, t) : failure,
4392
+ tool: toolName,
4393
+ summary: failure === "" ? fallbackCardSummary(toolName, args, cwd, t, cardArgsRaw(block)) : failure,
4007
4394
  summaryTone: failure === "" ? "default" : "error",
4395
+ filePath: link,
4396
+ openFile: link === null ? void 0 : openFile,
4008
4397
  text: output,
4009
4398
  inspect
4010
4399
  });
@@ -4054,76 +4443,6 @@ window.__ModuleLoader__.load({
4054
4443
  }
4055
4444
  };
4056
4445
  //#endregion
4057
- //#region src/web/client/rows/command-card-facts.ts
4058
- /**
4059
- * Collapsed-fact builders for the two command-shaped cards: the command a
4060
- * `bash` description hides, and the rewrite rule an `ast_edit` preview only
4061
- * shows once expanded.
4062
- *
4063
- * Both cards collapse to one head line. `bash`'s head used to be the model's
4064
- * own `description` — a sentence about intent, never the command that ran — so
4065
- * the only way to learn what executed was to expand the card. `ast_edit`'s head
4066
- * names the file and the replacement count, and its body previews the *result*:
4067
- * the rule that produced that result lives only in the call's `ops` argument,
4068
- * which no card showed at all. Each builder recovers one fact from the call's
4069
- * own arguments, so nothing is inferred from output, and a call head that fell
4070
- * outside the session window degrades to the facts the card already holds.
4071
- * @module @xiaoso/dsh-tool-plus/web/client/rows/command-card-facts
4072
- */
4073
- /** Whether `value` is a plain object — the only op shape the engine sends. */
4074
- function isRecord$1(value) {
4075
- return typeof value === "object" && value !== null && !Array.isArray(value);
4076
- }
4077
- /**
4078
- * The `bash` card's collapsed summary: what ran, placed after the sentence
4079
- * saying why.
4080
- *
4081
- * The command is the fact the head must carry, so the description can never be
4082
- * the whole summary — it is a prefix at most. Only its first line is used: a
4083
- * summary is one row high, and the rest of a multi-line command belongs to the
4084
- * expanded body, which draws the whole thing.
4085
- * @param description - the model's one-line intent, or `null` when absent.
4086
- * @param command - the command actually executed (never empty).
4087
- * @param outcome - the interruption word (`timedOut` / `cancelled`), or `null`.
4088
- * @returns the outcome alone when the run was interrupted; otherwise the
4089
- * description joined to the command's first line, or that line alone.
4090
- */
4091
- function bashCardSummary(description, command, outcome) {
4092
- if (outcome !== null && outcome !== "") return outcome;
4093
- const intent = description === null ? "" : description.trim();
4094
- const line = firstLine(command);
4095
- return intent === "" ? line : `${intent} · ${line}`;
4096
- }
4097
- /**
4098
- * The `ast_edit` card's rule line: the first usable `ops` entry, written the
4099
- * way the tool's own `ops` argument writes it (`pat → out`).
4100
- *
4101
- * Only the first rule is stated, with the count of the entries behind it: the
4102
- * card exists to preview one change, and a call may carry many rules whose
4103
- * patterns are whole AST fragments — listing them would bury the preview. The
4104
- * remaining count is the array tail, not the number of *usable* entries, so it
4105
- * always describes the call as it was made.
4106
- * @param args - the call's parsed arguments, or `null` when its head is gone.
4107
- * @param t - this plugin's card translate seat.
4108
- * @returns the rule line, or `null` when the call carries no usable rule.
4109
- */
4110
- function astEditRuleLine(args, t) {
4111
- const ops = args?.ops;
4112
- if (!Array.isArray(ops)) return null;
4113
- for (let index = 0; index < ops.length; index += 1) {
4114
- const op = ops[index];
4115
- if (!isRecord$1(op)) continue;
4116
- const pat = argText(op, "pat");
4117
- if (pat === null) continue;
4118
- const replacement = op.out;
4119
- if (typeof replacement !== "string") continue;
4120
- const rest = ops.length - 1;
4121
- const rule = `${t("astEdit.rules")}: ${pat} →${replacement === "" ? "" : ` ${replacement}`}`;
4122
- return rest === 0 ? rule : `${rule} · ${t("astEdit.rulesRest", { count: rest })}`;
4123
- }
4124
- return null;
4125
- }
4126
- //#endregion
4127
4446
  //#region src/web/client/rows/AstEditRow.tsx
4128
4447
  /**
4129
4448
  * `ast_edit` row: the engine's own change preview.
@@ -4161,7 +4480,7 @@ window.__ModuleLoader__.load({
4161
4480
  * @returns the preview card.
4162
4481
  */
4163
4482
  function AstEditCard(props) {
4164
- const { t, block, cwd, inspect, openFile, title } = props;
4483
+ const { t, block, cwd, inspect, openFile, title, toolName } = props;
4165
4484
  const args = parseCardArgs(block);
4166
4485
  const meta = narrowAstEditCardMeta(cardMeta(block));
4167
4486
  if (isSubCall(block) || block.isError === true || meta === null || meta.preview === "") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenericCard, {
@@ -4169,6 +4488,9 @@ window.__ModuleLoader__.load({
4169
4488
  title,
4170
4489
  block,
4171
4490
  args,
4491
+ toolName,
4492
+ cwd,
4493
+ openFile,
4172
4494
  inspect
4173
4495
  });
4174
4496
  const files = meta.files.length;
@@ -4181,6 +4503,7 @@ window.__ModuleLoader__.load({
4181
4503
  t,
4182
4504
  state: cardState(block),
4183
4505
  title,
4506
+ tool: toolName,
4184
4507
  summary,
4185
4508
  summarySuffix: `${t("astEdit.replacements", { count: meta.replacements })} · ${t("astEdit.files", { count: files })}`,
4186
4509
  filePath: single,
@@ -4243,7 +4566,7 @@ window.__ModuleLoader__.load({
4243
4566
  * @returns the diff card.
4244
4567
  */
4245
4568
  function FileMutationCard(props) {
4246
- const { t, block, cwd, inspect, openFile, title } = props;
4569
+ const { t, block, cwd, inspect, openFile, title, toolName } = props;
4247
4570
  const args = parseCardArgs(block);
4248
4571
  const meta = narrowEditCardMeta(cardMeta(block));
4249
4572
  const replaceForm = replaceHunks(args);
@@ -4252,6 +4575,9 @@ window.__ModuleLoader__.load({
4252
4575
  title,
4253
4576
  block,
4254
4577
  args,
4578
+ toolName,
4579
+ cwd,
4580
+ openFile,
4255
4581
  inspect
4256
4582
  });
4257
4583
  const diffs = meta === null ? replaceForm ?? [] : meta.diffs;
@@ -4263,6 +4589,7 @@ window.__ModuleLoader__.load({
4263
4589
  t,
4264
4590
  state: cardState(block),
4265
4591
  title,
4592
+ tool: toolName,
4266
4593
  summary,
4267
4594
  summarySuffix: `+${totals.added} -${totals.removed}`,
4268
4595
  filePath: single,
@@ -4410,43 +4737,6 @@ window.__ModuleLoader__.load({
4410
4737
  };
4411
4738
  }
4412
4739
  //#endregion
4413
- //#region src/web/client/rows/read-card-facts.ts
4414
- /**
4415
- * Collapsed-summary facts for the `read` card.
4416
- *
4417
- * The Host projection strips the inline selector off the path
4418
- * (`stripReadSelector` in `web/host/read.ts`) because the card's *link* must
4419
- * point at a real file, and it keeps only the file's own window. The collapsed
4420
- * head is then the one place where the question the model actually asked is
4421
- * still readable — and the projected path cannot answer it: a windowed read of
4422
- * `package.json:22-25,31-34` and a whole-file read of `package.json` project to
4423
- * the very same string, so the card hides the difference.
4424
- *
4425
- * The summary is therefore built from the call's own argument, selector and
4426
- * all, with the projected path kept as the fallback for a result whose call
4427
- * head fell outside the session window (a truncated window carries no
4428
- * `tool/call` event, so no argument survives). That fallback states a fact the
4429
- * card already holds; nothing is invented.
4430
- * @module @xiaoso/dsh-tool-plus/web/client/rows/read-card-facts
4431
- */
4432
- /**
4433
- * The `read` card's collapsed summary: the path the model asked for, keeping
4434
- * whatever inline selector it carried (`:22-25`, `:raw`, `:conflicts`, `:28-`,
4435
- * a sqlite row, an archive member) and shortened against the workspace root.
4436
- *
4437
- * The selector is deliberately not validated against the read grammar: the head
4438
- * reports the call verbatim, and a selector this plugin does not recognize is
4439
- * still what the model typed — substituting the projection's stripped path
4440
- * would erase the evidence that two calls differ.
4441
- * @param args - the call's parsed arguments, or `null` when its head is gone.
4442
- * @param metaPath - the Host-projected path (already selector-free).
4443
- * @param cwd - the session workspace root, when known.
4444
- * @returns the workspace-shortened argument path, or the projected path.
4445
- */
4446
- function readCardSummary(args, metaPath, cwd) {
4447
- return displayPath(argText(args, "path") ?? metaPath, cwd);
4448
- }
4449
- //#endregion
4450
4740
  //#region src/web/client/rows/ReadRow.tsx
4451
4741
  /**
4452
4742
  * `read` row: a line-numbered window view, or the image the read returned.
@@ -4491,7 +4781,7 @@ window.__ModuleLoader__.load({
4491
4781
  * @returns the read card.
4492
4782
  */
4493
4783
  function ReadCard(props) {
4494
- const { t, block, cwd, inspect, openFile, title } = props;
4784
+ const { t, block, cwd, inspect, openFile, title, toolName } = props;
4495
4785
  const args = parseCardArgs(block);
4496
4786
  const meta = narrowReadCardMeta(cardMeta(block));
4497
4787
  const material = (0, react.useMemo)(() => imageCardMaterial(block, cwd), [block, cwd]);
@@ -4500,6 +4790,9 @@ window.__ModuleLoader__.load({
4500
4790
  title,
4501
4791
  block,
4502
4792
  args,
4793
+ toolName,
4794
+ cwd,
4795
+ openFile,
4503
4796
  inspect
4504
4797
  });
4505
4798
  if (meta === null) {
@@ -4508,6 +4801,9 @@ window.__ModuleLoader__.load({
4508
4801
  title,
4509
4802
  block,
4510
4803
  args,
4804
+ toolName,
4805
+ cwd,
4806
+ openFile,
4511
4807
  inspect
4512
4808
  });
4513
4809
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageReadCard, {
@@ -4515,6 +4811,7 @@ window.__ModuleLoader__.load({
4515
4811
  block,
4516
4812
  material,
4517
4813
  title,
4814
+ toolName,
4518
4815
  inspect,
4519
4816
  openFile,
4520
4817
  loadImage: props.loadImage
@@ -4524,6 +4821,7 @@ window.__ModuleLoader__.load({
4524
4821
  t,
4525
4822
  state: cardState(block),
4526
4823
  title,
4824
+ tool: toolName,
4527
4825
  summary: readCardSummary(args, meta.path, cwd),
4528
4826
  filePath: meta.path,
4529
4827
  filePathLine: meta.offset,
@@ -4568,13 +4866,14 @@ window.__ModuleLoader__.load({
4568
4866
  * @param props - see {@link ImageReadCardProps}.
4569
4867
  * @returns the image card.
4570
4868
  */
4571
- function ImageReadCard({ t, block, material, title, inspect, openFile, loadImage }) {
4869
+ function ImageReadCard({ t, block, material, title, toolName, inspect, openFile, loadImage }) {
4572
4870
  const images = material.images;
4573
4871
  const text = resultText(block);
4574
4872
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ToolCardShell, {
4575
4873
  t,
4576
4874
  state: cardState(block),
4577
4875
  title,
4876
+ tool: toolName,
4578
4877
  summary: material.label,
4579
4878
  filePath: material.path,
4580
4879
  openFile,
@@ -4848,7 +5147,7 @@ window.__ModuleLoader__.load({
4848
5147
  * @returns the search card.
4849
5148
  */
4850
5149
  function SearchCard(props) {
4851
- const { t, block, cwd, inspect, title } = props;
5150
+ const { t, block, cwd, inspect, title, toolName } = props;
4852
5151
  const args = parseCardArgs(block);
4853
5152
  const meta = narrowSearchCardMeta(cardMeta(block));
4854
5153
  if (isSubCall(block) || block.isError === true || meta === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenericCard, {
@@ -4856,6 +5155,8 @@ window.__ModuleLoader__.load({
4856
5155
  title,
4857
5156
  block,
4858
5157
  args,
5158
+ toolName,
5159
+ cwd,
4859
5160
  inspect
4860
5161
  });
4861
5162
  const subject = argText(args, "pattern") ?? argText(args, "pat") ?? argText(args, "path");
@@ -4864,6 +5165,7 @@ window.__ModuleLoader__.load({
4864
5165
  t,
4865
5166
  state: cardState(block),
4866
5167
  title,
5168
+ tool: toolName,
4867
5169
  summary: subject ?? argsSummary(args, t),
4868
5170
  summarySuffix: searchCardSuffix(args, meta, cwd, t),
4869
5171
  inspect,
@@ -4922,7 +5224,7 @@ window.__ModuleLoader__.load({
4922
5224
  * @returns the terminal card or the generic shell.
4923
5225
  */
4924
5226
  function TerminalCard(props) {
4925
- const { t, block, cwd: sessionCwd, home, inspect, title } = props;
5227
+ const { t, block, cwd: sessionCwd, home, inspect, title, toolName } = props;
4926
5228
  const args = parseCardArgs(block);
4927
5229
  const meta = narrowTerminalCardMeta(cardMeta(block));
4928
5230
  const command = argText(args, "command");
@@ -4932,12 +5234,15 @@ window.__ModuleLoader__.load({
4932
5234
  title,
4933
5235
  block,
4934
5236
  args,
5237
+ toolName,
5238
+ cwd: sessionCwd,
4935
5239
  inspect
4936
5240
  });
4937
5241
  if (meta !== null && meta.mode === "background") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCardShell, {
4938
5242
  t,
4939
5243
  state,
4940
5244
  title,
5245
+ tool: toolName,
4941
5246
  summary: t("bash.background", { jobId: meta.jobId }),
4942
5247
  text: resultText(block),
4943
5248
  inspect
@@ -4955,6 +5260,7 @@ window.__ModuleLoader__.load({
4955
5260
  t,
4956
5261
  state,
4957
5262
  title,
5263
+ tool: toolName,
4958
5264
  summary,
4959
5265
  inspect,
4960
5266
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.TerminalBlock, {
@@ -5006,7 +5312,7 @@ window.__ModuleLoader__.load({
5006
5312
  * @returns the preview card.
5007
5313
  */
5008
5314
  function WriteCard(props) {
5009
- const { t, block, cwd, inspect, openFile, title } = props;
5315
+ const { t, block, cwd, inspect, openFile, title, toolName } = props;
5010
5316
  const args = parseCardArgs(block);
5011
5317
  const meta = narrowWriteCardMeta(cardMeta(block));
5012
5318
  const content = argText(args, "content");
@@ -5016,6 +5322,9 @@ window.__ModuleLoader__.load({
5016
5322
  title,
5017
5323
  block,
5018
5324
  args,
5325
+ toolName,
5326
+ cwd,
5327
+ openFile,
5019
5328
  inspect
5020
5329
  });
5021
5330
  const lines = t("write.lines", { count: lineCount(content) });
@@ -5025,6 +5334,7 @@ window.__ModuleLoader__.load({
5025
5334
  t,
5026
5335
  state: cardState(block),
5027
5336
  title,
5337
+ tool: toolName,
5028
5338
  summary: displayPath(path, cwd),
5029
5339
  summarySuffix: lines + skip,
5030
5340
  filePath: path,