pi-cc-extensions 1.0.9 → 1.0.11

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.
@@ -7,7 +7,7 @@ import {
7
7
  type Component,
8
8
  } from "@earendil-works/pi-tui";
9
9
  import { config } from "../../config/config.ts";
10
- import { aggregateToolCardBgSlot, toolCardStatus } from "./tool-state.ts";
10
+ import { TOOL_CARD_BG_SLOT, toolCardStatus } from "./tool-state.ts";
11
11
  import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
12
12
  import {
13
13
  ASK_USER_QUESTION_TOOL_NAME,
@@ -24,7 +24,13 @@ import {
24
24
  import { getToolMouseTui } from "../mouse/scroll.ts";
25
25
  import { mcpToolTitle } from "./mcp-title.ts";
26
26
  import { collapseHintText, isToolTuiFullscreen, showMoreHintText } from "./names.ts";
27
- import { stripAnsi, stripBackgroundAnsi, stripLeadingStatusIcon } from "../../utils/ansi-text.ts";
27
+ import {
28
+ contentRowRange,
29
+ imagePlacement,
30
+ stripAnsi,
31
+ stripBackgroundAnsi,
32
+ stripLeadingStatusIcon,
33
+ } from "../../utils/ansi-text.ts";
28
34
  import { walkComponentTree } from "../../utils/component-tree.ts";
29
35
  import {
30
36
  fitToolCallSummary,
@@ -36,6 +42,11 @@ import {
36
42
  } from "./names.ts";
37
43
  import { formatToolDuration, headTruncateToWidth, toolViewportWidth } from "./result.ts";
38
44
  import { isLiveToolCall } from "./tool-state.ts";
45
+ import {
46
+ AGENT_TOOL_NAME,
47
+ isSubagentNotificationComponent,
48
+ subagentNotificationItems,
49
+ } from "./subagent-notification.ts";
39
50
  import {
40
51
  patchRegistry,
41
52
  TOOL_GROUPING_GENERATION_KEY as GENERATION_KEY,
@@ -76,10 +87,15 @@ function toolTitle(tool: any): string {
76
87
  );
77
88
  }
78
89
 
79
- function isGroupable(value: unknown): boolean {
90
+ function isGroupableTool(value: unknown): boolean {
80
91
  return value instanceof ToolExecutionComponent && !NON_GROUPABLE.has(toolName(value));
81
92
  }
82
93
 
94
+ /** 可分组项:工具,或子代理完成通知(只是展示项,不是工具执行)。 */
95
+ function isGroupable(value: unknown): boolean {
96
+ return isGroupableTool(value) || isSubagentNotificationComponent(value);
97
+ }
98
+
83
99
  function isIgnorable(value: unknown): boolean {
84
100
  if (value instanceof Spacer) return true;
85
101
  if (!(value instanceof AssistantMessageComponent)) return false;
@@ -103,7 +119,36 @@ function previousSibling(
103
119
  return undefined;
104
120
  }
105
121
 
106
- type ToolStatus = "pending" | "success" | "error";
122
+ export type ToolStatus = "pending" | "success" | "error";
123
+
124
+ type Fg = (color: string, text: string) => string;
125
+
126
+ function themeFg(theme: any): Fg {
127
+ return (color, text) => theme?.fg?.(color, text) ?? text;
128
+ }
129
+
130
+ /**
131
+ * 分组里的一行显示项:工具一项;子代理完成通知按 agent 各一项。
132
+ * 分组只管计数、树形缩进与展开状态转发,每项自己负责摘要与展开内容。
133
+ */
134
+ export type GroupDisplayItem = {
135
+ /** 组头判定「是否同一种」用的身份:工具原名,或通知类型。 */
136
+ name: string;
137
+ /** 组头工具列表与子行用的显示名。 */
138
+ label: string;
139
+ status: ToolStatus;
140
+ /** 折叠行正文(接在树枝与状态图标之后),width 为剩余可用宽度。 */
141
+ summary(width: number, theme: any): string;
142
+ /**
143
+ * 展开行:首行接在树枝与状态图标之后,其余作为续行。width 为面板内宽(含树枝前缀)。
144
+ * 输入不变时返回同一数组:分组展开缓存按引用比对。
145
+ */
146
+ expandedRows(width: number, theme: any): string[];
147
+ /** 展开态里这些行对应的内部卡片;没有时行归分组本身(点击收起整组)。 */
148
+ hitTarget?: unknown;
149
+ /** 丢掉该项自己缓存的行(换主题后颜色要重算);分组 invalidate 时转发。 */
150
+ invalidate?(): void;
151
+ };
107
152
 
108
153
  function status(tool: any): ToolStatus {
109
154
  return toolCardStatus(tool);
@@ -154,8 +199,22 @@ function requestAnimationRender(patch: Patch): void {
154
199
  getToolMouseTui()?.requestRender?.();
155
200
  }
156
201
 
202
+ /** 丢掉空白行;内联图片锚点行与它占用的空行要留着,否则终端里图片盖住后续行。 */
157
203
  function visibleLines(lines: string[]): string[] {
158
- return lines.filter((line) => stripAnsi(line).trim());
204
+ const keep = new Set<number>();
205
+ for (let index = 0; index < lines.length; index++) {
206
+ const placement = imagePlacement(lines[index]);
207
+ if (!placement) {
208
+ if (stripAnsi(lines[index]).trim()) keep.add(index);
209
+ continue;
210
+ }
211
+ const start = placement.grows === "up" ? index - placement.rows + 1 : index;
212
+ const end = placement.grows === "down" ? index + placement.rows - 1 : index;
213
+ for (let row = Math.max(0, start); row <= Math.min(lines.length - 1, end); row++) {
214
+ keep.add(row);
215
+ }
216
+ }
217
+ return lines.filter((_line, index) => keep.has(index));
159
218
  }
160
219
 
161
220
  function stripLeadingSpaces(line: string, count: number): string {
@@ -275,29 +334,85 @@ function shellResultTail(
275
334
  return duration ? ` ${fg("dim", `· ${duration}`)}` : "";
276
335
  }
277
336
 
278
- /** 分组折叠态的一行子工具:`├/└` + 状态图标 + 与组内同一套摘要。 */
279
- function collapsedGroupToolLine(
280
- tool: any,
337
+ /** 子工具折叠行正文:与组内同一套摘要。 */
338
+ function toolSummaryText(tool: any, toolStatus: ToolStatus, width: number, fg: Fg): string {
339
+ const summary = askUserResultSummary(tool) ?? codemodeResultSummary(tool) ?? toolSummary(tool);
340
+ const detail = fg("dim", summary.detail);
341
+ const result = agentResultTail(tool, fg) + shellResultTail(tool, fg, toolStatus);
342
+ const mainWidth = Math.max(0, width - visibleWidth(detail) - visibleWidth(result));
343
+ return `${renderToolSummary(summary, mainWidth, fg)}${detail}${result}`;
344
+ }
345
+
346
+ /**
347
+ * Agent 的折叠行尾巴:`-> Running in background · ID: xxx`。参考 ask_user_question 的
348
+ * 问题 -> 回答写法:跑后台的结果状态放在 payload 后,比单独的 ↳ 行省一行。
349
+ */
350
+ function agentResultTail(tool: any, fg: Fg): string {
351
+ if (toolName(tool).toLowerCase() !== AGENT_TOOL_NAME.toLowerCase() || !tool?.result) return "";
352
+ const details = tool.result.details;
353
+ if (details?.status !== "background") return "";
354
+ const agentId = details.agentId ? ` ID: ${details.agentId}` : "";
355
+ return ` ${fg("dim", "\u2192")} ${fg("dim", `Running in background${agentId}`)}`;
356
+ }
357
+
358
+ /** 原始 paint → 去掉状态图标与外层缩进后的展开行;paint 引用不变就复用。 */
359
+ const expandedToolRowsCache = new WeakMap<string[], string[]>();
360
+
361
+ function expandedToolRows(tool: any, width: number): string[] {
362
+ const paint = tool.render?.(width);
363
+ if (!Array.isArray(paint)) return [toolSummary(tool).main];
364
+ const cached = expandedToolRowsCache.get(paint);
365
+ if (cached) return cached;
366
+ const rendered = visibleLines(paint);
367
+ if (rendered.length) {
368
+ rendered[0] = stripLeadingStatusIcon(rendered[0])
369
+ .replace(/^ +/, "")
370
+ .replace(/^((?:\x1b\[[0-?]*[ -/]*[@-~])*) +/, "$1");
371
+ }
372
+ const rows = rendered.length
373
+ ? // 续行只剥外层 Box 的 1 格 left pad,保留 Input/Output 相对缩进
374
+ rendered.map((line, index) => (index === 0 ? line : stripLeadingSpaces(line, 1)))
375
+ : [toolSummary(tool).main];
376
+ expandedToolRowsCache.set(paint, rows);
377
+ return rows;
378
+ }
379
+
380
+ function toolItem(tool: any): GroupDisplayItem {
381
+ const toolStatus = status(tool);
382
+ return {
383
+ name: toolName(tool),
384
+ label: toolTitle(tool),
385
+ status: toolStatus,
386
+ summary: (width, theme) => toolSummaryText(tool, toolStatus, width, themeFg(theme)),
387
+ expandedRows: (width) => expandedToolRows(tool, width),
388
+ hitTarget: tool,
389
+ };
390
+ }
391
+
392
+ function groupItems(children: readonly unknown[]): GroupDisplayItem[] {
393
+ return children.flatMap((child) => subagentNotificationItems(child) ?? [toolItem(child)]);
394
+ }
395
+
396
+ /** 分组折叠态的一行:`├/└` + 状态图标 + 该项摘要。 */
397
+ function collapsedGroupLine(
398
+ item: GroupDisplayItem,
281
399
  last: boolean,
282
- fg: (color: string, text: string) => string,
400
+ theme: any,
283
401
  width: number,
284
402
  ): string {
285
- const toolStatus = status(tool);
286
- const color = toolStatus === "pending" ? "dim" : toolStatus;
287
- const summary = askUserResultSummary(tool) ?? codemodeResultSummary(tool) ?? toolSummary(tool);
288
- const prefix = ` ${fg("dim", last ? "└" : "├")} ${fg(color, statusIcon(toolStatus))} `;
289
- const detail = fg("dim", summary.detail);
290
- const result = shellResultTail(tool, fg, toolStatus);
403
+ const fg = themeFg(theme);
404
+ const color = item.status === "pending" ? "dim" : item.status;
405
+ const prefix = ` ${fg("dim", last ? "└" : "├")} ${fg(color, statusIcon(item.status))} `;
291
406
  const rowWidth = toolViewportWidth(width);
292
- const mainWidth = Math.max(
293
- 0,
294
- rowWidth - visibleWidth(prefix) - visibleWidth(detail) - visibleWidth(result),
295
- );
296
- return truncateToWidth(
297
- `${prefix}${renderToolSummary(summary, mainWidth, fg)}${detail}${result}`,
298
- rowWidth,
299
- "",
300
- );
407
+ const room = Math.max(0, rowWidth - visibleWidth(prefix));
408
+ return truncateToWidth(`${prefix}${item.summary(room, theme)}`, rowWidth, "");
409
+ }
410
+
411
+ /** 末项结尾的尾随空行不计入卡高(内联图片的占位行除外,见 contentRowRange)。 */
412
+ function panelRows(rows: string[], last: boolean): string[] {
413
+ if (!last) return rows;
414
+ const end = Math.max(1, contentRowRange(rows).last + 1);
415
+ return end >= rows.length ? rows : rows.slice(0, end);
301
416
  }
302
417
 
303
418
  type GroupHeaderCounts = { pending: number; success: number; error: number };
@@ -326,10 +441,9 @@ function groupOverallStatus(counts: GroupHeaderCounts): ToolStatus {
326
441
  * 组头的工具列表:名称用与单组标题、子行同一套人性化写法,次数一律带上(各跑一次也报 ×1)。
327
442
  * 同名(同显示标题)的合并成一项,避免 `MCP call×1, MCP call×1` 这种重复。
328
443
  */
329
- function toolNameList(tools: any[]): string {
444
+ function toolNameList(items: readonly GroupDisplayItem[]): string {
330
445
  const counts = new Map<string, number>();
331
- for (const tool of tools) {
332
- const label = toolTitle(tool);
446
+ for (const { label } of items) {
333
447
  counts.set(label, (counts.get(label) ?? 0) + 1);
334
448
  }
335
449
  return [...counts].map(([label, count]) => `${label}×${count}`).join(", ");
@@ -425,17 +539,18 @@ export class ToolGroupComponent extends Container {
425
539
  childAtRow(localRow: number, width: number): { component: any; row: number } | null {
426
540
  if (!this._expanded || localRow < 2) return null;
427
541
  let offset = 2;
428
- for (const tool of this.children) {
429
- let lines: string[] = [];
542
+ const items = groupItems(this.children);
543
+ for (let index = 0; index < items.length; index++) {
544
+ const item = items[index];
545
+ let lineCount = 1;
430
546
  try {
431
- const rendered = tool.render?.(Math.max(1, width - 2));
432
- if (Array.isArray(rendered)) lines = visibleLines(rendered.map((line) => String(line)));
547
+ const rows = item.expandedRows(Math.max(1, width - 2), this.patch.theme);
548
+ lineCount = Math.max(1, panelRows(rows, index === items.length - 1).length);
433
549
  } catch {
434
- lines = [];
550
+ lineCount = 1;
435
551
  }
436
- const lineCount = Math.max(1, lines.length);
437
552
  if (localRow < offset + lineCount) {
438
- return { component: tool, row: localRow - offset };
553
+ return item.hitTarget ? { component: item.hitTarget, row: localRow - offset } : null;
439
554
  }
440
555
  offset += lineCount;
441
556
  }
@@ -445,6 +560,7 @@ export class ToolGroupComponent extends Container {
445
560
  invalidate(): void {
446
561
  this.clearPaintCache();
447
562
  for (const tool of this.children) tool.invalidate?.();
563
+ for (const item of groupItems(this.children)) item.invalidate?.();
448
564
  }
449
565
 
450
566
  private settledCacheHit(width: number): string[] | undefined {
@@ -466,7 +582,7 @@ export class ToolGroupComponent extends Container {
466
582
  cache.children[i] !== tool ||
467
583
  cache.args[i] !== tool?.args ||
468
584
  cache.results[i] !== tool?.result ||
469
- status(tool) === "pending"
585
+ (!isSubagentNotificationComponent(tool) && status(tool) === "pending")
470
586
  ) {
471
587
  return;
472
588
  }
@@ -491,12 +607,15 @@ export class ToolGroupComponent extends Container {
491
607
  const cached = this.settledCacheHit(width);
492
608
  if (cached) return cached;
493
609
  const theme = this.patch.theme;
494
- const fg = (color: string, text: string) => theme?.fg?.(color, text) ?? text;
610
+ const fg = themeFg(theme);
611
+ const items = groupItems(this.children);
495
612
  const counts = { pending: 0, success: 0, error: 0 };
496
- for (const tool of this.children) counts[status(tool)]++;
613
+ // 组头按行计数:通知的每个 agent 算一行(与工具各一行同一口径,避免
614
+ // 「5 done 其实是 4 工具 + 1 通知行」这类误导)。
615
+ for (const item of items) counts[item.status]++;
497
616
  const countText = groupCountText(counts, fg);
498
- const names = new Set(this.children.map(toolName));
499
- const label = names.size === 1 ? toolTitle(this.children[0]) : "Multiple Tools";
617
+ const names = new Set(items.map((item) => item.name));
618
+ const label = names.size === 1 ? items[0].label : "Multiple Tools";
500
619
  const overall = groupOverallStatus(counts);
501
620
  if ((this.children as any[]).some(isAnimatingTool)) scheduleGroupAnimation(this.patch);
502
621
  const overallColor = overall === "pending" ? "dim" : overall;
@@ -512,76 +631,58 @@ export class ToolGroupComponent extends Container {
512
631
  // 连一个工具名都放不下就不放列表:退化成 `· …` 只会把行撑爆。
513
632
  const nameList =
514
633
  names.size > 1 && listRoom >= 4
515
- ? ` ${fg("dim", `· ${headTruncateToWidth(toolNameList(this.children), listRoom)}`)}`
634
+ ? ` ${fg("dim", `· ${headTruncateToWidth(toolNameList(items), listRoom)}`)}`
516
635
  : "";
517
636
  const lines = ["", truncateToWidth(`${head}${nameList}${tail}`, rowWidth, "")];
518
- const total = this.children.length;
519
- const childPaints = this._expanded
520
- ? (this.children as any[]).map((tool) => tool.render?.(Math.max(1, width - 2)))
521
- : undefined;
522
- if (this._expanded && childPaints) {
523
- const expandedHit = this.expandedPaintCache;
524
- if (
525
- expandedHit &&
526
- expandedHit.width === width &&
527
- expandedHit.hover === this.hintHovered &&
528
- expandedHit.theme === this.patch.theme &&
529
- expandedHit.fullscreen === isToolTuiFullscreen() &&
530
- expandedHit.bgSlot === aggregateToolCardBgSlot(this.children) &&
531
- expandedHit.paints.length === childPaints.length &&
532
- expandedHit.paints.every((paint, index) => paint === childPaints[index])
533
- ) {
534
- return expandedHit.lines;
637
+ const total = items.length;
638
+ if (!this._expanded) {
639
+ for (let index = 0; index < total; index++) {
640
+ lines.push(collapsedGroupLine(items[index], index === total - 1, theme, width));
535
641
  }
642
+ if (counts.pending === 0) this.storeSettledCache(width, lines);
643
+ return lines;
644
+ }
645
+ // 有一个失败整卡用失败底色;没有失败但还有进行中用进行中底色。
646
+ const backgroundSlot = TOOL_CARD_BG_SLOT[overall];
647
+ const itemPaints = items.map((item) => item.expandedRows(Math.max(1, width - 2), theme));
648
+ const expandedHit = this.expandedPaintCache;
649
+ if (
650
+ expandedHit &&
651
+ expandedHit.width === width &&
652
+ expandedHit.hover === this.hintHovered &&
653
+ expandedHit.theme === this.patch.theme &&
654
+ expandedHit.fullscreen === isToolTuiFullscreen() &&
655
+ expandedHit.bgSlot === backgroundSlot &&
656
+ expandedHit.paints.length === itemPaints.length &&
657
+ expandedHit.paints.every((paint, index) => paint === itemPaints[index])
658
+ ) {
659
+ return expandedHit.lines;
536
660
  }
537
- const expandedLines: string[] = [];
538
661
  for (let index = 0; index < total; index++) {
539
- const tool = this.children[index];
540
- const toolStatus = status(tool);
541
- const color = toolStatus === "pending" ? "dim" : toolStatus;
542
- const branch = index === total - 1 ? "└" : "├";
543
- const continuation = index === total - 1 ? " " : "│ ";
544
- if (!this._expanded) {
545
- lines.push(collapsedGroupToolLine(tool, index === total - 1, fg, width));
546
- continue;
547
- }
548
- const rendered = visibleLines(Array.isArray(childPaints?.[index]) ? childPaints[index] : []);
549
- if (rendered.length) {
550
- rendered[0] = stripLeadingStatusIcon(rendered[0])
551
- .replace(/^ +/, "")
552
- .replace(/^((?:\x1b\[[0-?]*[ -/]*[@-~])*) +/, "$1");
553
- }
554
- const childLines = rendered.length ? rendered : [toolSummary(tool).main];
555
- for (let lineIndex = 0; lineIndex < childLines.length; lineIndex++) {
556
- const content =
557
- // 续行只剥外层 Box 的 1 格 left pad,保留 Input/Output 相对缩进
558
- lineIndex === 0 ? childLines[lineIndex] : stripLeadingSpaces(childLines[lineIndex], 1);
662
+ const item = items[index];
663
+ const color = item.status === "pending" ? "dim" : item.status;
664
+ const last = index === total - 1;
665
+ const branch = last ? "└" : "├";
666
+ const continuation = last ? " " : "│ ";
667
+ const rows = panelRows(itemPaints[index], last);
668
+ for (let lineIndex = 0; lineIndex < rows.length; lineIndex++) {
559
669
  const prefix =
560
670
  lineIndex === 0
561
- ? `${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} `
671
+ ? `${fg("dim", branch)} ${fg(color, statusIcon(item.status))} `
562
672
  : fg("dim", continuation);
563
- expandedLines.push(prefix + content);
564
- }
565
- }
566
- if (this._expanded) {
567
- // 有一个失败整卡用失败底色;没有失败但还有进行中用进行中底色。
568
- const backgroundSlot = aggregateToolCardBgSlot(this.children);
569
- for (const line of expandedLines) {
570
- lines.push(paddedBackgroundRow(theme, backgroundSlot, line, width));
673
+ lines.push(paddedBackgroundRow(theme, backgroundSlot, prefix + rows[lineIndex], width));
571
674
  }
572
- lines.push(paddedBackgroundRow(theme, backgroundSlot, "", width));
573
- this.expandedPaintCache = {
574
- width,
575
- hover: this.hintHovered,
576
- theme: this.patch.theme,
577
- fullscreen: isToolTuiFullscreen(),
578
- bgSlot: backgroundSlot,
579
- paints: childPaints ?? [],
580
- lines,
581
- };
582
- } else if (counts.pending === 0) {
583
- this.storeSettledCache(width, lines);
584
675
  }
676
+ lines.push(paddedBackgroundRow(theme, backgroundSlot, "", width));
677
+ this.expandedPaintCache = {
678
+ width,
679
+ hover: this.hintHovered,
680
+ theme: this.patch.theme,
681
+ fullscreen: isToolTuiFullscreen(),
682
+ bgSlot: backgroundSlot,
683
+ paints: itemPaints,
684
+ lines,
685
+ };
585
686
  return lines;
586
687
  }
587
688
  }
@@ -605,8 +706,9 @@ function ungroup(patch: Patch): void {
605
706
  }
606
707
  }
607
708
 
709
+ /** 至少两项展示内容就保留分组:一个通知组件也可能包含多个 agent。 */
608
710
  function normalizeGroup(patch: Patch, group: ToolGroupComponent): void {
609
- if (group.children.length > 1) return;
711
+ if (groupItems(group.children).length > 1) return;
610
712
  const parent = (group as any)[PARENT_KEY];
611
713
  const index = parent?.children?.indexOf(group) ?? -1;
612
714
  const tools = group.releaseTools();
@@ -615,12 +717,8 @@ function normalizeGroup(patch: Patch, group: ToolGroupComponent): void {
615
717
  for (const tool of tools) delete tool[PARENT_KEY];
616
718
  return;
617
719
  }
618
- if (tools.length === 1) {
619
- tools[0][PARENT_KEY] = parent;
620
- parent.children.splice(index, 1, tools[0]);
621
- } else {
622
- parent.children.splice(index, 1);
623
- }
720
+ for (const tool of tools) tool[PARENT_KEY] = parent;
721
+ parent.children.splice(index, 1, ...tools);
624
722
  }
625
723
 
626
724
  function maybeGroup(patch: Patch, parent: any, component: any): void {
@@ -636,19 +734,25 @@ function maybeGroup(patch: Patch, parent: any, component: any): void {
636
734
  if (!Array.isArray(children)) return;
637
735
  const index = children.indexOf(component);
638
736
  const prior = previousSibling(children, index - 1);
639
- if (!prior) return;
640
- if (prior.child instanceof ToolGroupComponent && (prior.child as any).patch === patch) {
737
+ if (prior?.child instanceof ToolGroupComponent && (prior.child as any).patch === patch) {
641
738
  children.splice(index, 1);
642
739
  prior.child.addTool(component);
643
740
  return;
644
741
  }
645
- if (!isGroupable(prior.child) || prior.child[GENERATION_KEY] !== patch.generation) return;
742
+ const members = [component];
743
+ let firstIndex = index;
744
+ if (prior && isGroupable(prior.child) && prior.child[GENERATION_KEY] === patch.generation) {
745
+ members.unshift(prior.child);
746
+ firstIndex = prior.index;
747
+ }
748
+ // 按展示项而非消息数决定:单条批量通知也成组,原消息仍完整地留在组内。
749
+ if (groupItems(members).length < 2) return;
646
750
  const group = new ToolGroupComponent(patch);
647
- group.addTool(prior.child);
648
- group.addTool(component);
751
+ for (const member of members) group.addTool(member);
649
752
  (group as any)[PARENT_KEY] = parent;
650
- children[prior.index] = group;
651
- children.splice(index, 1);
753
+ // 两个相邻组件合并时移走后者;夹在中间的空行留在分组之后。
754
+ if (firstIndex !== index) children.splice(index, 1);
755
+ children[firstIndex] = group;
652
756
  }
653
757
 
654
758
  /** /reload 不会重新 addChild;扫描当前 mounted roots,把已有工具重新送入同一分组规则。 */
@@ -23,6 +23,9 @@ const TOOL_LABEL_OVERRIDES: Readonly<Record<string, string>> = {
23
23
  // MCP 入口两个没有具体工具名,跟普通工具一样人性化;缩写固定大写
24
24
  mcp: "MCP",
25
25
  mcpscript: "MCP Script",
26
+ // pi-subagents 的 Agent 工具标题写 Subagent,与结束通知的 Subagent Finished 叫法一致。
27
+ agent: "Subagent",
28
+ agents: "Subagents",
26
29
  };
27
30
 
28
31
  /**
@@ -264,19 +267,23 @@ export function toolCallSummary(
264
267
  if (variant === "default" && AGENT_FAMILY_TOOL_NAMES.has(toolName) && args.agent_id) {
265
268
  return { main: title, detail: "", payload: clip(args.agent_id) };
266
269
  }
267
- if (variant === "grouping" && (name === "agent" || name === "agents")) {
270
+ if (name === "agent" || name === "agents") {
268
271
  const displayName = args.subagent_type ?? args.agent_type ?? args.agent;
272
+ const description =
273
+ typeof args.description === "string" ? oneLine(args.description, clipLimit()) : "";
269
274
  if (typeof displayName === "string" && displayName) {
270
- return { main: title, detail: "", payload: displayName };
275
+ // 类型进标题,`·` 分隔后描述作灰色载荷:单卡与组内行同一写法。
276
+ return {
277
+ main: `${title} ${displayName}`,
278
+ detail: "",
279
+ payload: description ? `\u00b7 ${description}` : undefined,
280
+ };
271
281
  }
272
282
  return value(name === "agent" ? "launch agent" : "launch agents", "description", "prompt");
273
283
  }
274
284
  if (variant === "grouping" && (name === "get_subagent_result" || name === "steer_subagent")) {
275
285
  return value(name === "get_subagent_result" ? "agent result" : "steer agent", "agent_id");
276
286
  }
277
- if (variant === "default" && name === "agents") {
278
- return value("launch agents", "description", "prompt");
279
- }
280
287
  if (name === "skill") return value("run skill", "name");
281
288
  if (name === "enterplanmode" || name === "enter_plan_mode") {
282
289
  return { main: title, detail: "", payload: "enable read-only planning" };