@huanlin/dsh-plugin-merge-tool-calls 0.1.0

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 ADDED
@@ -0,0 +1,1029 @@
1
+ window.__ModuleLoader__.load({ id: "@huanlin/dsh-plugin-merge-tool-calls", factory: (require) => {
2
+ var module = { exports: {} }; var exports = module.exports;
3
+ //#region rolldown:runtime
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
21
+ value: mod,
22
+ enumerable: true
23
+ }) : target, mod));
24
+
25
+ //#endregion
26
+ let react = require("react");
27
+ react = __toESM(react);
28
+ let __deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
29
+ __deepseek_ai_dsh_client_ui_primitives = __toESM(__deepseek_ai_dsh_client_ui_primitives);
30
+ let __deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
31
+ __deepseek_ai_dsh_client_runtime_client = __toESM(__deepseek_ai_dsh_client_runtime_client);
32
+ let react_jsx_runtime = require("react/jsx-runtime");
33
+ react_jsx_runtime = __toESM(react_jsx_runtime);
34
+
35
+ //#region src/types.ts
36
+ /** Runtime defaults applied when a half receives no config (defensive only). */
37
+ const DEFAULT_MERGE_CONFIG = {
38
+ tools: [],
39
+ groupBy: "adjacent",
40
+ maxGroupSize: 8
41
+ };
42
+
43
+ //#endregion
44
+ //#region src/client/locales.ts
45
+ /** Product copy for the merged tool-call rows. */
46
+ const zh = {
47
+ running: "进行中",
48
+ failed: "失败",
49
+ stopped: "已中断",
50
+ expand: "展开",
51
+ collapse: "折叠",
52
+ more: "+{n}",
53
+ showLess: "收起其余",
54
+ mergedCount: "已合并 {n} 次调用",
55
+ "terminal.signal": "信号 {signal}",
56
+ "terminal.exitCode": "退出码 {code}",
57
+ "terminal.running": "运行中",
58
+ "terminal.failed": "失败",
59
+ "terminal.done": "已完成",
60
+ "terminal.copy": "复制",
61
+ "terminal.copied": "已复制",
62
+ "terminal.noOutput": "无输出",
63
+ "terminal.collapseAria": "收起输出",
64
+ "terminal.collapse": "折叠",
65
+ "terminal.expandAria": "展开其余 {n} 行输出",
66
+ "terminal.expandRest": "… 其余 {n} 行"
67
+ };
68
+ const en = {
69
+ running: "Running",
70
+ failed: "Failed",
71
+ stopped: "Interrupted",
72
+ expand: "Expand",
73
+ collapse: "Collapse",
74
+ more: "+{n}",
75
+ showLess: "Show fewer",
76
+ mergedCount: "{n} calls merged",
77
+ "terminal.signal": "signal {signal}",
78
+ "terminal.exitCode": "exit code {code}",
79
+ "terminal.running": "Running",
80
+ "terminal.failed": "Failed",
81
+ "terminal.done": "Done",
82
+ "terminal.copy": "Copy",
83
+ "terminal.copied": "Copied",
84
+ "terminal.noOutput": "No output",
85
+ "terminal.collapseAria": "Collapse output",
86
+ "terminal.collapse": "Collapse",
87
+ "terminal.expandAria": "Expand the remaining {n} output lines",
88
+ "terminal.expandRest": "… {n} more lines"
89
+ };
90
+ const NS = "merge-tool-calls";
91
+
92
+ //#endregion
93
+ //#region src/client/tool-names.ts
94
+ /**
95
+ * The built-in tool universe this plugin shadows by default, plus the
96
+ * variant-classification vocabulary mirroring ui-tool's tool-call model.
97
+ *
98
+ * The keyed `tool.call.toolview` slot dispatches on the exact wire tool name,
99
+ * so a shadowed registration is needed per name (there is no wildcard). This
100
+ * list names every shipped tool whose row belongs to the generic ToolRow
101
+ * family — a variant title/icon plus read/search/diff/terminal/web card or
102
+ * IN/OUT text — which the merged row reproduces faithfully.
103
+ *
104
+ * Deliberately excluded: tools with custom row cards this plugin does not
105
+ * replicate (`skill`, `cordis_define`, and the live `cordis_run` /
106
+ * `cordis_stop` / `cordis_undefine` rows), and `todo_write` /
107
+ * `ask_user_question` whose summary formatting is tool-specific. They keep
108
+ * their built-in rows unless the user names them in the `tools` config
109
+ * explicitly.
110
+ * @module
111
+ */
112
+ /** Wire tool names merged by default (empty `tools` config = this list). */
113
+ const ALL_TOOL_NAMES = [
114
+ "read",
115
+ "grep",
116
+ "glob",
117
+ "edit",
118
+ "write",
119
+ "bash",
120
+ "pwsh",
121
+ "web_search",
122
+ "web_fetch",
123
+ "run_code",
124
+ "cordis_package_inspect",
125
+ "cordis_runtime_inspect"
126
+ ];
127
+ /** Figma row titles per variant (design literals, not translatable copy). */
128
+ const VARIANT_TITLES = {
129
+ search: "Search",
130
+ read: "Read",
131
+ bash: "Bash",
132
+ write: "Write",
133
+ edit: "Edit",
134
+ code: "Code",
135
+ others: "Tool call"
136
+ };
137
+ /**
138
+ * Known tool name -> row variant (mirrors ui-tool's classification table,
139
+ * minus the run-control verbs: their rows are custom and not shadowed).
140
+ */
141
+ const TOOL_VARIANTS = {
142
+ bash: "bash",
143
+ pwsh: "bash",
144
+ read: "read",
145
+ web_fetch: "read",
146
+ web_search: "search",
147
+ grep: "search",
148
+ glob: "search",
149
+ write: "write",
150
+ edit: "edit",
151
+ run_code: "code",
152
+ cordis_package_inspect: "read",
153
+ cordis_runtime_inspect: "read"
154
+ };
155
+ /** Tool-owned titles refining a generic row variant (mirrors ui-tool). */
156
+ const TOOL_TITLES = {
157
+ cordis_package_inspect: "Inspect",
158
+ cordis_runtime_inspect: "Inspect",
159
+ pwsh: "Pwsh"
160
+ };
161
+ /** Classify a tool name into its row variant (mirrors ui-tool). */
162
+ function classifyTool(toolName) {
163
+ return TOOL_VARIANTS[toolName] ?? "others";
164
+ }
165
+
166
+ //#endregion
167
+ //#region src/client/card-model.ts
168
+ /** True when a settled terminal card reports a failing exit (mirrors ui-tool). */
169
+ function terminalFailed(model) {
170
+ const { exitCode, signal, running } = model.card;
171
+ return running !== true && (exitCode !== void 0 && exitCode !== 0 || signal !== void 0);
172
+ }
173
+ /**
174
+ * Flatten a settled result's content blocks to display text.
175
+ * @param node - settled result node.
176
+ * @returns joined text (may be empty).
177
+ */
178
+ function resultText(node) {
179
+ const parts = [];
180
+ for (const block of node.content) if (block.type === "text") parts.push(block.text);
181
+ else parts.push(JSON.stringify(block));
182
+ if (parts.length === 0 && node.error !== void 0) parts.push(`${node.error.name}: ${node.error.code}`);
183
+ return parts.join("\n");
184
+ }
185
+ function firstLine(text) {
186
+ const nl = text.indexOf("\n");
187
+ return nl === -1 ? text : text.slice(0, nl);
188
+ }
189
+ /** Strip the workspace root from a workspace-rooted absolute path (display only). */
190
+ function relativizeToCwd(text, cwd) {
191
+ if (cwd === void 0 || cwd === "") return text;
192
+ const root = cwd.replace(/[/\\]+$/, "");
193
+ if (text.startsWith(`${root}/`) || text.startsWith(`${root}\\`)) return text.slice(root.length + 1);
194
+ return text;
195
+ }
196
+ function parseArgs(argsRaw) {
197
+ try {
198
+ const parsed = JSON.parse(argsRaw);
199
+ return typeof parsed === "object" && parsed !== null ? parsed : void 0;
200
+ } catch {
201
+ return;
202
+ }
203
+ }
204
+ function pickString(args, keys) {
205
+ for (const key of keys) {
206
+ const value = args[key];
207
+ if (typeof value === "string" && value !== "") return value;
208
+ }
209
+ }
210
+ /** Summary key preference per row variant (args-derived, mirrors ui-tool). */
211
+ const SUMMARY_KEYS = {
212
+ bash: ["description", "command"],
213
+ read: [
214
+ "path",
215
+ "file_path",
216
+ "url"
217
+ ],
218
+ search: [
219
+ "query",
220
+ "pattern",
221
+ "url"
222
+ ],
223
+ write: ["path", "file_path"],
224
+ edit: ["path", "file_path"],
225
+ code: ["description"],
226
+ others: []
227
+ };
228
+ function deriveSummary(variant, argsRaw) {
229
+ const parsed = parseArgs(argsRaw);
230
+ if (parsed === void 0) return firstLine(argsRaw);
231
+ const picked = pickString(parsed, SUMMARY_KEYS[variant]);
232
+ if (picked !== void 0) return firstLine(picked);
233
+ for (const value of Object.values(parsed)) if (typeof value === "string" && value !== "") return firstLine(value);
234
+ return firstLine(argsRaw);
235
+ }
236
+ /** Path keys only — never `url` (web_fetch lands on the read variant). */
237
+ const FILE_PATH_KEYS = ["path", "file_path"];
238
+ /** File-tool variants whose summary may be an openable workspace path. */
239
+ const FILE_PATH_VARIANTS = new Set([
240
+ "read",
241
+ "write",
242
+ "edit"
243
+ ]);
244
+ function deriveFilePath(variant, argsRaw) {
245
+ if (!FILE_PATH_VARIANTS.has(variant)) return void 0;
246
+ const parsed = parseArgs(argsRaw);
247
+ if (parsed === void 0) return void 0;
248
+ return pickString(parsed, FILE_PATH_KEYS)?.split("\n")[0];
249
+ }
250
+ function deriveBody(variant, argsRaw) {
251
+ if (argsRaw === "") return null;
252
+ const parsed = parseArgs(argsRaw);
253
+ if (parsed === void 0) return argsRaw;
254
+ if (variant === "code") {
255
+ const code = parsed.code;
256
+ if (typeof code === "string" && code !== "") return code;
257
+ }
258
+ return JSON.stringify(parsed, null, 2);
259
+ }
260
+ /** Derive the row model for one call of a grouped tool. */
261
+ function callRowModel(toolName, block, cwd) {
262
+ const done = "kind" in block;
263
+ const argsRaw = (done ? block.call?.argsRaw : block.argsRaw) ?? "";
264
+ const state = !done ? "running" : block.error?.code === "interrupted" ? "stopped" : block.isError ? "error" : "ok";
265
+ const variant = classifyTool(toolName);
266
+ const terminal = terminalCardOf(block, cwd);
267
+ const read = readCardOf(block, cwd);
268
+ const diff = diffCardOf(block);
269
+ const search = searchCardOf(block);
270
+ const web = webCardOf(block);
271
+ const base = argsRaw === "" ? block.callId : relativizeToCwd(deriveSummary(variant, argsRaw), cwd);
272
+ const toolTitle = TOOL_TITLES[toolName];
273
+ const argsSummary = variant === "others" && toolName !== "" && toolTitle === void 0 ? `${toolName} · ${base}` : base;
274
+ const filePath = deriveFilePath(variant, argsRaw);
275
+ const output = done ? resultText(block) || null : null;
276
+ const errorSummary = state === "error" && output !== null ? firstLine(output) : null;
277
+ const summary = terminal?.description ?? search?.title ?? argsSummary;
278
+ const body = filePath !== void 0 ? null : deriveBody(variant, argsRaw);
279
+ const finalState = state === "ok" && terminal !== null && terminalFailed(terminal) ? "error" : state;
280
+ const expandable = terminal !== null || diff !== null || read !== null || search !== null || web !== null || body !== null || output !== null;
281
+ return {
282
+ state: finalState,
283
+ variant,
284
+ title: toolTitle ?? VARIANT_TITLES[variant],
285
+ summary,
286
+ body,
287
+ output,
288
+ errorSummary,
289
+ filePath,
290
+ expandable,
291
+ terminal,
292
+ diff,
293
+ read,
294
+ search,
295
+ web
296
+ };
297
+ }
298
+ /** Read-card derivation, or null when this call is not a read card (mirrors ui-tool). */
299
+ function readCardOf(block, cwd) {
300
+ if (!("kind" in block)) return null;
301
+ const result = block.resultView?.card === "read" ? block.resultView : null;
302
+ if (result === null) return null;
303
+ if (!Array.isArray(result.lines)) return null;
304
+ const lines = result.lines.filter((line) => typeof line === "object" && line !== null && typeof line.number === "number" && typeof line.text === "string").map((line) => ({
305
+ number: line.number,
306
+ text: line.text
307
+ }));
308
+ return {
309
+ label: result.title ?? relativizeToCwd(result.path, cwd),
310
+ lines,
311
+ totalLines: typeof result.totalLines === "number" ? result.totalLines : lines.length,
312
+ lang: typeof result.lang === "string" ? result.lang : void 0
313
+ };
314
+ }
315
+ function isValidFiles(files) {
316
+ 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"));
317
+ }
318
+ function flattenContent(content) {
319
+ const text = content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
320
+ return text === "" ? void 0 : text;
321
+ }
322
+ /** Search-card derivation, or null when this call is not a search card (mirrors ui-tool). */
323
+ function searchCardOf(block) {
324
+ if (!("kind" in block)) return null;
325
+ const result = block.resultView?.card === "search" ? block.resultView : null;
326
+ if (result === null) return null;
327
+ const common = {
328
+ truncated: result.truncated,
329
+ total: result.total
330
+ };
331
+ const recovery = result.truncated ? flattenContent(block.content) : void 0;
332
+ if (result.shape === "matches") {
333
+ if (!isValidFiles(result.files)) return null;
334
+ return {
335
+ title: result.title,
336
+ recovery,
337
+ card: {
338
+ kind: "matches",
339
+ files: result.files,
340
+ ...common
341
+ }
342
+ };
343
+ }
344
+ if (result.shape !== "paths") return null;
345
+ if (!Array.isArray(result.paths) || !result.paths.every((path) => typeof path === "string")) return null;
346
+ return {
347
+ title: result.title,
348
+ recovery,
349
+ card: {
350
+ kind: "paths",
351
+ paths: result.paths,
352
+ ...common
353
+ }
354
+ };
355
+ }
356
+ /** Narrow a wire `card:'diff'` view's `diffs` to well-formed hunks (mirrors ui-tool). */
357
+ function narrowDiffs(diffs) {
358
+ if (!Array.isArray(diffs) || diffs.length === 0) return null;
359
+ const out = [];
360
+ for (const hunk of diffs) {
361
+ if (typeof hunk !== "object" || hunk === null) return null;
362
+ const { path, oldText, newText } = hunk;
363
+ if (typeof path !== "string") return null;
364
+ if (oldText !== null && typeof oldText !== "string") return null;
365
+ if (typeof newText !== "string") return null;
366
+ out.push({
367
+ path,
368
+ oldText,
369
+ newText
370
+ });
371
+ }
372
+ return out;
373
+ }
374
+ /** Diff-card derivation, or null when this call is not a diff card (mirrors ui-tool). */
375
+ function diffCardOf(block) {
376
+ if (!("kind" in block)) {
377
+ const call = block.callView?.card === "diff" ? block.callView : null;
378
+ const diffs$1 = call === null ? null : narrowDiffs(call.diffs);
379
+ return diffs$1 === null ? null : { card: { diffs: diffs$1 } };
380
+ }
381
+ const result = block.resultView?.card === "diff" ? block.resultView : null;
382
+ const diffs = result === null ? null : narrowDiffs(result.diffs);
383
+ return diffs === null ? null : { card: { diffs } };
384
+ }
385
+ /**
386
+ * Resolve a terminal view's working directory the way the render-intent
387
+ * contract assigns it: an absolute path is used as-is, a relative one joins
388
+ * under the session workspace, and an omitted one IS the session workspace.
389
+ * @param viewCwd - the cwd the terminal call view carries, if any.
390
+ * @param sessionCwd - the session workspace root, if the caller knows it.
391
+ * @returns the working directory for the prompt label, or undefined.
392
+ */
393
+ function resolveTerminalCwd(viewCwd, sessionCwd) {
394
+ if (viewCwd === void 0 || viewCwd === "") return sessionCwd;
395
+ if (sessionCwd === void 0 || sessionCwd === "") return viewCwd;
396
+ return (0, __deepseek_ai_dsh_client_runtime_client.resolveWorkspacePath)(sessionCwd, viewCwd);
397
+ }
398
+ /** Terminal-card derivation, or null when this call is not a terminal card (mirrors ui-tool). */
399
+ function terminalCardOf(block, sessionCwd) {
400
+ const call = block.callView?.card === "terminal" ? block.callView : null;
401
+ if (!("kind" in block)) return call === null ? null : {
402
+ description: call.description,
403
+ card: {
404
+ command: call.title,
405
+ cwd: resolveTerminalCwd(call.cwd, sessionCwd),
406
+ output: void 0,
407
+ exitCode: void 0,
408
+ signal: void 0,
409
+ running: true
410
+ }
411
+ };
412
+ const result = block.resultView?.card === "terminal" ? block.resultView : null;
413
+ if (result === null) return null;
414
+ return {
415
+ description: call?.description,
416
+ card: {
417
+ command: result.title ?? call?.title ?? "",
418
+ cwd: call === null ? void 0 : resolveTerminalCwd(call.cwd, sessionCwd),
419
+ output: result.output,
420
+ exitCode: result.exitCode,
421
+ signal: result.signal,
422
+ running: false
423
+ }
424
+ };
425
+ }
426
+ /** Web-card derivation, or null when this call is not a web card (mirrors ui-tool). */
427
+ function webCardOf(block) {
428
+ if (!("kind" in block)) return null;
429
+ const result = block.resultView;
430
+ if (result?.card !== "web") return null;
431
+ if (result.kind === "search") return {
432
+ kind: "search",
433
+ answer: result.answer,
434
+ sources: result.sources.map((source) => ({
435
+ url: source.url,
436
+ title: source.title,
437
+ snippet: source.snippet,
438
+ publishedAt: source.publishedAt
439
+ })),
440
+ truncated: result.truncated
441
+ };
442
+ if (result.kind === "fetch") return {
443
+ kind: "fetch",
444
+ url: result.url,
445
+ statusCode: result.statusCode,
446
+ truncated: result.truncated
447
+ };
448
+ return null;
449
+ }
450
+
451
+ //#endregion
452
+ //#region src/client/merge-run.ts
453
+ /** Tool-call node kind registered by ui-conversation's built-in tool definition. */
454
+ const TOOL_CALL_KIND = "tool-call";
455
+ /** Extract a root call block from any chat node, when it is a tool-call node. */
456
+ function toolRootOf(node) {
457
+ if (node.kind !== TOOL_CALL_KIND) return null;
458
+ const root = node.data.root;
459
+ return typeof root === "object" && root !== null ? root : null;
460
+ }
461
+ /** The wire tool name of a call in either lifecycle form (mirrors ui-tool). */
462
+ function callNameOf(block) {
463
+ return "kind" in block ? block.call?.name ?? "" : block.name;
464
+ }
465
+ /** Whether the call's wire name is one of the grouped tools (empty = all). */
466
+ function isGroupedTool(name, tools) {
467
+ return tools.length === 0 || tools.includes(name);
468
+ }
469
+ /** Step identity of a node's location; undefined when the node is not step-bound. */
470
+ function stepIdOf(node) {
471
+ const location = node.location;
472
+ return location.kind === "step" ? `${location.turn.turn}:${location.step.step}` : void 0;
473
+ }
474
+ /**
475
+ * Compute the merged group this call belongs to.
476
+ *
477
+ * 1. Locates this call's node in the chat order; null when it is not a chat
478
+ * tool-call node (e.g. a read dispatched as a subcall).
479
+ * 2. Walks backward/forward to the maximal consecutive run containing it. A
480
+ * call continues the run when it is a grouped tool AND same-tool-same-run:
481
+ * the identical wire name, or a sibling of the same known variant family
482
+ * (grep+glob, bash+pwsh, read+web_fetch…). Unknown names (variant
483
+ * `others`) only merge with themselves, so unrelated tools never share a
484
+ * card (`adjacent`: any consecutive run; `step`: same agent step as this
485
+ * call).
486
+ * 3. Partitions the run into `maxGroupSize`-sized groups; this call is the
487
+ * group's first only when it heads one of those partitions. Truncation
488
+ * therefore never orphans a call: the excess starts its own group.
489
+ *
490
+ * @param order - chat node key order.
491
+ * @param nodes - chat node store.
492
+ * @param myCallId - the call id of the seat asking about itself.
493
+ * @param tools - grouped wire tool names; empty means every tool.
494
+ * @param groupBy - grouping mode.
495
+ * @param maxGroupSize - per-group cap.
496
+ * @returns the group partition, or null when the call is not a chat tool-call node.
497
+ */
498
+ function readRun(order, nodes, myCallId, tools, groupBy, maxGroupSize) {
499
+ const myKey = (0, __deepseek_ai_dsh_client_runtime_client.conversationContextKey)(TOOL_CALL_KIND, myCallId);
500
+ const myIndex = order.indexOf(myKey);
501
+ if (myIndex < 0) return null;
502
+ const myRoot = rootAtNode(order, nodes, myIndex);
503
+ if (myRoot === null) return null;
504
+ const myName = callNameOf(myRoot);
505
+ const myVariant = classifyTool(myName);
506
+ const size = Math.max(1, maxGroupSize);
507
+ const rootAt = (index) => rootAtNode(order, nodes, index);
508
+ const myStep = groupBy === "step" ? stepIdOfNodeAt(order, nodes, myIndex) : void 0;
509
+ const inGroup = (index) => {
510
+ const root = rootAt(index);
511
+ if (root === null || !isGroupedTool(callNameOf(root), tools)) return false;
512
+ const name = callNameOf(root);
513
+ if (name !== myName) {
514
+ if (myVariant === "others" || classifyTool(name) !== myVariant) return false;
515
+ }
516
+ if (groupBy !== "step") return true;
517
+ return myStep !== void 0 && stepIdOfNodeAt(order, nodes, index) === myStep;
518
+ };
519
+ let start = myIndex;
520
+ while (start > 0 && inGroup(start - 1)) start--;
521
+ let end = myIndex;
522
+ while (end < order.length - 1 && inGroup(end + 1)) end++;
523
+ const groupStart = start + Math.floor((myIndex - start) / size) * size;
524
+ const groupEnd = Math.min(groupStart + size, end + 1);
525
+ const blocks = [];
526
+ for (let index = groupStart; index < groupEnd; index++) {
527
+ const root = rootAt(index);
528
+ if (root !== null) blocks.push(root);
529
+ }
530
+ return {
531
+ isFirst: myIndex === groupStart,
532
+ blocks
533
+ };
534
+ }
535
+ function rootAtNode(order, nodes, index) {
536
+ const key = order[index];
537
+ const node = key === void 0 ? void 0 : nodes.get(key);
538
+ return node === void 0 ? null : toolRootOf(node);
539
+ }
540
+ function stepIdOfNodeAt(order, nodes, index) {
541
+ const key = order[index];
542
+ const node = key === void 0 ? void 0 : nodes.get(key);
543
+ return node === void 0 ? void 0 : stepIdOf(node);
544
+ }
545
+
546
+ //#endregion
547
+ //#region src/client/rows.tsx
548
+ /** Chat rows show a capped card; the details panel stays the full-height surface. */
549
+ const CHAT_READ_MAX_LINES = 8;
550
+ const CHAT_SEARCH_MAX_LINES = 8;
551
+ const CHAT_DIFF_MAX_LINES = 8;
552
+ /** Variant leading icons (figma table); all glyphs render at 14 inside the 16px leading box. */
553
+ const VARIANT_ICONS = {
554
+ search: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, { size: 14 }),
555
+ read: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconBrowseOutline16, { size: 14 }),
556
+ bash: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconApiOutline14, { size: 14 }),
557
+ write: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, { size: 14 }),
558
+ edit: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, { size: 14 }),
559
+ code: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconCodeOutline16, { size: 14 }),
560
+ others: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconSparkle16, { size: 14 })
561
+ };
562
+ /** Leading-slot state substitution, mirroring the shipped row. */
563
+ function leadingFor(state, icon) {
564
+ switch (state) {
565
+ case "error": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "error" });
566
+ case "stopped": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "warning" });
567
+ default: return icon;
568
+ }
569
+ }
570
+ /** Visually hidden run-state label (the StateDot and sweep are colour-only). */
571
+ function stateStatus(state, t) {
572
+ switch (state) {
573
+ case "running": return t("running");
574
+ case "error": return t("failed");
575
+ case "stopped": return t("stopped");
576
+ default: return null;
577
+ }
578
+ }
579
+ /** TerminalBlock display copy from the plugin's own dictionary. */
580
+ function terminalLabels(t) {
581
+ return {
582
+ signal: (signal) => t("terminal.signal", { signal }),
583
+ exitCode: (code) => t("terminal.exitCode", { code }),
584
+ running: t("terminal.running"),
585
+ failed: t("terminal.failed"),
586
+ done: t("terminal.done"),
587
+ copy: t("terminal.copy"),
588
+ copied: t("terminal.copied"),
589
+ noOutput: t("terminal.noOutput"),
590
+ collapseAria: t("terminal.collapseAria"),
591
+ collapse: t("terminal.collapse"),
592
+ expandAria: (hidden) => t("terminal.expandAria", { n: hidden }),
593
+ expand: (hidden) => t("terminal.expandRest", { n: hidden })
594
+ };
595
+ }
596
+ /** One call's expanded-body card, mirroring the built-in ToolRow body. */
597
+ function CardBody({ model, t }) {
598
+ if (model.terminal !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.TerminalBlock, {
599
+ ...model.terminal.card,
600
+ maxLines: Infinity,
601
+ labels: terminalLabels(t)
602
+ });
603
+ if (model.diff !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.DiffBlock, {
604
+ ...model.diff.card,
605
+ maxLines: CHAT_DIFF_MAX_LINES
606
+ });
607
+ if (model.read !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.ReadBlock, {
608
+ ...model.read,
609
+ maxLines: CHAT_READ_MAX_LINES
610
+ });
611
+ if (model.search !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.SearchBlock, {
612
+ ...model.search.card,
613
+ maxLines: CHAT_SEARCH_MAX_LINES
614
+ }), model.search.recovery !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
615
+ className: "mtc-recovery",
616
+ children: model.search.recovery
617
+ })] });
618
+ if (model.web !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.WebBlock, { ...model.web });
619
+ const hasBody = model.body !== null;
620
+ const hasOutput = model.output !== null;
621
+ if (!hasBody && !hasOutput) return null;
622
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
623
+ className: "mtc-io-card",
624
+ children: [
625
+ hasBody && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
626
+ className: "mtc-io-section",
627
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
628
+ className: "mtc-io-label",
629
+ children: "IN"
630
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
631
+ className: "mtc-io-text",
632
+ children: model.body
633
+ })]
634
+ }),
635
+ hasBody && hasOutput && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
636
+ className: "mtc-io-divider",
637
+ "aria-hidden": true
638
+ }),
639
+ hasOutput && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
640
+ className: "mtc-io-section",
641
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
642
+ className: "mtc-io-label",
643
+ children: "OUT"
644
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
645
+ className: "mtc-io-text",
646
+ "data-error": model.state === "error" || void 0,
647
+ children: model.output
648
+ })]
649
+ })
650
+ ]
651
+ });
652
+ }
653
+ /** The run's main card: the first call's full row (chrome + expandable card). */
654
+ const RowCard = (0, react.memo)(function RowCard$1({ toolName, block, cwd, openFile, inspect, t, mergedCount }) {
655
+ const model = callRowModel(toolName, block, cwd);
656
+ const [expanded, setExpanded] = (0, react.useState)(false);
657
+ const open = expanded && model.expandable;
658
+ const status = stateStatus(model.state, t);
659
+ const failureLine = model.state === "error" ? model.errorSummary ?? null : null;
660
+ const summaryText = failureLine ?? model.summary;
661
+ const suffix = failureLine === null && mergedCount > 0 ? t("more", { n: String(mergedCount) }) : null;
662
+ const fileLink = model.filePath !== void 0 && openFile !== void 0 && failureLine === null;
663
+ const openFileClick = (event) => {
664
+ event.stopPropagation();
665
+ if (model.filePath !== void 0) openFile(model.filePath);
666
+ };
667
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
668
+ className: "mtc-row",
669
+ "data-state": model.state,
670
+ "data-variant": model.variant,
671
+ children: [status !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
672
+ className: "mtc-visually-hidden",
673
+ children: status
674
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
675
+ rowClassName: "mtc-title-row",
676
+ titleClassName: "mtc-title",
677
+ leadingClassName: "mtc-leading",
678
+ chevronClassName: "mtc-chevron",
679
+ icon: leadingFor(model.state, VARIANT_ICONS[model.variant]),
680
+ title: model.title,
681
+ open,
682
+ expandable: model.expandable,
683
+ expandOnRowClick: true,
684
+ keepContentWhenOpen: true,
685
+ onToggle: () => {
686
+ setExpanded((value) => !value);
687
+ },
688
+ collapsedContent: summaryText !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
689
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
690
+ className: "mtc-sep",
691
+ "aria-hidden": true
692
+ }),
693
+ fileLink ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
694
+ type: "button",
695
+ className: "mtc-summary-link",
696
+ onClick: openFileClick,
697
+ children: summaryText
698
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
699
+ className: "mtc-summary",
700
+ children: summaryText
701
+ }),
702
+ suffix !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
703
+ className: "mtc-summary-suffix",
704
+ children: suffix
705
+ })
706
+ ] }),
707
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
708
+ className: "mtc-card-body",
709
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBody, {
710
+ model,
711
+ t
712
+ }), inspect !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
713
+ type: "button",
714
+ className: "mtc-inspect",
715
+ onClick: inspect,
716
+ children: "Inspect"
717
+ })]
718
+ })
719
+ })]
720
+ });
721
+ });
722
+ /**
723
+ * One compact continuation row: the main row's tail structure ([sep dot][path]).
724
+ * An expandable call toggles the inline content card on click (mirroring the
725
+ * main row's whole-row disclosure); a read/write/edit-family path additionally
726
+ * renders as an open-file link (the sidebar preview) that stops propagation,
727
+ * exactly like the main row's summary link.
728
+ */
729
+ const ChildRow = (0, react.memo)(function ChildRow$1({ toolName, block, cwd, openFile, t }) {
730
+ const model = callRowModel(toolName, block, cwd);
731
+ const [open, setOpen] = (0, react.useState)(false);
732
+ const stateLabel = stateStatus(model.state, t);
733
+ const expandable = model.expandable;
734
+ const toggle = () => {
735
+ setOpen((value) => !value);
736
+ };
737
+ const openFileClick = (event) => {
738
+ event.stopPropagation();
739
+ if (model.filePath !== void 0) openFile(model.filePath);
740
+ };
741
+ const onRowKeyDown = (event) => {
742
+ if (event.key === "Enter" || event.key === " ") {
743
+ event.preventDefault();
744
+ toggle();
745
+ }
746
+ };
747
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
748
+ className: "mtc-child",
749
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
750
+ className: "mtc-child-row",
751
+ "data-open": expandable && open || void 0,
752
+ "data-static": expandable ? void 0 : true,
753
+ role: expandable ? "button" : void 0,
754
+ tabIndex: expandable ? 0 : void 0,
755
+ "aria-expanded": expandable ? open : void 0,
756
+ onClick: expandable ? toggle : void 0,
757
+ onKeyDown: expandable ? onRowKeyDown : void 0,
758
+ children: [
759
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
760
+ className: "mtc-sep",
761
+ "aria-hidden": true
762
+ }),
763
+ model.filePath !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
764
+ type: "button",
765
+ className: "mtc-child-path-link",
766
+ onClick: openFileClick,
767
+ children: model.summary
768
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
769
+ className: "mtc-child-path",
770
+ children: model.summary
771
+ }),
772
+ stateLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
773
+ className: "mtc-child-state",
774
+ "data-error": model.state === "error" || void 0,
775
+ children: stateLabel
776
+ })
777
+ ]
778
+ }), expandable && open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
779
+ className: "mtc-child-body",
780
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBody, {
781
+ model,
782
+ t
783
+ })
784
+ })]
785
+ });
786
+ });
787
+ /**
788
+ * The shadowed toolview: renders the merged run card for the run's first call,
789
+ * nothing for continuation calls, and a plain single row when this call is not
790
+ * a chat tool-call node.
791
+ *
792
+ * Child-row alignment is measured at runtime, not hardcoded: the main row's
793
+ * separator dot sits after a variable-width title ("Read"/"Search"/"Bash"/…),
794
+ * so its column depends on the rendered font. A layout effect measures the
795
+ * dot's offset from the card root (once, plus on reflow via ResizeObserver)
796
+ * and indents the children so their dots and paths land on the main row's
797
+ * columns — no font/title constants to keep in sync.
798
+ */
799
+ function MergedToolRow({ callId, toolName, block, cwd, openFile, inspect, t, cfg, useSession }) {
800
+ const run = useSession((snapshot) => readRun(snapshot.chat.order, snapshot.chat.nodes, callId, cfg.tools, cfg.groupBy, cfg.maxGroupSize));
801
+ const rootRef = (0, react.useRef)(null);
802
+ /** Main separator dot's left offset from the card root; null before first measure. */
803
+ const [sepLeft, setSepLeft] = (0, react.useState)(null);
804
+ (0, react.useLayoutEffect)(() => {
805
+ const root = rootRef.current;
806
+ if (root === null) return;
807
+ const sep = root.querySelector(".mtc-row .mtc-sep");
808
+ if (sep === null) return;
809
+ const measure = () => {
810
+ setSepLeft(Math.round(sep.getBoundingClientRect().left - root.getBoundingClientRect().left));
811
+ };
812
+ measure();
813
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
814
+ observer?.observe(root);
815
+ return () => {
816
+ observer?.disconnect();
817
+ };
818
+ }, [run === null ? null : run.blocks[0]?.callId ?? null]);
819
+ if (run === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
820
+ toolName,
821
+ block,
822
+ cwd,
823
+ openFile,
824
+ inspect,
825
+ t,
826
+ mergedCount: 0
827
+ });
828
+ if (!run.isFirst) return null;
829
+ const hasChildren = run.blocks.length > 1;
830
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
831
+ className: "mtc-root",
832
+ "data-tool": toolName,
833
+ ref: rootRef,
834
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
835
+ toolName,
836
+ block: run.blocks[0] ?? block,
837
+ cwd,
838
+ openFile,
839
+ inspect,
840
+ t,
841
+ mergedCount: run.blocks.length - 1
842
+ }), hasChildren && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
843
+ className: "mtc-children",
844
+ style: sepLeft === null ? void 0 : { ["--mtc-sep-left"]: `${sepLeft}px` },
845
+ children: run.blocks.slice(1).map((child) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChildRow, {
846
+ toolName,
847
+ block: child,
848
+ cwd,
849
+ openFile,
850
+ t
851
+ }, child.callId))
852
+ })]
853
+ });
854
+ }
855
+
856
+ //#endregion
857
+ //#region src/client/styles.ts
858
+ /**
859
+ * One scoped stylesheet injected for the lifetime of the client activation.
860
+ *
861
+ * Two jobs:
862
+ * 1. The empty-seat collapse rule: the shadowed toolview renders `null` for
863
+ * every non-first call of a merged run. The built-in `ToolCallTree` still
864
+ * wraps that null in its `.callRow` div, and the renderer wraps every
865
+ * toolview in an empty `<div data-slot="tool.call.toolview">` (display:
866
+ * contents), so the seat's `.flowItem` is never `:empty` itself and the
867
+ * built-in `.flowItem:empty { display:none }` rule does not fire. This
868
+ * rule extends the same intent ("a renderer may decline its row") to a
869
+ * tool seat whose toolview rendered nothing.
870
+ * 2. The plugin's own `.mtc-*` chrome for the merged card and its child rows.
871
+ *
872
+ * All colors come from the shared `--dsw-*` tokens (never literals).
873
+ */
874
+ const CSS = `
875
+ /* A tool-call seat whose toolview declined to render (merged-run continuation
876
+ calls) must not consume the flow column's gap. The empty toolview slot
877
+ wrapper is the decline signal; the callRow around it always exists. */
878
+ [data-chat-flow-kind="tool-call"]:has([data-slot="tool.call.toolview"]:empty) { display: none; }
879
+
880
+ .mtc-root { display: flex; flex-direction: column; min-width: 0; }
881
+ .mtc-row { display: flex; flex-direction: column; min-width: 0; }
882
+ .mtc-row[data-state='running'] .mtc-title-row { position: relative; overflow: hidden; }
883
+ .mtc-row[data-state='running'] .mtc-title-row::after {
884
+ content: '';
885
+ position: absolute;
886
+ top: 0; bottom: 0; left: 0;
887
+ width: 300px;
888
+ background: linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);
889
+ animation: dsh-mtc-sweep 2.6s ease-out infinite;
890
+ pointer-events: none;
891
+ }
892
+ @keyframes dsh-mtc-sweep {
893
+ 0% { left: -300px; }
894
+ 90%, 100% { left: 100%; }
895
+ }
896
+
897
+ .mtc-title { font-weight: 400; }
898
+ .mtc-sep { flex: none; width: 2px; height: 2px; border-radius: 1px; margin: 0 8px; background: var(--dsw-alias-label-caption); }
899
+ .mtc-summary {
900
+ flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
901
+ font-size: 14px; line-height: 24px; color: var(--dsw-alias-label-tertiary);
902
+ }
903
+ .mtc-summary-link {
904
+ flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
905
+ margin: 0; padding: 0; border: none; background: none; font: inherit; text-align: left;
906
+ font-size: 14px; line-height: 24px; color: var(--dsw-alias-label-secondary);
907
+ text-decoration: underline; text-decoration-color: var(--dsw-alias-label-quaternary);
908
+ text-underline-offset: 3px; cursor: pointer;
909
+ }
910
+ .mtc-summary-link:hover { color: var(--dsw-alias-label-primary); text-decoration-color: currentColor; }
911
+ .mtc-summary-error { color: var(--dsw-alias-state-error-primary); }
912
+ .mtc-summary-suffix { flex: none; margin-left: 4px; white-space: nowrap; font-size: 14px; line-height: 24px; color: var(--dsw-alias-label-tertiary); }
913
+
914
+ .mtc-card-body { margin: 4px 0 4px 4px; min-width: 0; }
915
+ .mtc-recovery { margin: 4px 0 4px 4px; white-space: pre-wrap; overflow-wrap: anywhere; font: var(--dsw-font-xs-13); color: var(--dsw-alias-label-tertiary); }
916
+ /* IN/OUT text card for calls without a card primitive (mirrors ToolRow). */
917
+ .mtc-io-card {
918
+ display: flex; flex-direction: column; min-width: 0;
919
+ border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px;
920
+ background: var(--dsw-alias-bg-base);
921
+ }
922
+ .mtc-io-section { display: flex; gap: 10px; padding: 8px 12px; min-width: 0; }
923
+ .mtc-io-label {
924
+ flex: none; font-size: 11px; line-height: 16px; font-weight: 600;
925
+ color: var(--dsw-alias-label-tertiary);
926
+ }
927
+ .mtc-io-text {
928
+ flex: 1 1 auto; min-width: 0; white-space: pre-wrap; overflow-wrap: anywhere;
929
+ font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary);
930
+ }
931
+ .mtc-io-text[data-error] { color: var(--dsw-alias-state-error-primary); }
932
+ .mtc-io-divider { height: 1px; background: var(--dsw-alias-border-l2); }
933
+ .mtc-inspect {
934
+ display: inline-flex; align-self: flex-start; align-items: center; gap: 4px;
935
+ margin: 4px 0 2px 4px; padding: 2px 8px;
936
+ border: 1px solid var(--dsw-alias-border-l2); border-radius: 999px;
937
+ background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-secondary);
938
+ font-size: 11px; line-height: 16px; cursor: pointer;
939
+ opacity: 0; transition: opacity 100ms ease;
940
+ }
941
+ .mtc-root:hover .mtc-inspect, .mtc-inspect:focus-visible { opacity: 1; }
942
+ .mtc-inspect:hover { background: var(--dsw-alias-interactive-bg-hover-solid); color: var(--dsw-alias-label-primary); }
943
+
944
+ /* Compact child rows: each is the main row's tail structure — [sep dot][path].
945
+ The dot/path columns are set at runtime by the component (it measures the
946
+ main row's sep offset and sets the --mtc-sep-left custom property on the
947
+ children block), so this is only the pre-measure fallback. */
948
+ .mtc-children { display: flex; flex-direction: column; gap: 1px; margin: 2px 0 2px var(--mtc-sep-left, 61px); }
949
+ .mtc-child { display: flex; flex-direction: column; min-width: 0; }
950
+ .mtc-child-row {
951
+ display: flex; align-items: center; gap: 0; min-width: 0; height: 20px;
952
+ margin: 0; padding: 0; border: 0; border-radius: 4px; background: none;
953
+ font: inherit; text-align: left; color: var(--dsw-alias-label-secondary); cursor: pointer;
954
+ }
955
+ .mtc-child-row:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
956
+ .mtc-child-row[data-static] { cursor: default; }
957
+ .mtc-child-row[data-static]:hover { background: none; color: var(--dsw-alias-label-secondary); }
958
+ .mtc-child-row .mtc-sep { margin: 0 8px 0 0; }
959
+ .mtc-child-path {
960
+ flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
961
+ font-size: 14px; line-height: 20px;
962
+ }
963
+ /* Read-family child paths are open-file links (sidebar preview), same affordance
964
+ as the main row's summary link. */
965
+ .mtc-child-path-link {
966
+ flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
967
+ margin: 0; padding: 0; border: none; background: none; font: inherit; text-align: left;
968
+ font-size: 14px; line-height: 20px; color: var(--dsw-alias-label-secondary);
969
+ text-decoration: underline; text-decoration-color: var(--dsw-alias-label-quaternary);
970
+ text-underline-offset: 3px; cursor: pointer;
971
+ }
972
+ .mtc-child-path-link:hover { color: var(--dsw-alias-label-primary); text-decoration-color: currentColor; }
973
+ .mtc-child-state { flex: none; font-size: 11px; line-height: 16px; color: var(--dsw-alias-label-tertiary); }
974
+ .mtc-child-state[data-error] { color: var(--dsw-alias-state-error-primary); }
975
+ /* The expanded child card must not inherit the children block's sep indent:
976
+ pull its left edge back to the main card body's column (root + 4px) so the
977
+ card spans the card width instead of leaving a blank gutter on its left. */
978
+ .mtc-child-body { margin: 2px 0 2px calc(4px - var(--mtc-sep-left, 61px)); min-width: 0; }
979
+
980
+ .mtc-visually-hidden {
981
+ position: absolute; width: 1px; height: 1px; overflow: hidden;
982
+ clip: rect(0 0 0 0); white-space: nowrap;
983
+ }
984
+ `;
985
+ /** Install the stylesheet and return its disposer. */
986
+ function installStyles() {
987
+ const style = document.createElement("style");
988
+ style.setAttribute("data-merge-tool-calls-style", "");
989
+ style.textContent = CSS;
990
+ document.head.appendChild(style);
991
+ return () => {
992
+ style.remove();
993
+ };
994
+ }
995
+
996
+ //#endregion
997
+ //#region src/client/index.ts
998
+ /** Required services: the slot registry (toolview shadowing) and locale. */
999
+ const inject = ["slots", "locale"];
1000
+ /**
1001
+ * Register one shadowed toolview per grouped tool.
1002
+ * @param ctx - client root context.
1003
+ * @param config - row config; defaults apply when the loader passes none.
1004
+ */
1005
+ function apply(ctx, config = {}) {
1006
+ const cfg = {
1007
+ ...DEFAULT_MERGE_CONFIG,
1008
+ ...config
1009
+ };
1010
+ ctx.effect(() => ctx.locale.register(NS, {
1011
+ zh,
1012
+ en
1013
+ }), "merge-tool-calls: dictionaries");
1014
+ ctx.effect(installStyles, "merge-tool-calls: styles");
1015
+ const toolNames = cfg.tools.length === 0 ? ALL_TOOL_NAMES : [...new Set(cfg.tools)];
1016
+ for (const tool of toolNames) ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
1017
+ name: "tool.call.toolview",
1018
+ key: tool,
1019
+ priority: -1,
1020
+ locale: NS,
1021
+ inject: () => ({ cfg })
1022
+ }, MergedToolRow));
1023
+ }
1024
+
1025
+ //#endregion
1026
+ exports.apply = apply;
1027
+ exports.inject = inject;
1028
+ return module.exports; } });
1029
+ //# sourceMappingURL=client.js.map