pi-cc-extensions 0.8.60 → 0.8.61
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 +19 -0
- package/extensions/config/panel.ts +26 -3
- package/extensions/feature/compact-thinking.ts +211 -22
- package/extensions/renderer/compact-mode.ts +112 -43
- 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 +1 -0
- package/extensions/renderer/tool/diff/diff-limits.ts +15 -1
- package/extensions/renderer/tool/diff/diff-write-render.ts +62 -2
- package/extensions/renderer/tool/grouping.ts +58 -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
|
-
"diffCollapsedLines": 24, // diff body lines shown when collapsed; beyond that shows the expand hint (Ctrl+O / click)
|
|
59
|
+
"diffCollapsedLines": 24, // edit/diff body lines shown when collapsed; 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
|
-
"diffCollapsedLines": 24, // 折叠时展示的 diff 行数,超出显示展开提示(Ctrl+O / 点击)
|
|
59
|
+
"diffCollapsedLines": 24, // 折叠时展示的 edit/diff 行数,超出显示展开提示(Ctrl+O / 点击)
|
|
60
|
+
"writeDiffCollapsedLines": 0, // write 折叠行数;0 = ↳ created • 展开提示(统计在标题)
|
|
60
61
|
"diffWordWrap": true, // 长 diff 行是否换行(否则截断)
|
|
61
62
|
"expandedPreviewMaxLines": 40, // 展开后输出/diff 正文的最大行数
|
|
62
63
|
"useSummaryTitlesAsThinkingTitle": true, // 用最新 provider 摘要作为思考标题
|
|
@@ -16,6 +16,8 @@ export interface ToolDisplayConfig {
|
|
|
16
16
|
diffIndicatorMode: DiffIndicatorMode;
|
|
17
17
|
diffSplitMinWidth: number;
|
|
18
18
|
diffCollapsedLines: 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
|
}
|
|
@@ -26,6 +28,11 @@ export const DEFAULT_TOOL_DISPLAY_CONFIG: ToolDisplayConfig = {
|
|
|
26
28
|
diffSplitMinWidth: 120,
|
|
27
29
|
/** Collapsed tool/diff body: ~half a typical terminal after chrome. */
|
|
28
30
|
diffCollapsedLines: 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,
|
|
@@ -42,6 +49,7 @@ export type Config = {
|
|
|
42
49
|
diffIndicatorMode: DiffIndicatorMode;
|
|
43
50
|
diffSplitMinWidth: number;
|
|
44
51
|
diffCollapsedLines: 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"];
|
|
@@ -90,6 +100,7 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
90
100
|
diffIndicatorMode: DEFAULT_TOOL_DISPLAY_CONFIG.diffIndicatorMode,
|
|
91
101
|
diffSplitMinWidth: DEFAULT_TOOL_DISPLAY_CONFIG.diffSplitMinWidth,
|
|
92
102
|
diffCollapsedLines: DEFAULT_TOOL_DISPLAY_CONFIG.diffCollapsedLines,
|
|
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,
|
|
@@ -164,6 +175,12 @@ export function normalizeConfig(input: unknown): Config {
|
|
|
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,
|
|
@@ -207,6 +224,7 @@ export function getToolDisplayConfig(source: Config = config): ToolDisplayConfig
|
|
|
207
224
|
diffIndicatorMode: source.diffIndicatorMode,
|
|
208
225
|
diffSplitMinWidth: source.diffSplitMinWidth,
|
|
209
226
|
diffCollapsedLines: source.diffCollapsedLines,
|
|
227
|
+
writeDiffCollapsedLines: source.writeDiffCollapsedLines,
|
|
210
228
|
diffWordWrap: source.diffWordWrap,
|
|
211
229
|
expandedPreviewMaxLines: source.expandedPreviewMaxLines,
|
|
212
230
|
};
|
|
@@ -224,6 +242,7 @@ export function formatConfigStatus(source: Config = config): string {
|
|
|
224
242
|
`diffIndicator=${source.diffIndicatorMode}`,
|
|
225
243
|
`diffSplitMin=${source.diffSplitMinWidth}`,
|
|
226
244
|
`diffCollapsed=${source.diffCollapsedLines}`,
|
|
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.";
|
|
@@ -288,14 +289,24 @@ export async function showCcstylePanel(
|
|
|
288
289
|
};
|
|
289
290
|
const diffCollapsedSetting = {
|
|
290
291
|
id: "diffCollapsedLines",
|
|
291
|
-
label: "
|
|
292
|
+
label: "Edit collapsed lines",
|
|
292
293
|
description:
|
|
293
|
-
"How many diff body lines to show before the expand hint (Ctrl+O / click).
|
|
294
|
+
"How many edit/diff body lines to show before the expand hint (Ctrl+O / click). Write uses its own setting below.",
|
|
294
295
|
currentValue: String(config.diffCollapsedLines),
|
|
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",
|
|
@@ -453,6 +464,17 @@ export async function showCcstylePanel(
|
|
|
453
464
|
});
|
|
454
465
|
diffCollapsedSetting.currentValue = String(config.diffCollapsedLines);
|
|
455
466
|
break;
|
|
467
|
+
case "writeDiffCollapsedLines":
|
|
468
|
+
updateConfig({
|
|
469
|
+
writeDiffCollapsedLines: pickPositiveInt(
|
|
470
|
+
value,
|
|
471
|
+
DEFAULT_CONFIG.writeDiffCollapsedLines,
|
|
472
|
+
0,
|
|
473
|
+
500,
|
|
474
|
+
),
|
|
475
|
+
});
|
|
476
|
+
writeDiffCollapsedSetting.currentValue = String(config.writeDiffCollapsedLines);
|
|
477
|
+
break;
|
|
456
478
|
case "diffWordWrap":
|
|
457
479
|
updateConfig({ diffWordWrap: value === "on" });
|
|
458
480
|
diffWordWrapSetting.description = config.diffWordWrap
|
|
@@ -519,6 +541,7 @@ export async function showCcstylePanel(
|
|
|
519
541
|
diffIndicatorSetting,
|
|
520
542
|
diffSplitSetting,
|
|
521
543
|
diffCollapsedSetting,
|
|
544
|
+
writeDiffCollapsedSetting,
|
|
522
545
|
diffWordWrapSetting,
|
|
523
546
|
expandedMaxSetting,
|
|
524
547
|
],
|
|
@@ -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,27 +169,206 @@ 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
|
+
const previewCache = new Map<string, { lines: string[]; hiddenBefore: number }>();
|
|
178
|
+
const PREVIEW_CACHE_MAX = 500;
|
|
179
|
+
const PREVIEW_BUDGET_MIN_CHARS = 2_000;
|
|
180
|
+
const PREVIEW_BUDGET_SLACK_LINES = 2;
|
|
181
|
+
|
|
182
|
+
/** 按可见宽度估算折行数,不走 Text 全量 wrap。无换行长段也能计到隐藏行。 */
|
|
183
|
+
function countWrappedLines(text: string, contentWidth: number): number {
|
|
184
|
+
if (!text) return 0;
|
|
185
|
+
const width = Math.max(1, contentWidth);
|
|
186
|
+
let lines = 0;
|
|
187
|
+
for (const raw of text.replace(/\t/g, " ").split(/\r\n|\r|\n/)) {
|
|
188
|
+
const w = visibleWidth(raw);
|
|
189
|
+
lines += w === 0 ? 1 : Math.ceil(w / width);
|
|
190
|
+
}
|
|
191
|
+
return lines;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function padPreviewLine(line: string, width: number, padding: number): string {
|
|
195
|
+
const left = padding > 0 ? " ".repeat(padding) : "";
|
|
196
|
+
const right = padding > 0 ? " ".repeat(padding) : "";
|
|
197
|
+
const withMargins = left + line + right;
|
|
198
|
+
return withMargins + " ".repeat(Math.max(0, width - visibleWidth(withMargins)));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function layoutThinkingPreview(
|
|
202
|
+
text: string,
|
|
203
|
+
width: number,
|
|
204
|
+
padding: number,
|
|
205
|
+
): { visible: string[]; hiddenLines: number } {
|
|
206
|
+
if (!text || config.previewLines <= 0) return { visible: [], hiddenLines: 0 };
|
|
207
|
+
const contentWidth = Math.max(1, width - padding * 2);
|
|
208
|
+
const budget = Math.max(
|
|
209
|
+
width * (config.previewLines + PREVIEW_BUDGET_SLACK_LINES),
|
|
210
|
+
PREVIEW_BUDGET_MIN_CHARS,
|
|
211
|
+
);
|
|
212
|
+
let source = text;
|
|
213
|
+
let cut = 0;
|
|
214
|
+
if (source.length > budget) {
|
|
215
|
+
const start = source.length - budget;
|
|
216
|
+
const newline = source.indexOf("\n", start);
|
|
217
|
+
cut = newline === -1 ? start : newline + 1;
|
|
218
|
+
if (cut < source.length) source = source.slice(cut);
|
|
219
|
+
else cut = 0;
|
|
220
|
+
}
|
|
221
|
+
const cacheKey = `${width}:${padding}:${cut}:${source}`;
|
|
222
|
+
let entry = previewCache.get(cacheKey);
|
|
223
|
+
if (!entry) {
|
|
224
|
+
entry = {
|
|
225
|
+
lines: wrapTextWithAnsi(source.replace(/\t/g, " "), contentWidth),
|
|
226
|
+
hiddenBefore: cut > 0 ? countWrappedLines(text.slice(0, cut), contentWidth) : 0,
|
|
227
|
+
};
|
|
228
|
+
if (previewCache.size >= PREVIEW_CACHE_MAX) previewCache.clear();
|
|
229
|
+
previewCache.set(cacheKey, entry);
|
|
230
|
+
}
|
|
231
|
+
const hiddenLines = entry.hiddenBefore + Math.max(0, entry.lines.length - config.previewLines);
|
|
232
|
+
const visible = hiddenLines > 0 ? entry.lines.slice(-config.previewLines) : entry.lines;
|
|
233
|
+
return { visible, hiddenLines };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function hiddenPreviewHint(
|
|
237
|
+
hiddenLines: number,
|
|
238
|
+
forceExpandHint = false,
|
|
239
|
+
): { prefix: string; action: string; suffix: string } | undefined {
|
|
240
|
+
const action = thinkingExpandAction() ?? "";
|
|
241
|
+
if (hiddenLines > 0) {
|
|
242
|
+
const noun = hiddenLines === 1 ? "line" : "lines";
|
|
243
|
+
return {
|
|
244
|
+
prefix: ` • (${hiddenLines} more ${noun}${action ? ", " : ""}`,
|
|
245
|
+
action,
|
|
246
|
+
suffix: ")",
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (forceExpandHint && isToolTuiFullscreen()) {
|
|
250
|
+
return { prefix: " • ", action: "click to show more", suffix: "" };
|
|
251
|
+
}
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const expandedThinking = new Set<number>();
|
|
256
|
+
|
|
257
|
+
/** 折叠预览 + 展开全文。fullscreen 点击 hint 展开、再点整块收起,对齐工具卡。 */
|
|
258
|
+
export class ThinkingPreviewBlock implements Component {
|
|
259
|
+
private heading: string;
|
|
167
260
|
private text: string;
|
|
168
261
|
private padding: number;
|
|
262
|
+
private messageTimestamp: number;
|
|
169
263
|
private style: (text: string) => string;
|
|
170
|
-
|
|
171
|
-
|
|
264
|
+
private theme: Theme | undefined;
|
|
265
|
+
private _expanded: boolean;
|
|
266
|
+
private hintHovered = false;
|
|
267
|
+
private expandedBody: { width: number; lines: string[] } | undefined;
|
|
268
|
+
|
|
269
|
+
constructor(
|
|
270
|
+
heading: string,
|
|
271
|
+
text: string,
|
|
272
|
+
padding: number,
|
|
273
|
+
messageTimestamp: number,
|
|
274
|
+
style: (text: string) => string,
|
|
275
|
+
theme?: Theme,
|
|
276
|
+
) {
|
|
277
|
+
this.heading = heading;
|
|
172
278
|
this.text = text;
|
|
173
279
|
this.padding = padding;
|
|
280
|
+
this.messageTimestamp = messageTimestamp;
|
|
174
281
|
this.style = style;
|
|
282
|
+
this.theme = theme;
|
|
283
|
+
this._expanded = expandedThinking.has(messageTimestamp);
|
|
175
284
|
}
|
|
176
285
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
286
|
+
private paint(color: string, text: string): string {
|
|
287
|
+
return this.theme && typeof this.theme.fg === "function"
|
|
288
|
+
? this.theme.fg(color as never, text)
|
|
289
|
+
: text;
|
|
290
|
+
}
|
|
180
291
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
292
|
+
get expanded(): boolean {
|
|
293
|
+
return this._expanded;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
setExpanded(expanded: boolean): void {
|
|
297
|
+
this._expanded = expanded;
|
|
298
|
+
if (expanded) expandedThinking.add(this.messageTimestamp);
|
|
299
|
+
else {
|
|
300
|
+
expandedThinking.delete(this.messageTimestamp);
|
|
301
|
+
this.expandedBody = undefined;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
setHintHovered(hovered: boolean): void {
|
|
306
|
+
this.hintHovered = hovered;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private headingLines(width: number, hiddenLines: number, padding: number): string[] {
|
|
310
|
+
const hint = this._expanded
|
|
311
|
+
? undefined
|
|
312
|
+
: hiddenPreviewHint(hiddenLines, Boolean(this.text) && config.previewLines <= 0);
|
|
313
|
+
if (!hint) return new Text(this.heading, padding, 0).render(width);
|
|
314
|
+
const rawHint = hint.prefix + hint.action + hint.suffix;
|
|
315
|
+
const contentWidth = Math.max(1, width - padding * 2);
|
|
316
|
+
const headingBudget = Math.max(0, contentWidth - visibleWidth(rawHint));
|
|
317
|
+
const clipped =
|
|
318
|
+
visibleWidth(this.heading) > headingBudget
|
|
319
|
+
? truncateToWidth(this.heading, headingBudget, "")
|
|
320
|
+
: this.heading;
|
|
321
|
+
const action = this.paint(this.hintHovered ? "text" : "dim", hint.action);
|
|
322
|
+
return new Text(
|
|
323
|
+
clipped + this.paint("dim", hint.prefix) + action + this.paint("dim", hint.suffix),
|
|
324
|
+
padding,
|
|
325
|
+
0,
|
|
326
|
+
).render(width);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private bodyLines(width: number, padding: number): { lines: string[]; hiddenLines: number } {
|
|
330
|
+
if (!this.text || (config.previewLines <= 0 && !this._expanded)) {
|
|
331
|
+
return { lines: [], hiddenLines: 0 };
|
|
332
|
+
}
|
|
333
|
+
if (this._expanded) {
|
|
334
|
+
if (this.expandedBody?.width !== width) {
|
|
335
|
+
this.expandedBody = {
|
|
336
|
+
width,
|
|
337
|
+
lines: wrapTextWithAnsi(this.text.replace(/\t/g, " "), Math.max(1, width)),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return { lines: this.expandedBody.lines, hiddenLines: 0 };
|
|
341
|
+
}
|
|
342
|
+
const preview = layoutThinkingPreview(this.text, width, padding);
|
|
343
|
+
return { lines: preview.visible, hiddenLines: preview.hiddenLines };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
render(width: number) {
|
|
347
|
+
if (this._expanded) {
|
|
348
|
+
const innerWidth = Math.max(1, width - 2);
|
|
349
|
+
const body = this.bodyLines(innerWidth, 0);
|
|
350
|
+
const heading = this.headingLines(innerWidth, 0, 0);
|
|
351
|
+
const inner = body.lines.length
|
|
352
|
+
? [...heading, ...body.lines.map((line) => this.style(line))]
|
|
353
|
+
: heading;
|
|
354
|
+
const bgFn =
|
|
355
|
+
this.theme && typeof this.theme.bg === "function"
|
|
356
|
+
? (text: string) => this.theme!.bg("userMessageBg" as never, text)
|
|
357
|
+
: undefined;
|
|
358
|
+
const box = new Box(1, 1, bgFn);
|
|
359
|
+
box.addChild({
|
|
360
|
+
render: () => inner,
|
|
361
|
+
invalidate() {},
|
|
362
|
+
});
|
|
363
|
+
return box.render(width);
|
|
364
|
+
}
|
|
365
|
+
const body = this.bodyLines(width, this.padding);
|
|
366
|
+
const heading = this.headingLines(width, body.hiddenLines, this.padding);
|
|
367
|
+
if (!body.lines.length) return heading;
|
|
368
|
+
return [
|
|
369
|
+
...heading,
|
|
370
|
+
...body.lines.map((line) => padPreviewLine(this.style(line), width, this.padding)),
|
|
371
|
+
];
|
|
187
372
|
}
|
|
188
373
|
|
|
189
374
|
invalidate() {}
|
|
@@ -461,14 +646,17 @@ function compactThinking(pi: ExtensionAPI) {
|
|
|
461
646
|
durationText ? `Thought for ${durationText}` : (latestSummary?.title ?? "Thought"),
|
|
462
647
|
);
|
|
463
648
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
649
|
+
const previewSource = (latestSummary?.body ?? thinkingBlocks.join("\n\n")).trim();
|
|
650
|
+
self.contentContainer.addChild(
|
|
651
|
+
new ThinkingPreviewBlock(
|
|
652
|
+
heading,
|
|
653
|
+
previewSource,
|
|
654
|
+
self.outputPad,
|
|
655
|
+
message.timestamp,
|
|
656
|
+
thinkingStyle,
|
|
657
|
+
activeTheme,
|
|
658
|
+
),
|
|
659
|
+
);
|
|
472
660
|
|
|
473
661
|
const hasVisibleContentAfter = message.content
|
|
474
662
|
.slice(i + 1)
|
|
@@ -733,6 +921,7 @@ function compactThinking(pi: ExtensionAPI) {
|
|
|
733
921
|
latestComponentTimestamp = undefined;
|
|
734
922
|
completedDurations.clear();
|
|
735
923
|
streamingComponents.clear();
|
|
924
|
+
expandedThinking.clear();
|
|
736
925
|
if (ctx.mode === "tui") ctx.ui.setWidget(WIDGET_ID, undefined);
|
|
737
926
|
|
|
738
927
|
if (patchInstalled) {
|
|
@@ -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
|
+
* 与 `diffCollapsedLines` / `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,50 @@ function compactEditWriteLine(
|
|
|
522
526
|
return ["", truncateToWidth(line, width, "")];
|
|
523
527
|
}
|
|
524
528
|
|
|
525
|
-
/**
|
|
526
|
-
|
|
529
|
+
/**
|
|
530
|
+
* compact edit/write:标题行 + mode=on 同一套 rich diff。
|
|
531
|
+
* 折叠/展开都走 `renderRichToolResult`,limits 不另开一套。
|
|
532
|
+
*/
|
|
533
|
+
function compactEditWriteLines(
|
|
527
534
|
component: any,
|
|
528
535
|
width: number,
|
|
529
536
|
writeMetadata?: WriteExecutionMetadataStore,
|
|
530
537
|
): string[] {
|
|
531
538
|
const theme = themeOf();
|
|
532
539
|
const result = component.result;
|
|
540
|
+
const expanded = component.expanded === true;
|
|
533
541
|
const isError = result?.isError === true;
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
542
|
+
const isPending = !result || component.isPartial === true;
|
|
543
|
+
const candidate =
|
|
544
|
+
!isPending && writeMetadata
|
|
545
|
+
? renderRichToolResult(
|
|
546
|
+
String(component.toolName ?? ""),
|
|
547
|
+
result,
|
|
548
|
+
{
|
|
549
|
+
expanded,
|
|
550
|
+
isPartial: component.isPartial === true,
|
|
551
|
+
isError,
|
|
552
|
+
isHovered: () => isToolCallHovered(component.toolCallId),
|
|
553
|
+
},
|
|
554
|
+
theme,
|
|
555
|
+
component,
|
|
556
|
+
writeMetadata,
|
|
557
|
+
getToolDisplayConfig,
|
|
558
|
+
)
|
|
559
|
+
: undefined;
|
|
560
|
+
const hasRich = isRichDiffComponent(candidate);
|
|
561
|
+
if (hasRich) {
|
|
551
562
|
component.resultRendererComponent = candidate;
|
|
552
|
-
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const title = compactEditWriteLine(component, width, writeMetadata, { hint: !hasRich });
|
|
566
|
+
if (!hasRich && !expanded) {
|
|
567
|
+
return title;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
let detail: any;
|
|
571
|
+
if (hasRich) {
|
|
572
|
+
detail = expanded ? candidate : insetComponent(candidate as any);
|
|
553
573
|
} else {
|
|
554
574
|
const outputText = sanitizeToolResultText(
|
|
555
575
|
Array.isArray(result?.content)
|
|
@@ -571,10 +591,14 @@ function compactEditWriteExpandedLines(
|
|
|
571
591
|
component.resultRendererComponent = detail;
|
|
572
592
|
}
|
|
573
593
|
|
|
594
|
+
if (!expanded) {
|
|
595
|
+
return [...title, ...detail.render(width)];
|
|
596
|
+
}
|
|
597
|
+
|
|
574
598
|
const box = editWriteExpandedCard(theme);
|
|
575
599
|
box.addChild({
|
|
576
600
|
render(innerWidth: number): string[] {
|
|
577
|
-
return compactEditWriteLine(component, innerWidth, writeMetadata).slice(1);
|
|
601
|
+
return compactEditWriteLine(component, innerWidth, writeMetadata, { hint: false }).slice(1);
|
|
578
602
|
},
|
|
579
603
|
invalidate() {},
|
|
580
604
|
});
|
|
@@ -788,7 +812,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
788
812
|
return;
|
|
789
813
|
}
|
|
790
814
|
try {
|
|
791
|
-
|
|
815
|
+
// 非 force:保留 fullscreen 布局缓存和差分绘制。
|
|
816
|
+
uiRef?.requestRender?.();
|
|
792
817
|
} catch {
|
|
793
818
|
/* 无 UI */
|
|
794
819
|
}
|
|
@@ -864,18 +889,65 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
864
889
|
}
|
|
865
890
|
|
|
866
891
|
if (round.anchor.expanded === true) {
|
|
867
|
-
const
|
|
892
|
+
const toolsById = new Map<string, any>();
|
|
893
|
+
for (const tool of trackedToolComponents) {
|
|
894
|
+
if (typeof tool?.toolCallId === "string") toolsById.set(tool.toolCallId, tool);
|
|
895
|
+
}
|
|
896
|
+
const cardItems: Array<{ child?: any; tool?: any }> = [];
|
|
897
|
+
const placedToolIds = new Set<string>();
|
|
898
|
+
const takeAssistantRun = (kids: any[], cursor: { i: number }) => {
|
|
899
|
+
while (cursor.i < kids.length && kids[cursor.i] instanceof Spacer) {
|
|
900
|
+
cardItems.push({ child: kids[cursor.i++] });
|
|
901
|
+
}
|
|
902
|
+
if (cursor.i < kids.length) cardItems.push({ child: kids[cursor.i++] });
|
|
903
|
+
};
|
|
904
|
+
const placeTool = (id: string, fallbackName?: string) => {
|
|
905
|
+
const tool = toolsById.get(id);
|
|
906
|
+
if (!tool || EDIT_WRITE_TOOLS.has(String(tool.toolName ?? fallbackName ?? ""))) return;
|
|
907
|
+
if (placedToolIds.has(id)) return;
|
|
908
|
+
placedToolIds.add(id);
|
|
909
|
+
cardItems.push({ tool });
|
|
910
|
+
};
|
|
868
911
|
for (const [component, message] of round.messages) {
|
|
869
912
|
passThroughAssistant(component, message);
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
913
|
+
const kids = Array.isArray(component.contentContainer?.children)
|
|
914
|
+
? [...component.contentContainer.children]
|
|
915
|
+
: [];
|
|
873
916
|
component.contentContainer?.clear?.();
|
|
917
|
+
const cursor = { i: 0 };
|
|
918
|
+
let inThinkingRun = false;
|
|
919
|
+
for (const item of Array.isArray(message?.content) ? message.content : []) {
|
|
920
|
+
if (item?.type === "thinking") {
|
|
921
|
+
if (!String(item.thinking ?? "").trim()) continue;
|
|
922
|
+
if (!inThinkingRun) {
|
|
923
|
+
takeAssistantRun(kids, cursor);
|
|
924
|
+
inThinkingRun = true;
|
|
925
|
+
}
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
inThinkingRun = false;
|
|
929
|
+
if (item?.type === "text" && String(item.text ?? "").trim()) {
|
|
930
|
+
takeAssistantRun(kids, cursor);
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (item?.type === "toolCall" && typeof item.id === "string") {
|
|
934
|
+
placeTool(item.id, item.name);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
while (cursor.i < kids.length) cardItems.push({ child: kids[cursor.i++] });
|
|
938
|
+
}
|
|
939
|
+
for (const message of round.detachedMessages) {
|
|
940
|
+
for (const item of Array.isArray(message?.content) ? message.content : []) {
|
|
941
|
+
if (item?.type === "toolCall" && typeof item.id === "string") {
|
|
942
|
+
placeTool(item.id, item.name);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
874
945
|
}
|
|
875
946
|
const ids = roundToolCallIds(round);
|
|
876
947
|
for (const id of ids) {
|
|
877
948
|
round.suppressedToolIds.add(id);
|
|
878
949
|
expandedRoundToolIds.add(id);
|
|
950
|
+
if (!placedToolIds.has(id)) placeTool(id);
|
|
879
951
|
}
|
|
880
952
|
// Round 展开只打开外层卡片。普通工具保持折叠,避免长输出递归撑满屏幕。
|
|
881
953
|
for (const tool of trackedToolComponents) {
|
|
@@ -896,21 +968,20 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
896
968
|
render(width: number): string[] {
|
|
897
969
|
const theme = themeOf();
|
|
898
970
|
const box = expandedToolCard(theme);
|
|
899
|
-
for (const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
971
|
+
for (const item of cardItems) {
|
|
972
|
+
if (item.child) box.addChild(item.child);
|
|
973
|
+
else if (item.tool) {
|
|
974
|
+
const tool = item.tool;
|
|
975
|
+
box.addToolChild({
|
|
976
|
+
render: (innerWidth: number) => patch.toolOriginalRender.call(tool, innerWidth),
|
|
977
|
+
invalidate: () => tool.invalidate?.(),
|
|
978
|
+
});
|
|
904
979
|
}
|
|
905
|
-
box.addToolChild({
|
|
906
|
-
render: (innerWidth: number) => patch.toolOriginalRender.call(tool, innerWidth),
|
|
907
|
-
invalidate: () => tool.invalidate?.(),
|
|
908
|
-
});
|
|
909
980
|
}
|
|
910
981
|
return ["", ...box.render(width)];
|
|
911
982
|
},
|
|
912
983
|
invalidate() {
|
|
913
|
-
for (const
|
|
984
|
+
for (const item of cardItems) item.child?.invalidate?.();
|
|
914
985
|
},
|
|
915
986
|
});
|
|
916
987
|
// 展开卡内工具会显示 error,外层仍挂 abort/length,避免只藏在折叠工具里。
|
|
@@ -1066,9 +1137,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1066
1137
|
const name = String(this.toolName ?? "");
|
|
1067
1138
|
if (EDIT_WRITE_TOOLS.has(name)) {
|
|
1068
1139
|
if (!this.result || this.isPartial === true) scheduleAnimation(this);
|
|
1069
|
-
|
|
1070
|
-
if (this.expanded !== true) return lines;
|
|
1071
|
-
return compactEditWriteExpandedLines(this, width, deps.writeMetadata);
|
|
1140
|
+
return compactEditWriteLines(this, width, deps.writeMetadata);
|
|
1072
1141
|
}
|
|
1073
1142
|
// Agent/Task 等同普通工具:折叠不外置(live 面板走独立 widget)。
|
|
1074
1143
|
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;
|
|
@@ -40,6 +40,7 @@ export function displayConfigCacheKey(config: ToolDisplayConfig): string {
|
|
|
40
40
|
config.diffIndicatorMode,
|
|
41
41
|
String(config.diffSplitMinWidth),
|
|
42
42
|
String(config.diffCollapsedLines),
|
|
43
|
+
String(config.writeDiffCollapsedLines),
|
|
43
44
|
config.diffWordWrap ? "1" : "0",
|
|
44
45
|
String(config.expandedPreviewMaxLines),
|
|
45
46
|
].join(":");
|
|
@@ -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,
|
|
@@ -45,6 +45,20 @@ export function resolveDiffDisplayLimit(
|
|
|
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 `diffCollapsedLines`. */
|
|
49
|
+
export function resolveWriteCollapsedLimit(
|
|
50
|
+
config: Pick<ToolDisplayConfig, "writeDiffCollapsedLines" | "diffCollapsedLines">,
|
|
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.diffCollapsedLines)
|
|
57
|
+
? config.diffCollapsedLines
|
|
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();
|
|
@@ -222,7 +235,49 @@ export class ToolGroupComponent extends Container {
|
|
|
222
235
|
for (const tool of this.children) tool.invalidate?.();
|
|
223
236
|
}
|
|
224
237
|
|
|
238
|
+
private settledCacheHit(width: number): string[] | undefined {
|
|
239
|
+
const cache = this.settledCache;
|
|
240
|
+
if (!cache || this._expanded) return;
|
|
241
|
+
if (
|
|
242
|
+
cache.width !== width ||
|
|
243
|
+
cache.hover !== this.hintHovered ||
|
|
244
|
+
cache.theme !== this.patch.theme ||
|
|
245
|
+
cache.fullscreen !== isToolTuiFullscreen()
|
|
246
|
+
) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const tools = this.children as any[];
|
|
250
|
+
if (cache.children.length !== tools.length) return;
|
|
251
|
+
for (let i = 0; i < tools.length; i++) {
|
|
252
|
+
const tool = tools[i];
|
|
253
|
+
if (
|
|
254
|
+
cache.children[i] !== tool ||
|
|
255
|
+
cache.args[i] !== tool?.args ||
|
|
256
|
+
cache.results[i] !== tool?.result ||
|
|
257
|
+
status(tool) === "pending"
|
|
258
|
+
) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return cache.lines;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private storeSettledCache(width: number, lines: string[]): void {
|
|
266
|
+
this.settledCache = {
|
|
267
|
+
width,
|
|
268
|
+
hover: this.hintHovered,
|
|
269
|
+
theme: this.patch.theme,
|
|
270
|
+
fullscreen: isToolTuiFullscreen(),
|
|
271
|
+
children: [...this.children],
|
|
272
|
+
args: (this.children as any[]).map((tool) => tool?.args),
|
|
273
|
+
results: (this.children as any[]).map((tool) => tool?.result),
|
|
274
|
+
lines,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
225
278
|
render(width: number): string[] {
|
|
279
|
+
const cached = this.settledCacheHit(width);
|
|
280
|
+
if (cached) return cached;
|
|
226
281
|
const theme = this.patch.theme;
|
|
227
282
|
const fg = (color: string, text: string) => theme?.fg?.(color, text) ?? text;
|
|
228
283
|
const counts = { pending: 0, success: 0, error: 0 };
|
|
@@ -296,6 +351,8 @@ export class ToolGroupComponent extends Container {
|
|
|
296
351
|
lines.push(paddedBackgroundRow(theme, backgroundSlot, line, width));
|
|
297
352
|
}
|
|
298
353
|
lines.push(paddedBackgroundRow(theme, backgroundSlot, "", width));
|
|
354
|
+
} else if (counts.pending === 0) {
|
|
355
|
+
this.storeSettledCache(width, lines);
|
|
299
356
|
}
|
|
300
357
|
return lines;
|
|
301
358
|
}
|