pi-cc-extensions 1.0.8 → 1.0.10
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 +5 -7
- package/README.md +5 -7
- package/extensions/config/config.ts +1 -0
- package/extensions/config/panel.ts +14 -8
- package/extensions/feature/shell/footer.ts +28 -6
- package/extensions/feature/shell/prompt-editor.ts +33 -12
- package/extensions/renderer/compact-mode.ts +16 -3
- package/extensions/renderer/default-mode.ts +1 -5
- package/extensions/renderer/index.ts +16 -2
- 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 +196 -111
- package/extensions/renderer/tool/names.ts +12 -5
- package/extensions/renderer/tool/subagent-notification.ts +556 -0
- package/extensions/utils/format.ts +11 -0
- package/extensions/utils/patch-keys.ts +4 -0
- package/package.json +1 -1
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
expandedBodyComponent,
|
|
31
31
|
formatToolDuration,
|
|
32
32
|
insetComponent,
|
|
33
|
+
renderCollapsedToolResult,
|
|
33
34
|
renderCollapsedToolResultToWidth,
|
|
34
35
|
textFromResult,
|
|
35
36
|
toolViewportWidth,
|
|
@@ -46,6 +47,12 @@ import {
|
|
|
46
47
|
toolIconColor,
|
|
47
48
|
} from "./tool-state.ts";
|
|
48
49
|
import { showMoreHintText } from "./names.ts";
|
|
50
|
+
import { oneLine } from "../../utils/format.ts";
|
|
51
|
+
import {
|
|
52
|
+
isFailedSubagentStatus,
|
|
53
|
+
measuredStats,
|
|
54
|
+
agentStatusLabel,
|
|
55
|
+
} from "./subagent-notification.ts";
|
|
49
56
|
|
|
50
57
|
/** 成功勾:亮绿 truecolor(与 message-display 一致)。 */
|
|
51
58
|
const BRIGHT_GREEN = "[38;2;80;220;100m";
|
|
@@ -82,6 +89,8 @@ function renderDefault(tool: any, slot: "renderCall" | "renderResult", args: any
|
|
|
82
89
|
return new Text(fallback, 0, 0);
|
|
83
90
|
}
|
|
84
91
|
|
|
92
|
+
export const AGENT_TOOL_NAME = "Agent";
|
|
93
|
+
|
|
85
94
|
type ParsedTask = { id: string; status: string; subject: string };
|
|
86
95
|
|
|
87
96
|
function parseTaskList(text: string): ParsedTask[] {
|
|
@@ -185,6 +194,92 @@ function shellDurationText(toolName: string, context: any, isPartial: boolean):
|
|
|
185
194
|
return elapsed === undefined ? "" : formatToolDuration(elapsed);
|
|
186
195
|
}
|
|
187
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Agent 的 renderResult:与 pi-subagents 保持同样的分支,但收敛成 ccstyle 的 ↳ 行。
|
|
199
|
+
* running/partial 转交原渲染器(只有它有 spinner frame 和 activity 文案);
|
|
200
|
+
* background/completed/stopped/error 画单行摘要,展开态结果正文交给通用展开视图。
|
|
201
|
+
*/
|
|
202
|
+
function renderAgentResult(
|
|
203
|
+
originalTool: any,
|
|
204
|
+
result: any,
|
|
205
|
+
options: any,
|
|
206
|
+
theme: any,
|
|
207
|
+
context: any,
|
|
208
|
+
expanded: boolean,
|
|
209
|
+
isError: boolean,
|
|
210
|
+
): any | undefined {
|
|
211
|
+
const details = result?.details;
|
|
212
|
+
const text = textFromResult(result);
|
|
213
|
+
// 没拿到子代理状态(pi-subagents 原始路径的兜底)就沿用通用渲染。
|
|
214
|
+
if (!details?.status) return undefined;
|
|
215
|
+
|
|
216
|
+
const isPartial = Boolean(options?.isPartial);
|
|
217
|
+
const status = String(details.status);
|
|
218
|
+
|
|
219
|
+
// running:pi-subagents 的 spinner/activity 组件才能实时动,原样转交。
|
|
220
|
+
if (isPartial || status === "running") {
|
|
221
|
+
return renderDefault(originalTool, "renderResult", [result, options, theme, context], text);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const stats = measuredStats(details).join(" · ");
|
|
225
|
+
|
|
226
|
+
const agentId = details.agentId ? `ID: ${details.agentId}` : "";
|
|
227
|
+
const tail = (body: string) => (stats ? `${body} · ${stats}` : body);
|
|
228
|
+
|
|
229
|
+
if (status === "background") {
|
|
230
|
+
if (!expanded) {
|
|
231
|
+
const body = tail(`Running in background${agentId ? ` · ${agentId}` : ""}`);
|
|
232
|
+
return new Text(theme.fg("muted", renderCollapsedToolResult(body, "")), 0, 0);
|
|
233
|
+
}
|
|
234
|
+
const view = renderExpandedToolResult(
|
|
235
|
+
`Running in background${agentId ? ` · ${agentId}` : ""}`,
|
|
236
|
+
theme,
|
|
237
|
+
false,
|
|
238
|
+
context?.lastComponent,
|
|
239
|
+
context?.args,
|
|
240
|
+
context,
|
|
241
|
+
true,
|
|
242
|
+
);
|
|
243
|
+
return view;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (status === "completed" || status === "steered") {
|
|
247
|
+
if (!expanded) {
|
|
248
|
+
const label = status === "steered" ? "Wrapped up (turn limit)" : "Done";
|
|
249
|
+
const body = [label, stats].filter(Boolean).join(" · ");
|
|
250
|
+
return new Text(theme.fg("muted", renderCollapsedToolResult(body, "")), 0, 0);
|
|
251
|
+
}
|
|
252
|
+
const view = renderExpandedToolResult(
|
|
253
|
+
text || "Done",
|
|
254
|
+
theme,
|
|
255
|
+
Boolean(isError),
|
|
256
|
+
context?.lastComponent,
|
|
257
|
+
context?.args,
|
|
258
|
+
context,
|
|
259
|
+
true,
|
|
260
|
+
);
|
|
261
|
+
return view;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (status === "stopped") {
|
|
265
|
+
const body = tail("Stopped");
|
|
266
|
+
return new Text(theme.fg("muted", renderCollapsedToolResult(body, "")), 0, 0);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// error / aborted:首行错误文案 + 统计,与通用失败行口径一致。
|
|
270
|
+
const label = agentStatusLabel(status);
|
|
271
|
+
const firstLine = text.split("\n").find((line) => line.trim()) ?? label;
|
|
272
|
+
const body = tail(oneLine(firstLine, 72));
|
|
273
|
+
return new Text(
|
|
274
|
+
theme.fg(
|
|
275
|
+
isFailedSubagentStatus(status) ? "error" : "muted",
|
|
276
|
+
renderCollapsedToolResult(body, ""),
|
|
277
|
+
),
|
|
278
|
+
0,
|
|
279
|
+
0,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
188
283
|
/** 用 ccstyle call/result 包装任意工具定义。 */
|
|
189
284
|
export function createCcstyleTool(
|
|
190
285
|
originalTool: any,
|
|
@@ -315,6 +410,20 @@ export function createCcstyleTool(
|
|
|
315
410
|
|
|
316
411
|
const expanded = isToolExpanded(options, context);
|
|
317
412
|
const isError = Boolean(options?.isError || context?.isError);
|
|
413
|
+
// Agent:running 转交原渲染器(spinner/activity 只有它有);其余画 ↳ 摘要行。
|
|
414
|
+
// toolName 大小写宽松匹配:上游改名也不静默失效。
|
|
415
|
+
if (toolName.toLowerCase() === AGENT_TOOL_NAME.toLowerCase()) {
|
|
416
|
+
const agentResult = renderAgentResult(
|
|
417
|
+
originalTool,
|
|
418
|
+
result,
|
|
419
|
+
options,
|
|
420
|
+
theme,
|
|
421
|
+
context,
|
|
422
|
+
expanded,
|
|
423
|
+
isError,
|
|
424
|
+
);
|
|
425
|
+
if (agentResult) return agentResult;
|
|
426
|
+
}
|
|
318
427
|
// codemode:折叠态自己画子调用树,展开态给全部子调用与去掉脚本头的输出
|
|
319
428
|
if (toolName === CODEMODE_TOOL_NAME) {
|
|
320
429
|
if (!options?.isPartial) setToolVisualState(context, isError ? "error" : "success");
|
|
@@ -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,
|
|
@@ -36,6 +36,11 @@ import {
|
|
|
36
36
|
} from "./names.ts";
|
|
37
37
|
import { formatToolDuration, headTruncateToWidth, toolViewportWidth } from "./result.ts";
|
|
38
38
|
import { isLiveToolCall } from "./tool-state.ts";
|
|
39
|
+
import {
|
|
40
|
+
AGENT_TOOL_NAME,
|
|
41
|
+
isSubagentNotificationComponent,
|
|
42
|
+
subagentNotificationItems,
|
|
43
|
+
} from "./subagent-notification.ts";
|
|
39
44
|
import {
|
|
40
45
|
patchRegistry,
|
|
41
46
|
TOOL_GROUPING_GENERATION_KEY as GENERATION_KEY,
|
|
@@ -76,10 +81,15 @@ function toolTitle(tool: any): string {
|
|
|
76
81
|
);
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
function
|
|
84
|
+
function isGroupableTool(value: unknown): boolean {
|
|
80
85
|
return value instanceof ToolExecutionComponent && !NON_GROUPABLE.has(toolName(value));
|
|
81
86
|
}
|
|
82
87
|
|
|
88
|
+
/** 可分组项:工具,或子代理完成通知(只是展示项,不是工具执行)。 */
|
|
89
|
+
function isGroupable(value: unknown): boolean {
|
|
90
|
+
return isGroupableTool(value) || isSubagentNotificationComponent(value);
|
|
91
|
+
}
|
|
92
|
+
|
|
83
93
|
function isIgnorable(value: unknown): boolean {
|
|
84
94
|
if (value instanceof Spacer) return true;
|
|
85
95
|
if (!(value instanceof AssistantMessageComponent)) return false;
|
|
@@ -103,7 +113,36 @@ function previousSibling(
|
|
|
103
113
|
return undefined;
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
type ToolStatus = "pending" | "success" | "error";
|
|
116
|
+
export type ToolStatus = "pending" | "success" | "error";
|
|
117
|
+
|
|
118
|
+
type Fg = (color: string, text: string) => string;
|
|
119
|
+
|
|
120
|
+
function themeFg(theme: any): Fg {
|
|
121
|
+
return (color, text) => theme?.fg?.(color, text) ?? text;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 分组里的一行显示项:工具一项;子代理完成通知按 agent 各一项。
|
|
126
|
+
* 分组只管计数、树形缩进与展开状态转发,每项自己负责摘要与展开内容。
|
|
127
|
+
*/
|
|
128
|
+
export type GroupDisplayItem = {
|
|
129
|
+
/** 组头判定「是否同一种」用的身份:工具原名,或通知类型。 */
|
|
130
|
+
name: string;
|
|
131
|
+
/** 组头工具列表与子行用的显示名。 */
|
|
132
|
+
label: string;
|
|
133
|
+
status: ToolStatus;
|
|
134
|
+
/** 折叠行正文(接在树枝与状态图标之后),width 为剩余可用宽度。 */
|
|
135
|
+
summary(width: number, theme: any): string;
|
|
136
|
+
/**
|
|
137
|
+
* 展开行:首行接在树枝与状态图标之后,其余作为续行。width 为面板内宽(含树枝前缀)。
|
|
138
|
+
* 输入不变时返回同一数组:分组展开缓存按引用比对。
|
|
139
|
+
*/
|
|
140
|
+
expandedRows(width: number, theme: any): string[];
|
|
141
|
+
/** 展开态里这些行对应的内部卡片;没有时行归分组本身(点击收起整组)。 */
|
|
142
|
+
hitTarget?: unknown;
|
|
143
|
+
/** 丢掉该项自己缓存的行(换主题后颜色要重算);分组 invalidate 时转发。 */
|
|
144
|
+
invalidate?(): void;
|
|
145
|
+
};
|
|
107
146
|
|
|
108
147
|
function status(tool: any): ToolStatus {
|
|
109
148
|
return toolCardStatus(tool);
|
|
@@ -275,29 +314,86 @@ function shellResultTail(
|
|
|
275
314
|
return duration ? ` ${fg("dim", `· ${duration}`)}` : "";
|
|
276
315
|
}
|
|
277
316
|
|
|
278
|
-
/**
|
|
279
|
-
function
|
|
280
|
-
tool
|
|
317
|
+
/** 子工具折叠行正文:与组内同一套摘要。 */
|
|
318
|
+
function toolSummaryText(tool: any, toolStatus: ToolStatus, width: number, fg: Fg): string {
|
|
319
|
+
const summary = askUserResultSummary(tool) ?? codemodeResultSummary(tool) ?? toolSummary(tool);
|
|
320
|
+
const detail = fg("dim", summary.detail);
|
|
321
|
+
const result = agentResultTail(tool, fg) + shellResultTail(tool, fg, toolStatus);
|
|
322
|
+
const mainWidth = Math.max(0, width - visibleWidth(detail) - visibleWidth(result));
|
|
323
|
+
return `${renderToolSummary(summary, mainWidth, fg)}${detail}${result}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Agent 的折叠行尾巴:`-> Running in background · ID: xxx`。参考 ask_user_question 的
|
|
328
|
+
* 问题 -> 回答写法:跑后台的结果状态放在 payload 后,比单独的 ↳ 行省一行。
|
|
329
|
+
*/
|
|
330
|
+
function agentResultTail(tool: any, fg: Fg): string {
|
|
331
|
+
if (toolName(tool).toLowerCase() !== AGENT_TOOL_NAME.toLowerCase() || !tool?.result) return "";
|
|
332
|
+
const details = tool.result.details;
|
|
333
|
+
if (details?.status !== "background") return "";
|
|
334
|
+
const agentId = details.agentId ? ` ID: ${details.agentId}` : "";
|
|
335
|
+
return ` ${fg("dim", "\u2192")} ${fg("dim", `Running in background${agentId}`)}`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** 原始 paint → 去掉状态图标与外层缩进后的展开行;paint 引用不变就复用。 */
|
|
339
|
+
const expandedToolRowsCache = new WeakMap<string[], string[]>();
|
|
340
|
+
|
|
341
|
+
function expandedToolRows(tool: any, width: number): string[] {
|
|
342
|
+
const paint = tool.render?.(width);
|
|
343
|
+
if (!Array.isArray(paint)) return [toolSummary(tool).main];
|
|
344
|
+
const cached = expandedToolRowsCache.get(paint);
|
|
345
|
+
if (cached) return cached;
|
|
346
|
+
const rendered = visibleLines(paint);
|
|
347
|
+
if (rendered.length) {
|
|
348
|
+
rendered[0] = stripLeadingStatusIcon(rendered[0])
|
|
349
|
+
.replace(/^ +/, "")
|
|
350
|
+
.replace(/^((?:\x1b\[[0-?]*[ -/]*[@-~])*) +/, "$1");
|
|
351
|
+
}
|
|
352
|
+
const rows = rendered.length
|
|
353
|
+
? // 续行只剥外层 Box 的 1 格 left pad,保留 Input/Output 相对缩进
|
|
354
|
+
rendered.map((line, index) => (index === 0 ? line : stripLeadingSpaces(line, 1)))
|
|
355
|
+
: [toolSummary(tool).main];
|
|
356
|
+
expandedToolRowsCache.set(paint, rows);
|
|
357
|
+
return rows;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function toolItem(tool: any): GroupDisplayItem {
|
|
361
|
+
const toolStatus = status(tool);
|
|
362
|
+
return {
|
|
363
|
+
name: toolName(tool),
|
|
364
|
+
label: toolTitle(tool),
|
|
365
|
+
status: toolStatus,
|
|
366
|
+
summary: (width, theme) => toolSummaryText(tool, toolStatus, width, themeFg(theme)),
|
|
367
|
+
expandedRows: (width) => expandedToolRows(tool, width),
|
|
368
|
+
hitTarget: tool,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function groupItems(children: readonly unknown[]): GroupDisplayItem[] {
|
|
373
|
+
return children.flatMap((child) => subagentNotificationItems(child) ?? [toolItem(child)]);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** 分组折叠态的一行:`├/└` + 状态图标 + 该项摘要。 */
|
|
377
|
+
function collapsedGroupLine(
|
|
378
|
+
item: GroupDisplayItem,
|
|
281
379
|
last: boolean,
|
|
282
|
-
|
|
380
|
+
theme: any,
|
|
283
381
|
width: number,
|
|
284
382
|
): 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);
|
|
383
|
+
const fg = themeFg(theme);
|
|
384
|
+
const color = item.status === "pending" ? "dim" : item.status;
|
|
385
|
+
const prefix = ` ${fg("dim", last ? "└" : "├")} ${fg(color, statusIcon(item.status))} `;
|
|
291
386
|
const rowWidth = toolViewportWidth(width);
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
)
|
|
387
|
+
const room = Math.max(0, rowWidth - visibleWidth(prefix));
|
|
388
|
+
return truncateToWidth(`${prefix}${item.summary(room, theme)}`, rowWidth, "");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** 展开面板里一项占的行:最后一项不留尾部空行(面板自带底部留白)。 */
|
|
392
|
+
function panelRows(rows: string[], last: boolean): string[] {
|
|
393
|
+
if (!last) return rows;
|
|
394
|
+
let end = rows.length;
|
|
395
|
+
while (end > 1 && !stripAnsi(rows[end - 1]).trim()) end--;
|
|
396
|
+
return end === rows.length ? rows : rows.slice(0, end);
|
|
301
397
|
}
|
|
302
398
|
|
|
303
399
|
type GroupHeaderCounts = { pending: number; success: number; error: number };
|
|
@@ -326,10 +422,9 @@ function groupOverallStatus(counts: GroupHeaderCounts): ToolStatus {
|
|
|
326
422
|
* 组头的工具列表:名称用与单组标题、子行同一套人性化写法,次数一律带上(各跑一次也报 ×1)。
|
|
327
423
|
* 同名(同显示标题)的合并成一项,避免 `MCP call×1, MCP call×1` 这种重复。
|
|
328
424
|
*/
|
|
329
|
-
function toolNameList(
|
|
425
|
+
function toolNameList(items: readonly GroupDisplayItem[]): string {
|
|
330
426
|
const counts = new Map<string, number>();
|
|
331
|
-
for (const
|
|
332
|
-
const label = toolTitle(tool);
|
|
427
|
+
for (const { label } of items) {
|
|
333
428
|
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
334
429
|
}
|
|
335
430
|
return [...counts].map(([label, count]) => `${label}×${count}`).join(", ");
|
|
@@ -425,17 +520,18 @@ export class ToolGroupComponent extends Container {
|
|
|
425
520
|
childAtRow(localRow: number, width: number): { component: any; row: number } | null {
|
|
426
521
|
if (!this._expanded || localRow < 2) return null;
|
|
427
522
|
let offset = 2;
|
|
428
|
-
|
|
429
|
-
|
|
523
|
+
const items = groupItems(this.children);
|
|
524
|
+
for (let index = 0; index < items.length; index++) {
|
|
525
|
+
const item = items[index];
|
|
526
|
+
let lineCount = 1;
|
|
430
527
|
try {
|
|
431
|
-
const
|
|
432
|
-
|
|
528
|
+
const rows = item.expandedRows(Math.max(1, width - 2), this.patch.theme);
|
|
529
|
+
lineCount = Math.max(1, panelRows(rows, index === items.length - 1).length);
|
|
433
530
|
} catch {
|
|
434
|
-
|
|
531
|
+
lineCount = 1;
|
|
435
532
|
}
|
|
436
|
-
const lineCount = Math.max(1, lines.length);
|
|
437
533
|
if (localRow < offset + lineCount) {
|
|
438
|
-
return { component:
|
|
534
|
+
return item.hitTarget ? { component: item.hitTarget, row: localRow - offset } : null;
|
|
439
535
|
}
|
|
440
536
|
offset += lineCount;
|
|
441
537
|
}
|
|
@@ -445,6 +541,7 @@ export class ToolGroupComponent extends Container {
|
|
|
445
541
|
invalidate(): void {
|
|
446
542
|
this.clearPaintCache();
|
|
447
543
|
for (const tool of this.children) tool.invalidate?.();
|
|
544
|
+
for (const item of groupItems(this.children)) item.invalidate?.();
|
|
448
545
|
}
|
|
449
546
|
|
|
450
547
|
private settledCacheHit(width: number): string[] | undefined {
|
|
@@ -466,7 +563,7 @@ export class ToolGroupComponent extends Container {
|
|
|
466
563
|
cache.children[i] !== tool ||
|
|
467
564
|
cache.args[i] !== tool?.args ||
|
|
468
565
|
cache.results[i] !== tool?.result ||
|
|
469
|
-
status(tool) === "pending"
|
|
566
|
+
(!isSubagentNotificationComponent(tool) && status(tool) === "pending")
|
|
470
567
|
) {
|
|
471
568
|
return;
|
|
472
569
|
}
|
|
@@ -491,12 +588,15 @@ export class ToolGroupComponent extends Container {
|
|
|
491
588
|
const cached = this.settledCacheHit(width);
|
|
492
589
|
if (cached) return cached;
|
|
493
590
|
const theme = this.patch.theme;
|
|
494
|
-
const fg = (
|
|
591
|
+
const fg = themeFg(theme);
|
|
592
|
+
const items = groupItems(this.children);
|
|
495
593
|
const counts = { pending: 0, success: 0, error: 0 };
|
|
496
|
-
|
|
594
|
+
// 组头按行计数:通知的每个 agent 算一行(与工具各一行同一口径,避免
|
|
595
|
+
// 「5 done 其实是 4 工具 + 1 通知行」这类误导)。
|
|
596
|
+
for (const item of items) counts[item.status]++;
|
|
497
597
|
const countText = groupCountText(counts, fg);
|
|
498
|
-
const names = new Set(
|
|
499
|
-
const label = names.size === 1 ?
|
|
598
|
+
const names = new Set(items.map((item) => item.name));
|
|
599
|
+
const label = names.size === 1 ? items[0].label : "Multiple Tools";
|
|
500
600
|
const overall = groupOverallStatus(counts);
|
|
501
601
|
if ((this.children as any[]).some(isAnimatingTool)) scheduleGroupAnimation(this.patch);
|
|
502
602
|
const overallColor = overall === "pending" ? "dim" : overall;
|
|
@@ -512,76 +612,58 @@ export class ToolGroupComponent extends Container {
|
|
|
512
612
|
// 连一个工具名都放不下就不放列表:退化成 `· …` 只会把行撑爆。
|
|
513
613
|
const nameList =
|
|
514
614
|
names.size > 1 && listRoom >= 4
|
|
515
|
-
? ` ${fg("dim", `· ${headTruncateToWidth(toolNameList(
|
|
615
|
+
? ` ${fg("dim", `· ${headTruncateToWidth(toolNameList(items), listRoom)}`)}`
|
|
516
616
|
: "";
|
|
517
617
|
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;
|
|
618
|
+
const total = items.length;
|
|
619
|
+
if (!this._expanded) {
|
|
620
|
+
for (let index = 0; index < total; index++) {
|
|
621
|
+
lines.push(collapsedGroupLine(items[index], index === total - 1, theme, width));
|
|
535
622
|
}
|
|
623
|
+
if (counts.pending === 0) this.storeSettledCache(width, lines);
|
|
624
|
+
return lines;
|
|
625
|
+
}
|
|
626
|
+
// 有一个失败整卡用失败底色;没有失败但还有进行中用进行中底色。
|
|
627
|
+
const backgroundSlot = TOOL_CARD_BG_SLOT[overall];
|
|
628
|
+
const itemPaints = items.map((item) => item.expandedRows(Math.max(1, width - 2), theme));
|
|
629
|
+
const expandedHit = this.expandedPaintCache;
|
|
630
|
+
if (
|
|
631
|
+
expandedHit &&
|
|
632
|
+
expandedHit.width === width &&
|
|
633
|
+
expandedHit.hover === this.hintHovered &&
|
|
634
|
+
expandedHit.theme === this.patch.theme &&
|
|
635
|
+
expandedHit.fullscreen === isToolTuiFullscreen() &&
|
|
636
|
+
expandedHit.bgSlot === backgroundSlot &&
|
|
637
|
+
expandedHit.paints.length === itemPaints.length &&
|
|
638
|
+
expandedHit.paints.every((paint, index) => paint === itemPaints[index])
|
|
639
|
+
) {
|
|
640
|
+
return expandedHit.lines;
|
|
536
641
|
}
|
|
537
|
-
const expandedLines: string[] = [];
|
|
538
642
|
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);
|
|
643
|
+
const item = items[index];
|
|
644
|
+
const color = item.status === "pending" ? "dim" : item.status;
|
|
645
|
+
const last = index === total - 1;
|
|
646
|
+
const branch = last ? "└" : "├";
|
|
647
|
+
const continuation = last ? " " : "│ ";
|
|
648
|
+
const rows = panelRows(itemPaints[index], last);
|
|
649
|
+
for (let lineIndex = 0; lineIndex < rows.length; lineIndex++) {
|
|
559
650
|
const prefix =
|
|
560
651
|
lineIndex === 0
|
|
561
|
-
? `${fg("dim", branch)} ${fg(color, statusIcon(
|
|
652
|
+
? `${fg("dim", branch)} ${fg(color, statusIcon(item.status))} `
|
|
562
653
|
: fg("dim", continuation);
|
|
563
|
-
|
|
654
|
+
lines.push(paddedBackgroundRow(theme, backgroundSlot, prefix + rows[lineIndex], width));
|
|
564
655
|
}
|
|
565
656
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
}
|
|
657
|
+
lines.push(paddedBackgroundRow(theme, backgroundSlot, "", width));
|
|
658
|
+
this.expandedPaintCache = {
|
|
659
|
+
width,
|
|
660
|
+
hover: this.hintHovered,
|
|
661
|
+
theme: this.patch.theme,
|
|
662
|
+
fullscreen: isToolTuiFullscreen(),
|
|
663
|
+
bgSlot: backgroundSlot,
|
|
664
|
+
paints: itemPaints,
|
|
665
|
+
lines,
|
|
666
|
+
};
|
|
585
667
|
return lines;
|
|
586
668
|
}
|
|
587
669
|
}
|
|
@@ -605,8 +687,9 @@ function ungroup(patch: Patch): void {
|
|
|
605
687
|
}
|
|
606
688
|
}
|
|
607
689
|
|
|
690
|
+
/** 至少两项展示内容就保留分组:一个通知组件也可能包含多个 agent。 */
|
|
608
691
|
function normalizeGroup(patch: Patch, group: ToolGroupComponent): void {
|
|
609
|
-
if (group.children.length > 1) return;
|
|
692
|
+
if (groupItems(group.children).length > 1) return;
|
|
610
693
|
const parent = (group as any)[PARENT_KEY];
|
|
611
694
|
const index = parent?.children?.indexOf(group) ?? -1;
|
|
612
695
|
const tools = group.releaseTools();
|
|
@@ -615,12 +698,8 @@ function normalizeGroup(patch: Patch, group: ToolGroupComponent): void {
|
|
|
615
698
|
for (const tool of tools) delete tool[PARENT_KEY];
|
|
616
699
|
return;
|
|
617
700
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
parent.children.splice(index, 1, tools[0]);
|
|
621
|
-
} else {
|
|
622
|
-
parent.children.splice(index, 1);
|
|
623
|
-
}
|
|
701
|
+
for (const tool of tools) tool[PARENT_KEY] = parent;
|
|
702
|
+
parent.children.splice(index, 1, ...tools);
|
|
624
703
|
}
|
|
625
704
|
|
|
626
705
|
function maybeGroup(patch: Patch, parent: any, component: any): void {
|
|
@@ -636,19 +715,25 @@ function maybeGroup(patch: Patch, parent: any, component: any): void {
|
|
|
636
715
|
if (!Array.isArray(children)) return;
|
|
637
716
|
const index = children.indexOf(component);
|
|
638
717
|
const prior = previousSibling(children, index - 1);
|
|
639
|
-
if (
|
|
640
|
-
if (prior.child instanceof ToolGroupComponent && (prior.child as any).patch === patch) {
|
|
718
|
+
if (prior?.child instanceof ToolGroupComponent && (prior.child as any).patch === patch) {
|
|
641
719
|
children.splice(index, 1);
|
|
642
720
|
prior.child.addTool(component);
|
|
643
721
|
return;
|
|
644
722
|
}
|
|
645
|
-
|
|
723
|
+
const members = [component];
|
|
724
|
+
let firstIndex = index;
|
|
725
|
+
if (prior && isGroupable(prior.child) && prior.child[GENERATION_KEY] === patch.generation) {
|
|
726
|
+
members.unshift(prior.child);
|
|
727
|
+
firstIndex = prior.index;
|
|
728
|
+
}
|
|
729
|
+
// 按展示项而非消息数决定:单条批量通知也成组,原消息仍完整地留在组内。
|
|
730
|
+
if (groupItems(members).length < 2) return;
|
|
646
731
|
const group = new ToolGroupComponent(patch);
|
|
647
|
-
group.addTool(
|
|
648
|
-
group.addTool(component);
|
|
732
|
+
for (const member of members) group.addTool(member);
|
|
649
733
|
(group as any)[PARENT_KEY] = parent;
|
|
650
|
-
|
|
651
|
-
children.splice(index, 1);
|
|
734
|
+
// 两个相邻组件合并时移走后者;夹在中间的空行留在分组之后。
|
|
735
|
+
if (firstIndex !== index) children.splice(index, 1);
|
|
736
|
+
children[firstIndex] = group;
|
|
652
737
|
}
|
|
653
738
|
|
|
654
739
|
/** /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" };
|