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.
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/extensions/config/config.ts +1 -0
- package/extensions/config/panel.ts +3 -6
- package/extensions/feature/context.ts +22 -5
- package/extensions/renderer/compact-mode.ts +44 -14
- package/extensions/renderer/default-mode.ts +1 -5
- package/extensions/renderer/index.ts +20 -5
- package/extensions/renderer/markdown-enhance.ts +4 -1
- package/extensions/renderer/mouse/interaction.ts +30 -18
- package/extensions/renderer/mouse/layout.ts +12 -3
- package/extensions/renderer/tool/ccstyle-tool.ts +109 -0
- package/extensions/renderer/tool/grouping.ts +217 -113
- package/extensions/renderer/tool/names.ts +12 -5
- package/extensions/renderer/tool/subagent-notification.ts +556 -0
- package/extensions/utils/ansi-text.ts +50 -0
- package/extensions/utils/format.ts +11 -0
- package/extensions/utils/patch-keys.ts +4 -0
- package/package.json +1 -1
|
@@ -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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
280
|
-
tool
|
|
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
|
-
|
|
400
|
+
theme: any,
|
|
283
401
|
width: number,
|
|
284
402
|
): string {
|
|
285
|
-
const
|
|
286
|
-
const color =
|
|
287
|
-
const
|
|
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
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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(
|
|
444
|
+
function toolNameList(items: readonly GroupDisplayItem[]): string {
|
|
330
445
|
const counts = new Map<string, number>();
|
|
331
|
-
for (const
|
|
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
|
-
|
|
429
|
-
|
|
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
|
|
432
|
-
|
|
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
|
-
|
|
550
|
+
lineCount = 1;
|
|
435
551
|
}
|
|
436
|
-
const lineCount = Math.max(1, lines.length);
|
|
437
552
|
if (localRow < offset + lineCount) {
|
|
438
|
-
return { component:
|
|
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 = (
|
|
610
|
+
const fg = themeFg(theme);
|
|
611
|
+
const items = groupItems(this.children);
|
|
495
612
|
const counts = { pending: 0, success: 0, error: 0 };
|
|
496
|
-
|
|
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(
|
|
499
|
-
const label = names.size === 1 ?
|
|
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(
|
|
634
|
+
? ` ${fg("dim", `· ${headTruncateToWidth(toolNameList(items), listRoom)}`)}`
|
|
516
635
|
: "";
|
|
517
636
|
const lines = ["", truncateToWidth(`${head}${nameList}${tail}`, rowWidth, "")];
|
|
518
|
-
const total =
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
|
540
|
-
const
|
|
541
|
-
const
|
|
542
|
-
const branch =
|
|
543
|
-
const continuation =
|
|
544
|
-
|
|
545
|
-
|
|
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(
|
|
671
|
+
? `${fg("dim", branch)} ${fg(color, statusIcon(item.status))} `
|
|
562
672
|
: fg("dim", continuation);
|
|
563
|
-
|
|
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
|
-
|
|
619
|
-
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
648
|
-
group.addTool(component);
|
|
751
|
+
for (const member of members) group.addTool(member);
|
|
649
752
|
(group as any)[PARENT_KEY] = parent;
|
|
650
|
-
|
|
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 (
|
|
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
|
-
|
|
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" };
|