pi-cc-extensions 0.9.9 → 0.9.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 +49 -50
- package/README.md +7 -8
- package/extensions/config/config.ts +48 -8
- package/extensions/config/panel.ts +158 -30
- package/extensions/feature/compact-thinking.ts +19 -16
- package/extensions/feature/shell/footer-currency.ts +29 -0
- package/extensions/feature/shell/footer.ts +52 -7
- package/extensions/feature/shell/mcp-chip.ts +136 -0
- package/extensions/renderer/compact-mode.ts +78 -14
- package/extensions/renderer/default-mode.ts +51 -13
- package/extensions/renderer/mouse/interaction.ts +32 -21
- package/extensions/renderer/tool/codemode.ts +306 -0
- package/extensions/renderer/tool/diff/diff-header.ts +1 -1
- package/extensions/renderer/tool/diff/diff-highlight.ts +36 -0
- package/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
- package/extensions/renderer/tool/diff/diff-write-render.ts +2 -2
- package/extensions/renderer/tool/diff/line-width-safety.ts +6 -5
- package/extensions/renderer/tool/grouping.ts +31 -6
- package/extensions/renderer/tool/message-display.ts +3 -3
- package/extensions/renderer/tool/names.ts +3 -0
- package/extensions/renderer/tool/result.ts +173 -36
- package/extensions/utils/ansi-text.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codemode 的结果渲染:折叠态把子调用摊成工具组那样的树,最后一行用 └ 收尾。
|
|
3
|
+
*
|
|
4
|
+
* 数据来自 `result.details.calls`(CodemodeNestedCall[],partial 更新里也有),所以运行中就能
|
|
5
|
+
* 看到子调用进度。两个坑:
|
|
6
|
+
*
|
|
7
|
+
* - 子调用不会单独成行:pi 会跳过带 `parentToolCallId` 的 tool_execution_start,进度只能在这里显示。
|
|
8
|
+
* - 输出以 "Script completed\nWall time …\nOutput:\n" 开头(pi 原生也切掉),行数统计与展开正文都得去掉它,
|
|
9
|
+
* 否则折叠摘要会多算 3 行。
|
|
10
|
+
*/
|
|
11
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
12
|
+
import { stripAnsi } from "../../utils/ansi-text.ts";
|
|
13
|
+
import { toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
|
|
14
|
+
import { sanitizeToolResultText } from "../../utils/tool-result-sanitize.ts";
|
|
15
|
+
import { mcpToolTitle } from "./mcp-title.ts";
|
|
16
|
+
import {
|
|
17
|
+
type ToolCallSummary,
|
|
18
|
+
humanizeToolLabel,
|
|
19
|
+
renderToolSummary,
|
|
20
|
+
toolCallSummary,
|
|
21
|
+
} from "./names.ts";
|
|
22
|
+
import { showMoreHintText } from "./show-more-hint.ts";
|
|
23
|
+
import { headTruncateToWidth, toolViewportWidth } from "./result.ts";
|
|
24
|
+
|
|
25
|
+
export const CODEMODE_TOOL_NAME = "codemode";
|
|
26
|
+
|
|
27
|
+
/** 折叠态最多列几条子调用(与 pi 原生一致,更早的折叠成一行)。 */
|
|
28
|
+
const COLLAPSED_CALL_LIMIT = 8;
|
|
29
|
+
/** 折叠态正文缩进,与 ` ↳ …` 对齐。 */
|
|
30
|
+
const INDENT = " ";
|
|
31
|
+
|
|
32
|
+
/** 脚本结果头;只有整块正文恰好等于它时才算头。 */
|
|
33
|
+
const SCRIPT_HEADER = /^Script (completed|failed)\nWall time [\d.]+ seconds\nOutput:\n$/;
|
|
34
|
+
|
|
35
|
+
export type CodemodeNestedCall = {
|
|
36
|
+
name?: unknown;
|
|
37
|
+
/** 紧凑 JSON;过长时 pi 会截断,截断后 JSON.parse 会失败。 */
|
|
38
|
+
args?: unknown;
|
|
39
|
+
status?: unknown;
|
|
40
|
+
durationMs?: unknown;
|
|
41
|
+
error?: unknown;
|
|
42
|
+
cost?: unknown;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function codemodeCalls(result: any): CodemodeNestedCall[] {
|
|
46
|
+
const calls = result?.details?.calls;
|
|
47
|
+
return Array.isArray(calls) ? calls : [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 去掉脚本头之后的输出正文(已清理终端转义)。 */
|
|
51
|
+
export function codemodeOutputText(result: any): string {
|
|
52
|
+
const content = Array.isArray(result?.content) ? result.content : [];
|
|
53
|
+
const blocks = content
|
|
54
|
+
.filter((item: any) => item?.type === "text")
|
|
55
|
+
.map((item: any) => String(item?.text ?? ""));
|
|
56
|
+
if (blocks.length > 0 && SCRIPT_HEADER.test(blocks[0]!)) blocks.shift();
|
|
57
|
+
return sanitizeToolResultText(blocks.join("\n"), 16_384).trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function codemodeOutputLineCount(result: any): number {
|
|
61
|
+
const text = codemodeOutputText(result).replace(/\n+$/, "");
|
|
62
|
+
return text ? text.split("\n").length : 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 汇总行片段(按重要度先后排列,渲染时从尾部丢以适配宽度)。 */
|
|
66
|
+
export function codemodeSummaryParts(
|
|
67
|
+
calls: readonly CodemodeNestedCall[],
|
|
68
|
+
outputLines: number,
|
|
69
|
+
running: boolean,
|
|
70
|
+
): string[] {
|
|
71
|
+
const total = calls.length;
|
|
72
|
+
const failed = calls.filter((call) => call?.status === "error").length;
|
|
73
|
+
const runningCount = calls.filter((call) => !call?.status || call?.status === "running").length;
|
|
74
|
+
const parts: string[] = [];
|
|
75
|
+
if (running) {
|
|
76
|
+
if (runningCount)
|
|
77
|
+
parts.push(`${runningCount} ${runningCount === 1 ? "call" : "calls"} running`);
|
|
78
|
+
const done = total - runningCount;
|
|
79
|
+
if (done) parts.push(`${done} done`);
|
|
80
|
+
if (failed) parts.push(`${failed} failed`);
|
|
81
|
+
return parts.length ? parts : ["running…"];
|
|
82
|
+
}
|
|
83
|
+
if (total) parts.push(`${total} ${total === 1 ? "call" : "calls"}`);
|
|
84
|
+
if (failed) parts.push(`${failed} failed`);
|
|
85
|
+
if (outputLines) parts.push(`${outputLines} ${outputLines === 1 ? "line" : "lines"} output`);
|
|
86
|
+
return parts.length ? parts : ["Done"];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 宽度不够时从尾部丢片段(先丢输出行数,再丢条数),不断词。 */
|
|
90
|
+
function fitSummaryParts(parts: readonly string[], width: number): string {
|
|
91
|
+
const kept = [...parts];
|
|
92
|
+
while (kept.length > 1 && visibleWidth(kept.join(" · ")) > width) kept.pop();
|
|
93
|
+
const text = kept.join(" · ");
|
|
94
|
+
return visibleWidth(text) <= width ? text : headTruncateToWidth(text, width);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatDuration(ms: number): string {
|
|
98
|
+
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 与 pi 原生一致:不到 1 分显示两位有效数字。 */
|
|
102
|
+
function formatCost(cost: number): string {
|
|
103
|
+
return `$${cost >= 0.01 ? cost.toFixed(2) : cost.toPrecision(2)}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 单次子调用的耗时/费用(纯文本,着色由调用方决定)。 */
|
|
107
|
+
function callMeta(call: CodemodeNestedCall): string[] {
|
|
108
|
+
const out: string[] = [];
|
|
109
|
+
const duration = call?.durationMs;
|
|
110
|
+
if (typeof duration === "number" && Number.isFinite(duration) && duration >= 0) {
|
|
111
|
+
out.push(formatDuration(duration));
|
|
112
|
+
}
|
|
113
|
+
const cost = call?.cost;
|
|
114
|
+
if (typeof cost === "number" && Number.isFinite(cost) && cost > 0) out.push(formatCost(cost));
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 截断的紧凑 JSON 里能恢复出的对象;complete 标记是否原本就完整。 */
|
|
119
|
+
type ParsedCallArgs = { args: Record<string, unknown>; complete: boolean };
|
|
120
|
+
|
|
121
|
+
/** 截断串尾部补闭合符号的候选:先试收字符串,再收数组与对象。 */
|
|
122
|
+
const REPAIR_CLOSERS = ['"}', '"}]}', '"}}', "}", "]}"];
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 子调用参数:先直接解析;pi 从尾部截断过(超 200 字符)的串再补救一次——
|
|
126
|
+
* 逐字回退补上缺少的闭合符号,取最长能解析出的对象。截断保留的是开头字段,
|
|
127
|
+
* path/command/pattern 这类先出现的取得到;被切掉的尾部值本就无从恢复。
|
|
128
|
+
*/
|
|
129
|
+
function parseCallArgs(raw: string): ParsedCallArgs | undefined {
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(raw);
|
|
132
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
133
|
+
return { args: parsed, complete: true };
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
} catch {
|
|
137
|
+
// 截断串:走补救
|
|
138
|
+
}
|
|
139
|
+
const body = raw.endsWith("...") ? raw.slice(0, -3) : raw;
|
|
140
|
+
if (!body.startsWith("{")) return undefined;
|
|
141
|
+
for (let end = body.length; end > 0; end--) {
|
|
142
|
+
for (const closer of REPAIR_CLOSERS) {
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(body.slice(0, end) + closer);
|
|
145
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
146
|
+
// 空对象等于没恢复出字段,再短也只会更空
|
|
147
|
+
if (Object.keys(parsed).length === 0) return undefined;
|
|
148
|
+
return { args: parsed, complete: false };
|
|
149
|
+
} catch {
|
|
150
|
+
// 试下一个闭合符号 / 下一个长度
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 子调用参数:能解析就复用字段链摘要,认不出(截断后也不成形)就贴原文。 */
|
|
158
|
+
function callSummary(call: CodemodeNestedCall): ToolCallSummary {
|
|
159
|
+
const name = String(call?.name ?? "tool");
|
|
160
|
+
const title = mcpToolTitle({ toolName: name }) ?? humanizeToolLabel(name);
|
|
161
|
+
const raw = typeof call?.args === "string" ? call.args : "";
|
|
162
|
+
if (!raw) return { main: title, detail: "" };
|
|
163
|
+
const parsed = parseCallArgs(raw);
|
|
164
|
+
if (parsed) {
|
|
165
|
+
const summary = toolCallSummary(name, parsed.args, { title, variant: "default" });
|
|
166
|
+
// 截断串只恢复出部分字段:字段链认出来了才用,认不出仍旧贴原文,
|
|
167
|
+
// 免得把不完整的 JSON 当成完整入参摊开。
|
|
168
|
+
if (parsed.complete || summary.payload === undefined) return summary;
|
|
169
|
+
}
|
|
170
|
+
return { main: title, detail: "", payload: raw };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function statusIcon(status: unknown, theme: any): string {
|
|
174
|
+
switch (status) {
|
|
175
|
+
case "ok":
|
|
176
|
+
return theme.fg("success", "✓");
|
|
177
|
+
case "error":
|
|
178
|
+
return theme.fg("error", "✗");
|
|
179
|
+
case "cancelled":
|
|
180
|
+
return theme.fg("muted", "⊘");
|
|
181
|
+
default:
|
|
182
|
+
// 运行中:帧在 render() 内取,动画定时器只负责 requestRender
|
|
183
|
+
return theme.fg("accent", toolLoadingIcon());
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function callRow(call: CodemodeNestedCall, theme: any, rowWidth: number): string {
|
|
188
|
+
const fg = theme.fg.bind(theme);
|
|
189
|
+
const prefix = `${INDENT}${fg("dim", "├")} ${statusIcon(call?.status, theme)} `;
|
|
190
|
+
// detail(read 的 offset/limit 等)与工具组行一样接在标题后
|
|
191
|
+
const summary = callSummary(call);
|
|
192
|
+
const detail = fg("dim", summary.detail);
|
|
193
|
+
const suffix = callMeta(call)
|
|
194
|
+
.map((text) => ` ${fg("dim", text)}`)
|
|
195
|
+
.join("");
|
|
196
|
+
const mainWidth = Math.max(
|
|
197
|
+
0,
|
|
198
|
+
rowWidth - visibleWidth(prefix) - visibleWidth(detail) - visibleWidth(suffix),
|
|
199
|
+
);
|
|
200
|
+
const row = `${prefix}${renderToolSummary(summary, mainWidth, fg)}${detail}${suffix}`;
|
|
201
|
+
return truncateToWidth(row, rowWidth, "");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 折叠态(含运行中的 partial 更新)的行:子调用全用 ├,最后一行用 └ 收汇总,
|
|
206
|
+
* 与工具组的树一致。
|
|
207
|
+
*/
|
|
208
|
+
export function codemodeCollapsedLines(options: {
|
|
209
|
+
result: any;
|
|
210
|
+
theme: any;
|
|
211
|
+
running: boolean;
|
|
212
|
+
isError: boolean;
|
|
213
|
+
width: number;
|
|
214
|
+
/** 鼠标悬停整卡时展开提示用 text 色,与其它折叠卡一致。 */
|
|
215
|
+
hovered?: boolean;
|
|
216
|
+
}): string[] {
|
|
217
|
+
const { result, theme, running, isError } = options;
|
|
218
|
+
const fg = theme.fg.bind(theme);
|
|
219
|
+
const calls = codemodeCalls(result);
|
|
220
|
+
const rowWidth = toolViewportWidth(options.width);
|
|
221
|
+
const shown = calls.slice(-COLLAPSED_CALL_LIMIT);
|
|
222
|
+
const lines: string[] = [];
|
|
223
|
+
if (shown.length < calls.length) {
|
|
224
|
+
const hidden = calls.length - shown.length;
|
|
225
|
+
lines.push(
|
|
226
|
+
truncateToWidth(
|
|
227
|
+
`${INDENT}${fg("dim", "├")} ${fg("dim", `… ${hidden} earlier ${hidden === 1 ? "call" : "calls"}`)}`,
|
|
228
|
+
rowWidth,
|
|
229
|
+
"",
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
for (const call of shown) lines.push(callRow(call, theme, rowWidth));
|
|
234
|
+
|
|
235
|
+
const summary = codemodeSummaryParts(calls, codemodeOutputLineCount(result), running);
|
|
236
|
+
// 没有子调用时不画衔接符,避免孤零零一个 └
|
|
237
|
+
const prefix = shown.length > 0 ? `${INDENT}${fg("dim", "└")} ` : INDENT;
|
|
238
|
+
// 圆点保持 dim;hover 只高亮可点击文字。
|
|
239
|
+
const hint = running
|
|
240
|
+
? ""
|
|
241
|
+
: ` ${fg("dim", "·")} ${fg(options.hovered ? "text" : "dim", showMoreHintText())}`;
|
|
242
|
+
const bodyWidth = Math.max(1, rowWidth - visibleWidth(prefix) - visibleWidth(hint));
|
|
243
|
+
const body = fg(isError ? "error" : "muted", fitSummaryParts(summary, bodyWidth));
|
|
244
|
+
lines.push(truncateToWidth(`${prefix}${body}${hint}`, rowWidth, ""));
|
|
245
|
+
return lines;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** 展开态正文:全部子调用(含错误文本)+ 去掉脚本头的输出 + 全量输出路径。 */
|
|
249
|
+
export function codemodeExpandedBody(result: any): string {
|
|
250
|
+
const parts: string[] = [];
|
|
251
|
+
const calls = codemodeCalls(result);
|
|
252
|
+
if (calls.length > 0) {
|
|
253
|
+
parts.push(
|
|
254
|
+
calls
|
|
255
|
+
.map((call) => {
|
|
256
|
+
const name = String(call?.name ?? "tool");
|
|
257
|
+
const raw = typeof call?.args === "string" ? call.args : "";
|
|
258
|
+
const meta = callMeta(call);
|
|
259
|
+
const head = [name, raw, ...meta].filter(Boolean).join(" ");
|
|
260
|
+
const error =
|
|
261
|
+
typeof call?.error === "string" && call.error
|
|
262
|
+
? `\n ${call.error.split("\n").join("\n ")}`
|
|
263
|
+
: "";
|
|
264
|
+
return `${head}${error}`;
|
|
265
|
+
})
|
|
266
|
+
.join("\n"),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
const output = codemodeOutputText(result);
|
|
270
|
+
if (output) parts.push(output);
|
|
271
|
+
const full = result?.details?.fullOutputPath;
|
|
272
|
+
if (typeof full === "string" && full.trim()) parts.push(`Full output: ${full.trim()}`);
|
|
273
|
+
return parts.join("\n\n");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 折叠态结果组件。只有汇总行是展开入口,与 diff 卡一致;行本身很便宜,
|
|
278
|
+
* 不自己缓存(settled 卡另有整行 paint 缓存,运行中本就按帧重算)。hover 状态在
|
|
279
|
+
* render() 内取,才能跟鼠标 motion 的 requestRender 同步。
|
|
280
|
+
*/
|
|
281
|
+
export function createCodemodeResultComponent(options: {
|
|
282
|
+
result: any;
|
|
283
|
+
theme: any;
|
|
284
|
+
running: boolean;
|
|
285
|
+
isError: boolean;
|
|
286
|
+
/** 折叠态 hover 查询,与 diff 卡同一套(mouse/hover)。 */
|
|
287
|
+
isHovered?: () => boolean;
|
|
288
|
+
}): any {
|
|
289
|
+
const { result, theme, running, isError } = options;
|
|
290
|
+
return {
|
|
291
|
+
render(width: number): string[] {
|
|
292
|
+
return codemodeCollapsedLines({
|
|
293
|
+
result,
|
|
294
|
+
theme,
|
|
295
|
+
running,
|
|
296
|
+
isError,
|
|
297
|
+
width,
|
|
298
|
+
hovered: options.isHovered?.() ?? false,
|
|
299
|
+
});
|
|
300
|
+
},
|
|
301
|
+
invalidate(): void {},
|
|
302
|
+
isCollapsedHintLine(plainLine: string): boolean {
|
|
303
|
+
return !running && /(click to show more|to show more)\s*$/.test(stripAnsi(plainLine));
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
@@ -81,7 +81,7 @@ export function renderHeaderRows(
|
|
|
81
81
|
|
|
82
82
|
const summaryPieces = [...buildDiffSummaryBasePieces(stats, theme), theme.fg("muted", mode)];
|
|
83
83
|
|
|
84
|
-
const summary = summaryPieces.join(mode === "split" ? " " : theme.fg("muted", "
|
|
84
|
+
const summary = summaryPieces.join(mode === "split" ? " " : theme.fg("muted", " · "));
|
|
85
85
|
const meter = renderDiffStatBar(stats, width, theme);
|
|
86
86
|
if (!meter) {
|
|
87
87
|
return [{ text: stabilizeBackgroundResets(truncateToWidth(summary, width)), hunkIndex: null }];
|
|
@@ -91,6 +91,42 @@ export function createCodeLineHighlighter(
|
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* 通用代码块着色(codemode 的 `code` 字段等):先用 pi 的同步 highlightCode 打底,
|
|
96
|
+
* 首帧就有颜色;再用 shiki 异步升级(与 diff 共用缓存与主题解析),落地后回调
|
|
97
|
+
* invalidate 重画。返回行数组,按源行一一对应;语言不识或未就绪时给打底结果。
|
|
98
|
+
*/
|
|
99
|
+
export function createCodeBlockHighlighter(
|
|
100
|
+
code: string,
|
|
101
|
+
language: string | undefined,
|
|
102
|
+
theme: DiffTheme,
|
|
103
|
+
invalidate?: () => void,
|
|
104
|
+
): () => string[] {
|
|
105
|
+
const cleanCode = sanitizeToolResultText(code).replace(/\r/g, "");
|
|
106
|
+
const cleanLines = cleanCode.split("\n").map(cleanCodeLine);
|
|
107
|
+
if (!language || !shouldHighlightCodeBlock(cleanCode)) {
|
|
108
|
+
return () => cleanLines.map(sanitizeAnsiForThemedOutput);
|
|
109
|
+
}
|
|
110
|
+
let fallback: string[];
|
|
111
|
+
try {
|
|
112
|
+
fallback = highlightCode(cleanCode, language).map(sanitizeAnsiForThemedOutput);
|
|
113
|
+
} catch {
|
|
114
|
+
fallback = cleanLines.map(sanitizeAnsiForThemedOutput);
|
|
115
|
+
}
|
|
116
|
+
const shikiTheme = resolveShikiTheme(theme);
|
|
117
|
+
let highlighted: string[] | undefined;
|
|
118
|
+
const resolve = () => {
|
|
119
|
+
if (highlighted) return;
|
|
120
|
+
const result = shikiHighlightCache.get(cleanCode, language, shikiTheme, fallback, invalidate);
|
|
121
|
+
if (result) highlighted = result.map(sanitizeAnsiForThemedOutput);
|
|
122
|
+
};
|
|
123
|
+
resolve();
|
|
124
|
+
return () => {
|
|
125
|
+
resolve();
|
|
126
|
+
return highlighted ?? fallback;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
94
130
|
export function highlightDiffLine(
|
|
95
131
|
codeText: string,
|
|
96
132
|
entry: DiffLineEntry,
|
|
@@ -52,8 +52,8 @@ export function buildDiffSummaryText(stats: DiffSummaryStats, width: number): st
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
const summaryCandidates = [
|
|
55
|
-
`↳ diff +${stats.added} -${stats.removed}
|
|
56
|
-
`↳ diff +${stats.added} -${stats.removed}
|
|
55
|
+
`↳ diff +${stats.added} -${stats.removed} · ${stats.hunks} ${pluralize(stats.hunks, "hunk")} · ${stats.files} ${pluralize(stats.files, "file")}`,
|
|
56
|
+
`↳ diff +${stats.added} -${stats.removed} · ${stats.hunks}h · ${stats.files}f`,
|
|
57
57
|
`↳ diff +${stats.added} -${stats.removed}`,
|
|
58
58
|
`+${stats.added} -${stats.removed}`,
|
|
59
59
|
"diff",
|
|
@@ -290,7 +290,7 @@ function renderWriteOverwriteGuardRows(
|
|
|
290
290
|
return renderSingleDiffRow(buildWriteOverwriteGuardText(guard, width), "warning", width, theme);
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
/** Folded write with `writeDiffCollapsedLines: 0`: `↳ created
|
|
293
|
+
/** Folded write with `writeDiffCollapsedLines: 0`: `↳ created · click to show more`. */
|
|
294
294
|
function renderWriteCollapsedHintLine(
|
|
295
295
|
wasOverwrite: boolean,
|
|
296
296
|
width: number,
|
|
@@ -304,7 +304,7 @@ function renderWriteCollapsedHintLine(
|
|
|
304
304
|
}
|
|
305
305
|
const actionLabel = headerLabel?.trim() || (wasOverwrite ? "overwritten" : "created");
|
|
306
306
|
const clickLabel = showMoreHintText();
|
|
307
|
-
const candidates = [`↳ ${actionLabel}
|
|
307
|
+
const candidates = [`↳ ${actionLabel} · ${clickLabel}`, `↳ ${actionLabel}`, actionLabel, "…"];
|
|
308
308
|
let text = candidates[candidates.length - 1] ?? "…";
|
|
309
309
|
for (const candidate of candidates) {
|
|
310
310
|
if (visibleWidth(candidate) <= safeWidth) {
|
|
@@ -71,13 +71,14 @@ export function buildCollapsedDiffHintText(
|
|
|
71
71
|
? `${options.hiddenHunks} ${pluralize(options.hiddenHunks, "hunk")}`
|
|
72
72
|
: undefined;
|
|
73
73
|
|
|
74
|
+
// 括号去掉:与工具卡的同名提示一致,只用小圆点连接
|
|
74
75
|
const candidates = [
|
|
75
|
-
`…
|
|
76
|
-
`…
|
|
77
|
-
`…
|
|
76
|
+
`… ${[remainingText, hiddenHunksText, showMoreHintText()].filter(Boolean).join(" · ")}`,
|
|
77
|
+
`… ${[remainingText, hiddenHunksText].filter(Boolean).join(" · ")}`,
|
|
78
|
+
`… ${[shortRemainingText, shortHiddenHunksText].filter(Boolean).join(" · ")}`,
|
|
78
79
|
options.hiddenHunks > 0
|
|
79
|
-
? `…
|
|
80
|
-
: `…
|
|
80
|
+
? `… +${options.remainingLines} · +${options.hiddenHunks}h`
|
|
81
|
+
: `… +${options.remainingLines}`,
|
|
81
82
|
"…",
|
|
82
83
|
];
|
|
83
84
|
|
|
@@ -6,7 +6,14 @@ import {
|
|
|
6
6
|
visibleWidth,
|
|
7
7
|
type Component,
|
|
8
8
|
} from "@earendil-works/pi-tui";
|
|
9
|
+
import { config } from "../../config/config.ts";
|
|
9
10
|
import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
|
|
11
|
+
import {
|
|
12
|
+
CODEMODE_TOOL_NAME,
|
|
13
|
+
codemodeCalls,
|
|
14
|
+
codemodeOutputLineCount,
|
|
15
|
+
codemodeSummaryParts,
|
|
16
|
+
} from "./codemode.ts";
|
|
10
17
|
import { getToolMouseTui } from "../mouse/scroll.ts";
|
|
11
18
|
import { mcpToolTitle } from "./mcp-title.ts";
|
|
12
19
|
import { collapseHintText, isToolTuiFullscreen, showMoreHintText } from "./show-more-hint.ts";
|
|
@@ -192,6 +199,20 @@ function toolSummary(tool: any): ToolCallSummary {
|
|
|
192
199
|
});
|
|
193
200
|
}
|
|
194
201
|
|
|
202
|
+
/**
|
|
203
|
+
* codemode 的折叠行:有结果就报该次调用的统计(与单卡汇总行同一套口径),
|
|
204
|
+
* 免得一张卡里几行长代码预览互相看不出区别。还没结果时退回代码预览。
|
|
205
|
+
*/
|
|
206
|
+
function codemodeResultSummary(tool: any): ToolCallSummary | undefined {
|
|
207
|
+
if (toolName(tool) !== CODEMODE_TOOL_NAME || !tool?.result) return undefined;
|
|
208
|
+
const parts = codemodeSummaryParts(
|
|
209
|
+
codemodeCalls(tool.result),
|
|
210
|
+
codemodeOutputLineCount(tool.result),
|
|
211
|
+
tool?.isPartial === true,
|
|
212
|
+
);
|
|
213
|
+
return { main: `${toolTitle(tool)} ${parts.join(" · ")}`, detail: "" };
|
|
214
|
+
}
|
|
215
|
+
|
|
195
216
|
function toolNameList(tools: any[]): string {
|
|
196
217
|
const counts = new Map<string, number>();
|
|
197
218
|
for (const tool of tools) counts.set(toolName(tool), (counts.get(toolName(tool)) ?? 0) + 1);
|
|
@@ -216,6 +237,8 @@ type ExpandedGroupCache = {
|
|
|
216
237
|
hover: boolean;
|
|
217
238
|
theme: unknown;
|
|
218
239
|
fullscreen: boolean;
|
|
240
|
+
/** 背景槽位参与命中判断:改配置后已展开的分组不能继续用旧底色。 */
|
|
241
|
+
bgSlot: string;
|
|
219
242
|
paints: readonly unknown[];
|
|
220
243
|
lines: string[];
|
|
221
244
|
};
|
|
@@ -362,7 +385,7 @@ export class ToolGroupComponent extends Container {
|
|
|
362
385
|
const color = key === "pending" ? "accent" : key;
|
|
363
386
|
return `${fg(color, String(counts[key]))} ${label}`;
|
|
364
387
|
})
|
|
365
|
-
.join(` ${fg("dim", "
|
|
388
|
+
.join(` ${fg("dim", "·")} `);
|
|
366
389
|
const names = new Set(this.children.map(toolName));
|
|
367
390
|
const label = names.size === 1 ? toolTitle(this.children[0]) : "Multiple Tools";
|
|
368
391
|
const overall: ToolStatus = counts.error ? "error" : counts.pending ? "pending" : "success";
|
|
@@ -371,10 +394,10 @@ export class ToolGroupComponent extends Container {
|
|
|
371
394
|
)
|
|
372
395
|
scheduleGroupAnimation(this.patch);
|
|
373
396
|
const overallColor = overall === "pending" ? "accent" : overall;
|
|
374
|
-
const nameList = names.size > 1 ? ` ${fg("dim",
|
|
397
|
+
const nameList = names.size > 1 ? ` ${fg("dim", `· ${toolNameList(this.children)}`)}` : "";
|
|
375
398
|
// 圆点保持 dim;hover 只高亮可点击文字。
|
|
376
399
|
const hintText = this._expanded ? collapseHintText() : showMoreHintText();
|
|
377
|
-
const hint = `${fg("dim", "
|
|
400
|
+
const hint = `${fg("dim", "·")} ${fg(this.hintHovered ? "text" : "dim", hintText)}`;
|
|
378
401
|
const lines = [
|
|
379
402
|
"",
|
|
380
403
|
truncateToWidth(
|
|
@@ -395,6 +418,7 @@ export class ToolGroupComponent extends Container {
|
|
|
395
418
|
expandedHit.hover === this.hintHovered &&
|
|
396
419
|
expandedHit.theme === this.patch.theme &&
|
|
397
420
|
expandedHit.fullscreen === isToolTuiFullscreen() &&
|
|
421
|
+
expandedHit.bgSlot === config.expandedCardBackground &&
|
|
398
422
|
expandedHit.paints.length === childPaints.length &&
|
|
399
423
|
expandedHit.paints.every((paint, index) => paint === childPaints[index])
|
|
400
424
|
) {
|
|
@@ -409,7 +433,7 @@ export class ToolGroupComponent extends Container {
|
|
|
409
433
|
const branch = index === total - 1 ? "└" : "├";
|
|
410
434
|
const continuation = index === total - 1 ? " " : "│ ";
|
|
411
435
|
if (!this._expanded) {
|
|
412
|
-
const summary = toolSummary(tool);
|
|
436
|
+
const summary = codemodeResultSummary(tool) ?? toolSummary(tool);
|
|
413
437
|
const prefix = ` ${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} `;
|
|
414
438
|
const detail = fg("dim", summary.detail);
|
|
415
439
|
// 与单工具卡标题同宽,宽屏右侧留白一致
|
|
@@ -443,8 +467,8 @@ export class ToolGroupComponent extends Container {
|
|
|
443
467
|
}
|
|
444
468
|
}
|
|
445
469
|
if (this._expanded) {
|
|
446
|
-
//
|
|
447
|
-
const backgroundSlot =
|
|
470
|
+
// 展开面板统一一个背景槽位(ccstyle 约定),不按状态区分。
|
|
471
|
+
const backgroundSlot = config.expandedCardBackground;
|
|
448
472
|
for (const line of expandedLines) {
|
|
449
473
|
lines.push(paddedBackgroundRow(theme, backgroundSlot, line, width));
|
|
450
474
|
}
|
|
@@ -454,6 +478,7 @@ export class ToolGroupComponent extends Container {
|
|
|
454
478
|
hover: this.hintHovered,
|
|
455
479
|
theme: this.patch.theme,
|
|
456
480
|
fullscreen: isToolTuiFullscreen(),
|
|
481
|
+
bgSlot: backgroundSlot,
|
|
457
482
|
paints: childPaints ?? [],
|
|
458
483
|
lines,
|
|
459
484
|
};
|
|
@@ -78,15 +78,15 @@ function renderCcstyle(component: any, kind: DisplayKind): void {
|
|
|
78
78
|
const title = theme.fg("toolTitle", kind.title(component));
|
|
79
79
|
if (!component.expanded) {
|
|
80
80
|
const hovered = component.hintHovered === true;
|
|
81
|
-
const hint = `${theme.fg("dim", "
|
|
81
|
+
const hint = `${theme.fg("dim", " · ")}${theme.fg(hovered ? "text" : "dim", showMoreHintText())}`;
|
|
82
82
|
component.addChild(new Text(`${icon} ${title}${hint}`, 0, 0));
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
|
-
// 展开卡与 tool
|
|
85
|
+
// 展开卡与 tool 一致:同一个展开卡背景 + 上下左右 1 格
|
|
86
86
|
component.paddingX = 1;
|
|
87
87
|
component.paddingY = 1;
|
|
88
88
|
if (typeof theme.bg === "function" && typeof component.setBgFn === "function") {
|
|
89
|
-
component.setBgFn((text: string) => theme.bg(
|
|
89
|
+
component.setBgFn((text: string) => theme.bg(config.expandedCardBackground, text));
|
|
90
90
|
}
|
|
91
91
|
component.addChild(new Text(`${icon} ${title}`, 0, 0));
|
|
92
92
|
component.addChild(new Spacer(1));
|
|
@@ -255,6 +255,9 @@ export function toolCallSummary(
|
|
|
255
255
|
const code = typeof args.code === "string" && args.code ? clip(args.code) : "";
|
|
256
256
|
return code ? { main: title, detail: "", payload: code } : { main: title, detail: "" };
|
|
257
257
|
}
|
|
258
|
+
// 内置 codemode:标题只报工具名。脚本很长,塞进行首既挤又读不出重点;
|
|
259
|
+
// 要看代码展开 Input,要看跑了什么看子调用行。
|
|
260
|
+
if (name === "codemode") return { main: title, detail: "" };
|
|
258
261
|
|
|
259
262
|
const value = (fallback: string, ...keys: string[]) => {
|
|
260
263
|
const found = keys.map((key) => args[key]).find((item) => typeof item === "string" && item);
|