@herbertgao/pi-extensions 2026.10.4 → 2026.10.6
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.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +13 -9
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +13 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +72 -16
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +262 -48
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +2 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +131 -28
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +2 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +3 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer-currency.ts +29 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +96 -59
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/git-stats.ts +162 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/token-rate.ts +204 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +51 -86
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +212 -74
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +50 -362
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +35 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +16 -83
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +28 -26
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/sticky-prompt.ts +450 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ask-user-question.ts +173 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ccstyle-tool.ts +467 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +5 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +1 -24
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +64 -11
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +41 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +5 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +2 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +36 -10
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +140 -51
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/message-display.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +44 -4
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +34 -803
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-io.ts +735 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-state.ts +138 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/format.ts +9 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/patch-keys.ts +5 -0
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +5 -4
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
- package/node_modules/@herbertgao/sol-pi/README.md +3 -3
- package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
- package/node_modules/@herbertgao/sol-pi/agents-install.md +3 -3
- package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
- package/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/show-more-hint.ts +0 -48
package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import { pluralize } from "./diff-text.ts";
|
|
3
|
+
import type { DiffTheme } from "./diff-palette.ts";
|
|
3
4
|
import type { ToolDisplayConfig } from "../../../config/config.ts";
|
|
4
5
|
|
|
5
6
|
export interface DiffSummaryStats {
|
|
@@ -68,3 +69,43 @@ export function buildDiffSummaryText(stats: DiffSummaryStats, width: number): st
|
|
|
68
69
|
|
|
69
70
|
return truncateToWidth(summaryCandidates[summaryCandidates.length - 1] ?? "", safeWidth, "");
|
|
70
71
|
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 折叠摘要行样式:整行 muted,`+A -D` 用 diff 头行同一套绿/红,hover 只把动作词提到 text。
|
|
75
|
+
* 0 行预览与多行预览共用这一套颜色:改折叠行数时同一处信息不变色。
|
|
76
|
+
*/
|
|
77
|
+
export function styleDiffSummaryLine(
|
|
78
|
+
text: string,
|
|
79
|
+
stats: Pick<DiffSummaryStats, "added" | "removed"> | undefined,
|
|
80
|
+
hint: string,
|
|
81
|
+
theme: DiffTheme,
|
|
82
|
+
hovered: boolean,
|
|
83
|
+
): string {
|
|
84
|
+
const counts = stats ? `+${stats.added} -${stats.removed}` : "";
|
|
85
|
+
const countsIndex = counts ? text.indexOf(counts) : -1;
|
|
86
|
+
const hintIndex = hint ? text.lastIndexOf(hint) : -1;
|
|
87
|
+
if (countsIndex < 0 && hintIndex < 0) {
|
|
88
|
+
return theme.fg("muted", text);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const pieces: string[] = [];
|
|
92
|
+
let cursor = 0;
|
|
93
|
+
const pushMuted = (end: number) => {
|
|
94
|
+
const gap = text.slice(cursor, end);
|
|
95
|
+
if (gap) pieces.push(theme.fg("muted", gap));
|
|
96
|
+
};
|
|
97
|
+
if (countsIndex >= 0 && stats) {
|
|
98
|
+
pushMuted(countsIndex);
|
|
99
|
+
pieces.push(theme.fg("toolDiffAdded", `+${stats.added}`));
|
|
100
|
+
pieces.push(theme.fg("muted", " "));
|
|
101
|
+
pieces.push(theme.fg("toolDiffRemoved", `-${stats.removed}`));
|
|
102
|
+
cursor = countsIndex + counts.length;
|
|
103
|
+
}
|
|
104
|
+
if (hintIndex >= cursor) {
|
|
105
|
+
pushMuted(hintIndex);
|
|
106
|
+
pieces.push(theme.fg(hovered ? "text" : "muted", hint));
|
|
107
|
+
cursor = hintIndex + hint.length;
|
|
108
|
+
}
|
|
109
|
+
pushMuted(text.length);
|
|
110
|
+
return pieces.join("");
|
|
111
|
+
}
|
package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts
CHANGED
|
@@ -20,4 +20,8 @@ export { shouldHighlightCodeBlock } from "./diff-highlight.ts";
|
|
|
20
20
|
export type { DisplayConfigInput } from "./diff-component.ts";
|
|
21
21
|
export { isRichDiffComponent } from "./diff-component.ts";
|
|
22
22
|
export { countEditDiffStats, renderEditDiffResult } from "./diff-edit-render.ts";
|
|
23
|
-
export {
|
|
23
|
+
export {
|
|
24
|
+
countWriteDiffStats,
|
|
25
|
+
renderWriteDiffResult,
|
|
26
|
+
writeStatsInCollapsedBody,
|
|
27
|
+
} from "./diff-write-render.ts";
|
package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts
CHANGED
|
@@ -9,7 +9,8 @@ export function splitWriteContentLines(content: string): string[] {
|
|
|
9
9
|
return [];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const normalized = content.replace(/\r/g, "");
|
|
13
|
+
const lines = normalized.split("\n");
|
|
13
14
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
14
15
|
lines.pop();
|
|
15
16
|
}
|
package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Text, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { ToolDisplayConfig } from "../../../config/config.ts";
|
|
2
3
|
import { sanitizeToolResultText } from "../../../utils/tool-result-sanitize.ts";
|
|
3
|
-
import { showMoreHintText } from "../
|
|
4
|
+
import { showMoreHintText } from "../names.ts";
|
|
4
5
|
import { splitWriteContentLines } from "./diff-text.ts";
|
|
5
6
|
import {
|
|
6
7
|
collectDiffStats,
|
|
@@ -54,6 +55,7 @@ import {
|
|
|
54
55
|
buildDiffSummaryText,
|
|
55
56
|
normalizeDiffRenderWidth,
|
|
56
57
|
resolveDiffPresentationMode,
|
|
58
|
+
styleDiffSummaryLine,
|
|
57
59
|
type DiffPresentationMode,
|
|
58
60
|
} from "./diff-presentation.ts";
|
|
59
61
|
|
|
@@ -290,13 +292,14 @@ function renderWriteOverwriteGuardRows(
|
|
|
290
292
|
return renderSingleDiffRow(buildWriteOverwriteGuardText(guard, width), "warning", width, theme);
|
|
291
293
|
}
|
|
292
294
|
|
|
293
|
-
/** Folded write with `writeDiffCollapsedLines: 0`: `↳ created · click to show more`. */
|
|
295
|
+
/** Folded write with `writeDiffCollapsedLines: 0`: `↳ diff +A -D · created · click to show more`. */
|
|
294
296
|
function renderWriteCollapsedHintLine(
|
|
295
297
|
wasOverwrite: boolean,
|
|
296
298
|
width: number,
|
|
297
299
|
theme: DiffTheme,
|
|
298
300
|
hovered: boolean,
|
|
299
301
|
headerLabel?: string,
|
|
302
|
+
stats?: { added: number; removed: number },
|
|
300
303
|
): string[] {
|
|
301
304
|
const safeWidth = normalizeDiffRenderWidth(width);
|
|
302
305
|
if (safeWidth === 0) {
|
|
@@ -304,7 +307,20 @@ function renderWriteCollapsedHintLine(
|
|
|
304
307
|
}
|
|
305
308
|
const actionLabel = headerLabel?.trim() || (wasOverwrite ? "overwritten" : "created");
|
|
306
309
|
const clickLabel = showMoreHintText();
|
|
307
|
-
const
|
|
310
|
+
const counts = stats ? `+${stats.added} -${stats.removed}` : "";
|
|
311
|
+
const candidates = stats
|
|
312
|
+
? [
|
|
313
|
+
`↳ diff ${counts} · ${actionLabel} · ${clickLabel}`,
|
|
314
|
+
`↳ diff ${counts} · ${clickLabel}`,
|
|
315
|
+
`↳ diff ${counts} · ${actionLabel}`,
|
|
316
|
+
`↳ diff ${counts}`,
|
|
317
|
+
`↳ ${counts} · ${clickLabel}`,
|
|
318
|
+
`↳ ${actionLabel} · ${clickLabel}`,
|
|
319
|
+
`↳ ${actionLabel}`,
|
|
320
|
+
actionLabel,
|
|
321
|
+
"…",
|
|
322
|
+
]
|
|
323
|
+
: [`↳ ${actionLabel} · ${clickLabel}`, `↳ ${actionLabel}`, actionLabel, "…"];
|
|
308
324
|
let text = candidates[candidates.length - 1] ?? "…";
|
|
309
325
|
for (const candidate of candidates) {
|
|
310
326
|
if (visibleWidth(candidate) <= safeWidth) {
|
|
@@ -315,16 +331,21 @@ function renderWriteCollapsedHintLine(
|
|
|
315
331
|
if (visibleWidth(text) > safeWidth) {
|
|
316
332
|
text = truncateToWidth(text, safeWidth, "");
|
|
317
333
|
}
|
|
318
|
-
const
|
|
319
|
-
const styled =
|
|
320
|
-
hovered && clickIndex >= 0
|
|
321
|
-
? theme.fg("muted", text.slice(0, clickIndex)) +
|
|
322
|
-
theme.fg("text", clickLabel) +
|
|
323
|
-
theme.fg("muted", text.slice(clickIndex + clickLabel.length))
|
|
324
|
-
: theme.fg("muted", text);
|
|
334
|
+
const styled = styleDiffSummaryLine(text, stats, clickLabel, theme, hovered);
|
|
325
335
|
return [clampDiffLineToWidth(styled, safeWidth)];
|
|
326
336
|
}
|
|
327
337
|
|
|
338
|
+
/**
|
|
339
|
+
* 折叠 0 行预览时计数由正文那行显示(`↳ diff +A -D · created · click to show more`),
|
|
340
|
+
* 标题不再重复。compact 与 default 两个模式的写标题共用这条口径。
|
|
341
|
+
*/
|
|
342
|
+
export function writeStatsInCollapsedBody(
|
|
343
|
+
expanded: boolean,
|
|
344
|
+
display: Pick<ToolDisplayConfig, "writeDiffCollapsedLines">,
|
|
345
|
+
): boolean {
|
|
346
|
+
return !expanded && resolveWriteCollapsedLimit(display) === 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
328
349
|
/**
|
|
329
350
|
* write 变更行统计(compact 单行用):复用当前 write diff 算法。
|
|
330
351
|
* 新文件 = 全部新内容为 added;覆盖已有文件用 LCS 精确统计。
|
|
@@ -413,6 +434,10 @@ export function renderWriteDiffResult(
|
|
|
413
434
|
const hint = cache.getHintLine();
|
|
414
435
|
return hint !== undefined && normalizeCollapsedHintLine(plainLine) === hint;
|
|
415
436
|
},
|
|
437
|
+
/** 折叠 0 行预览时正文那行自带 +A -D:标题不再重复计数。 */
|
|
438
|
+
bodyShowsStats(): boolean {
|
|
439
|
+
return writeStatsInCollapsedBody(options.expanded, resolveLiveDisplayConfig(config));
|
|
440
|
+
},
|
|
416
441
|
render(width: number): string[] {
|
|
417
442
|
// Live config: panel can change indicator/wrap/limits after this component is created.
|
|
418
443
|
const live = resolveLiveDisplayConfig(config);
|
|
@@ -445,6 +470,7 @@ export function renderWriteDiffResult(
|
|
|
445
470
|
theme,
|
|
446
471
|
hovered,
|
|
447
472
|
options.headerLabel,
|
|
473
|
+
countWriteDiffStats(content, options.previousContent, options.fileExistedBeforeWrite),
|
|
448
474
|
);
|
|
449
475
|
return cache.set(
|
|
450
476
|
safeWidth,
|
package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { normalizeDiffRenderWidth } from "./diff-presentation.ts";
|
|
2
2
|
import { pluralize } from "./diff-text.ts";
|
|
3
|
-
import { showMoreHintText } from "../
|
|
3
|
+
import { showMoreHintText } from "../names.ts";
|
|
4
4
|
|
|
5
5
|
export interface WidthMeasurementOps {
|
|
6
6
|
measure(text: string): number;
|
|
@@ -8,6 +8,12 @@ import {
|
|
|
8
8
|
} from "@earendil-works/pi-tui";
|
|
9
9
|
import { config } from "../../config/config.ts";
|
|
10
10
|
import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
|
|
11
|
+
import {
|
|
12
|
+
ASK_USER_QUESTION_TOOL_NAME,
|
|
13
|
+
askUserAnswerText,
|
|
14
|
+
askUserAnswers,
|
|
15
|
+
type AskUserAnswer,
|
|
16
|
+
} from "./ask-user-question.ts";
|
|
11
17
|
import {
|
|
12
18
|
CODEMODE_TOOL_NAME,
|
|
13
19
|
codemodeCalls,
|
|
@@ -16,17 +22,19 @@ import {
|
|
|
16
22
|
} from "./codemode.ts";
|
|
17
23
|
import { getToolMouseTui } from "../mouse/scroll.ts";
|
|
18
24
|
import { mcpToolTitle } from "./mcp-title.ts";
|
|
19
|
-
import { collapseHintText, isToolTuiFullscreen, showMoreHintText } from "./
|
|
25
|
+
import { collapseHintText, isToolTuiFullscreen, showMoreHintText } from "./names.ts";
|
|
20
26
|
import { stripAnsi, stripBackgroundAnsi, stripLeadingStatusIcon } from "../../utils/ansi-text.ts";
|
|
21
27
|
import { walkComponentTree } from "../../utils/component-tree.ts";
|
|
22
28
|
import {
|
|
23
29
|
fitToolCallSummary,
|
|
24
30
|
humanizeToolLabel,
|
|
25
31
|
renderToolSummary,
|
|
32
|
+
SHELL_TOOL_NAMES,
|
|
26
33
|
toolCallSummary,
|
|
27
34
|
type ToolCallSummary,
|
|
28
35
|
} from "./names.ts";
|
|
29
|
-
import { toolViewportWidth } from "./result.ts";
|
|
36
|
+
import { formatToolDuration, headTruncateToWidth, toolViewportWidth } from "./result.ts";
|
|
37
|
+
import { isLiveToolCall } from "./tool-state.ts";
|
|
30
38
|
import {
|
|
31
39
|
patchRegistry,
|
|
32
40
|
TOOL_GROUPING_GENERATION_KEY as GENERATION_KEY,
|
|
@@ -108,6 +116,12 @@ function statusIcon(value: ToolStatus): string {
|
|
|
108
116
|
return toolLoadingIcon();
|
|
109
117
|
}
|
|
110
118
|
|
|
119
|
+
/** 等待窗口或运行中的行:spinner 逐帧变化,分组卡要跟着跳帧。 */
|
|
120
|
+
function isAnimatingTool(tool: any): boolean {
|
|
121
|
+
if (status(tool) !== "pending") return false;
|
|
122
|
+
return Boolean(tool?.executionStarted) || isLiveToolCall(tool?.toolCallId);
|
|
123
|
+
}
|
|
124
|
+
|
|
111
125
|
function scheduleGroupAnimation(patch: Patch): void {
|
|
112
126
|
if (patch.animationTimer || !patch.active) return;
|
|
113
127
|
patch.animationTimer = setTimeout(() => {
|
|
@@ -115,11 +129,7 @@ function scheduleGroupAnimation(patch: Patch): void {
|
|
|
115
129
|
if (!patch.active) return;
|
|
116
130
|
let needsRender = false;
|
|
117
131
|
for (const group of patch.groups) {
|
|
118
|
-
if (
|
|
119
|
-
(group.children as any[]).some(
|
|
120
|
-
(tool) => tool?.executionStarted && status(tool) === "pending",
|
|
121
|
-
)
|
|
122
|
-
) {
|
|
132
|
+
if ((group.children as any[]).some(isAnimatingTool)) {
|
|
123
133
|
group.invalidate();
|
|
124
134
|
// 分组卡不是 ToolExecutionComponent:它的 invalidate() 不会请求渲染,
|
|
125
135
|
// 不补这一步 spinner 只在别人渲染时才跳帧(并行工具下表现为卡顿/冻结)。
|
|
@@ -203,6 +213,39 @@ function toolSummary(tool: any): ToolCallSummary {
|
|
|
203
213
|
* codemode 的折叠行:有结果就报该次调用的统计(与单卡汇总行同一套口径),
|
|
204
214
|
* 免得一张卡里几行长代码预览互相看不出区别。还没结果时退回代码预览。
|
|
205
215
|
*/
|
|
216
|
+
/**
|
|
217
|
+
* ask_user_question 的折叠行:有结果就报「问题 -> 回答」,
|
|
218
|
+
* 多题压成一行并标注剩余条数。还没结果时退回问题预览。
|
|
219
|
+
*/
|
|
220
|
+
function askUserResultSummary(tool: any): ToolCallSummary | undefined {
|
|
221
|
+
if (toolName(tool) !== ASK_USER_QUESTION_TOOL_NAME || !tool?.result) return undefined;
|
|
222
|
+
const answers = askUserAnswers(tool.result)
|
|
223
|
+
.map((answer: AskUserAnswer, index: number) => ({ answer, index }))
|
|
224
|
+
.sort((left, right) => {
|
|
225
|
+
const leftIndex = Number(left.answer.questionIndex);
|
|
226
|
+
const rightIndex = Number(right.answer.questionIndex);
|
|
227
|
+
const leftMissing = !Number.isFinite(leftIndex);
|
|
228
|
+
const rightMissing = !Number.isFinite(rightIndex);
|
|
229
|
+
if (leftMissing || rightMissing) {
|
|
230
|
+
if (leftMissing !== rightMissing) return leftMissing ? 1 : -1;
|
|
231
|
+
return left.index - right.index;
|
|
232
|
+
}
|
|
233
|
+
return leftIndex - rightIndex;
|
|
234
|
+
})
|
|
235
|
+
.map((entry) => entry.answer);
|
|
236
|
+
if (answers.length === 0) {
|
|
237
|
+
const label = tool.result?.details?.cancelled === true ? "Declined" : "Done";
|
|
238
|
+
return { main: `${toolTitle(tool)} ${label}`, detail: "" };
|
|
239
|
+
}
|
|
240
|
+
const first = answers[0]!;
|
|
241
|
+
const question = String(first.question ?? "").trim() || "Question";
|
|
242
|
+
const more = answers.length > 1 ? ` (+${answers.length - 1})` : "";
|
|
243
|
+
return {
|
|
244
|
+
main: `${toolTitle(tool)} ${question} -> ${askUserAnswerText(first)}${more}`,
|
|
245
|
+
detail: "",
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
206
249
|
function codemodeResultSummary(tool: any): ToolCallSummary | undefined {
|
|
207
250
|
if (toolName(tool) !== CODEMODE_TOOL_NAME || !tool?.result) return undefined;
|
|
208
251
|
const parts = codemodeSummaryParts(
|
|
@@ -213,10 +256,83 @@ function codemodeResultSummary(tool: any): ToolCallSummary | undefined {
|
|
|
213
256
|
return { main: `${toolTitle(tool)} ${parts.join(" · ")}`, detail: "" };
|
|
214
257
|
}
|
|
215
258
|
|
|
259
|
+
/**
|
|
260
|
+
* 组内 shell 行的尾巴:只报耗时(`· 12.4s`)。输出和错误正文都在展开后看;
|
|
261
|
+
* 失败靠行首的 ✗,组头另有 failed 计数。
|
|
262
|
+
* 耗时取 pi 记在结果上的 durationMs(单卡渲染上下文里的同一个值),运行中还拿不到就不报。
|
|
263
|
+
*/
|
|
264
|
+
function shellResultTail(
|
|
265
|
+
tool: any,
|
|
266
|
+
fg: (color: string, text: string) => string,
|
|
267
|
+
toolStatus: ToolStatus,
|
|
268
|
+
): string {
|
|
269
|
+
if (!SHELL_TOOL_NAMES.has(toolName(tool)) || toolStatus === "pending" || !tool?.result) return "";
|
|
270
|
+
const durationMs = tool.result?.durationMs;
|
|
271
|
+
const duration =
|
|
272
|
+
typeof durationMs === "number" && Number.isFinite(durationMs) && durationMs >= 0
|
|
273
|
+
? formatToolDuration(durationMs)
|
|
274
|
+
: "";
|
|
275
|
+
return duration ? ` ${fg("dim", `· ${duration}`)}` : "";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** 分组折叠态的一行子工具:`├/└` + 状态图标 + 与组内同一套摘要。 */
|
|
279
|
+
function collapsedGroupToolLine(
|
|
280
|
+
tool: any,
|
|
281
|
+
last: boolean,
|
|
282
|
+
fg: (color: string, text: string) => string,
|
|
283
|
+
width: number,
|
|
284
|
+
): string {
|
|
285
|
+
const toolStatus = status(tool);
|
|
286
|
+
const color = toolStatus === "pending" ? "accent" : 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);
|
|
291
|
+
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
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
type GroupHeaderCounts = { pending: number; success: number; error: number };
|
|
304
|
+
|
|
305
|
+
/** 组头计数:`2 done · 1 failed`。没有计数时返回空串。 */
|
|
306
|
+
function groupCountText(
|
|
307
|
+
counts: GroupHeaderCounts,
|
|
308
|
+
fg: (color: string, text: string) => string,
|
|
309
|
+
): string {
|
|
310
|
+
return (["pending", "success", "error"] as const)
|
|
311
|
+
.filter((key) => counts[key])
|
|
312
|
+
.map((key) => {
|
|
313
|
+
const label = key === "pending" ? "running" : key === "success" ? "done" : "failed";
|
|
314
|
+
const color = key === "pending" ? "accent" : key;
|
|
315
|
+
return `${fg(color, String(counts[key]))} ${label}`;
|
|
316
|
+
})
|
|
317
|
+
.join(` ${fg("dim", "·")} `);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 组头圆点色:有失败取 error,否则有运行中取 accent,否则 success。 */
|
|
321
|
+
function groupOverallStatus(counts: GroupHeaderCounts): ToolStatus {
|
|
322
|
+
return counts.error ? "error" : counts.pending ? "pending" : "success";
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 组头的工具列表:名称用与单组标题、子行同一套人性化写法,次数一律带上(各跑一次也报 ×1)。
|
|
327
|
+
* 同名(同显示标题)的合并成一项,避免 `MCP call×1, MCP call×1` 这种重复。
|
|
328
|
+
*/
|
|
216
329
|
function toolNameList(tools: any[]): string {
|
|
217
330
|
const counts = new Map<string, number>();
|
|
218
|
-
for (const tool of tools)
|
|
219
|
-
|
|
331
|
+
for (const tool of tools) {
|
|
332
|
+
const label = toolTitle(tool);
|
|
333
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
334
|
+
}
|
|
335
|
+
return [...counts].map(([label, count]) => `${label}×${count}`).join(", ");
|
|
220
336
|
}
|
|
221
337
|
|
|
222
338
|
let nextGroupId = 1;
|
|
@@ -378,42 +494,27 @@ export class ToolGroupComponent extends Container {
|
|
|
378
494
|
const fg = (color: string, text: string) => theme?.fg?.(color, text) ?? text;
|
|
379
495
|
const counts = { pending: 0, success: 0, error: 0 };
|
|
380
496
|
for (const tool of this.children) counts[status(tool)]++;
|
|
381
|
-
const countText = (
|
|
382
|
-
.filter((key) => counts[key])
|
|
383
|
-
.map((key) => {
|
|
384
|
-
const label = key === "pending" ? "running" : key === "success" ? "done" : "failed";
|
|
385
|
-
const color = key === "pending" ? "accent" : key;
|
|
386
|
-
return `${fg(color, String(counts[key]))} ${label}`;
|
|
387
|
-
})
|
|
388
|
-
.join(` ${fg("dim", "·")} `);
|
|
497
|
+
const countText = groupCountText(counts, fg);
|
|
389
498
|
const names = new Set(this.children.map(toolName));
|
|
390
499
|
const label = names.size === 1 ? toolTitle(this.children[0]) : "Multiple Tools";
|
|
391
|
-
const overall
|
|
392
|
-
if (
|
|
393
|
-
(this.children as any[]).some((tool) => tool?.executionStarted && status(tool) === "pending")
|
|
394
|
-
)
|
|
395
|
-
scheduleGroupAnimation(this.patch);
|
|
500
|
+
const overall = groupOverallStatus(counts);
|
|
501
|
+
if ((this.children as any[]).some(isAnimatingTool)) scheduleGroupAnimation(this.patch);
|
|
396
502
|
const overallColor = overall === "pending" ? "accent" : overall;
|
|
397
|
-
const nameList = names.size > 1 ? ` ${fg("dim", `· ${toolNameList(this.children)}`)}` : "";
|
|
398
503
|
// 圆点保持 dim;hover 只高亮可点击文字。
|
|
399
504
|
const hintText = this._expanded ? collapseHintText() : showMoreHintText();
|
|
400
505
|
const hint = `${fg("dim", "·")} ${fg(this.hintHovered ? "text" : "dim", hintText)}`;
|
|
401
|
-
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
const viewport = toolViewportWidth(width);
|
|
506
|
+
const rowWidth = toolViewportWidth(width);
|
|
507
|
+
// 工具列表让位给展开提示:提示被截断的话,鼠标层要在行尾找 `click to show more`
|
|
508
|
+
// 就找不到,整张卡在窄宽度下点不开。名字截断后子行里仍能看全。
|
|
405
509
|
const head = ` ${fg(overallColor, "●")} ${label}: ${countText}`;
|
|
406
|
-
const
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const headBudget = viewport - hintWidth - visibleWidth(elidedName);
|
|
415
|
-
const titleLine = `${visibleWidth(head) <= headBudget ? head : truncateToWidth(head, headBudget, "…")}${elidedName} ${hint}`;
|
|
416
|
-
const lines = ["", truncateToWidth(titleLine, viewport, "…")];
|
|
510
|
+
const tail = ` ${hint}`;
|
|
511
|
+
const listRoom = rowWidth - visibleWidth(head) - visibleWidth(tail) - 3;
|
|
512
|
+
// 连一个工具名都放不下就不放列表:退化成 `· …` 只会把行撑爆。
|
|
513
|
+
const nameList =
|
|
514
|
+
names.size > 1 && listRoom >= 4
|
|
515
|
+
? ` ${fg("dim", `· ${headTruncateToWidth(toolNameList(this.children), listRoom)}`)}`
|
|
516
|
+
: "";
|
|
517
|
+
const lines = ["", truncateToWidth(`${head}${nameList}${tail}`, rowWidth, "")];
|
|
417
518
|
const total = this.children.length;
|
|
418
519
|
const childPaints = this._expanded
|
|
419
520
|
? (this.children as any[]).map((tool) => tool.render?.(Math.max(1, width - 2)))
|
|
@@ -441,19 +542,7 @@ export class ToolGroupComponent extends Container {
|
|
|
441
542
|
const branch = index === total - 1 ? "└" : "├";
|
|
442
543
|
const continuation = index === total - 1 ? " " : "│ ";
|
|
443
544
|
if (!this._expanded) {
|
|
444
|
-
|
|
445
|
-
const prefix = ` ${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} `;
|
|
446
|
-
const detail = fg("dim", summary.detail);
|
|
447
|
-
// 与单工具卡标题同宽,宽屏右侧留白一致
|
|
448
|
-
const rowWidth = toolViewportWidth(width);
|
|
449
|
-
const mainWidth = Math.max(0, rowWidth - visibleWidth(prefix) - visibleWidth(detail));
|
|
450
|
-
lines.push(
|
|
451
|
-
truncateToWidth(
|
|
452
|
-
`${prefix}${renderToolSummary(summary, mainWidth, fg)}${detail}`,
|
|
453
|
-
rowWidth,
|
|
454
|
-
"",
|
|
455
|
-
),
|
|
456
|
-
);
|
|
545
|
+
lines.push(collapsedGroupToolLine(tool, index === total - 1, fg, width));
|
|
457
546
|
continue;
|
|
458
547
|
}
|
|
459
548
|
const rendered = visibleLines(Array.isArray(childPaints?.[index]) ? childPaints[index] : []);
|
package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/message-display.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import { Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
7
7
|
import { MESSAGE_DISPLAY_PATCH, patchRegistry } from "../../utils/patch-keys.ts";
|
|
8
8
|
import { config } from "../../config/config.ts";
|
|
9
|
-
import { showMoreHintText } from "./
|
|
9
|
+
import { showMoreHintText } from "./names.ts";
|
|
10
10
|
import { walkComponentTree } from "../../utils/component-tree.ts";
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { posix, win32 } from "node:path";
|
|
2
2
|
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { getKeybindings, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
4
|
import { config } from "../../config/config.ts";
|
|
5
5
|
import { oneLine } from "../../utils/format.ts";
|
|
6
6
|
import { headTruncateToWidth } from "./result.ts";
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/** toolLabelClip=0 时不设字符上限,交给渲染时的实际宽度截断。 */
|
|
9
9
|
function clipLimit(): number {
|
|
10
|
-
return config.
|
|
10
|
+
return config.toolLabelClip > 0 ? config.toolLabelClip : Number.POSITIVE_INFINITY;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function clip(value: unknown): string {
|
|
@@ -157,7 +157,7 @@ function pathSummary(
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/** 入参是 shell 命令的工具。 */
|
|
160
|
-
const SHELL_TOOL_NAMES = new Set(["bash", "powershell", "pwsh", "sh", "zsh", "cmd"]);
|
|
160
|
+
export const SHELL_TOOL_NAMES = new Set(["bash", "powershell", "pwsh", "sh", "zsh", "cmd"]);
|
|
161
161
|
|
|
162
162
|
/** 内置检索工具:pattern 缺失(无效调用)时保留 "…" 占位。 */
|
|
163
163
|
const SEARCH_TOOL_NAMES = new Set(["grep", "find"]);
|
|
@@ -427,3 +427,43 @@ export function renderToolSummary(
|
|
|
427
427
|
? `${fg("toolTitle", title)}${fg("dim", headTruncateToWidth(` ${summary.payload}`, room))}`
|
|
428
428
|
: fg("toolTitle", title);
|
|
429
429
|
}
|
|
430
|
+
|
|
431
|
+
let toolTuiFullscreen: boolean | undefined;
|
|
432
|
+
|
|
433
|
+
export function setToolTuiFullscreen(value: boolean): void {
|
|
434
|
+
toolTuiFullscreen = value;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function isToolTuiFullscreen(): boolean {
|
|
438
|
+
return toolTuiFullscreen !== false;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function formatKeyPart(part: string): string {
|
|
442
|
+
// 与 pi 内置 keybinding-hints 一致:macOS 上 alt 显示为 option
|
|
443
|
+
return process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** 默认展开快捷键(app.tools.expand)显示文本;未配置时回退 ctrl+o。 */
|
|
447
|
+
export function expandShortcutText(): string {
|
|
448
|
+
const keys = getKeybindings().getKeys("app.tools.expand");
|
|
449
|
+
if (keys.length === 0) return "ctrl+o";
|
|
450
|
+
return keys
|
|
451
|
+
.join("/")
|
|
452
|
+
.split("/")
|
|
453
|
+
.map((part) => part.split("+").map(formatKeyPart).join("+"))
|
|
454
|
+
.join("/");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function showMoreHintText(): string {
|
|
458
|
+
return isToolTuiFullscreen() ? "click to show more" : `${expandShortcutText()} to show more`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** 展开卡的收起提示,与折叠态 showMoreHintText 同一位置对称。 */
|
|
462
|
+
export function collapseHintText(): string {
|
|
463
|
+
return isToolTuiFullscreen() ? "↑ Collapse" : `${expandShortcutText()} to collapse`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** 该行是否带展开态收起提示(用于展开卡的 hint hover 命中)。 */
|
|
467
|
+
export function isCollapseHintLine(plainLine: string): boolean {
|
|
468
|
+
return plainLine.includes(collapseHintText());
|
|
469
|
+
}
|