pi-cc-extensions 0.8.60 → 0.8.62
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 +2 -1
- package/README.md +2 -1
- package/extensions/config/config.ts +29 -10
- package/extensions/config/panel.ts +36 -8
- package/extensions/feature/compact-thinking.ts +272 -22
- package/extensions/feature/shell/model-status.ts +19 -0
- package/extensions/index.ts +2 -0
- package/extensions/renderer/compact-mode.ts +118 -33
- package/extensions/renderer/default-mode.ts +19 -2
- package/extensions/renderer/mouse/hover.ts +14 -0
- package/extensions/renderer/mouse/interaction.ts +9 -1
- package/extensions/renderer/mouse/layout.ts +6 -1
- package/extensions/renderer/tool/diff/diff-component.ts +2 -1
- package/extensions/renderer/tool/diff/diff-edit-render.ts +2 -2
- package/extensions/renderer/tool/diff/diff-limits.ts +16 -2
- package/extensions/renderer/tool/diff/diff-write-render.ts +62 -2
- package/extensions/renderer/tool/grouping.ts +64 -1
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -56,7 +56,8 @@ Run `/reload` after installation.
|
|
|
56
56
|
"diffViewMode": "auto", // diff layout: auto / split / unified
|
|
57
57
|
"diffIndicatorMode": "bars", // diff change indicators: bars / classic / none
|
|
58
58
|
"diffSplitMinWidth": 120, // min terminal width before auto layout uses side-by-side columns
|
|
59
|
-
"
|
|
59
|
+
"editDiffCollapsedLines": 24, // Edit collapsed lines; beyond that shows the expand hint (Ctrl+O / click)
|
|
60
|
+
"writeDiffCollapsedLines": 0, // write collapsed body lines; 0 = ↳ created • expand hint (stats stay on the title)
|
|
60
61
|
"diffWordWrap": true, // whether long diff lines wrap (otherwise truncated)
|
|
61
62
|
"expandedPreviewMaxLines": 40, // max body lines for expanded output/diff
|
|
62
63
|
"useSummaryTitlesAsThinkingTitle": true, // use the latest provider summary as the active thinking title
|
package/README.md
CHANGED
|
@@ -56,7 +56,8 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
56
56
|
"diffViewMode": "auto", // diff 布局:auto / split / unified
|
|
57
57
|
"diffIndicatorMode": "bars", // diff 变更指示:bars / classic / none
|
|
58
58
|
"diffSplitMinWidth": 120, // 自动布局下使用左右分栏的最小终端宽度
|
|
59
|
-
"
|
|
59
|
+
"editDiffCollapsedLines": 24, // Edit 折叠行数;超出显示展开提示(Ctrl+O / 点击)
|
|
60
|
+
"writeDiffCollapsedLines": 0, // write 折叠行数;0 = ↳ created • 展开提示(统计在标题)
|
|
60
61
|
"diffWordWrap": true, // 长 diff 行是否换行(否则截断)
|
|
61
62
|
"expandedPreviewMaxLines": 40, // 展开后输出/diff 正文的最大行数
|
|
62
63
|
"useSummaryTitlesAsThinkingTitle": true, // 用最新 provider 摘要作为思考标题
|
|
@@ -15,7 +15,9 @@ export interface ToolDisplayConfig {
|
|
|
15
15
|
diffViewMode: DiffViewMode;
|
|
16
16
|
diffIndicatorMode: DiffIndicatorMode;
|
|
17
17
|
diffSplitMinWidth: number;
|
|
18
|
-
|
|
18
|
+
editDiffCollapsedLines: number;
|
|
19
|
+
/** Write-only collapsed body lines. 0 = `↳ created • click to show more`. */
|
|
20
|
+
writeDiffCollapsedLines: number;
|
|
19
21
|
diffWordWrap: boolean;
|
|
20
22
|
expandedPreviewMaxLines: number;
|
|
21
23
|
}
|
|
@@ -24,8 +26,13 @@ export const DEFAULT_TOOL_DISPLAY_CONFIG: ToolDisplayConfig = {
|
|
|
24
26
|
diffViewMode: "auto",
|
|
25
27
|
diffIndicatorMode: "bars",
|
|
26
28
|
diffSplitMinWidth: 120,
|
|
27
|
-
/** Collapsed
|
|
28
|
-
|
|
29
|
+
/** Collapsed edit/diff body: ~half a typical terminal after chrome. */
|
|
30
|
+
editDiffCollapsedLines: 24,
|
|
31
|
+
/**
|
|
32
|
+
* Write create/overwrite collapsed body.
|
|
33
|
+
* 0 = `↳ created • click to show more` (stats stay on the title).
|
|
34
|
+
*/
|
|
35
|
+
writeDiffCollapsedLines: 0,
|
|
29
36
|
diffWordWrap: true,
|
|
30
37
|
/**
|
|
31
38
|
* Expanded tool/diff body cap. 40 ≈ one screen of content after title,
|
|
@@ -41,7 +48,8 @@ export type Config = {
|
|
|
41
48
|
diffViewMode: DiffViewMode;
|
|
42
49
|
diffIndicatorMode: DiffIndicatorMode;
|
|
43
50
|
diffSplitMinWidth: number;
|
|
44
|
-
|
|
51
|
+
editDiffCollapsedLines: number;
|
|
52
|
+
writeDiffCollapsedLines: number;
|
|
45
53
|
diffWordWrap: boolean;
|
|
46
54
|
expandedPreviewMaxLines: number;
|
|
47
55
|
useSummaryTitlesAsThinkingTitle: boolean;
|
|
@@ -64,6 +72,8 @@ export const DIFF_VIEW_MODES: DiffViewMode[] = ["auto", "split", "unified"];
|
|
|
64
72
|
export const DIFF_INDICATOR_MODES: DiffIndicatorMode[] = ["bars", "classic", "none"];
|
|
65
73
|
export const DIFF_SPLIT_MIN_WIDTH_VALUES = ["80", "100", "120", "140", "160", "180"];
|
|
66
74
|
export const DIFF_COLLAPSED_LINES_VALUES = ["12", "24", "36", "48", "80", "120"];
|
|
75
|
+
/** Write collapsed presets. 0 = stats only (`+N -0` + expand hint). */
|
|
76
|
+
export const WRITE_DIFF_COLLAPSED_LINES_VALUES = ["0", "4", "8", "12", "24", "36"];
|
|
67
77
|
/** Presets for expanded body height — keep low options first so cycling stays TUI-friendly. */
|
|
68
78
|
export const EXPANDED_PREVIEW_MAX_LINES_VALUES = ["40", "60", "80", "120", "200", "500", "2000"];
|
|
69
79
|
export const THINKING_PREVIEW_LINES_VALUES = ["0", "1", "3", "5", "10"];
|
|
@@ -89,7 +99,8 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
89
99
|
diffViewMode: DEFAULT_TOOL_DISPLAY_CONFIG.diffViewMode,
|
|
90
100
|
diffIndicatorMode: DEFAULT_TOOL_DISPLAY_CONFIG.diffIndicatorMode,
|
|
91
101
|
diffSplitMinWidth: DEFAULT_TOOL_DISPLAY_CONFIG.diffSplitMinWidth,
|
|
92
|
-
|
|
102
|
+
editDiffCollapsedLines: DEFAULT_TOOL_DISPLAY_CONFIG.editDiffCollapsedLines,
|
|
103
|
+
writeDiffCollapsedLines: DEFAULT_TOOL_DISPLAY_CONFIG.writeDiffCollapsedLines,
|
|
93
104
|
diffWordWrap: DEFAULT_TOOL_DISPLAY_CONFIG.diffWordWrap,
|
|
94
105
|
expandedPreviewMaxLines: DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines,
|
|
95
106
|
useSummaryTitlesAsThinkingTitle: true,
|
|
@@ -158,12 +169,18 @@ export function normalizeConfig(input: unknown): Config {
|
|
|
158
169
|
40,
|
|
159
170
|
300,
|
|
160
171
|
),
|
|
161
|
-
|
|
162
|
-
source.diffCollapsedLines,
|
|
163
|
-
DEFAULT_CONFIG.
|
|
172
|
+
editDiffCollapsedLines: pickPositiveInt(
|
|
173
|
+
source.editDiffCollapsedLines ?? source.diffCollapsedLines,
|
|
174
|
+
DEFAULT_CONFIG.editDiffCollapsedLines,
|
|
164
175
|
1,
|
|
165
176
|
500,
|
|
166
177
|
),
|
|
178
|
+
writeDiffCollapsedLines: pickPositiveInt(
|
|
179
|
+
source.writeDiffCollapsedLines,
|
|
180
|
+
DEFAULT_CONFIG.writeDiffCollapsedLines,
|
|
181
|
+
0,
|
|
182
|
+
500,
|
|
183
|
+
),
|
|
167
184
|
diffWordWrap: source.diffWordWrap !== false,
|
|
168
185
|
expandedPreviewMaxLines: pickPositiveInt(
|
|
169
186
|
source.expandedPreviewMaxLines,
|
|
@@ -206,7 +223,8 @@ export function getToolDisplayConfig(source: Config = config): ToolDisplayConfig
|
|
|
206
223
|
diffViewMode: source.diffViewMode,
|
|
207
224
|
diffIndicatorMode: source.diffIndicatorMode,
|
|
208
225
|
diffSplitMinWidth: source.diffSplitMinWidth,
|
|
209
|
-
|
|
226
|
+
editDiffCollapsedLines: source.editDiffCollapsedLines,
|
|
227
|
+
writeDiffCollapsedLines: source.writeDiffCollapsedLines,
|
|
210
228
|
diffWordWrap: source.diffWordWrap,
|
|
211
229
|
expandedPreviewMaxLines: source.expandedPreviewMaxLines,
|
|
212
230
|
};
|
|
@@ -223,7 +241,8 @@ export function formatConfigStatus(source: Config = config): string {
|
|
|
223
241
|
`diffView=${source.diffViewMode}`,
|
|
224
242
|
`diffIndicator=${source.diffIndicatorMode}`,
|
|
225
243
|
`diffSplitMin=${source.diffSplitMinWidth}`,
|
|
226
|
-
`
|
|
244
|
+
`editCollapsed=${source.editDiffCollapsedLines}`,
|
|
245
|
+
`writeCollapsed=${source.writeDiffCollapsedLines}`,
|
|
227
246
|
`diffWordWrap=${source.diffWordWrap ? "on" : "off"}`,
|
|
228
247
|
`expandedMax=${source.expandedPreviewMaxLines}`,
|
|
229
248
|
`thinkingTitle=${source.useSummaryTitlesAsThinkingTitle ? "summary" : "default"}`,
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
SCROLL_STEP_LINES_VALUES,
|
|
26
26
|
THINKING_ANIMATION_INTERVAL_VALUES,
|
|
27
27
|
THINKING_PREVIEW_LINES_VALUES,
|
|
28
|
+
WRITE_DIFF_COLLAPSED_LINES_VALUES,
|
|
28
29
|
updateConfig,
|
|
29
30
|
type CompactStyleMode,
|
|
30
31
|
type Config,
|
|
@@ -40,7 +41,7 @@ export type CcstylePanelHooks = {
|
|
|
40
41
|
|
|
41
42
|
function modeSettingDescription(mode: CompactStyleMode): string {
|
|
42
43
|
if (mode === "compact") {
|
|
43
|
-
return "(Experimental) One summary line per assistant round;
|
|
44
|
+
return "(Experimental) One summary line per assistant round; edit/write reuse the same Diff preview settings as on.";
|
|
44
45
|
}
|
|
45
46
|
if (mode === "off") {
|
|
46
47
|
return "Pi native tool rendering. Diff options below still apply independently.";
|
|
@@ -287,15 +288,25 @@ export async function showCcstylePanel(
|
|
|
287
288
|
buildNumberInputSubmenu(theme, diffSplitSetting, closeSubmenu),
|
|
288
289
|
};
|
|
289
290
|
const diffCollapsedSetting = {
|
|
290
|
-
id: "
|
|
291
|
-
label: "
|
|
291
|
+
id: "editDiffCollapsedLines",
|
|
292
|
+
label: "Edit collapsed lines",
|
|
292
293
|
description:
|
|
293
|
-
"How many diff body lines to show before the expand hint (Ctrl+O / click).
|
|
294
|
-
currentValue: String(config.
|
|
294
|
+
"How many edit/diff body lines to show before the expand hint (Ctrl+O / click). Write uses its own setting below.",
|
|
295
|
+
currentValue: String(config.editDiffCollapsedLines),
|
|
295
296
|
values: [...DIFF_COLLAPSED_LINES_VALUES],
|
|
296
297
|
submenu: (_current: string, closeSubmenu: (selected?: string) => void) =>
|
|
297
298
|
buildNumberInputSubmenu(theme, diffCollapsedSetting, closeSubmenu),
|
|
298
299
|
};
|
|
300
|
+
const writeDiffCollapsedSetting = {
|
|
301
|
+
id: "writeDiffCollapsedLines",
|
|
302
|
+
label: "Write collapsed lines",
|
|
303
|
+
description:
|
|
304
|
+
"Write-only collapsed body lines. 0 shows ↳ created + expand hint (stats stay on the title). Enter to type a custom value.",
|
|
305
|
+
currentValue: String(config.writeDiffCollapsedLines),
|
|
306
|
+
values: [...WRITE_DIFF_COLLAPSED_LINES_VALUES],
|
|
307
|
+
submenu: (_current: string, closeSubmenu: (selected?: string) => void) =>
|
|
308
|
+
buildNumberInputSubmenu(theme, writeDiffCollapsedSetting, closeSubmenu),
|
|
309
|
+
};
|
|
299
310
|
const diffWordWrapSetting = {
|
|
300
311
|
id: "diffWordWrap",
|
|
301
312
|
label: "Diff word wrap",
|
|
@@ -447,11 +458,27 @@ export async function showCcstylePanel(
|
|
|
447
458
|
});
|
|
448
459
|
diffSplitSetting.currentValue = String(config.diffSplitMinWidth);
|
|
449
460
|
break;
|
|
450
|
-
case "
|
|
461
|
+
case "editDiffCollapsedLines":
|
|
462
|
+
updateConfig({
|
|
463
|
+
editDiffCollapsedLines: pickPositiveInt(
|
|
464
|
+
value,
|
|
465
|
+
DEFAULT_CONFIG.editDiffCollapsedLines,
|
|
466
|
+
1,
|
|
467
|
+
500,
|
|
468
|
+
),
|
|
469
|
+
});
|
|
470
|
+
diffCollapsedSetting.currentValue = String(config.editDiffCollapsedLines);
|
|
471
|
+
break;
|
|
472
|
+
case "writeDiffCollapsedLines":
|
|
451
473
|
updateConfig({
|
|
452
|
-
|
|
474
|
+
writeDiffCollapsedLines: pickPositiveInt(
|
|
475
|
+
value,
|
|
476
|
+
DEFAULT_CONFIG.writeDiffCollapsedLines,
|
|
477
|
+
0,
|
|
478
|
+
500,
|
|
479
|
+
),
|
|
453
480
|
});
|
|
454
|
-
|
|
481
|
+
writeDiffCollapsedSetting.currentValue = String(config.writeDiffCollapsedLines);
|
|
455
482
|
break;
|
|
456
483
|
case "diffWordWrap":
|
|
457
484
|
updateConfig({ diffWordWrap: value === "on" });
|
|
@@ -519,6 +546,7 @@ export async function showCcstylePanel(
|
|
|
519
546
|
diffIndicatorSetting,
|
|
520
547
|
diffSplitSetting,
|
|
521
548
|
diffCollapsedSetting,
|
|
549
|
+
writeDiffCollapsedSetting,
|
|
522
550
|
diffWordWrapSetting,
|
|
523
551
|
expandedMaxSetting,
|
|
524
552
|
],
|
|
@@ -11,8 +11,14 @@ import {
|
|
|
11
11
|
type ExtensionAPI,
|
|
12
12
|
type Theme,
|
|
13
13
|
} from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import {
|
|
15
|
-
|
|
14
|
+
import {
|
|
15
|
+
getKeybindings,
|
|
16
|
+
truncateToWidth,
|
|
17
|
+
visibleWidth,
|
|
18
|
+
wrapTextWithAnsi,
|
|
19
|
+
} from "@earendil-works/pi-tui";
|
|
20
|
+
import { Box, Markdown, Spacer, Text, type Component } from "@earendil-works/pi-tui";
|
|
21
|
+
import { isToolTuiFullscreen } from "../renderer/tool/show-more-hint.ts";
|
|
16
22
|
import {
|
|
17
23
|
animateCompactThinkingText,
|
|
18
24
|
formatThoughtDuration,
|
|
@@ -163,30 +169,269 @@ function getThinkingToggleHint() {
|
|
|
163
169
|
return keys.length > 0 ? `${keys.join("/")} to expand` : undefined;
|
|
164
170
|
}
|
|
165
171
|
|
|
166
|
-
|
|
172
|
+
function thinkingExpandAction(): string | undefined {
|
|
173
|
+
return isToolTuiFullscreen() ? "click to show more" : getThinkingToggleHint();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 只 wrap 尾部窗口;缓存未着色折行,着色放到取出时做,避免主题切换命中旧 ANSI。
|
|
177
|
+
type PreviewCacheEntry = { lines: string[]; hiddenBefore: number };
|
|
178
|
+
const previewCache = new Map<string, PreviewCacheEntry>();
|
|
179
|
+
export const THINKING_PREVIEW_CACHE_MAX = 2_048;
|
|
180
|
+
const PREVIEW_BUDGET_MIN_CHARS = 2_000;
|
|
181
|
+
const PREVIEW_BUDGET_SLACK_LINES = 2;
|
|
182
|
+
|
|
183
|
+
function getCachedPreview(key: string): PreviewCacheEntry | undefined {
|
|
184
|
+
const entry = previewCache.get(key);
|
|
185
|
+
if (!entry) return undefined;
|
|
186
|
+
previewCache.delete(key);
|
|
187
|
+
previewCache.set(key, entry);
|
|
188
|
+
return entry;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function cachePreview(key: string, entry: PreviewCacheEntry): void {
|
|
192
|
+
previewCache.delete(key);
|
|
193
|
+
while (previewCache.size >= THINKING_PREVIEW_CACHE_MAX) {
|
|
194
|
+
const oldest = previewCache.keys().next().value;
|
|
195
|
+
if (oldest === undefined) break;
|
|
196
|
+
previewCache.delete(oldest);
|
|
197
|
+
}
|
|
198
|
+
previewCache.set(key, entry);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function clearThinkingPreviewCache(): void {
|
|
202
|
+
previewCache.clear();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function thinkingPreviewCacheSize(): number {
|
|
206
|
+
return previewCache.size;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 按可见宽度估算折行数,不走 Text 全量 wrap。无换行长段也能计到隐藏行。 */
|
|
210
|
+
function countWrappedLines(text: string, contentWidth: number): number {
|
|
211
|
+
if (!text) return 0;
|
|
212
|
+
const width = Math.max(1, contentWidth);
|
|
213
|
+
let lines = 0;
|
|
214
|
+
for (const raw of text.replace(/\t/g, " ").split(/\r\n|\r|\n/)) {
|
|
215
|
+
const w = visibleWidth(raw);
|
|
216
|
+
lines += w === 0 ? 1 : Math.ceil(w / width);
|
|
217
|
+
}
|
|
218
|
+
return lines;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function padPreviewLine(line: string, width: number, padding: number): string {
|
|
222
|
+
const left = padding > 0 ? " ".repeat(padding) : "";
|
|
223
|
+
const right = padding > 0 ? " ".repeat(padding) : "";
|
|
224
|
+
const withMargins = left + line + right;
|
|
225
|
+
return withMargins + " ".repeat(Math.max(0, width - visibleWidth(withMargins)));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function layoutThinkingPreview(
|
|
229
|
+
text: string,
|
|
230
|
+
width: number,
|
|
231
|
+
padding: number,
|
|
232
|
+
): { visible: string[]; hiddenLines: number } {
|
|
233
|
+
if (!text || config.previewLines <= 0) return { visible: [], hiddenLines: 0 };
|
|
234
|
+
const contentWidth = Math.max(1, width - padding * 2);
|
|
235
|
+
const budget = Math.max(
|
|
236
|
+
width * (config.previewLines + PREVIEW_BUDGET_SLACK_LINES),
|
|
237
|
+
PREVIEW_BUDGET_MIN_CHARS,
|
|
238
|
+
);
|
|
239
|
+
let source = text;
|
|
240
|
+
let cut = 0;
|
|
241
|
+
if (source.length > budget) {
|
|
242
|
+
const start = source.length - budget;
|
|
243
|
+
const newline = source.indexOf("\n", start);
|
|
244
|
+
cut = newline === -1 ? start : newline + 1;
|
|
245
|
+
if (cut < source.length) source = source.slice(cut);
|
|
246
|
+
else cut = 0;
|
|
247
|
+
}
|
|
248
|
+
const cacheKey = `${width}:${padding}:${cut}:${source}`;
|
|
249
|
+
let entry = getCachedPreview(cacheKey);
|
|
250
|
+
if (!entry) {
|
|
251
|
+
entry = {
|
|
252
|
+
lines: wrapTextWithAnsi(source.replace(/\t/g, " "), contentWidth),
|
|
253
|
+
hiddenBefore: cut > 0 ? countWrappedLines(text.slice(0, cut), contentWidth) : 0,
|
|
254
|
+
};
|
|
255
|
+
cachePreview(cacheKey, entry);
|
|
256
|
+
}
|
|
257
|
+
const hiddenLines = entry.hiddenBefore + Math.max(0, entry.lines.length - config.previewLines);
|
|
258
|
+
const visible = hiddenLines > 0 ? entry.lines.slice(-config.previewLines) : entry.lines;
|
|
259
|
+
return { visible, hiddenLines };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function hiddenPreviewHint(
|
|
263
|
+
hiddenLines: number,
|
|
264
|
+
forceExpandHint = false,
|
|
265
|
+
): { prefix: string; action: string; suffix: string } | undefined {
|
|
266
|
+
const action = thinkingExpandAction() ?? "";
|
|
267
|
+
if (hiddenLines > 0) {
|
|
268
|
+
const noun = hiddenLines === 1 ? "line" : "lines";
|
|
269
|
+
return {
|
|
270
|
+
prefix: ` • (${hiddenLines} more ${noun}${action ? ", " : ""}`,
|
|
271
|
+
action,
|
|
272
|
+
suffix: ")",
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (forceExpandHint && isToolTuiFullscreen()) {
|
|
276
|
+
return { prefix: " • ", action: "click to show more", suffix: "" };
|
|
277
|
+
}
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const expandedThinking = new Set<number>();
|
|
282
|
+
|
|
283
|
+
/** 折叠预览 + 展开全文。fullscreen 点击 hint 展开、再点整块收起,对齐工具卡。 */
|
|
284
|
+
export class ThinkingPreviewBlock implements Component {
|
|
285
|
+
private heading: string;
|
|
167
286
|
private text: string;
|
|
168
287
|
private padding: number;
|
|
288
|
+
private messageTimestamp: number;
|
|
169
289
|
private style: (text: string) => string;
|
|
170
|
-
|
|
171
|
-
|
|
290
|
+
private theme: Theme | undefined;
|
|
291
|
+
private _expanded: boolean;
|
|
292
|
+
private hintHovered = false;
|
|
293
|
+
private expandedBody: { width: number; lines: string[] } | undefined;
|
|
294
|
+
private collapsedMemo:
|
|
295
|
+
| {
|
|
296
|
+
width: number;
|
|
297
|
+
previewLines: number;
|
|
298
|
+
hintHovered: boolean;
|
|
299
|
+
expandAction: string | undefined;
|
|
300
|
+
lines: string[];
|
|
301
|
+
}
|
|
302
|
+
| undefined;
|
|
303
|
+
|
|
304
|
+
constructor(
|
|
305
|
+
heading: string,
|
|
306
|
+
text: string,
|
|
307
|
+
padding: number,
|
|
308
|
+
messageTimestamp: number,
|
|
309
|
+
style: (text: string) => string,
|
|
310
|
+
theme?: Theme,
|
|
311
|
+
) {
|
|
312
|
+
this.heading = heading;
|
|
172
313
|
this.text = text;
|
|
173
314
|
this.padding = padding;
|
|
315
|
+
this.messageTimestamp = messageTimestamp;
|
|
174
316
|
this.style = style;
|
|
317
|
+
this.theme = theme;
|
|
318
|
+
this._expanded = expandedThinking.has(messageTimestamp);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private paint(color: string, text: string): string {
|
|
322
|
+
return this.theme && typeof this.theme.fg === "function"
|
|
323
|
+
? this.theme.fg(color as never, text)
|
|
324
|
+
: text;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
get expanded(): boolean {
|
|
328
|
+
return this._expanded;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
setExpanded(expanded: boolean): void {
|
|
332
|
+
if (this._expanded !== expanded) this.collapsedMemo = undefined;
|
|
333
|
+
this._expanded = expanded;
|
|
334
|
+
if (expanded) expandedThinking.add(this.messageTimestamp);
|
|
335
|
+
else {
|
|
336
|
+
expandedThinking.delete(this.messageTimestamp);
|
|
337
|
+
this.expandedBody = undefined;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
setHintHovered(hovered: boolean): void {
|
|
342
|
+
if (this.hintHovered !== hovered) this.collapsedMemo = undefined;
|
|
343
|
+
this.hintHovered = hovered;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private headingLines(width: number, hiddenLines: number, padding: number): string[] {
|
|
347
|
+
const hint = this._expanded
|
|
348
|
+
? undefined
|
|
349
|
+
: hiddenPreviewHint(hiddenLines, Boolean(this.text) && config.previewLines <= 0);
|
|
350
|
+
if (!hint) return new Text(this.heading, padding, 0).render(width);
|
|
351
|
+
const rawHint = hint.prefix + hint.action + hint.suffix;
|
|
352
|
+
const contentWidth = Math.max(1, width - padding * 2);
|
|
353
|
+
const headingBudget = Math.max(0, contentWidth - visibleWidth(rawHint));
|
|
354
|
+
const clipped =
|
|
355
|
+
visibleWidth(this.heading) > headingBudget
|
|
356
|
+
? truncateToWidth(this.heading, headingBudget, "")
|
|
357
|
+
: this.heading;
|
|
358
|
+
const action = this.paint(this.hintHovered ? "text" : "dim", hint.action);
|
|
359
|
+
return new Text(
|
|
360
|
+
clipped + this.paint("dim", hint.prefix) + action + this.paint("dim", hint.suffix),
|
|
361
|
+
padding,
|
|
362
|
+
0,
|
|
363
|
+
).render(width);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private bodyLines(width: number, padding: number): { lines: string[]; hiddenLines: number } {
|
|
367
|
+
if (!this.text || (config.previewLines <= 0 && !this._expanded)) {
|
|
368
|
+
return { lines: [], hiddenLines: 0 };
|
|
369
|
+
}
|
|
370
|
+
if (this._expanded) {
|
|
371
|
+
if (this.expandedBody?.width !== width) {
|
|
372
|
+
this.expandedBody = {
|
|
373
|
+
width,
|
|
374
|
+
lines: wrapTextWithAnsi(this.text.replace(/\t/g, " "), Math.max(1, width)),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
return { lines: this.expandedBody.lines, hiddenLines: 0 };
|
|
378
|
+
}
|
|
379
|
+
const preview = layoutThinkingPreview(this.text, width, padding);
|
|
380
|
+
return { lines: preview.visible, hiddenLines: preview.hiddenLines };
|
|
175
381
|
}
|
|
176
382
|
|
|
177
383
|
render(width: number) {
|
|
178
|
-
|
|
179
|
-
|
|
384
|
+
if (this._expanded) {
|
|
385
|
+
const innerWidth = Math.max(1, width - 2);
|
|
386
|
+
const body = this.bodyLines(innerWidth, 0);
|
|
387
|
+
const heading = this.headingLines(innerWidth, 0, 0);
|
|
388
|
+
const inner = body.lines.length
|
|
389
|
+
? [...heading, ...body.lines.map((line) => this.style(line))]
|
|
390
|
+
: heading;
|
|
391
|
+
const bgFn =
|
|
392
|
+
this.theme && typeof this.theme.bg === "function"
|
|
393
|
+
? (text: string) => this.theme!.bg("userMessageBg" as never, text)
|
|
394
|
+
: undefined;
|
|
395
|
+
const box = new Box(1, 1, bgFn);
|
|
396
|
+
box.addChild({
|
|
397
|
+
render: () => inner,
|
|
398
|
+
invalidate() {},
|
|
399
|
+
});
|
|
400
|
+
return box.render(width);
|
|
401
|
+
}
|
|
180
402
|
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
403
|
+
const expandAction = thinkingExpandAction();
|
|
404
|
+
if (
|
|
405
|
+
this.collapsedMemo?.width === width &&
|
|
406
|
+
this.collapsedMemo.previewLines === config.previewLines &&
|
|
407
|
+
this.collapsedMemo.hintHovered === this.hintHovered &&
|
|
408
|
+
this.collapsedMemo.expandAction === expandAction
|
|
409
|
+
) {
|
|
410
|
+
return this.collapsedMemo.lines;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const body = this.bodyLines(width, this.padding);
|
|
414
|
+
const heading = this.headingLines(width, body.hiddenLines, this.padding);
|
|
415
|
+
const lines = body.lines.length
|
|
416
|
+
? [
|
|
417
|
+
...heading,
|
|
418
|
+
...body.lines.map((line) => padPreviewLine(this.style(line), width, this.padding)),
|
|
419
|
+
]
|
|
420
|
+
: heading;
|
|
421
|
+
this.collapsedMemo = {
|
|
422
|
+
width,
|
|
423
|
+
previewLines: config.previewLines,
|
|
424
|
+
hintHovered: this.hintHovered,
|
|
425
|
+
expandAction,
|
|
426
|
+
lines,
|
|
427
|
+
};
|
|
428
|
+
return lines;
|
|
187
429
|
}
|
|
188
430
|
|
|
189
|
-
invalidate() {
|
|
431
|
+
invalidate() {
|
|
432
|
+
this.collapsedMemo = undefined;
|
|
433
|
+
this.expandedBody = undefined;
|
|
434
|
+
}
|
|
190
435
|
}
|
|
191
436
|
|
|
192
437
|
function parseSummaryPart(text: string): SummaryPart | undefined {
|
|
@@ -461,14 +706,17 @@ function compactThinking(pi: ExtensionAPI) {
|
|
|
461
706
|
durationText ? `Thought for ${durationText}` : (latestSummary?.title ?? "Thought"),
|
|
462
707
|
);
|
|
463
708
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
709
|
+
const previewSource = (latestSummary?.body ?? thinkingBlocks.join("\n\n")).trim();
|
|
710
|
+
self.contentContainer.addChild(
|
|
711
|
+
new ThinkingPreviewBlock(
|
|
712
|
+
heading,
|
|
713
|
+
previewSource,
|
|
714
|
+
self.outputPad,
|
|
715
|
+
message.timestamp,
|
|
716
|
+
thinkingStyle,
|
|
717
|
+
activeTheme,
|
|
718
|
+
),
|
|
719
|
+
);
|
|
472
720
|
|
|
473
721
|
const hasVisibleContentAfter = message.content
|
|
474
722
|
.slice(i + 1)
|
|
@@ -733,6 +981,8 @@ function compactThinking(pi: ExtensionAPI) {
|
|
|
733
981
|
latestComponentTimestamp = undefined;
|
|
734
982
|
completedDurations.clear();
|
|
735
983
|
streamingComponents.clear();
|
|
984
|
+
expandedThinking.clear();
|
|
985
|
+
clearThinkingPreviewCache();
|
|
736
986
|
if (ctx.mode === "tui") ctx.ui.setWidget(WIDGET_ID, undefined);
|
|
737
987
|
|
|
738
988
|
if (patchInstalled) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
function modelLabel(model: any): string | undefined {
|
|
4
|
+
if (!model) return undefined;
|
|
5
|
+
const name = model.name || model.id;
|
|
6
|
+
if (!name) return undefined;
|
|
7
|
+
return model.provider ? `${model.provider} · ${name}` : name;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function apply(ctx: any): void {
|
|
11
|
+
if (!ctx?.hasUI) return;
|
|
12
|
+
ctx.ui?.setStatus?.("model", modelLabel(ctx.model));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 用 setStatus 挂模型名,不 setFooter,不接管原生 footer。 */
|
|
16
|
+
export default function (pi: ExtensionAPI): void {
|
|
17
|
+
pi.on("session_start", async (_event, ctx) => apply(ctx));
|
|
18
|
+
pi.on("model_select", async (_event, ctx) => apply(ctx));
|
|
19
|
+
}
|
package/extensions/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { config } from "./config/config.ts";
|
|
|
5
5
|
import piAliases from "./feature/shell/aliases.ts";
|
|
6
6
|
import piStartupHeader from "./feature/shell/startup-header.ts";
|
|
7
7
|
import workingMessage from "./feature/shell/working-message.ts";
|
|
8
|
+
import modelStatus from "./feature/shell/model-status.ts";
|
|
8
9
|
|
|
9
10
|
// feature
|
|
10
11
|
import agentAutocomplete from "./feature/reference/subagent.ts";
|
|
@@ -22,6 +23,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
22
23
|
if (config.enableAliases) piAliases(pi);
|
|
23
24
|
piStartupHeader(pi);
|
|
24
25
|
if (config.enableWorkingMessage) workingMessage(pi);
|
|
26
|
+
modelStatus(pi);
|
|
25
27
|
|
|
26
28
|
// render stack:thinking controller 直接交给 style 作 query
|
|
27
29
|
markdownEnhance(pi);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Compact mode:每条含 toolCall 的 assistant message 折叠为一条逐步累加的摘要行
|
|
3
|
-
* (`Ran for 8s, bash×2, read×2`),edit/write
|
|
3
|
+
* (`Ran for 8s, bash×2, read×2`),edit/write 独立标题行(`✓ write <path> (+25 -0)`),
|
|
4
4
|
* 普通工具折叠时不显示独立行;展开(Ctrl+O / fullscreen 点击)在单个工具卡中恢复
|
|
5
|
-
* compact-thinking/Pi 原生或专用 renderer。edit/write
|
|
5
|
+
* compact-thinking/Pi 原生或专用 renderer。edit/write 复用 mode=on 的 rich diff
|
|
6
|
+
* 与 `editDiffCollapsedLines` / `writeDiffCollapsedLines` / `expandedPreviewMaxLines`。
|
|
6
7
|
* Agent/Task 族:调用只进摘要计数,tool 卡始终折叠(避免 pending→完成高度闪动)。
|
|
7
8
|
* 底部 Agents/Tasks 面板由 pi-subagents/pi-tasks 独立 widget 负责,不经 tool 卡外置。
|
|
8
9
|
*
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
ToolExecutionComponent,
|
|
20
21
|
type Theme,
|
|
21
22
|
} from "@earendil-works/pi-coding-agent";
|
|
22
|
-
import { truncateToWidth, visibleWidth, Box } from "@earendil-works/pi-tui";
|
|
23
|
+
import { truncateToWidth, visibleWidth, Box, Spacer } from "@earendil-works/pi-tui";
|
|
23
24
|
import { config, getToolDisplayConfig } from "../config/config.ts";
|
|
24
25
|
import { toolLoadingIcon } from "../utils/tool-loading-icon.ts";
|
|
25
26
|
import { sanitizeToolResultText } from "../utils/tool-result-sanitize.ts";
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
} from "./tool/diff/diff-renderer.ts";
|
|
34
35
|
import { renderRichToolResult } from "./tool/diff/index.ts";
|
|
35
36
|
import type { WriteExecutionMetadataStore } from "./tool/diff/write-execution.ts";
|
|
37
|
+
import { isToolCallHovered } from "./mouse/hover.ts";
|
|
36
38
|
import { insetComponent, renderExpandedToolResult, scheduleAnimation } from "./tool/result.ts";
|
|
37
39
|
import { oneLine } from "../utils/format.ts";
|
|
38
40
|
import { paddedBackgroundRow } from "./tool/grouping.ts";
|
|
@@ -475,6 +477,7 @@ function compactEditWriteLine(
|
|
|
475
477
|
component: any,
|
|
476
478
|
width: number,
|
|
477
479
|
writeMetadata?: WriteExecutionMetadataStore,
|
|
480
|
+
options: { hint?: boolean } = {},
|
|
478
481
|
): string[] {
|
|
479
482
|
const theme = themeOf();
|
|
480
483
|
const name = String(component.toolName ?? "tool");
|
|
@@ -510,7 +513,8 @@ function compactEditWriteLine(
|
|
|
510
513
|
}
|
|
511
514
|
const iconPart = ` ${theme.fg(iconColor, icon)} `;
|
|
512
515
|
const namePart = theme.fg("toolTitle", name);
|
|
513
|
-
const hintText =
|
|
516
|
+
const hintText =
|
|
517
|
+
options.hint !== false && component.expanded !== true ? ` • ${showMoreHintText()}` : "";
|
|
514
518
|
const fixedWidth =
|
|
515
519
|
visibleWidth(iconPart) +
|
|
516
520
|
visibleWidth(namePart) +
|
|
@@ -522,34 +526,66 @@ function compactEditWriteLine(
|
|
|
522
526
|
return ["", truncateToWidth(line, width, "")];
|
|
523
527
|
}
|
|
524
528
|
|
|
525
|
-
/**
|
|
526
|
-
|
|
529
|
+
/** expanded 写进 rich diff 闭包,折叠/展开分槽缓存,避免每帧 parseDiff。 */
|
|
530
|
+
const compactRichDiffCache = new WeakMap<
|
|
531
|
+
object,
|
|
532
|
+
{ result: unknown; collapsed?: unknown; expanded?: unknown }
|
|
533
|
+
>();
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* compact edit/write:标题行 + mode=on 同一套 rich diff。
|
|
537
|
+
* 折叠/展开都走 `renderRichToolResult`,limits 不另开一套。
|
|
538
|
+
*/
|
|
539
|
+
function compactEditWriteLines(
|
|
527
540
|
component: any,
|
|
528
541
|
width: number,
|
|
529
542
|
writeMetadata?: WriteExecutionMetadataStore,
|
|
530
543
|
): string[] {
|
|
531
544
|
const theme = themeOf();
|
|
532
545
|
const result = component.result;
|
|
546
|
+
const expanded = component.expanded === true;
|
|
533
547
|
const isError = result?.isError === true;
|
|
534
|
-
const
|
|
535
|
-
|
|
548
|
+
const isPending = !result || component.isPartial === true;
|
|
549
|
+
let candidate: unknown;
|
|
550
|
+
if (!isPending && writeMetadata) {
|
|
551
|
+
let entry = compactRichDiffCache.get(component);
|
|
552
|
+
if (!entry || entry.result !== result) {
|
|
553
|
+
entry = { result };
|
|
554
|
+
compactRichDiffCache.set(component, entry);
|
|
555
|
+
}
|
|
556
|
+
const slot = expanded ? "expanded" : "collapsed";
|
|
557
|
+
candidate = entry[slot];
|
|
558
|
+
if (!isRichDiffComponent(candidate)) {
|
|
559
|
+
candidate = renderRichToolResult(
|
|
536
560
|
String(component.toolName ?? ""),
|
|
537
561
|
result,
|
|
538
562
|
{
|
|
539
|
-
expanded
|
|
563
|
+
expanded,
|
|
540
564
|
isPartial: component.isPartial === true,
|
|
541
565
|
isError,
|
|
566
|
+
isHovered: () => isToolCallHovered(component.toolCallId),
|
|
542
567
|
},
|
|
543
568
|
theme,
|
|
544
569
|
component,
|
|
545
570
|
writeMetadata,
|
|
546
571
|
getToolDisplayConfig,
|
|
547
|
-
)
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
572
|
+
);
|
|
573
|
+
if (isRichDiffComponent(candidate)) entry[slot] = candidate;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const hasRich = isRichDiffComponent(candidate);
|
|
577
|
+
if (hasRich) {
|
|
551
578
|
component.resultRendererComponent = candidate;
|
|
552
|
-
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const title = compactEditWriteLine(component, width, writeMetadata, { hint: !hasRich });
|
|
582
|
+
if (!hasRich && !expanded) {
|
|
583
|
+
return title;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
let detail: any;
|
|
587
|
+
if (hasRich) {
|
|
588
|
+
detail = expanded ? candidate : insetComponent(candidate as any);
|
|
553
589
|
} else {
|
|
554
590
|
const outputText = sanitizeToolResultText(
|
|
555
591
|
Array.isArray(result?.content)
|
|
@@ -571,10 +607,14 @@ function compactEditWriteExpandedLines(
|
|
|
571
607
|
component.resultRendererComponent = detail;
|
|
572
608
|
}
|
|
573
609
|
|
|
610
|
+
if (!expanded) {
|
|
611
|
+
return [...title, ...detail.render(width)];
|
|
612
|
+
}
|
|
613
|
+
|
|
574
614
|
const box = editWriteExpandedCard(theme);
|
|
575
615
|
box.addChild({
|
|
576
616
|
render(innerWidth: number): string[] {
|
|
577
|
-
return compactEditWriteLine(component, innerWidth, writeMetadata).slice(1);
|
|
617
|
+
return compactEditWriteLine(component, innerWidth, writeMetadata, { hint: false }).slice(1);
|
|
578
618
|
},
|
|
579
619
|
invalidate() {},
|
|
580
620
|
});
|
|
@@ -788,7 +828,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
788
828
|
return;
|
|
789
829
|
}
|
|
790
830
|
try {
|
|
791
|
-
|
|
831
|
+
// 非 force:保留 fullscreen 布局缓存和差分绘制。
|
|
832
|
+
uiRef?.requestRender?.();
|
|
792
833
|
} catch {
|
|
793
834
|
/* 无 UI */
|
|
794
835
|
}
|
|
@@ -864,18 +905,65 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
864
905
|
}
|
|
865
906
|
|
|
866
907
|
if (round.anchor.expanded === true) {
|
|
867
|
-
const
|
|
908
|
+
const toolsById = new Map<string, any>();
|
|
909
|
+
for (const tool of trackedToolComponents) {
|
|
910
|
+
if (typeof tool?.toolCallId === "string") toolsById.set(tool.toolCallId, tool);
|
|
911
|
+
}
|
|
912
|
+
const cardItems: Array<{ child?: any; tool?: any }> = [];
|
|
913
|
+
const placedToolIds = new Set<string>();
|
|
914
|
+
const takeAssistantRun = (kids: any[], cursor: { i: number }) => {
|
|
915
|
+
while (cursor.i < kids.length && kids[cursor.i] instanceof Spacer) {
|
|
916
|
+
cardItems.push({ child: kids[cursor.i++] });
|
|
917
|
+
}
|
|
918
|
+
if (cursor.i < kids.length) cardItems.push({ child: kids[cursor.i++] });
|
|
919
|
+
};
|
|
920
|
+
const placeTool = (id: string, fallbackName?: string) => {
|
|
921
|
+
const tool = toolsById.get(id);
|
|
922
|
+
if (!tool || EDIT_WRITE_TOOLS.has(String(tool.toolName ?? fallbackName ?? ""))) return;
|
|
923
|
+
if (placedToolIds.has(id)) return;
|
|
924
|
+
placedToolIds.add(id);
|
|
925
|
+
cardItems.push({ tool });
|
|
926
|
+
};
|
|
868
927
|
for (const [component, message] of round.messages) {
|
|
869
928
|
passThroughAssistant(component, message);
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
929
|
+
const kids = Array.isArray(component.contentContainer?.children)
|
|
930
|
+
? [...component.contentContainer.children]
|
|
931
|
+
: [];
|
|
873
932
|
component.contentContainer?.clear?.();
|
|
933
|
+
const cursor = { i: 0 };
|
|
934
|
+
let inThinkingRun = false;
|
|
935
|
+
for (const item of Array.isArray(message?.content) ? message.content : []) {
|
|
936
|
+
if (item?.type === "thinking") {
|
|
937
|
+
if (!String(item.thinking ?? "").trim()) continue;
|
|
938
|
+
if (!inThinkingRun) {
|
|
939
|
+
takeAssistantRun(kids, cursor);
|
|
940
|
+
inThinkingRun = true;
|
|
941
|
+
}
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
inThinkingRun = false;
|
|
945
|
+
if (item?.type === "text" && String(item.text ?? "").trim()) {
|
|
946
|
+
takeAssistantRun(kids, cursor);
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (item?.type === "toolCall" && typeof item.id === "string") {
|
|
950
|
+
placeTool(item.id, item.name);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
while (cursor.i < kids.length) cardItems.push({ child: kids[cursor.i++] });
|
|
954
|
+
}
|
|
955
|
+
for (const message of round.detachedMessages) {
|
|
956
|
+
for (const item of Array.isArray(message?.content) ? message.content : []) {
|
|
957
|
+
if (item?.type === "toolCall" && typeof item.id === "string") {
|
|
958
|
+
placeTool(item.id, item.name);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
874
961
|
}
|
|
875
962
|
const ids = roundToolCallIds(round);
|
|
876
963
|
for (const id of ids) {
|
|
877
964
|
round.suppressedToolIds.add(id);
|
|
878
965
|
expandedRoundToolIds.add(id);
|
|
966
|
+
if (!placedToolIds.has(id)) placeTool(id);
|
|
879
967
|
}
|
|
880
968
|
// Round 展开只打开外层卡片。普通工具保持折叠,避免长输出递归撑满屏幕。
|
|
881
969
|
for (const tool of trackedToolComponents) {
|
|
@@ -896,21 +984,20 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
896
984
|
render(width: number): string[] {
|
|
897
985
|
const theme = themeOf();
|
|
898
986
|
const box = expandedToolCard(theme);
|
|
899
|
-
for (const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
987
|
+
for (const item of cardItems) {
|
|
988
|
+
if (item.child) box.addChild(item.child);
|
|
989
|
+
else if (item.tool) {
|
|
990
|
+
const tool = item.tool;
|
|
991
|
+
box.addToolChild({
|
|
992
|
+
render: (innerWidth: number) => patch.toolOriginalRender.call(tool, innerWidth),
|
|
993
|
+
invalidate: () => tool.invalidate?.(),
|
|
994
|
+
});
|
|
904
995
|
}
|
|
905
|
-
box.addToolChild({
|
|
906
|
-
render: (innerWidth: number) => patch.toolOriginalRender.call(tool, innerWidth),
|
|
907
|
-
invalidate: () => tool.invalidate?.(),
|
|
908
|
-
});
|
|
909
996
|
}
|
|
910
997
|
return ["", ...box.render(width)];
|
|
911
998
|
},
|
|
912
999
|
invalidate() {
|
|
913
|
-
for (const
|
|
1000
|
+
for (const item of cardItems) item.child?.invalidate?.();
|
|
914
1001
|
},
|
|
915
1002
|
});
|
|
916
1003
|
// 展开卡内工具会显示 error,外层仍挂 abort/length,避免只藏在折叠工具里。
|
|
@@ -1066,9 +1153,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1066
1153
|
const name = String(this.toolName ?? "");
|
|
1067
1154
|
if (EDIT_WRITE_TOOLS.has(name)) {
|
|
1068
1155
|
if (!this.result || this.isPartial === true) scheduleAnimation(this);
|
|
1069
|
-
|
|
1070
|
-
if (this.expanded !== true) return lines;
|
|
1071
|
-
return compactEditWriteExpandedLines(this, width, deps.writeMetadata);
|
|
1156
|
+
return compactEditWriteLines(this, width, deps.writeMetadata);
|
|
1072
1157
|
}
|
|
1073
1158
|
// Agent/Task 等同普通工具:折叠不外置(live 面板走独立 widget)。
|
|
1074
1159
|
if (expandedRoundToolIds.has(String(this.toolCallId ?? ""))) return [];
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "./tool/result.ts";
|
|
35
35
|
import { oneLine } from "../utils/format.ts";
|
|
36
36
|
import { showMoreHintText } from "./tool/show-more-hint.ts";
|
|
37
|
+
import { countWriteDiffStats } from "./tool/diff/diff-renderer.ts";
|
|
37
38
|
import { renderRichToolResult, type WriteExecutionMetadataStore } from "./tool/diff/index.ts";
|
|
38
39
|
import { getMessageDisplayTheme } from "./tool/message-display.ts";
|
|
39
40
|
import { humanizeToolLabel, toolCallSummary } from "./tool/names.ts";
|
|
@@ -242,6 +243,22 @@ function createCcstyleTool(
|
|
|
242
243
|
title: label === toolName ? humanizeToolLabel(label) : label,
|
|
243
244
|
variant: "default",
|
|
244
245
|
});
|
|
246
|
+
let writeStatsText = "";
|
|
247
|
+
let writeStatsStyled = "";
|
|
248
|
+
if (toolName === "write" && visualState === "success") {
|
|
249
|
+
const meta = writeExecutionMetadata.get(context?.toolCallId);
|
|
250
|
+
const stats = countWriteDiffStats(
|
|
251
|
+
typeof args?.content === "string" ? args.content : undefined,
|
|
252
|
+
meta?.previousContent,
|
|
253
|
+
meta?.fileExistedBeforeWrite,
|
|
254
|
+
);
|
|
255
|
+
if (stats) {
|
|
256
|
+
writeStatsText = ` (+${stats.added} -${stats.removed})`;
|
|
257
|
+
writeStatsStyled = ` ${theme.fg("dim", "(")}${theme.fg("success", `+${stats.added}`)} ${theme.fg("error", `-${stats.removed}`)}${theme.fg("dim", ")")}`;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const extraText = writeStatsText || summary.detail;
|
|
261
|
+
const extraStyled = writeStatsStyled || theme.fg("dim", summary.detail);
|
|
245
262
|
let cachedWidth: number | undefined;
|
|
246
263
|
let cachedLine: string | undefined;
|
|
247
264
|
const expanded = Boolean(context?.expanded);
|
|
@@ -255,10 +272,10 @@ function createCcstyleTool(
|
|
|
255
272
|
0,
|
|
256
273
|
viewportWidth - visibleWidth(icon) - 1 - (expanded ? 0 : 1),
|
|
257
274
|
);
|
|
258
|
-
const mainWidth = Math.max(0, callWidth - visibleWidth(
|
|
275
|
+
const mainWidth = Math.max(0, callWidth - visibleWidth(extraText));
|
|
259
276
|
cachedWidth = width;
|
|
260
277
|
// 纯文本先截断再着色(省略号不带 ANSI);从头截断,与多 tool 一致
|
|
261
|
-
cachedLine = `${lead}${icon} ${theme.fg("toolTitle", headTruncateToWidth(summary.main, mainWidth))}${
|
|
278
|
+
cachedLine = `${lead}${icon} ${theme.fg("toolTitle", headTruncateToWidth(summary.main, mainWidth))}${extraStyled}`;
|
|
262
279
|
return [truncateToWidth(cachedLine, viewportWidth, "")];
|
|
263
280
|
},
|
|
264
281
|
invalidate() {},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ThinkingPreviewBlock } from "../../feature/compact-thinking.ts";
|
|
1
2
|
import { setHoveredCompactAssistant } from "../compact-mode.ts";
|
|
2
3
|
import { patchRegistry, TOOL_HOVER_STATE_KEY } from "../../utils/patch-keys.ts";
|
|
3
4
|
import {
|
|
@@ -43,6 +44,16 @@ export function setHoveredToolGroup(group: ToolGroupComponent | null): boolean {
|
|
|
43
44
|
return true;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
let hoveredThinking: ThinkingPreviewBlock | null = null;
|
|
48
|
+
|
|
49
|
+
export function setHoveredThinking(block: ThinkingPreviewBlock | null): boolean {
|
|
50
|
+
if (block === hoveredThinking) return false;
|
|
51
|
+
hoveredThinking?.setHintHovered(false);
|
|
52
|
+
hoveredThinking = block;
|
|
53
|
+
block?.setHintHovered(true);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
let hoveredToolIoView: ExpandedToolIoView | null = null;
|
|
47
58
|
let hoveredToolIoSection: ToolIoSection | null = null;
|
|
48
59
|
|
|
@@ -98,6 +109,7 @@ export function cachedFullscreenComponentAtRow(
|
|
|
98
109
|
export type FullscreenHoverTarget =
|
|
99
110
|
| { kind: "button" }
|
|
100
111
|
| { kind: "group"; component: ToolGroupComponent }
|
|
112
|
+
| { kind: "thinking"; component: ThinkingPreviewBlock }
|
|
101
113
|
| { kind: "assistant"; component: any }
|
|
102
114
|
| {
|
|
103
115
|
kind: "tool";
|
|
@@ -119,6 +131,8 @@ export function applyFullscreenHover(tui: any, target: FullscreenHoverTarget | n
|
|
|
119
131
|
}
|
|
120
132
|
const nextGroup = target?.kind === "group" ? target.component : null;
|
|
121
133
|
if (setHoveredToolGroup(nextGroup)) changed = true;
|
|
134
|
+
const nextThinking = target?.kind === "thinking" ? target.component : null;
|
|
135
|
+
if (setHoveredThinking(nextThinking)) changed = true;
|
|
122
136
|
const nextAssistant = target?.kind === "assistant" ? target.component : null;
|
|
123
137
|
if (setHoveredCompactAssistant(nextAssistant)) changed = true;
|
|
124
138
|
const nextView = target?.kind === "tool" ? target.view : null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hasActiveTextPreview, showTextPreview } from "../../feature/context.ts";
|
|
2
|
+
import { ThinkingPreviewBlock } from "../../feature/compact-thinking.ts";
|
|
2
3
|
import { patchRegistry, TOOL_MOUSE_OWNER_KEY } from "../../utils/patch-keys.ts";
|
|
3
4
|
import { ToolGroupComponent } from "../tool/grouping.ts";
|
|
4
5
|
import { isCompactAssistantComponent, setHoveredCompactAssistant } from "../compact-mode.ts";
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
sharedToolHoverState,
|
|
57
58
|
setHoveredToolCallId,
|
|
58
59
|
setHoveredToolGroup,
|
|
60
|
+
setHoveredThinking,
|
|
59
61
|
setHoveredToolIo,
|
|
60
62
|
type FullscreenHoverTarget,
|
|
61
63
|
} from "./hover.ts";
|
|
@@ -273,7 +275,8 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
273
275
|
const isTool = isToolExecutionComponent(component);
|
|
274
276
|
const isGroup = component instanceof ToolGroupComponent;
|
|
275
277
|
const isAssistant = isCompactAssistantComponent(component);
|
|
276
|
-
|
|
278
|
+
const isThinking = component instanceof ThinkingPreviewBlock;
|
|
279
|
+
if (!isTool && !isGroup && !isAssistant && !isThinking) return false;
|
|
277
280
|
if (!component.expanded) {
|
|
278
281
|
// collapsed 仅按钮文本可展开,不能把同一行正文/留白变成点击区。
|
|
279
282
|
const hint = collapsedHintHitbox(line);
|
|
@@ -315,6 +318,7 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
315
318
|
// 点击后清 hover 高亮。
|
|
316
319
|
setHoveredToolCallId(null);
|
|
317
320
|
setHoveredToolGroup(null);
|
|
321
|
+
setHoveredThinking(null);
|
|
318
322
|
setHoveredToolIo(null, null);
|
|
319
323
|
setHoveredCompactAssistant(null);
|
|
320
324
|
card.invalidate?.();
|
|
@@ -360,6 +364,8 @@ function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
|
|
|
360
364
|
);
|
|
361
365
|
if (component instanceof ToolGroupComponent) {
|
|
362
366
|
if (overHint) target = { kind: "group", component };
|
|
367
|
+
} else if (component instanceof ThinkingPreviewBlock) {
|
|
368
|
+
if (component.expanded || overHint) target = { kind: "thinking", component };
|
|
363
369
|
} else if (isToolExecutionComponent(component)) {
|
|
364
370
|
let view: ExpandedToolIoView | null = null;
|
|
365
371
|
let section: ToolIoSection | null = null;
|
|
@@ -778,6 +784,7 @@ export function teardownToolMouseInteraction(
|
|
|
778
784
|
toolMouseInputUnsubscribe = null;
|
|
779
785
|
setHoveredToolCallId(null);
|
|
780
786
|
setHoveredToolGroup(null);
|
|
787
|
+
setHoveredThinking(null);
|
|
781
788
|
setHoveredToolIo(null, null);
|
|
782
789
|
setHoveredCompactAssistant(null);
|
|
783
790
|
try {
|
|
@@ -803,6 +810,7 @@ export function teardownToolMouseInteraction(
|
|
|
803
810
|
/** off 模式清理:清空 hover 与回到底部按钮状态(跨模块 rebind 统一经由此函数)。 */
|
|
804
811
|
export function resetToolHoverState(): void {
|
|
805
812
|
setHoveredToolCallId(null);
|
|
813
|
+
setHoveredThinking(null);
|
|
806
814
|
setHoveredCompactAssistant(null);
|
|
807
815
|
setScrollButtonVisible(false);
|
|
808
816
|
setScrollButtonHovered(false);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { ThinkingPreviewBlock } from "../../feature/compact-thinking.ts";
|
|
2
3
|
import { isCompactAssistantComponent } from "../compact-mode.ts";
|
|
3
4
|
import { ToolGroupComponent } from "../tool/grouping.ts";
|
|
4
5
|
import { isToolExecutionComponent, stripTerminalSequencesPreservingLayout } from "./packets.ts";
|
|
@@ -97,6 +98,9 @@ export function componentAtLocalRow(
|
|
|
97
98
|
// compact 摘要行整体作为可展开卡片(折叠摘要 / 展开内容)。
|
|
98
99
|
return { component, row: localRow };
|
|
99
100
|
}
|
|
101
|
+
if (component instanceof ThinkingPreviewBlock) {
|
|
102
|
+
return { component, row: localRow };
|
|
103
|
+
}
|
|
100
104
|
if (component === getScrollButtonWidget()) {
|
|
101
105
|
return { component, row: localRow };
|
|
102
106
|
}
|
|
@@ -134,7 +138,8 @@ export function collectFullscreenToolCards(
|
|
|
134
138
|
if (
|
|
135
139
|
isToolExecutionComponent(component) ||
|
|
136
140
|
component instanceof ToolGroupComponent ||
|
|
137
|
-
isCompactAssistantComponent(component)
|
|
141
|
+
isCompactAssistantComponent(component) ||
|
|
142
|
+
component instanceof ThinkingPreviewBlock
|
|
138
143
|
) {
|
|
139
144
|
out.push(component);
|
|
140
145
|
return;
|
|
@@ -39,7 +39,8 @@ export function displayConfigCacheKey(config: ToolDisplayConfig): string {
|
|
|
39
39
|
config.diffViewMode,
|
|
40
40
|
config.diffIndicatorMode,
|
|
41
41
|
String(config.diffSplitMinWidth),
|
|
42
|
-
String(config.
|
|
42
|
+
String(config.editDiffCollapsedLines),
|
|
43
|
+
String(config.writeDiffCollapsedLines),
|
|
43
44
|
config.diffWordWrap ? "1" : "0",
|
|
44
45
|
String(config.expandedPreviewMaxLines),
|
|
45
46
|
].join(":");
|
|
@@ -143,7 +143,7 @@ export function renderEditDiffResult(
|
|
|
143
143
|
const headerRows = renderHeaderRows(parsed.stats, mode, safeWidth, theme);
|
|
144
144
|
const displayLimit = resolveDiffDisplayLimit(
|
|
145
145
|
options.expanded,
|
|
146
|
-
live.
|
|
146
|
+
live.editDiffCollapsedLines,
|
|
147
147
|
live.expandedPreviewMaxLines,
|
|
148
148
|
);
|
|
149
149
|
const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
|
|
@@ -177,7 +177,7 @@ export function renderEditDiffResult(
|
|
|
177
177
|
bodyRows,
|
|
178
178
|
safeWidth,
|
|
179
179
|
options.expanded,
|
|
180
|
-
live.
|
|
180
|
+
live.editDiffCollapsedLines,
|
|
181
181
|
live.expandedPreviewMaxLines,
|
|
182
182
|
parsed.stats.hunks,
|
|
183
183
|
theme,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
-
import { DEFAULT_TOOL_DISPLAY_CONFIG } from "../../../config/config.ts";
|
|
2
|
+
import { DEFAULT_TOOL_DISPLAY_CONFIG, type ToolDisplayConfig } from "../../../config/config.ts";
|
|
3
3
|
import { showMoreHintText } from "../show-more-hint.ts";
|
|
4
4
|
import {
|
|
5
5
|
buildCollapsedDiffHintText,
|
|
@@ -41,10 +41,24 @@ export function resolveDiffDisplayLimit(
|
|
|
41
41
|
: DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines;
|
|
42
42
|
const collapsedLimit = Number.isFinite(maxCollapsedLines)
|
|
43
43
|
? maxCollapsedLines
|
|
44
|
-
: DEFAULT_TOOL_DISPLAY_CONFIG.
|
|
44
|
+
: DEFAULT_TOOL_DISPLAY_CONFIG.editDiffCollapsedLines;
|
|
45
45
|
return expanded ? Math.max(0, expandedLimit) : Math.max(1, collapsedLimit);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** Write collapsed body limit. Missing/NaN falls back to edit's `editDiffCollapsedLines`. */
|
|
49
|
+
export function resolveWriteCollapsedLimit(
|
|
50
|
+
config: Pick<ToolDisplayConfig, "writeDiffCollapsedLines" | "editDiffCollapsedLines">,
|
|
51
|
+
): number {
|
|
52
|
+
const value = config.writeDiffCollapsedLines;
|
|
53
|
+
if (Number.isFinite(value)) {
|
|
54
|
+
return Math.max(0, value);
|
|
55
|
+
}
|
|
56
|
+
const fallback = Number.isFinite(config.editDiffCollapsedLines)
|
|
57
|
+
? config.editDiffCollapsedLines
|
|
58
|
+
: DEFAULT_TOOL_DISPLAY_CONFIG.writeDiffCollapsedLines;
|
|
59
|
+
return Math.max(0, fallback);
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
/**
|
|
49
63
|
* How many source rows/entries to fully highlight+render before applyLineLimit.
|
|
50
64
|
* Extra headroom covers word-wrap expansion so the display limit can still fill.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Text, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
2
2
|
import { sanitizeToolResultText } from "../../../utils/tool-result-sanitize.ts";
|
|
3
|
+
import { showMoreHintText } from "../show-more-hint.ts";
|
|
3
4
|
import { splitWriteContentLines } from "./diff-text.ts";
|
|
4
5
|
import {
|
|
5
6
|
collectDiffStats,
|
|
@@ -31,9 +32,11 @@ import {
|
|
|
31
32
|
import { renderDiffFrameLine, renderSingleDiffRow, renderWriteHeader } from "./diff-header.ts";
|
|
32
33
|
import {
|
|
33
34
|
applyLineLimit,
|
|
35
|
+
clampDiffLineToWidth,
|
|
34
36
|
clampDiffLinesToWidth,
|
|
35
37
|
resolveDiffDisplayLimit,
|
|
36
38
|
resolveDiffProcessBudget,
|
|
39
|
+
resolveWriteCollapsedLimit,
|
|
37
40
|
takeEntriesForLineBudget,
|
|
38
41
|
takeSplitRowsForBudget,
|
|
39
42
|
} from "./diff-limits.ts";
|
|
@@ -286,6 +289,41 @@ function renderWriteOverwriteGuardRows(
|
|
|
286
289
|
return renderSingleDiffRow(buildWriteOverwriteGuardText(guard, width), "warning", width, theme);
|
|
287
290
|
}
|
|
288
291
|
|
|
292
|
+
/** Folded write with `writeDiffCollapsedLines: 0`: `↳ created • click to show more`. */
|
|
293
|
+
function renderWriteCollapsedHintLine(
|
|
294
|
+
wasOverwrite: boolean,
|
|
295
|
+
width: number,
|
|
296
|
+
theme: DiffTheme,
|
|
297
|
+
hovered: boolean,
|
|
298
|
+
headerLabel?: string,
|
|
299
|
+
): string[] {
|
|
300
|
+
const safeWidth = normalizeDiffRenderWidth(width);
|
|
301
|
+
if (safeWidth === 0) {
|
|
302
|
+
return [""];
|
|
303
|
+
}
|
|
304
|
+
const actionLabel = headerLabel?.trim() || (wasOverwrite ? "overwritten" : "created");
|
|
305
|
+
const clickLabel = showMoreHintText();
|
|
306
|
+
const candidates = [`↳ ${actionLabel} • ${clickLabel}`, `↳ ${actionLabel}`, actionLabel, "…"];
|
|
307
|
+
let text = candidates[candidates.length - 1] ?? "…";
|
|
308
|
+
for (const candidate of candidates) {
|
|
309
|
+
if (visibleWidth(candidate) <= safeWidth) {
|
|
310
|
+
text = candidate;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (visibleWidth(text) > safeWidth) {
|
|
315
|
+
text = truncateToWidth(text, safeWidth, "");
|
|
316
|
+
}
|
|
317
|
+
const clickIndex = text.lastIndexOf(clickLabel);
|
|
318
|
+
const styled =
|
|
319
|
+
hovered && clickIndex >= 0
|
|
320
|
+
? theme.fg("muted", text.slice(0, clickIndex)) +
|
|
321
|
+
theme.fg("text", clickLabel) +
|
|
322
|
+
theme.fg("muted", text.slice(clickIndex + clickLabel.length))
|
|
323
|
+
: theme.fg("muted", text);
|
|
324
|
+
return [clampDiffLineToWidth(styled, safeWidth)];
|
|
325
|
+
}
|
|
326
|
+
|
|
289
327
|
/**
|
|
290
328
|
* write 变更行统计(compact 单行用):复用当前 write diff 算法。
|
|
291
329
|
* 新文件 = 全部新内容为 added;覆盖已有文件用 LCS 精确统计。
|
|
@@ -392,6 +430,28 @@ export function renderWriteDiffResult(
|
|
|
392
430
|
return cached;
|
|
393
431
|
}
|
|
394
432
|
|
|
433
|
+
const writeCollapsedLimit = resolveWriteCollapsedLimit(live);
|
|
434
|
+
const statsOnlyCollapsed = !options.expanded && writeCollapsedLimit === 0;
|
|
435
|
+
if (statsOnlyCollapsed) {
|
|
436
|
+
return cache.set(
|
|
437
|
+
safeWidth,
|
|
438
|
+
options.expanded,
|
|
439
|
+
mode,
|
|
440
|
+
configKey,
|
|
441
|
+
hovered,
|
|
442
|
+
clampDiffLinesToWidth(
|
|
443
|
+
renderWriteCollapsedHintLine(
|
|
444
|
+
options.fileExistedBeforeWrite === true,
|
|
445
|
+
safeWidth,
|
|
446
|
+
theme,
|
|
447
|
+
hovered,
|
|
448
|
+
options.headerLabel,
|
|
449
|
+
),
|
|
450
|
+
safeWidth,
|
|
451
|
+
),
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
395
455
|
const header = renderWriteHeader(
|
|
396
456
|
options.fileExistedBeforeWrite === true,
|
|
397
457
|
safeWidth,
|
|
@@ -442,7 +502,7 @@ export function renderWriteDiffResult(
|
|
|
442
502
|
const data = getDetailedData();
|
|
443
503
|
const displayLimit = resolveDiffDisplayLimit(
|
|
444
504
|
options.expanded,
|
|
445
|
-
|
|
505
|
+
writeCollapsedLimit,
|
|
446
506
|
live.expandedPreviewMaxLines,
|
|
447
507
|
);
|
|
448
508
|
const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
|
|
@@ -480,7 +540,7 @@ export function renderWriteDiffResult(
|
|
|
480
540
|
bodyRows,
|
|
481
541
|
safeWidth,
|
|
482
542
|
options.expanded,
|
|
483
|
-
|
|
543
|
+
writeCollapsedLimit,
|
|
484
544
|
live.expandedPreviewMaxLines,
|
|
485
545
|
data.hunkCount,
|
|
486
546
|
theme,
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type Component,
|
|
8
8
|
} from "@earendil-works/pi-tui";
|
|
9
9
|
import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
|
|
10
|
-
import { showMoreHintText } from "./show-more-hint.ts";
|
|
10
|
+
import { isToolTuiFullscreen, showMoreHintText } from "./show-more-hint.ts";
|
|
11
11
|
import { stripAnsi, stripBackgroundAnsi, stripLeadingStatusIcon } from "../../utils/ansi-text.ts";
|
|
12
12
|
import { walkComponentTree } from "../../utils/component-tree.ts";
|
|
13
13
|
import { humanizeToolLabel, toolCallSummary } from "./names.ts";
|
|
@@ -149,6 +149,17 @@ function toolNameList(tools: any[]): string {
|
|
|
149
149
|
|
|
150
150
|
let nextGroupId = 1;
|
|
151
151
|
|
|
152
|
+
type SettledGroupCache = {
|
|
153
|
+
width: number;
|
|
154
|
+
hover: boolean;
|
|
155
|
+
theme: unknown;
|
|
156
|
+
fullscreen: boolean;
|
|
157
|
+
children: readonly unknown[];
|
|
158
|
+
args: unknown[];
|
|
159
|
+
results: unknown[];
|
|
160
|
+
lines: string[];
|
|
161
|
+
};
|
|
162
|
+
|
|
152
163
|
export class ToolGroupComponent extends Container {
|
|
153
164
|
readonly toolCallId = `ccstyle-tool-group-${nextGroupId++}`;
|
|
154
165
|
readonly toolName = "Tool group";
|
|
@@ -159,6 +170,8 @@ export class ToolGroupComponent extends Container {
|
|
|
159
170
|
}
|
|
160
171
|
private hintHovered = false;
|
|
161
172
|
private readonly patch: Patch;
|
|
173
|
+
/** 仅缓存已完成且折叠的分组;pending / expanded 每帧现算。 */
|
|
174
|
+
private settledCache: SettledGroupCache | undefined;
|
|
162
175
|
|
|
163
176
|
constructor(patch: Patch) {
|
|
164
177
|
super();
|
|
@@ -167,11 +180,13 @@ export class ToolGroupComponent extends Container {
|
|
|
167
180
|
}
|
|
168
181
|
|
|
169
182
|
addTool(tool: any): void {
|
|
183
|
+
this.settledCache = undefined;
|
|
170
184
|
this.children.push(tool);
|
|
171
185
|
tool[PARENT_KEY] = this;
|
|
172
186
|
}
|
|
173
187
|
|
|
174
188
|
releaseTools(): any[] {
|
|
189
|
+
this.settledCache = undefined;
|
|
175
190
|
const tools = [...this.children];
|
|
176
191
|
this.children.length = 0;
|
|
177
192
|
this.patch.groups.delete(this);
|
|
@@ -179,18 +194,21 @@ export class ToolGroupComponent extends Container {
|
|
|
179
194
|
}
|
|
180
195
|
|
|
181
196
|
removeTool(tool: any): void {
|
|
197
|
+
this.settledCache = undefined;
|
|
182
198
|
const index = this.children.indexOf(tool);
|
|
183
199
|
if (index >= 0) this.children.splice(index, 1);
|
|
184
200
|
if (tool?.[PARENT_KEY] === this) delete tool[PARENT_KEY];
|
|
185
201
|
}
|
|
186
202
|
|
|
187
203
|
setExpanded(expanded: boolean): void {
|
|
204
|
+
if (this._expanded !== expanded) this.settledCache = undefined;
|
|
188
205
|
this._expanded = expanded;
|
|
189
206
|
for (const tool of this.children)
|
|
190
207
|
(tool as Component & { setExpanded?: (expanded: boolean) => void }).setExpanded?.(expanded);
|
|
191
208
|
}
|
|
192
209
|
|
|
193
210
|
setHintHovered(hovered: boolean): void {
|
|
211
|
+
if (this.hintHovered !== hovered) this.settledCache = undefined;
|
|
194
212
|
this.hintHovered = hovered;
|
|
195
213
|
}
|
|
196
214
|
|
|
@@ -219,10 +237,53 @@ export class ToolGroupComponent extends Container {
|
|
|
219
237
|
}
|
|
220
238
|
|
|
221
239
|
invalidate(): void {
|
|
240
|
+
this.settledCache = undefined;
|
|
222
241
|
for (const tool of this.children) tool.invalidate?.();
|
|
223
242
|
}
|
|
224
243
|
|
|
244
|
+
private settledCacheHit(width: number): string[] | undefined {
|
|
245
|
+
const cache = this.settledCache;
|
|
246
|
+
if (!cache || this._expanded) return;
|
|
247
|
+
if (
|
|
248
|
+
cache.width !== width ||
|
|
249
|
+
cache.hover !== this.hintHovered ||
|
|
250
|
+
cache.theme !== this.patch.theme ||
|
|
251
|
+
cache.fullscreen !== isToolTuiFullscreen()
|
|
252
|
+
) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const tools = this.children as any[];
|
|
256
|
+
if (cache.children.length !== tools.length) return;
|
|
257
|
+
for (let i = 0; i < tools.length; i++) {
|
|
258
|
+
const tool = tools[i];
|
|
259
|
+
if (
|
|
260
|
+
cache.children[i] !== tool ||
|
|
261
|
+
cache.args[i] !== tool?.args ||
|
|
262
|
+
cache.results[i] !== tool?.result ||
|
|
263
|
+
status(tool) === "pending"
|
|
264
|
+
) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return cache.lines;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private storeSettledCache(width: number, lines: string[]): void {
|
|
272
|
+
this.settledCache = {
|
|
273
|
+
width,
|
|
274
|
+
hover: this.hintHovered,
|
|
275
|
+
theme: this.patch.theme,
|
|
276
|
+
fullscreen: isToolTuiFullscreen(),
|
|
277
|
+
children: [...this.children],
|
|
278
|
+
args: (this.children as any[]).map((tool) => tool?.args),
|
|
279
|
+
results: (this.children as any[]).map((tool) => tool?.result),
|
|
280
|
+
lines,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
225
284
|
render(width: number): string[] {
|
|
285
|
+
const cached = this.settledCacheHit(width);
|
|
286
|
+
if (cached) return cached;
|
|
226
287
|
const theme = this.patch.theme;
|
|
227
288
|
const fg = (color: string, text: string) => theme?.fg?.(color, text) ?? text;
|
|
228
289
|
const counts = { pending: 0, success: 0, error: 0 };
|
|
@@ -296,6 +357,8 @@ export class ToolGroupComponent extends Container {
|
|
|
296
357
|
lines.push(paddedBackgroundRow(theme, backgroundSlot, line, width));
|
|
297
358
|
}
|
|
298
359
|
lines.push(paddedBackgroundRow(theme, backgroundSlot, "", width));
|
|
360
|
+
} else if (counts.pending === 0) {
|
|
361
|
+
this.storeSettledCache(width, lines);
|
|
299
362
|
}
|
|
300
363
|
return lines;
|
|
301
364
|
}
|