pi-cc-extensions 1.0.4 → 1.0.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.en.md +6 -6
- package/README.md +6 -6
- package/extensions/config/config.ts +16 -27
- package/extensions/config/panel.ts +49 -48
- package/extensions/feature/compact-thinking.ts +2 -3
- package/extensions/feature/context.ts +70 -2
- package/extensions/feature/reference/index.ts +2 -3
- package/extensions/feature/shell/footer-layout.ts +108 -23
- package/extensions/feature/shell/footer.ts +59 -43
- package/extensions/feature/shell/prompt-editor.ts +96 -0
- package/extensions/index.ts +6 -0
- package/extensions/renderer/compact-mode.ts +47 -40
- package/extensions/renderer/default-mode.ts +7 -4
- package/extensions/renderer/mouse/interaction.ts +18 -4
- package/extensions/renderer/mouse/scroll.ts +23 -8
- package/extensions/renderer/mouse/sticky-prompt.ts +2 -2
- package/extensions/renderer/tool/ccstyle-tool.ts +24 -12
- package/extensions/renderer/tool/codemode.ts +121 -6
- package/extensions/renderer/tool/diff/diff-limits.ts +3 -2
- package/extensions/renderer/tool/diff/diff-palette.ts +39 -8
- package/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
- package/extensions/renderer/tool/grouping.ts +13 -13
- package/extensions/renderer/tool/message-display.ts +3 -2
- package/extensions/renderer/tool/names.ts +49 -49
- package/extensions/renderer/tool/result.ts +24 -0
- package/extensions/renderer/tool/tool-state.ts +51 -2
- package/package.json +1 -1
- package/themes/cc-dark.json +72 -71
- package/themes/cc-light.json +72 -71
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
* - 输出以 "Script completed\nWall time …\nOutput:\n" 开头(pi 原生也切掉),行数统计与展开正文都得去掉它,
|
|
9
9
|
* 否则折叠摘要会多算 3 行。
|
|
10
10
|
*/
|
|
11
|
+
import { highlightCode } from "@earendil-works/pi-coding-agent";
|
|
11
12
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
13
|
+
import { config } from "../../config/config.ts";
|
|
12
14
|
import { stripAnsi } from "../../utils/ansi-text.ts";
|
|
15
|
+
import { oneLine } from "../../utils/format.ts";
|
|
13
16
|
import { toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
|
|
14
17
|
import { sanitizeToolResultText } from "../../utils/tool-result-sanitize.ts";
|
|
15
18
|
import { mcpToolTitle } from "./mcp-title.ts";
|
|
@@ -94,6 +97,105 @@ function fitSummaryParts(parts: readonly string[], width: number): string {
|
|
|
94
97
|
return visibleWidth(text) <= width ? text : headTruncateToWidth(text, width);
|
|
95
98
|
}
|
|
96
99
|
|
|
100
|
+
/**
|
|
101
|
+
* 摘要行的打分:调用最能说明脚本在干什么,字符串次之,行长只在前两者相同时决胜。
|
|
102
|
+
* 调用权重必须大于「字符串分 + 行长上限」,否则长赋值会压过短调用。
|
|
103
|
+
*/
|
|
104
|
+
const CODE_SUMMARY_CALL_SCORE = 4;
|
|
105
|
+
const CODE_SUMMARY_STRING_SCORE = 2;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 无子调用时的脚本摘要:取信息量最大的一行语句,压成单行。
|
|
109
|
+
*
|
|
110
|
+
* 要的是一段真实的 JavaScript 片段,折叠行才能用 highlightCode 着色;
|
|
111
|
+
* 标识符链(`a → b → c`)着色器认不出,染不上。跳过注释、空行和 `// @options`。
|
|
112
|
+
* 只是折叠行的提示,不求还原语义;看代码还是展开 Input。
|
|
113
|
+
*/
|
|
114
|
+
export function codemodeCodeSummary(code: unknown): string | undefined {
|
|
115
|
+
if (typeof code !== "string" || !code.trim()) return undefined;
|
|
116
|
+
let best: { text: string; score: number } | undefined;
|
|
117
|
+
let skippingBlock = false;
|
|
118
|
+
for (const raw of code.split("\n")) {
|
|
119
|
+
const line = raw.trim();
|
|
120
|
+
if (skippingBlock) {
|
|
121
|
+
if (line.includes("*/")) skippingBlock = false;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (line.startsWith("/*")) {
|
|
125
|
+
if (!line.includes("*/")) skippingBlock = true;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (!line || line.startsWith("//") || line.startsWith("*")) continue;
|
|
129
|
+
const text = oneLine(line);
|
|
130
|
+
if (!text) continue;
|
|
131
|
+
const score =
|
|
132
|
+
(text.includes("(") ? CODE_SUMMARY_CALL_SCORE : 0) +
|
|
133
|
+
(/["'`]/.test(text) ? CODE_SUMMARY_STRING_SCORE : 0) +
|
|
134
|
+
Math.min(text.length, 100) / 100;
|
|
135
|
+
if (!best || score > best.score) best = { text, score };
|
|
136
|
+
}
|
|
137
|
+
return best?.text;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 没有子调用时的脚本行:语句逐行用 │ 树线列出,行数跟 expandedInputMaxLines。
|
|
142
|
+
* 超出的收成一行 `… N more lines`。注释、空行和 `// @options` 不占行。
|
|
143
|
+
*/
|
|
144
|
+
export function codemodeCodeLines(
|
|
145
|
+
code: unknown,
|
|
146
|
+
fg: (color: string, text: string) => string,
|
|
147
|
+
rowWidth: number,
|
|
148
|
+
): string[] {
|
|
149
|
+
if (typeof code !== "string" || !code.trim()) return [];
|
|
150
|
+
const statements: string[] = [];
|
|
151
|
+
let skippingBlock = false;
|
|
152
|
+
for (const raw of code.split("\n")) {
|
|
153
|
+
const line = raw.trim();
|
|
154
|
+
if (skippingBlock) {
|
|
155
|
+
if (line.includes("*/")) skippingBlock = false;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (line.startsWith("/*")) {
|
|
159
|
+
if (!line.includes("*/")) skippingBlock = true;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (!line || line.startsWith("//") || line.startsWith("*")) continue;
|
|
163
|
+
const text = oneLine(line);
|
|
164
|
+
if (text) statements.push(text);
|
|
165
|
+
}
|
|
166
|
+
if (statements.length === 0) return [];
|
|
167
|
+
const limit = Math.max(1, config.expandedInputMaxLines);
|
|
168
|
+
const visible = statements.slice(0, limit);
|
|
169
|
+
const rail = `${INDENT}${fg("dim", "│")} `;
|
|
170
|
+
const lines = visible.map((statement) =>
|
|
171
|
+
truncateToWidth(`${rail}${highlightCodeSummary(statement)}`, rowWidth, ""),
|
|
172
|
+
);
|
|
173
|
+
const hidden = statements.length - visible.length;
|
|
174
|
+
if (hidden > 0) {
|
|
175
|
+
lines.push(
|
|
176
|
+
truncateToWidth(
|
|
177
|
+
`${rail}${fg("dim", `… ${hidden} more ${hidden === 1 ? "line" : "lines"}`)}`,
|
|
178
|
+
rowWidth,
|
|
179
|
+
"",
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return lines;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 所以包一层 `() => { … }` 再取中间那一行。失败或没着色时退回原文。
|
|
188
|
+
*/
|
|
189
|
+
export function highlightCodeSummary(summary: string): string {
|
|
190
|
+
try {
|
|
191
|
+
const lines = highlightCode(`() => { ${summary} }`, "javascript");
|
|
192
|
+
const colored = lines[0]?.replace(/^\(\) => \{ /, "").replace(/ \}$/, "");
|
|
193
|
+
return colored && stripAnsi(colored) === summary ? colored : summary;
|
|
194
|
+
} catch {
|
|
195
|
+
return summary;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
97
199
|
/** 与 pi 原生一致:不到 1 分显示两位有效数字。 */
|
|
98
200
|
function formatCost(cost: number): string {
|
|
99
201
|
return `$${cost >= 0.01 ? cost.toFixed(2) : cost.toPrecision(2)}`;
|
|
@@ -159,9 +261,12 @@ function callSummary(call: CodemodeNestedCall): ToolCallSummary {
|
|
|
159
261
|
const parsed = parseCallArgs(raw);
|
|
160
262
|
if (parsed) {
|
|
161
263
|
const summary = toolCallSummary(name, parsed.args, { title, variant: "default" });
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
264
|
+
// 截断串只恢复出部分字段:字段链认出来了才用(payload 是字段链挑出的值,
|
|
265
|
+
// path/main 的兜底标题不算);认不出仍旧贴原文,免得把不完整的 JSON 当成完整入参摊开。
|
|
266
|
+
const recognized = summary.payload !== undefined || summary.path !== undefined;
|
|
267
|
+
// 完全还原的入参:标题与载荷分色后的 summary 直接用;恢复了字段的也用结构化
|
|
268
|
+
// 结果(字段链认出的值比截断原文更可读)。认不出的才退回贴原文。
|
|
269
|
+
if (parsed.complete || recognized) return summary;
|
|
165
270
|
}
|
|
166
271
|
return { main: title, detail: "", payload: raw };
|
|
167
272
|
}
|
|
@@ -176,7 +281,7 @@ function statusIcon(status: unknown, theme: any): string {
|
|
|
176
281
|
return theme.fg("muted", "⊘");
|
|
177
282
|
default:
|
|
178
283
|
// 运行中:帧在 render() 内取,动画定时器只负责 requestRender
|
|
179
|
-
return theme.fg("
|
|
284
|
+
return theme.fg("dim", toolLoadingIcon());
|
|
180
285
|
}
|
|
181
286
|
}
|
|
182
287
|
|
|
@@ -209,6 +314,8 @@ export function codemodeCollapsedLines(options: {
|
|
|
209
314
|
width: number;
|
|
210
315
|
/** 鼠标悬停整卡时展开提示用 text 色,与其它折叠卡一致。 */
|
|
211
316
|
hovered?: boolean;
|
|
317
|
+
/** 脚本源码;没有子调用时用来补一行 `code:` 摘要。 */
|
|
318
|
+
code?: unknown;
|
|
212
319
|
}): string[] {
|
|
213
320
|
const { result, theme, running, isError } = options;
|
|
214
321
|
const fg = theme.fg.bind(theme);
|
|
@@ -216,6 +323,11 @@ export function codemodeCollapsedLines(options: {
|
|
|
216
323
|
const rowWidth = toolViewportWidth(options.width);
|
|
217
324
|
const shown = calls.slice(-COLLAPSED_CALL_LIMIT);
|
|
218
325
|
const lines: string[] = [];
|
|
326
|
+
// 没有子调用时脚本本身是唯一线索:逐行用 │ 树线列出,行数跟 expandedInputMaxLines,
|
|
327
|
+
// 汇总行仍用 └ 收尾。
|
|
328
|
+
if (shown.length === 0) {
|
|
329
|
+
lines.push(...codemodeCodeLines(options.code, fg, rowWidth));
|
|
330
|
+
}
|
|
219
331
|
if (shown.length < calls.length) {
|
|
220
332
|
const hidden = calls.length - shown.length;
|
|
221
333
|
lines.push(
|
|
@@ -229,8 +341,8 @@ export function codemodeCollapsedLines(options: {
|
|
|
229
341
|
for (const call of shown) lines.push(callRow(call, theme, rowWidth));
|
|
230
342
|
|
|
231
343
|
const summary = codemodeSummaryParts(calls, codemodeOutputLineCount(result), running);
|
|
232
|
-
//
|
|
233
|
-
const prefix =
|
|
344
|
+
// 上面已有行(子调用或代码摘要)时用 └ 收尾,否则不画衔接符
|
|
345
|
+
const prefix = lines.length > 0 ? `${INDENT}${fg("dim", "└")} ` : INDENT;
|
|
234
346
|
// 圆点保持 dim;hover 只高亮可点击文字。
|
|
235
347
|
const hint = running
|
|
236
348
|
? ""
|
|
@@ -281,6 +393,8 @@ export function createCodemodeResultComponent(options: {
|
|
|
281
393
|
isError: boolean;
|
|
282
394
|
/** 折叠态 hover 查询,与 diff 卡同一套(mouse/hover)。 */
|
|
283
395
|
isHovered?: () => boolean;
|
|
396
|
+
/** 脚本源码;没有子调用时用来补一行 `code:` 摘要。 */
|
|
397
|
+
code?: unknown;
|
|
284
398
|
}): any {
|
|
285
399
|
const { result, theme, running, isError } = options;
|
|
286
400
|
return {
|
|
@@ -292,6 +406,7 @@ export function createCodemodeResultComponent(options: {
|
|
|
292
406
|
isError,
|
|
293
407
|
width,
|
|
294
408
|
hovered: options.isHovered?.() ?? false,
|
|
409
|
+
code: options.code,
|
|
295
410
|
});
|
|
296
411
|
},
|
|
297
412
|
invalidate(): void {},
|
|
@@ -135,10 +135,11 @@ export function applyLineLimit(
|
|
|
135
135
|
const clickLabel = showMoreHintText();
|
|
136
136
|
const clickIndex = hintText.lastIndexOf(clickLabel);
|
|
137
137
|
// 展开态不会再截断,剩下的分支必是折叠态提示。
|
|
138
|
+
// 提示文本与其余折叠卡一致用 dim,hover 时 clickLabel 提到 text。
|
|
138
139
|
const styledHint =
|
|
139
|
-
|
|
140
|
+
clickIndex >= 0
|
|
140
141
|
? theme.fg("muted", hintText.slice(0, clickIndex)) +
|
|
141
|
-
theme.fg("text", clickLabel) +
|
|
142
|
+
theme.fg(hovered ? "text" : "dim", clickLabel) +
|
|
142
143
|
theme.fg("muted", hintText.slice(clickIndex + clickLabel.length))
|
|
143
144
|
: theme.fg("muted", hintText);
|
|
144
145
|
return {
|
|
@@ -31,12 +31,18 @@ export interface DiffPalette {
|
|
|
31
31
|
removeEmphasisBgAnsi: string;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const ADD_ROW_BACKGROUND_MIX_RATIO = 0.
|
|
34
|
+
const ADD_ROW_BACKGROUND_MIX_RATIO = 0.04;
|
|
35
35
|
const REMOVE_ROW_BACKGROUND_MIX_RATIO = 0.12;
|
|
36
36
|
const ADD_INLINE_EMPHASIS_MIX_RATIO = 0.26;
|
|
37
|
-
const REMOVE_INLINE_EMPHASIS_MIX_RATIO = 0.
|
|
37
|
+
const REMOVE_INLINE_EMPHASIS_MIX_RATIO = 0.5;
|
|
38
38
|
const ADDITION_TINT_TARGET: RgbColor = { r: 84, g: 190, b: 118 };
|
|
39
39
|
const DELETION_TINT_TARGET: RgbColor = { r: 232, g: 95, b: 122 };
|
|
40
|
+
// GitHub dark 删除行底 ~#67060c:混向深酒红目标,压掉主题基底的粉/灰感。
|
|
41
|
+
const DELETION_ROW_TARGET: RgbColor = { r: 103, g: 6, b: 12 };
|
|
42
|
+
const DELETION_EMPHASIS_TARGET: RgbColor = { r: 160, g: 17, b: 23 };
|
|
43
|
+
// 行底/强调底混完再向自身亮度去饱和,留灰感,避免纯色底过艳。
|
|
44
|
+
const ROW_DESATURATE_RATIO = 0.3;
|
|
45
|
+
const EMPHASIS_DESATURATE_RATIO = 0.2;
|
|
40
46
|
export const ANSI_BG_RESET = "\x1b[49m";
|
|
41
47
|
|
|
42
48
|
export function emphasis(theme: DiffTheme, text: string): string {
|
|
@@ -202,6 +208,11 @@ function rgbToBgAnsi(color: RgbColor): string {
|
|
|
202
208
|
return `\x1b[48;2;${r};${g};${b}m`;
|
|
203
209
|
}
|
|
204
210
|
|
|
211
|
+
function desaturateRgb(color: RgbColor, ratio: number): RgbColor {
|
|
212
|
+
const luma = (color.r * 299 + color.g * 587 + color.b * 114) / 1000;
|
|
213
|
+
return mixRgb(color, { r: luma, g: luma, b: luma }, ratio);
|
|
214
|
+
}
|
|
215
|
+
|
|
205
216
|
function mixRgb(base: RgbColor, tint: RgbColor, ratio: number): RgbColor {
|
|
206
217
|
const clamped = Math.max(0, Math.min(1, ratio));
|
|
207
218
|
return {
|
|
@@ -263,9 +274,11 @@ export function readThemeAnsi(
|
|
|
263
274
|
}
|
|
264
275
|
|
|
265
276
|
export function resolveDiffPalette(theme: DiffTheme): DiffPalette {
|
|
266
|
-
const
|
|
267
|
-
parseAnsiColorCode(readThemeAnsi(theme, "bg", "toolPendingBg")) ??
|
|
277
|
+
const fallbackBg = parseAnsiColorCode(readThemeAnsi(theme, "bg", "toolPendingBg")) ??
|
|
268
278
|
parseAnsiColorCode(readThemeAnsi(theme, "bg", "userMessageBg")) ?? { r: 32, g: 35, b: 42 };
|
|
279
|
+
// 删除行用 toolErrorBg 做基底(主题里是 diffDelBg 暗红);不定义时退回通用基底混 tint。
|
|
280
|
+
const addBaseBg = parseAnsiColorCode(readThemeAnsi(theme, "bg", "toolSuccessBg")) ?? fallbackBg;
|
|
281
|
+
const removeBaseBg = parseAnsiColorCode(readThemeAnsi(theme, "bg", "toolErrorBg")) ?? addBaseBg;
|
|
269
282
|
const addFg = parseAnsiColorCode(readThemeAnsi(theme, "fg", "toolDiffAdded")) ?? {
|
|
270
283
|
r: 88,
|
|
271
284
|
g: 173,
|
|
@@ -279,10 +292,28 @@ export function resolveDiffPalette(theme: DiffTheme): DiffPalette {
|
|
|
279
292
|
const addTint = mixRgb(addFg, ADDITION_TINT_TARGET, 0.35);
|
|
280
293
|
const removeTint = mixRgb(removeFg, DELETION_TINT_TARGET, 0.65);
|
|
281
294
|
|
|
282
|
-
const addRowBg =
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
295
|
+
const addRowBg = desaturateRgb(
|
|
296
|
+
mixRgb(addBaseBg, addTint, ADD_ROW_BACKGROUND_MIX_RATIO),
|
|
297
|
+
ROW_DESATURATE_RATIO,
|
|
298
|
+
);
|
|
299
|
+
const addEmphasisBg = desaturateRgb(
|
|
300
|
+
mixRgb(addBaseBg, addTint, ADD_INLINE_EMPHASIS_MIX_RATIO),
|
|
301
|
+
EMPHASIS_DESATURATE_RATIO,
|
|
302
|
+
);
|
|
303
|
+
// 暗色基底下向 GitHub 深酒红(~#67060c)加权,压掉主题基底的粉/灰感;
|
|
304
|
+
// 浅色基底保持原 tint 口径,免得行底深得盖过文字。
|
|
305
|
+
const removeBaseLuminance =
|
|
306
|
+
(removeBaseBg.r * 299 + removeBaseBg.g * 587 + removeBaseBg.b * 114) / 1000;
|
|
307
|
+
const removeRowTarget = removeBaseLuminance < 128 ? DELETION_ROW_TARGET : removeTint;
|
|
308
|
+
const removeEmphasisTarget = removeBaseLuminance < 128 ? DELETION_EMPHASIS_TARGET : removeTint;
|
|
309
|
+
const removeRowBg = desaturateRgb(
|
|
310
|
+
mixRgb(removeBaseBg, removeRowTarget, REMOVE_ROW_BACKGROUND_MIX_RATIO),
|
|
311
|
+
ROW_DESATURATE_RATIO,
|
|
312
|
+
);
|
|
313
|
+
const removeEmphasisBg = desaturateRgb(
|
|
314
|
+
mixRgb(removeBaseBg, removeEmphasisTarget, REMOVE_INLINE_EMPHASIS_MIX_RATIO),
|
|
315
|
+
EMPHASIS_DESATURATE_RATIO,
|
|
316
|
+
);
|
|
286
317
|
|
|
287
318
|
return {
|
|
288
319
|
addRowBgAnsi: rgbToBgAnsi(addRowBg),
|
|
@@ -71,7 +71,7 @@ export function buildDiffSummaryText(stats: DiffSummaryStats, width: number): st
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* 折叠摘要行样式:整行 muted,`+A -D` 用 diff
|
|
74
|
+
* 折叠摘要行样式:整行 muted,`+A -D` 用 diff 头行同一套绿/红,`click to show more` 与其余折叠卡一致用 dim,hover 提到 text。
|
|
75
75
|
* 0 行预览与多行预览共用这一套颜色:改折叠行数时同一处信息不变色。
|
|
76
76
|
*/
|
|
77
77
|
export function styleDiffSummaryLine(
|
|
@@ -103,7 +103,7 @@ export function styleDiffSummaryLine(
|
|
|
103
103
|
}
|
|
104
104
|
if (hintIndex >= cursor) {
|
|
105
105
|
pushMuted(hintIndex);
|
|
106
|
-
pieces.push(theme.fg(hovered ? "text" : "
|
|
106
|
+
pieces.push(theme.fg(hovered ? "text" : "dim", hint));
|
|
107
107
|
cursor = hintIndex + hint.length;
|
|
108
108
|
}
|
|
109
109
|
pushMuted(text.length);
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type Component,
|
|
8
8
|
} from "@earendil-works/pi-tui";
|
|
9
9
|
import { config } from "../../config/config.ts";
|
|
10
|
+
import { aggregateToolCardBgSlot, toolCardStatus } from "./tool-state.ts";
|
|
10
11
|
import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
|
|
11
12
|
import {
|
|
12
13
|
ASK_USER_QUESTION_TOOL_NAME,
|
|
@@ -105,9 +106,7 @@ function previousSibling(
|
|
|
105
106
|
type ToolStatus = "pending" | "success" | "error";
|
|
106
107
|
|
|
107
108
|
function status(tool: any): ToolStatus {
|
|
108
|
-
|
|
109
|
-
if (tool?.isPartial === true || (tool?.executionStarted && !tool?.result)) return "pending";
|
|
110
|
-
return tool?.result ? "success" : "pending";
|
|
109
|
+
return toolCardStatus(tool);
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
function statusIcon(value: ToolStatus): string {
|
|
@@ -235,14 +234,15 @@ function askUserResultSummary(tool: any): ToolCallSummary | undefined {
|
|
|
235
234
|
.map((entry) => entry.answer);
|
|
236
235
|
if (answers.length === 0) {
|
|
237
236
|
const label = tool.result?.details?.cancelled === true ? "Declined" : "Done";
|
|
238
|
-
return { main:
|
|
237
|
+
return { main: toolTitle(tool), detail: "", payload: label };
|
|
239
238
|
}
|
|
240
239
|
const first = answers[0]!;
|
|
241
240
|
const question = String(first.question ?? "").trim() || "Question";
|
|
242
241
|
const more = answers.length > 1 ? ` (+${answers.length - 1})` : "";
|
|
243
242
|
return {
|
|
244
|
-
main:
|
|
243
|
+
main: toolTitle(tool),
|
|
245
244
|
detail: "",
|
|
245
|
+
payload: `${question} -> ${askUserAnswerText(first)}${more}`,
|
|
246
246
|
};
|
|
247
247
|
}
|
|
248
248
|
|
|
@@ -253,7 +253,7 @@ function codemodeResultSummary(tool: any): ToolCallSummary | undefined {
|
|
|
253
253
|
codemodeOutputLineCount(tool.result),
|
|
254
254
|
tool?.isPartial === true,
|
|
255
255
|
);
|
|
256
|
-
return { main:
|
|
256
|
+
return { main: toolTitle(tool), detail: "", payload: parts.join(" · ") };
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
/**
|
|
@@ -283,7 +283,7 @@ function collapsedGroupToolLine(
|
|
|
283
283
|
width: number,
|
|
284
284
|
): string {
|
|
285
285
|
const toolStatus = status(tool);
|
|
286
|
-
const color = toolStatus === "pending" ? "
|
|
286
|
+
const color = toolStatus === "pending" ? "dim" : toolStatus;
|
|
287
287
|
const summary = askUserResultSummary(tool) ?? codemodeResultSummary(tool) ?? toolSummary(tool);
|
|
288
288
|
const prefix = ` ${fg("dim", last ? "└" : "├")} ${fg(color, statusIcon(toolStatus))} `;
|
|
289
289
|
const detail = fg("dim", summary.detail);
|
|
@@ -311,7 +311,7 @@ function groupCountText(
|
|
|
311
311
|
.filter((key) => counts[key])
|
|
312
312
|
.map((key) => {
|
|
313
313
|
const label = key === "pending" ? "running" : key === "success" ? "done" : "failed";
|
|
314
|
-
const color = key === "pending" ? "
|
|
314
|
+
const color = key === "pending" ? "dim" : key;
|
|
315
315
|
return `${fg(color, String(counts[key]))} ${label}`;
|
|
316
316
|
})
|
|
317
317
|
.join(` ${fg("dim", "·")} `);
|
|
@@ -499,7 +499,7 @@ export class ToolGroupComponent extends Container {
|
|
|
499
499
|
const label = names.size === 1 ? toolTitle(this.children[0]) : "Multiple Tools";
|
|
500
500
|
const overall = groupOverallStatus(counts);
|
|
501
501
|
if ((this.children as any[]).some(isAnimatingTool)) scheduleGroupAnimation(this.patch);
|
|
502
|
-
const overallColor = overall === "pending" ? "
|
|
502
|
+
const overallColor = overall === "pending" ? "dim" : overall;
|
|
503
503
|
// 圆点保持 dim;hover 只高亮可点击文字。
|
|
504
504
|
const hintText = this._expanded ? collapseHintText() : showMoreHintText();
|
|
505
505
|
const hint = `${fg("dim", "·")} ${fg(this.hintHovered ? "text" : "dim", hintText)}`;
|
|
@@ -527,7 +527,7 @@ export class ToolGroupComponent extends Container {
|
|
|
527
527
|
expandedHit.hover === this.hintHovered &&
|
|
528
528
|
expandedHit.theme === this.patch.theme &&
|
|
529
529
|
expandedHit.fullscreen === isToolTuiFullscreen() &&
|
|
530
|
-
expandedHit.bgSlot ===
|
|
530
|
+
expandedHit.bgSlot === aggregateToolCardBgSlot(this.children) &&
|
|
531
531
|
expandedHit.paints.length === childPaints.length &&
|
|
532
532
|
expandedHit.paints.every((paint, index) => paint === childPaints[index])
|
|
533
533
|
) {
|
|
@@ -538,7 +538,7 @@ export class ToolGroupComponent extends Container {
|
|
|
538
538
|
for (let index = 0; index < total; index++) {
|
|
539
539
|
const tool = this.children[index];
|
|
540
540
|
const toolStatus = status(tool);
|
|
541
|
-
const color = toolStatus === "pending" ? "
|
|
541
|
+
const color = toolStatus === "pending" ? "dim" : toolStatus;
|
|
542
542
|
const branch = index === total - 1 ? "└" : "├";
|
|
543
543
|
const continuation = index === total - 1 ? " " : "│ ";
|
|
544
544
|
if (!this._expanded) {
|
|
@@ -564,8 +564,8 @@ export class ToolGroupComponent extends Container {
|
|
|
564
564
|
}
|
|
565
565
|
}
|
|
566
566
|
if (this._expanded) {
|
|
567
|
-
//
|
|
568
|
-
const backgroundSlot =
|
|
567
|
+
// 有一个失败整卡用失败底色;没有失败但还有进行中用进行中底色。
|
|
568
|
+
const backgroundSlot = aggregateToolCardBgSlot(this.children);
|
|
569
569
|
for (const line of expandedLines) {
|
|
570
570
|
lines.push(paddedBackgroundRow(theme, backgroundSlot, line, width));
|
|
571
571
|
}
|
|
@@ -6,6 +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 { TOOL_CARD_BG_SLOT } from "./tool-state.ts";
|
|
9
10
|
import { showMoreHintText } from "./names.ts";
|
|
10
11
|
import { walkComponentTree } from "../../utils/component-tree.ts";
|
|
11
12
|
|
|
@@ -82,11 +83,11 @@ function renderCcstyle(component: any, kind: DisplayKind): void {
|
|
|
82
83
|
component.addChild(new Text(`${icon} ${title}${hint}`, 0, 0));
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
|
-
//
|
|
86
|
+
// 展开卡与成功的工具卡同底色,上下左右各 1 格。
|
|
86
87
|
component.paddingX = 1;
|
|
87
88
|
component.paddingY = 1;
|
|
88
89
|
if (typeof theme.bg === "function" && typeof component.setBgFn === "function") {
|
|
89
|
-
component.setBgFn((text: string) => theme.bg(
|
|
90
|
+
component.setBgFn((text: string) => theme.bg(TOOL_CARD_BG_SLOT.success, text));
|
|
90
91
|
}
|
|
91
92
|
component.addChild(new Text(`${icon} ${title}`, 0, 0));
|
|
92
93
|
component.addChild(new Spacer(1));
|
|
@@ -14,7 +14,7 @@ function clip(value: unknown): string {
|
|
|
14
14
|
return oneLine(value, clipLimit());
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/** 载荷(入参 JSON /
|
|
17
|
+
/** 载荷(入参 JSON / 脚本代码 / 路径值)至少留出这么多宽度才显示,否则整段省略(避免只剩一个孤零零的 " {…")。 */
|
|
18
18
|
const MIN_PAYLOAD_WIDTH = 10;
|
|
19
19
|
|
|
20
20
|
/** 品牌大小写固定写法;humanize 推导不出来的工具名写在这里。 */
|
|
@@ -62,7 +62,7 @@ export type ToolCallSummary = {
|
|
|
62
62
|
detail: string;
|
|
63
63
|
/** 路径摘要保留结构,供最终渲染按实际宽度优先保留文件名。 */
|
|
64
64
|
path?: { prefix: string; value: string };
|
|
65
|
-
/**
|
|
65
|
+
/** 标题之外的原始载荷(入参 JSON、脚本代码、命令、路径、查询…),已压成单行,渲染时用 muted 接在标题后。 */
|
|
66
66
|
payload?: string;
|
|
67
67
|
};
|
|
68
68
|
|
|
@@ -149,11 +149,8 @@ function pathSummary(
|
|
|
149
149
|
detail = "",
|
|
150
150
|
): ToolCallSummary {
|
|
151
151
|
const path = displayPath(value, cwd);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
detail,
|
|
155
|
-
path: { prefix, value: path },
|
|
156
|
-
};
|
|
152
|
+
// main 只留标题:渲染时 prefix 用标题色、路径值用次级色,截断按 path 结构走中间省略。
|
|
153
|
+
return { main: prefix, detail, path: { prefix, value: path } };
|
|
157
154
|
}
|
|
158
155
|
|
|
159
156
|
/** 入参是 shell 命令的工具。 */
|
|
@@ -192,7 +189,7 @@ const GENERIC_SUMMARY_FIELDS = [
|
|
|
192
189
|
] as const;
|
|
193
190
|
|
|
194
191
|
/** 通用字段链取值:字符串直接用;数组取首个可展示项并标注剩余条数。 */
|
|
195
|
-
type SummaryValue = { text: string; more: number };
|
|
192
|
+
type SummaryValue = { text: string; more: number; input: boolean };
|
|
196
193
|
|
|
197
194
|
function arrayItemText(item: unknown): string | undefined {
|
|
198
195
|
if (typeof item === "string" && item) return item;
|
|
@@ -205,7 +202,7 @@ function arrayItemText(item: unknown): string | undefined {
|
|
|
205
202
|
}
|
|
206
203
|
|
|
207
204
|
function summaryFieldValue(value: unknown): SummaryValue | undefined {
|
|
208
|
-
if (typeof value === "string" && value) return { text: value, more: 0 };
|
|
205
|
+
if (typeof value === "string" && value) return { text: value, more: 0, input: true };
|
|
209
206
|
if (!Array.isArray(value)) return undefined;
|
|
210
207
|
let first: string | undefined;
|
|
211
208
|
let count = 0;
|
|
@@ -215,7 +212,7 @@ function summaryFieldValue(value: unknown): SummaryValue | undefined {
|
|
|
215
212
|
count += 1;
|
|
216
213
|
first ??= text;
|
|
217
214
|
}
|
|
218
|
-
return first === undefined ? undefined : { text: first, more: count - 1 };
|
|
215
|
+
return first === undefined ? undefined : { text: first, more: count - 1, input: true };
|
|
219
216
|
}
|
|
220
217
|
|
|
221
218
|
/** 通用字段链取值;无可用字段时返回 undefined,交给调用方继续降级。 */
|
|
@@ -225,7 +222,7 @@ function genericSummary(title: string, args: any): ToolCallSummary | undefined {
|
|
|
225
222
|
if (!found) continue;
|
|
226
223
|
// (+N) 追加在截断之后,避免首个元素过长时把条数挤没
|
|
227
224
|
const more = found.more > 0 ? ` (+${found.more})` : "";
|
|
228
|
-
return { main:
|
|
225
|
+
return { main: title, detail: "", payload: `${clip(found.text)}${more}` };
|
|
229
226
|
}
|
|
230
227
|
return undefined;
|
|
231
228
|
}
|
|
@@ -261,45 +258,39 @@ export function toolCallSummary(
|
|
|
261
258
|
|
|
262
259
|
const value = (fallback: string, ...keys: string[]) => {
|
|
263
260
|
const found = keys.map((key) => args[key]).find((item) => typeof item === "string" && item);
|
|
264
|
-
return
|
|
261
|
+
return { main: title, detail: "", payload: clip(found || fallback) };
|
|
265
262
|
};
|
|
266
263
|
|
|
267
264
|
if (variant === "default" && AGENT_FAMILY_TOOL_NAMES.has(toolName) && args.agent_id) {
|
|
268
|
-
return { main:
|
|
265
|
+
return { main: title, detail: "", payload: clip(args.agent_id) };
|
|
269
266
|
}
|
|
270
267
|
if (variant === "grouping" && (name === "agent" || name === "agents")) {
|
|
271
268
|
const displayName = args.subagent_type ?? args.agent_type ?? args.agent;
|
|
272
269
|
if (typeof displayName === "string" && displayName) {
|
|
273
|
-
return { main:
|
|
270
|
+
return { main: title, detail: "", payload: displayName };
|
|
274
271
|
}
|
|
275
|
-
return
|
|
276
|
-
main: value(name === "agent" ? "launch agent" : "launch agents", "description", "prompt"),
|
|
277
|
-
detail: "",
|
|
278
|
-
};
|
|
272
|
+
return value(name === "agent" ? "launch agent" : "launch agents", "description", "prompt");
|
|
279
273
|
}
|
|
280
274
|
if (variant === "grouping" && (name === "get_subagent_result" || name === "steer_subagent")) {
|
|
281
|
-
return
|
|
282
|
-
main: value(name === "get_subagent_result" ? "agent result" : "steer agent", "agent_id"),
|
|
283
|
-
detail: "",
|
|
284
|
-
};
|
|
275
|
+
return value(name === "get_subagent_result" ? "agent result" : "steer agent", "agent_id");
|
|
285
276
|
}
|
|
286
277
|
if (variant === "default" && name === "agents") {
|
|
287
|
-
return
|
|
278
|
+
return value("launch agents", "description", "prompt");
|
|
288
279
|
}
|
|
289
|
-
if (name === "skill") return
|
|
280
|
+
if (name === "skill") return value("run skill", "name");
|
|
290
281
|
if (name === "enterplanmode" || name === "enter_plan_mode") {
|
|
291
|
-
return { main:
|
|
282
|
+
return { main: title, detail: "", payload: "enable read-only planning" };
|
|
292
283
|
}
|
|
293
284
|
if (name === "exitplanmode" || name === "exit_plan_mode") {
|
|
294
|
-
return { main:
|
|
285
|
+
return { main: title, detail: "", payload: "present plan" };
|
|
295
286
|
}
|
|
296
|
-
if (name === "taskcreate") return
|
|
297
|
-
if (name === "tasklist") return { main:
|
|
287
|
+
if (name === "taskcreate") return value("create task", "subject");
|
|
288
|
+
if (name === "tasklist") return { main: title, detail: "", payload: "task list" };
|
|
298
289
|
if (name === "taskget" || name === "taskupdate") {
|
|
299
|
-
return
|
|
290
|
+
return value("task", "taskId", "task_id");
|
|
300
291
|
}
|
|
301
292
|
if (name === "taskoutput" || name === "taskstop") {
|
|
302
|
-
return
|
|
293
|
+
return value("background task", "task_id", "taskId");
|
|
303
294
|
}
|
|
304
295
|
if (name === "taskexecute") {
|
|
305
296
|
const ids = Array.isArray(args.task_ids)
|
|
@@ -310,7 +301,7 @@ export function toolCallSummary(
|
|
|
310
301
|
const summary = ids.length
|
|
311
302
|
? `${ids[0]}${ids.length > 1 ? ` (+${ids.length - 1} tasks)` : ""}`
|
|
312
303
|
: "start tasks";
|
|
313
|
-
return { main:
|
|
304
|
+
return { main: title, detail: "", payload: summary };
|
|
314
305
|
}
|
|
315
306
|
if (toolName === "read") {
|
|
316
307
|
const details = [
|
|
@@ -322,20 +313,21 @@ export function toolCallSummary(
|
|
|
322
313
|
return pathSummary(variant === "grouping" ? "Read" : title, args.path, opts.cwd, detail);
|
|
323
314
|
}
|
|
324
315
|
return {
|
|
325
|
-
main: variant === "grouping" ? "Read
|
|
316
|
+
main: variant === "grouping" ? "Read" : title,
|
|
326
317
|
detail,
|
|
318
|
+
payload: variant === "grouping" ? "..." : undefined,
|
|
327
319
|
};
|
|
328
320
|
}
|
|
329
321
|
if (variant === "grouping" && SHELL_TOOL_NAMES.has(name)) {
|
|
330
322
|
// 分组行缺 command 时保留占位,避免与单工具卡一样只剩标题
|
|
331
|
-
return
|
|
323
|
+
return value("...", "command");
|
|
332
324
|
}
|
|
333
325
|
|
|
334
326
|
// 检索类:pattern 是正文,path 只是范围;两个变体同格式
|
|
335
327
|
if (typeof args.pattern === "string" || SEARCH_TOOL_NAMES.has(name)) {
|
|
336
328
|
const pattern = clip(args.pattern || "...");
|
|
337
329
|
const scope = typeof args.path === "string" && args.path ? ` in ${clip(args.path)}` : "";
|
|
338
|
-
return { main:
|
|
330
|
+
return { main: title, detail: "", payload: `${JSON.stringify(pattern)}${scope}` };
|
|
339
331
|
}
|
|
340
332
|
|
|
341
333
|
const generic = genericSummary(title, args);
|
|
@@ -368,28 +360,29 @@ function mcpGatewaySummary(title: string, args: any): ToolCallSummary | undefine
|
|
|
368
360
|
const action = text(args.action);
|
|
369
361
|
const scoped = (target: string) => (server ? `${target} @ ${server}` : target);
|
|
370
362
|
|
|
371
|
-
if (action === "ui-messages") return { main:
|
|
363
|
+
if (action === "ui-messages") return { main: title, detail: "", payload: action };
|
|
372
364
|
if (tool) {
|
|
373
|
-
const
|
|
374
|
-
const payload =
|
|
375
|
-
return
|
|
365
|
+
const argsPayload = innerArgsPayload(args.args);
|
|
366
|
+
const payload = argsPayload ? `call ${scoped(tool)} ${argsPayload}` : `call ${scoped(tool)}`;
|
|
367
|
+
return { main: title, detail: "", payload };
|
|
376
368
|
}
|
|
377
|
-
if (connect) return { main:
|
|
378
|
-
if (describe) return { main:
|
|
379
|
-
if (instructions) return { main:
|
|
369
|
+
if (connect) return { main: title, detail: "", payload: `connect ${connect}` };
|
|
370
|
+
if (describe) return { main: title, detail: "", payload: `describe ${scoped(describe)}` };
|
|
371
|
+
if (instructions) return { main: title, detail: "", payload: `instructions ${instructions}` };
|
|
380
372
|
if (search) {
|
|
381
373
|
const flags = [
|
|
382
374
|
args.regex === true ? "regex" : "",
|
|
383
375
|
args.includeSchemas === false ? "schemas hidden" : "",
|
|
384
376
|
].filter(Boolean);
|
|
385
377
|
return {
|
|
386
|
-
main:
|
|
378
|
+
main: title,
|
|
387
379
|
detail: flags.length ? ` (${flags.join(", ")})` : "",
|
|
380
|
+
payload: `search ${scoped(search)}`,
|
|
388
381
|
};
|
|
389
382
|
}
|
|
390
|
-
if (server) return { main:
|
|
391
|
-
if (action) return { main:
|
|
392
|
-
if (Object.keys(args).length === 0) return { main:
|
|
383
|
+
if (server) return { main: title, detail: "", payload: `list ${server}` };
|
|
384
|
+
if (action) return { main: title, detail: "", payload: action };
|
|
385
|
+
if (Object.keys(args).length === 0) return { main: title, detail: "", payload: "status" };
|
|
393
386
|
return undefined;
|
|
394
387
|
}
|
|
395
388
|
|
|
@@ -411,20 +404,27 @@ function jsonArgs(args: any): string {
|
|
|
411
404
|
}
|
|
412
405
|
|
|
413
406
|
/**
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
* MIN_PAYLOAD_WIDTH 都放不下时整段省略。
|
|
407
|
+
* 标题行内容:main 是身份(工具名),用 toolTitle 色;路径值与载荷(入参 JSON、
|
|
408
|
+
* 脚本代码、命令、查询…)是输入,单独用 muted。标题优先占宽,剩下的宽度给载荷;
|
|
409
|
+
* 载荷超宽时尾部省略,连 MIN_PAYLOAD_WIDTH 都放不下时整段省略。
|
|
417
410
|
*/
|
|
418
411
|
export function renderToolSummary(
|
|
419
412
|
summary: ToolCallSummary,
|
|
420
413
|
width: number,
|
|
421
414
|
fg: (color: ThemeColor, text: string) => string,
|
|
422
415
|
): string {
|
|
416
|
+
// 路径摘要:前缀用标题色,值用次级色并按可用宽度中间截断(文件名优先保留)。
|
|
417
|
+
if (summary.path) {
|
|
418
|
+
const prefix = headToWidth(summary.path.prefix, width, "…");
|
|
419
|
+
const pathWidth = width - visibleWidth(prefix) - 1;
|
|
420
|
+
if (pathWidth < MIN_PAYLOAD_WIDTH) return fg("toolTitle", prefix);
|
|
421
|
+
return `${fg("toolTitle", prefix)} ${fg("muted", truncatePathToWidth(summary.path.value, Math.min(pathWidth, clipLimit())))}`;
|
|
422
|
+
}
|
|
423
423
|
const title = fitToolCallSummary(summary, width);
|
|
424
424
|
if (!summary.payload) return fg("toolTitle", title);
|
|
425
425
|
const room = width - visibleWidth(title);
|
|
426
426
|
return room >= MIN_PAYLOAD_WIDTH
|
|
427
|
-
? `${fg("toolTitle", title)}${fg("
|
|
427
|
+
? `${fg("toolTitle", title)}${fg("muted", headTruncateToWidth(` ${summary.payload}`, room))}`
|
|
428
428
|
: fg("toolTitle", title);
|
|
429
429
|
}
|
|
430
430
|
|