pi-pretty-tui 0.1.8 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -8
- package/README.zh-CN.md +17 -8
- package/extensions/activity-timeline.ts +340 -0
- package/extensions/index.ts +1656 -227
- package/package.json +2 -2
package/extensions/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AssistantMessageComponent,
|
|
3
3
|
CustomEditor,
|
|
4
|
+
CustomMessageComponent,
|
|
4
5
|
InteractiveMode,
|
|
5
6
|
ToolExecutionComponent,
|
|
6
7
|
UserMessageComponent,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
createWriteTool,
|
|
14
15
|
copyToClipboard,
|
|
15
16
|
getAgentDir,
|
|
17
|
+
getMarkdownTheme,
|
|
16
18
|
type ExtensionAPI,
|
|
17
19
|
} from "@earendil-works/pi-coding-agent";
|
|
18
20
|
import {
|
|
@@ -27,10 +29,42 @@ import {
|
|
|
27
29
|
} from "@earendil-works/pi-tui";
|
|
28
30
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
29
31
|
import { join } from "node:path";
|
|
32
|
+
import {
|
|
33
|
+
ActivityTimeline,
|
|
34
|
+
assistantMessageKey,
|
|
35
|
+
assistantTerminalState,
|
|
36
|
+
thinkingText,
|
|
37
|
+
visibleAssistantText,
|
|
38
|
+
type ActivityGroup,
|
|
39
|
+
type ActivityMember,
|
|
40
|
+
type ActivitySeverity,
|
|
41
|
+
} from "./activity-timeline.js";
|
|
30
42
|
|
|
31
43
|
type PrettyTuiMode = "full" | "compact" | "clean";
|
|
32
44
|
const CLEAN_TOOL_ACTIVITY_MIN_MS = 1000;
|
|
45
|
+
const TRANSIENT_UI_STATUS_MS = 10_000;
|
|
46
|
+
const SPECIALIZED_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
|
|
47
|
+
|
|
48
|
+
const stripAnsiBackgrounds = (value: string): string =>
|
|
49
|
+
value.replace(/\x1b\[([0-9:;]*)m/g, (sequence, raw: string) => {
|
|
50
|
+
const fields = raw === "" ? [0] : raw.split(";").map((field) => Number(field));
|
|
51
|
+
const kept: number[] = [];
|
|
52
|
+
for (let index = 0; index < fields.length; index++) {
|
|
53
|
+
const field = fields[index];
|
|
54
|
+
if ((field >= 40 && field <= 49) || (field >= 100 && field <= 107)) continue;
|
|
55
|
+
if (field === 48) {
|
|
56
|
+
const mode = fields[index + 1];
|
|
57
|
+
if (mode === 5) index += 2;
|
|
58
|
+
else if (mode === 2) index += 4;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
kept.push(field);
|
|
62
|
+
}
|
|
63
|
+
if (raw.includes(":") && /(?:^|;)48:/u.test(raw)) return "";
|
|
64
|
+
return kept.length > 0 ? `\x1b[${kept.join(";")}m` : "";
|
|
65
|
+
});
|
|
33
66
|
type PrettyTuiConfig = {
|
|
67
|
+
enabled?: boolean;
|
|
34
68
|
mode?: PrettyTuiMode;
|
|
35
69
|
/** Legacy location used by the first mode implementation. */
|
|
36
70
|
bash?: {
|
|
@@ -52,6 +86,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
52
86
|
const cwd = process.cwd();
|
|
53
87
|
const configPath = join(getAgentDir(), "pretty-tui.json");
|
|
54
88
|
let config = loadConfig(configPath);
|
|
89
|
+
const activeForSession = config.enabled !== false;
|
|
55
90
|
const configuredMode = config.mode ?? config.bash?.mode;
|
|
56
91
|
let renderMode: PrettyTuiMode = configuredMode === "full" || configuredMode === "compact" || configuredMode === "clean"
|
|
57
92
|
? configuredMode
|
|
@@ -62,6 +97,90 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
62
97
|
let currentTui: any;
|
|
63
98
|
let currentExtensionUi: any;
|
|
64
99
|
let changingAllToolsExpansion = false;
|
|
100
|
+
const latestActivityWidgetKey = "pretty-tui-latest-activity";
|
|
101
|
+
type LatestActivityStatus = {
|
|
102
|
+
title: string;
|
|
103
|
+
detail?: string;
|
|
104
|
+
severity?: TransientStatusSeverity;
|
|
105
|
+
};
|
|
106
|
+
// The editor-adjacent flash reports Pi UI state ("status") and pretty-tui's
|
|
107
|
+
// own UI outcomes, so it has its own severities. Activity members only ever
|
|
108
|
+
// carry info, warning, or error.
|
|
109
|
+
type TransientStatusSeverity = "status" | "success" | "error";
|
|
110
|
+
let latestActivityStatus: LatestActivityStatus | undefined;
|
|
111
|
+
let latestActivityWidgetInstalled = false;
|
|
112
|
+
let latestActivityStatusTimer: ReturnType<typeof setTimeout> | undefined;
|
|
113
|
+
let latestActivityStatusGeneration = 0;
|
|
114
|
+
const renderLatestActivityStatus = (status: LatestActivityStatus, width: number, theme: any): string => {
|
|
115
|
+
const severity = status.severity ?? "status";
|
|
116
|
+
const icon = severity === "error" ? "✕" : severity === "success" ? "✓" : "◇";
|
|
117
|
+
const color = severity === "error"
|
|
118
|
+
? "error"
|
|
119
|
+
: severity === "success"
|
|
120
|
+
? "success"
|
|
121
|
+
: "dim";
|
|
122
|
+
const detail = status.detail?.trim() ? ` — ${status.detail.trim()}` : "";
|
|
123
|
+
return truncateToWidth(` ${theme.fg(color, `${icon} ${status.title}`)}${theme.fg("dim", detail)}`, Math.max(1, width), "…");
|
|
124
|
+
};
|
|
125
|
+
const refreshLatestActivityWidget = () => {
|
|
126
|
+
if (!currentExtensionUi?.setWidget) return;
|
|
127
|
+
if (renderMode !== "clean" || !latestActivityStatus) {
|
|
128
|
+
if (latestActivityWidgetInstalled) {
|
|
129
|
+
currentExtensionUi.setWidget(latestActivityWidgetKey, undefined);
|
|
130
|
+
latestActivityWidgetInstalled = false;
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (latestActivityWidgetInstalled) {
|
|
135
|
+
currentTui?.requestRender?.();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
currentExtensionUi.setWidget(
|
|
139
|
+
latestActivityWidgetKey,
|
|
140
|
+
(_tui: any, theme: any) => ({
|
|
141
|
+
render: (width: number) => latestActivityStatus
|
|
142
|
+
? [renderLatestActivityStatus(latestActivityStatus, width, theme)]
|
|
143
|
+
: [],
|
|
144
|
+
invalidate() {},
|
|
145
|
+
}),
|
|
146
|
+
{ placement: "aboveEditor" },
|
|
147
|
+
);
|
|
148
|
+
latestActivityWidgetInstalled = true;
|
|
149
|
+
};
|
|
150
|
+
const clearLatestActivityStatus = () => {
|
|
151
|
+
latestActivityStatusGeneration++;
|
|
152
|
+
if (latestActivityStatusTimer) clearTimeout(latestActivityStatusTimer);
|
|
153
|
+
latestActivityStatusTimer = undefined;
|
|
154
|
+
latestActivityStatus = undefined;
|
|
155
|
+
if (latestActivityWidgetInstalled) {
|
|
156
|
+
currentExtensionUi?.setWidget?.(latestActivityWidgetKey, undefined);
|
|
157
|
+
latestActivityWidgetInstalled = false;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const setLatestActivityStatus = (status: LatestActivityStatus) => {
|
|
161
|
+
const generation = ++latestActivityStatusGeneration;
|
|
162
|
+
if (latestActivityStatusTimer) clearTimeout(latestActivityStatusTimer);
|
|
163
|
+
latestActivityStatus = status;
|
|
164
|
+
refreshLatestActivityWidget();
|
|
165
|
+
latestActivityStatusTimer = setTimeout(() => {
|
|
166
|
+
if (generation !== latestActivityStatusGeneration) return;
|
|
167
|
+
clearLatestActivityStatus();
|
|
168
|
+
currentTui?.requestRender?.();
|
|
169
|
+
}, TRANSIENT_UI_STATUS_MS);
|
|
170
|
+
};
|
|
171
|
+
const activityTimeline = new ActivityTimeline();
|
|
172
|
+
const revealedActivityGroups = new Set<string>();
|
|
173
|
+
const expandedThinkingMembers = new Set<string>();
|
|
174
|
+
const thinkingComponents = new Map<string, any>();
|
|
175
|
+
const activityFallbackThemes = new Map<string, any>();
|
|
176
|
+
const activityUpdateComponents = new Map<string, Component>();
|
|
177
|
+
const activityUpdateMarkdownComponents = new Map<string, {
|
|
178
|
+
content: string;
|
|
179
|
+
theme: any;
|
|
180
|
+
component: Markdown;
|
|
181
|
+
}>();
|
|
182
|
+
let activityUpdateSequence = 0;
|
|
183
|
+
const assistantBoundaryKeys = new Set<string>();
|
|
65
184
|
const cleanCompactToolCallIds = new Set<string>();
|
|
66
185
|
const cleanGroupToolCallIds = new Map<string, string[]>();
|
|
67
186
|
const cleanToolCallGroupOwners = new Map<string, string>();
|
|
@@ -70,6 +189,20 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
70
189
|
const cleanToolNames = new Map<string, string>();
|
|
71
190
|
let renderCleanGroupSummary = (_lastToolCallId: string, _width: number): string[] => [];
|
|
72
191
|
|
|
192
|
+
// Markdown enhancements are active only while pi-pretty-tui renders the main
|
|
193
|
+
// conversation transcript. Plugin overlays also use Pi's Markdown class, so
|
|
194
|
+
// a global always-on prototype patch would leak transcript styling into them.
|
|
195
|
+
let transcriptMarkdownDepth = 0;
|
|
196
|
+
const withTranscriptMarkdown = <T>(callback: () => T): T => {
|
|
197
|
+
transcriptMarkdownDepth++;
|
|
198
|
+
try {
|
|
199
|
+
return callback();
|
|
200
|
+
} finally {
|
|
201
|
+
transcriptMarkdownDepth--;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const transcriptMarkdownActive = () => transcriptMarkdownDepth > 0;
|
|
205
|
+
|
|
73
206
|
const setCleanGroupMembers = (lastToolCallId: string, toolCallIds: string[]) => {
|
|
74
207
|
cleanGroupToolCallIds.set(lastToolCallId, toolCallIds);
|
|
75
208
|
for (const toolCallId of toolCallIds) cleanToolCallGroupOwners.set(toolCallId, lastToolCallId);
|
|
@@ -77,31 +210,288 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
77
210
|
|
|
78
211
|
const isCleanGroupRevealed = (toolCallId: string): boolean => {
|
|
79
212
|
if (cleanCompactToolCallIds.has(toolCallId)) return true;
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
213
|
+
const activityGroup = activityTimeline.groupForTool(toolCallId);
|
|
214
|
+
if (activityGroup && activityGroupRevealed(activityGroup)) return true;
|
|
215
|
+
const legacyOwner = cleanToolCallGroupOwners.get(toolCallId);
|
|
216
|
+
const legacyIds = legacyOwner ? cleanGroupToolCallIds.get(legacyOwner) : undefined;
|
|
217
|
+
return legacyIds?.some((id) => cleanCompactToolCallIds.has(id)) ?? false;
|
|
83
218
|
};
|
|
84
219
|
|
|
85
220
|
const cleanThemeForToolCall = (toolCallId: string): any => {
|
|
86
221
|
const directTheme = cleanToolThemes.get(toolCallId);
|
|
87
222
|
if (directTheme) return directTheme;
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
223
|
+
const activityGroup = activityTimeline.groupForTool(toolCallId);
|
|
224
|
+
// Prefer a real tool renderer theme over the Markdown-derived fallback.
|
|
225
|
+
// A group that starts with Thought installs the fallback first; returning
|
|
226
|
+
// it here made later third-party success dots use the heading color.
|
|
227
|
+
if (activityGroup) {
|
|
228
|
+
for (const id of activityGroup.toolCallIds) {
|
|
229
|
+
const activityTheme = cleanToolThemes.get(id);
|
|
230
|
+
if (activityTheme) {
|
|
231
|
+
activityFallbackThemes.set(activityGroup.id, activityTheme);
|
|
232
|
+
return activityTheme;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const cachedActivityTheme = activityGroup
|
|
237
|
+
? activityFallbackThemes.get(activityGroup.id)
|
|
238
|
+
: undefined;
|
|
239
|
+
if (cachedActivityTheme) return cachedActivityTheme;
|
|
240
|
+
const legacyOwner = cleanToolCallGroupOwners.get(toolCallId);
|
|
241
|
+
const legacyIds = legacyOwner ? cleanGroupToolCallIds.get(legacyOwner) : undefined;
|
|
242
|
+
return legacyIds?.map((id) => cleanToolThemes.get(id)).find(Boolean);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const activityGroupOwner = (group: ActivityGroup): string | undefined => {
|
|
246
|
+
for (let index = group.toolCallIds.length - 1; index >= 0; index--) {
|
|
247
|
+
const toolCallId = group.toolCallIds[index];
|
|
248
|
+
if (settledSummaries.has(toolCallId)) return toolCallId;
|
|
249
|
+
}
|
|
250
|
+
if (cleanRun.activeToolCallId && group.toolCallIds.includes(cleanRun.activeToolCallId)) {
|
|
251
|
+
return cleanRun.activeToolCallId;
|
|
252
|
+
}
|
|
253
|
+
if (cleanRun.lastCompletedToolCallId && group.toolCallIds.includes(cleanRun.lastCompletedToolCallId)) {
|
|
254
|
+
return cleanRun.lastCompletedToolCallId;
|
|
255
|
+
}
|
|
256
|
+
return group.toolCallIds[group.toolCallIds.length - 1];
|
|
93
257
|
};
|
|
94
258
|
|
|
95
|
-
const
|
|
96
|
-
|
|
259
|
+
const defaultActivityTheme = () => {
|
|
260
|
+
if (currentExtensionUi?.theme?.fg) return currentExtensionUi.theme;
|
|
261
|
+
const markdownTheme = getMarkdownTheme();
|
|
262
|
+
return {
|
|
263
|
+
fg: (color: string, text: string) => {
|
|
264
|
+
if (color === "accent") return markdownTheme.listBullet(text);
|
|
265
|
+
// Session UI normally supplies Pi's active Theme above. Keep portable
|
|
266
|
+
// semantic fallbacks for early startup and isolated renderer tests.
|
|
267
|
+
if (color === "warning") return `\x1b[33m${text}\x1b[39m`;
|
|
268
|
+
if (color === "error") return `\x1b[31m${text}\x1b[39m`;
|
|
269
|
+
if (color === "success") return `\x1b[32m${text}\x1b[39m`;
|
|
270
|
+
if (color === "syntaxComment") return markdownTheme.codeBlock(text);
|
|
271
|
+
if (color === "thinkingLow") return markdownTheme.link(text);
|
|
272
|
+
if (color === "dim" || color === "muted" || color === "thinkingText") {
|
|
273
|
+
return markdownTheme.quote(text);
|
|
274
|
+
}
|
|
275
|
+
return text;
|
|
276
|
+
},
|
|
277
|
+
bold: markdownTheme.bold,
|
|
278
|
+
italic: markdownTheme.italic,
|
|
279
|
+
};
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const activityGroupTheme = (group: ActivityGroup): any => {
|
|
283
|
+
// A Thought can render before its tools are constructed and install a
|
|
284
|
+
// Markdown fallback. Always promote a real tool theme once available so
|
|
285
|
+
// light mode receives explicit, high-contrast text colors.
|
|
286
|
+
for (const toolCallId of group.toolCallIds) {
|
|
287
|
+
const toolTheme = cleanToolThemes.get(toolCallId);
|
|
288
|
+
if (toolTheme) {
|
|
289
|
+
activityFallbackThemes.set(group.id, toolTheme);
|
|
290
|
+
return toolTheme;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const cached = activityFallbackThemes.get(group.id);
|
|
294
|
+
if (cached) return cached;
|
|
295
|
+
const fallback = defaultActivityTheme();
|
|
296
|
+
activityFallbackThemes.set(group.id, fallback);
|
|
297
|
+
return fallback;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const activityGroupRevealed = (group: ActivityGroup): boolean =>
|
|
301
|
+
cleanToolsExpanded || revealedActivityGroups.has(group.id);
|
|
302
|
+
|
|
303
|
+
// A group is revealable once it owns anything renderable beneath its parent
|
|
304
|
+
// row. Thinking counts on its own: a tool-free turn still gets a parent row
|
|
305
|
+
// with its Thought as the child member.
|
|
306
|
+
const activityGroupCollapsible = (group: ActivityGroup): boolean =>
|
|
307
|
+
activityTimeline.hasWork(group);
|
|
308
|
+
|
|
309
|
+
const activityMemberPosition = (group: ActivityGroup, member: ActivityMember) => ({
|
|
310
|
+
first: group.members[0]?.id === member.id,
|
|
311
|
+
last: group.members[group.members.length - 1]?.id === member.id,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const activityTreeStyle = (
|
|
315
|
+
group: ActivityGroup,
|
|
316
|
+
member: ActivityMember,
|
|
317
|
+
width: number,
|
|
318
|
+
memberTheme?: any,
|
|
319
|
+
) => {
|
|
320
|
+
const { last } = activityMemberPosition(group, member);
|
|
321
|
+
const rawPrefix = last ? " └─ " : " ├─ ";
|
|
322
|
+
const rawContinuation = last ? " " : " │ ";
|
|
323
|
+
const prefixWidth = visibleWidth(rawPrefix);
|
|
324
|
+
if (width <= prefixWidth) {
|
|
325
|
+
return { prefix: "", continuation: "", childWidth: Math.max(1, width), prefixWidth: 0 };
|
|
326
|
+
}
|
|
327
|
+
const resolvedTheme = memberTheme ?? activityGroupTheme(group);
|
|
328
|
+
return {
|
|
329
|
+
prefix: resolvedTheme ? resolvedTheme.fg("dim", rawPrefix) : rawPrefix,
|
|
330
|
+
continuation: resolvedTheme ? resolvedTheme.fg("dim", rawContinuation) : rawContinuation,
|
|
331
|
+
childWidth: Math.max(1, width - prefixWidth),
|
|
332
|
+
prefixWidth,
|
|
333
|
+
};
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const humanizeCustomType = (value: string): string =>
|
|
337
|
+
value
|
|
338
|
+
.split(/[-_\s]+/u)
|
|
339
|
+
.filter(Boolean)
|
|
340
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
341
|
+
.join(" ") || "Update";
|
|
342
|
+
|
|
343
|
+
const renderActivityUpdateMarkdown = (
|
|
344
|
+
member: ActivityMember,
|
|
345
|
+
width: number,
|
|
346
|
+
theme: any,
|
|
347
|
+
): string[] => {
|
|
348
|
+
const content = member.updateContent ?? "";
|
|
349
|
+
if (!content) return [];
|
|
350
|
+
let cached = activityUpdateMarkdownComponents.get(member.id);
|
|
351
|
+
if (!cached || cached.content !== content || cached.theme !== theme) {
|
|
352
|
+
cached = {
|
|
353
|
+
content,
|
|
354
|
+
theme,
|
|
355
|
+
component: new Markdown(
|
|
356
|
+
content,
|
|
357
|
+
0,
|
|
358
|
+
0,
|
|
359
|
+
getMarkdownTheme(),
|
|
360
|
+
{ color: (text: string) => theme.fg("muted", text) },
|
|
361
|
+
),
|
|
362
|
+
};
|
|
363
|
+
activityUpdateMarkdownComponents.set(member.id, cached);
|
|
364
|
+
}
|
|
365
|
+
const lines = withTranscriptMarkdown(() => cached.component.render(Math.max(1, width)));
|
|
366
|
+
while (lines.length > 0 && stripTerminalSequences(lines[lines.length - 1]).trim() === "") {
|
|
367
|
+
lines.pop();
|
|
368
|
+
}
|
|
369
|
+
return lines;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const activityUpdateStyle = (member: ActivityMember, theme: any) => {
|
|
373
|
+
const severity = member.updateSeverity ?? "info";
|
|
374
|
+
const styles: Record<ActivitySeverity, { icon: string; color: string }> = {
|
|
375
|
+
info: { icon: "◇", color: "accent" },
|
|
376
|
+
warning: { icon: "⚠", color: "warning" },
|
|
377
|
+
error: { icon: "✕", color: "error" },
|
|
378
|
+
};
|
|
379
|
+
const style = styles[severity];
|
|
380
|
+
return {
|
|
381
|
+
severity,
|
|
382
|
+
icon: theme.fg(style.color, `${style.icon} `),
|
|
383
|
+
title: (text: string) => theme.fg(style.color, theme.bold(text)),
|
|
384
|
+
};
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const renderActivityUpdate = (
|
|
388
|
+
group: ActivityGroup,
|
|
389
|
+
member: ActivityMember,
|
|
390
|
+
width: number,
|
|
391
|
+
theme = activityGroupTheme(group),
|
|
392
|
+
): string[] => {
|
|
393
|
+
if (renderMode !== "clean" || !activityGroupRevealed(group)) return [];
|
|
394
|
+
const { prefix, continuation, childWidth } = activityTreeStyle(group, member, width, theme);
|
|
395
|
+
const title = truncateToWidth(member.updateTitle ?? "Update", Math.max(1, childWidth - 2), "…");
|
|
396
|
+
const style = activityUpdateStyle(member, theme);
|
|
397
|
+
const lines = [style.icon + style.title(title)];
|
|
398
|
+
if (member.updateContent) {
|
|
399
|
+
const detailWidth = Math.max(1, childWidth - visibleWidth(" │ "));
|
|
400
|
+
const detailLines = renderActivityUpdateMarkdown(member, detailWidth, theme);
|
|
401
|
+
lines.push(...detailLines.map((line, index) =>
|
|
402
|
+
theme.fg("dim", index === detailLines.length - 1 ? " └ " : " │ ") + line
|
|
403
|
+
));
|
|
404
|
+
}
|
|
405
|
+
const decorated = lines.map((line, index) =>
|
|
406
|
+
truncateToWidth((index === 0 ? prefix : continuation) + line, Math.max(1, width), "")
|
|
407
|
+
);
|
|
408
|
+
const position = activityMemberPosition(group, member);
|
|
409
|
+
return position.first
|
|
410
|
+
? ["", ...renderActivityGroupSummary(group, width), ...decorated]
|
|
411
|
+
: decorated;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const renderPendingActivityUpdate = (member: ActivityMember, width: number): string[] => {
|
|
415
|
+
const theme = defaultActivityTheme();
|
|
416
|
+
const title = truncateToWidth(member.updateTitle ?? "Update", Math.max(1, width - 2), "…");
|
|
417
|
+
const style = activityUpdateStyle(member, theme);
|
|
418
|
+
const lines = [style.icon + style.title(title)];
|
|
419
|
+
if (member.updateContent) {
|
|
420
|
+
const detailWidth = Math.max(1, width - visibleWidth(" │ "));
|
|
421
|
+
const details = renderActivityUpdateMarkdown(member, detailWidth, theme);
|
|
422
|
+
lines.push(...details.map((line, index) =>
|
|
423
|
+
theme.fg("dim", index === details.length - 1 ? " └ " : " │ ") + line
|
|
424
|
+
));
|
|
425
|
+
}
|
|
426
|
+
return ["", ...lines.map((line) => truncateToWidth(line, Math.max(1, width), ""))];
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const renderActivityUpdateProjection = (
|
|
430
|
+
group: ActivityGroup,
|
|
431
|
+
member: ActivityMember,
|
|
432
|
+
width: number,
|
|
433
|
+
): string[] => {
|
|
434
|
+
if (!activityGroupCollapsible(group)) return renderPendingActivityUpdate(member, width);
|
|
435
|
+
const position = activityMemberPosition(group, member);
|
|
436
|
+
if (!activityGroupRevealed(group)) {
|
|
437
|
+
return position.first
|
|
438
|
+
? ["", ...renderActivityGroupSummary(group, width, true)]
|
|
439
|
+
: [];
|
|
440
|
+
}
|
|
441
|
+
return renderActivityUpdate(group, member, width);
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const handleActivityUpdateMouse = (
|
|
445
|
+
group: ActivityGroup,
|
|
446
|
+
member: ActivityMember,
|
|
447
|
+
event: any,
|
|
448
|
+
) => {
|
|
449
|
+
const isLeftClick = event.type === "click" && event.button === "left";
|
|
450
|
+
if (!isLeftClick || !activityGroupCollapsible(group)) return undefined;
|
|
451
|
+
const position = activityMemberPosition(group, member);
|
|
452
|
+
if (!activityGroupRevealed(group)) {
|
|
453
|
+
if (!position.first) return undefined;
|
|
454
|
+
revealedActivityGroups.add(group.id);
|
|
455
|
+
for (const toolCallId of group.toolCallIds) cleanCompactToolCallIds.add(toolCallId);
|
|
456
|
+
} else {
|
|
457
|
+
const summaryHeight = position.first ? renderActivityGroupSummary(group, event.width).length : 0;
|
|
458
|
+
if (!position.first || event.y <= 0 || event.y > summaryHeight) return undefined;
|
|
459
|
+
revealedActivityGroups.delete(group.id);
|
|
460
|
+
for (const toolCallId of group.toolCallIds) {
|
|
461
|
+
cleanCompactToolCallIds.delete(toolCallId);
|
|
462
|
+
const component = cleanToolComponents.get(toolCallId);
|
|
463
|
+
if (component?.expanded) component.setExpanded(false);
|
|
464
|
+
}
|
|
465
|
+
for (const child of group.members) expandedThinkingMembers.delete(child.id);
|
|
466
|
+
}
|
|
467
|
+
for (const child of group.members) {
|
|
468
|
+
if (child.kind === "tool" && child.toolCallId) {
|
|
469
|
+
cleanToolComponents.get(child.toolCallId)?.updateDisplay?.();
|
|
470
|
+
} else if (child.kind === "thinking" && child.messageKey) {
|
|
471
|
+
thinkingComponents.get(child.messageKey)?.invalidate?.();
|
|
472
|
+
} else if (child.kind === "update") {
|
|
473
|
+
activityUpdateComponents.get(child.id)?.invalidate?.();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
currentTui?.requestRender?.();
|
|
477
|
+
return { handled: true };
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const saveConfig = (nextConfig: Record<string, any>) => {
|
|
481
|
+
config = nextConfig;
|
|
97
482
|
mkdirSync(getAgentDir(), { recursive: true });
|
|
98
483
|
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
99
484
|
};
|
|
100
485
|
|
|
486
|
+
const saveRenderMode = (mode: PrettyTuiMode) => saveConfig({ ...config, mode });
|
|
487
|
+
const saveEnabled = (enabled: boolean) => saveConfig({ ...config, enabled });
|
|
488
|
+
|
|
101
489
|
pi.registerCommand("pretty-tui", {
|
|
102
|
-
description: "Configure pi-pretty-tui
|
|
490
|
+
description: "Configure pi-pretty-tui",
|
|
103
491
|
getArgumentCompletions: (prefix: string) => {
|
|
104
492
|
const items = [
|
|
493
|
+
{ value: "enable", label: "enable", description: "Enable the extension and reload automatically" },
|
|
494
|
+
{ value: "disable", label: "disable", description: "Disable the extension and reload automatically" },
|
|
105
495
|
{ value: "full", label: "full", description: "Full tool details and output" },
|
|
106
496
|
{ value: "compact", label: "compact", description: "Concise summaries for all built-in tools" },
|
|
107
497
|
{ value: "clean", label: "clean", description: "Group supported tools into Running/Done status" },
|
|
@@ -115,29 +505,69 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
115
505
|
|
|
116
506
|
if (!requested) {
|
|
117
507
|
if (!ctx.hasUI) {
|
|
118
|
-
ctx.ui.notify(
|
|
508
|
+
ctx.ui.notify(
|
|
509
|
+
`pi-pretty-tui: ${config.enabled !== false ? "enabled" : "disabled"} · mode: ${renderMode}`,
|
|
510
|
+
"info",
|
|
511
|
+
);
|
|
119
512
|
return;
|
|
120
513
|
}
|
|
514
|
+
const enabled = `Enabled — ${config.enabled !== false ? "on" : "off"}`;
|
|
121
515
|
const full = `Full — full tool details and output${renderMode === "full" ? " (current)" : ""}`;
|
|
122
516
|
const compact = `Compact — concise summaries for all built-in tools${renderMode === "compact" ? " (current)" : ""}`;
|
|
123
517
|
const clean = `Clean — group supported tools into Running/Done status${renderMode === "clean" ? " (current)" : ""}`;
|
|
124
|
-
const selected = await ctx.ui.select("pi-pretty-tui
|
|
518
|
+
const selected = await ctx.ui.select("pi-pretty-tui settings", [enabled, full, compact, clean]);
|
|
125
519
|
if (!selected) return;
|
|
126
|
-
requested = selected ===
|
|
520
|
+
requested = selected === enabled
|
|
521
|
+
? config.enabled !== false ? "disable" : "enable"
|
|
522
|
+
: selected === full
|
|
523
|
+
? "full"
|
|
524
|
+
: selected === compact
|
|
525
|
+
? "compact"
|
|
526
|
+
: "clean";
|
|
127
527
|
}
|
|
128
528
|
|
|
129
529
|
if (requested === "status") {
|
|
130
|
-
ctx.ui.notify(
|
|
530
|
+
ctx.ui.notify(
|
|
531
|
+
`pi-pretty-tui: ${config.enabled !== false ? "enabled" : "disabled"} · mode: ${renderMode}`,
|
|
532
|
+
"info",
|
|
533
|
+
);
|
|
131
534
|
return;
|
|
132
535
|
}
|
|
536
|
+
if (requested === "enable" || requested === "disable") {
|
|
537
|
+
const enabled = requested === "enable";
|
|
538
|
+
try {
|
|
539
|
+
saveEnabled(enabled);
|
|
540
|
+
} catch (error) {
|
|
541
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
542
|
+
ctx.ui.notify(`Could not save pi-pretty-tui setting: ${message}`, "error");
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
ctx.ui.notify(
|
|
546
|
+
`pi-pretty-tui ${enabled ? "enabled" : "disabled"}; reloading…`,
|
|
547
|
+
"info",
|
|
548
|
+
);
|
|
549
|
+
try {
|
|
550
|
+
await ctx.reload();
|
|
551
|
+
return;
|
|
552
|
+
} catch (error) {
|
|
553
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
554
|
+
ctx.ui.notify(`Setting saved, but automatic reload failed: ${message}`, "warning");
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
133
558
|
if (requested !== "full" && requested !== "compact" && requested !== "clean") {
|
|
134
|
-
ctx.ui.notify("Usage: /pretty-tui [full|compact|clean|status]", "error");
|
|
559
|
+
ctx.ui.notify("Usage: /pretty-tui [enable|disable|full|compact|clean|status]", "error");
|
|
135
560
|
return;
|
|
136
561
|
}
|
|
137
562
|
|
|
138
563
|
cleanCompactToolCallIds.clear();
|
|
564
|
+
revealedActivityGroups.clear();
|
|
565
|
+
expandedThinkingMembers.clear();
|
|
139
566
|
renderMode = requested;
|
|
567
|
+
if (renderMode === "clean") refreshLatestActivityWidget();
|
|
568
|
+
else clearLatestActivityStatus();
|
|
140
569
|
for (const component of cleanToolComponents.values()) component.updateDisplay?.();
|
|
570
|
+
for (const component of thinkingComponents.values()) component.invalidate?.();
|
|
141
571
|
try {
|
|
142
572
|
saveRenderMode(renderMode);
|
|
143
573
|
ctx.ui.notify(`pi-pretty-tui mode set to: ${renderMode}`, "info");
|
|
@@ -148,6 +578,10 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
148
578
|
},
|
|
149
579
|
});
|
|
150
580
|
|
|
581
|
+
// Keep the settings command available while disabled, but do not install
|
|
582
|
+
// render patches or override built-in tools until the next enabled reload.
|
|
583
|
+
if (!activeForSession) return;
|
|
584
|
+
|
|
151
585
|
// Give Pi's main prompt editor a complete rounded frame. Render the native
|
|
152
586
|
// editor at a two-column narrower width so cursor layout, wrapping, IME, and
|
|
153
587
|
// autocomplete remain native, then add one themed border column per side.
|
|
@@ -258,12 +692,35 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
258
692
|
const top = border(topStart + markdownTheme.bold(title) + topTail + "╮");
|
|
259
693
|
const bottom = border("╰" + "─".repeat(Math.max(0, frameWidth - 2)) + "╯");
|
|
260
694
|
const contentWidth = Math.max(1, frameWidth - 4);
|
|
261
|
-
const body = content.render(contentWidth).map((line: string) => {
|
|
695
|
+
const body = withTranscriptMarkdown(() => content.render(contentWidth)).map((line: string) => {
|
|
262
696
|
const padding = " ".repeat(Math.max(0, contentWidth - visibleWidth(line)));
|
|
263
697
|
return border("│ ") + line + padding + border(" │");
|
|
264
698
|
});
|
|
265
699
|
return [top, ...body, bottom].map((line: string) => outer + paintBackground(line));
|
|
266
700
|
},
|
|
701
|
+
handleMouse(event: any) {
|
|
702
|
+
if (!content.handleMouse) return undefined;
|
|
703
|
+
const sidePad = Math.min(outputPad, Math.max(0, Math.floor((event.width - 1) / 2)));
|
|
704
|
+
const frameWidth = Math.max(1, event.width - sidePad * 2);
|
|
705
|
+
if (frameWidth < 4) {
|
|
706
|
+
return withTranscriptMarkdown(() => content.handleMouse({
|
|
707
|
+
...event,
|
|
708
|
+
x: event.x - sidePad,
|
|
709
|
+
width: frameWidth,
|
|
710
|
+
}));
|
|
711
|
+
}
|
|
712
|
+
const contentWidth = Math.max(1, frameWidth - 4);
|
|
713
|
+
if (event.y <= 0 || event.x < sidePad + 2 || event.x >= sidePad + 2 + contentWidth) {
|
|
714
|
+
return undefined;
|
|
715
|
+
}
|
|
716
|
+
return withTranscriptMarkdown(() => content.handleMouse({
|
|
717
|
+
...event,
|
|
718
|
+
x: event.x - sidePad - 2,
|
|
719
|
+
y: event.y - 1,
|
|
720
|
+
width: contentWidth,
|
|
721
|
+
height: Math.max(0, event.height - 2),
|
|
722
|
+
}));
|
|
723
|
+
},
|
|
267
724
|
invalidate() {
|
|
268
725
|
content.invalidate?.();
|
|
269
726
|
},
|
|
@@ -291,63 +748,437 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
291
748
|
});
|
|
292
749
|
}
|
|
293
750
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
751
|
+
const customMessageKey = (message: any): string => {
|
|
752
|
+
if (message?.timestamp !== undefined) {
|
|
753
|
+
const parsed = typeof message.timestamp === "string"
|
|
754
|
+
? Date.parse(message.timestamp)
|
|
755
|
+
: Number(message.timestamp);
|
|
756
|
+
return Number.isFinite(parsed) ? String(parsed) : String(message.timestamp);
|
|
757
|
+
}
|
|
758
|
+
return `${message?.customType ?? "custom"}:${JSON.stringify(message?.content ?? "")}`;
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
const customMessageText = (message: any): string =>
|
|
762
|
+
typeof message?.content === "string"
|
|
763
|
+
? message.content
|
|
764
|
+
: Array.isArray(message?.content)
|
|
765
|
+
? message.content
|
|
766
|
+
.filter((item: any) => item?.type === "text" && typeof item.text === "string")
|
|
767
|
+
.map((item: any) => item.text)
|
|
768
|
+
.join("\n")
|
|
769
|
+
: "";
|
|
770
|
+
|
|
771
|
+
// Delegates to the timeline so the precondition is stated once. The `??` is
|
|
772
|
+
// deliberate: the callee may still decline, and a dropped notification must
|
|
773
|
+
// never throw inside a third-party extension's ctx.ui.notify() call.
|
|
774
|
+
const hasWorkActivity = (group: ActivityGroup | undefined): boolean =>
|
|
775
|
+
activityTimeline.hasWork(group);
|
|
776
|
+
|
|
777
|
+
const addPersistentActivityUpdate = (message: any): ActivityMember => {
|
|
778
|
+
const key = customMessageKey(message);
|
|
779
|
+
const title = humanizeCustomType(message?.customType ?? "update");
|
|
780
|
+
const content = customMessageText(message);
|
|
781
|
+
const existingGroup = activityTimeline.currentGroup();
|
|
782
|
+
const joinsWork = hasWorkActivity(existingGroup);
|
|
783
|
+
const member = joinsWork
|
|
784
|
+
? activityTimeline.addUpdate(key, title, content, true, "info") ??
|
|
785
|
+
activityTimeline.addPendingUpdate(key, title, content, true, "info")
|
|
786
|
+
: activityTimeline.addPendingUpdate(key, title, content, true, "info");
|
|
787
|
+
// A displayed custom message without current model/tool work is an
|
|
788
|
+
// independent durable update, never a pending member of the next run.
|
|
789
|
+
if (!hasWorkActivity(existingGroup)) activityTimeline.boundary();
|
|
790
|
+
return member;
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
const nativeCustomBoundaryKeys = new Set<string>();
|
|
794
|
+
let closeCleanAtNativeCustomBoundary = () => activityTimeline.boundary();
|
|
795
|
+
let closeCleanAtStandaloneAlert = () => activityTimeline.boundary();
|
|
796
|
+
const customMessagePrototype = CustomMessageComponent.prototype as any;
|
|
797
|
+
const customMessagePatchKey = Symbol.for("pretty-tui.clean-custom-message");
|
|
798
|
+
if (!customMessagePrototype[customMessagePatchKey]) {
|
|
799
|
+
const originalCustomRender = customMessagePrototype.render;
|
|
800
|
+
const originalCustomHandleMouse = customMessagePrototype.handleMouse;
|
|
801
|
+
const releaseSemanticCustomMessage = (component: any): boolean => {
|
|
802
|
+
if (!component.customRenderer) return false;
|
|
803
|
+
const key = customMessageKey(component.message);
|
|
804
|
+
if (!nativeCustomBoundaryKeys.has(key)) {
|
|
805
|
+
nativeCustomBoundaryKeys.add(key);
|
|
806
|
+
const member = activityTimeline.memberForUpdate(key);
|
|
807
|
+
if (member) activityUpdateComponents.delete(member.id);
|
|
808
|
+
activityTimeline.removeUpdate(key);
|
|
809
|
+
closeCleanAtNativeCustomBoundary();
|
|
810
|
+
}
|
|
811
|
+
return true;
|
|
812
|
+
};
|
|
813
|
+
const patchedCustomRender = function (this: any, width: number): string[] {
|
|
814
|
+
if (releaseSemanticCustomMessage(this)) {
|
|
815
|
+
return originalCustomRender.call(this, width);
|
|
816
|
+
}
|
|
817
|
+
const message = this.message;
|
|
818
|
+
const key = customMessageKey(message);
|
|
819
|
+
let member = activityTimeline.memberForUpdate(key);
|
|
820
|
+
// Extension-injected custom messages do not always emit Pi's model
|
|
821
|
+
// message_start event. Claim them at the component boundary so live
|
|
822
|
+
// rendering matches transcript restoration instead of failing open to
|
|
823
|
+
// the purple native box until the next reload.
|
|
824
|
+
if (!member && renderMode === "clean") {
|
|
825
|
+
member = addPersistentActivityUpdate(message);
|
|
826
|
+
}
|
|
827
|
+
const group = member ? activityTimeline.groupForMember(member.id) : undefined;
|
|
828
|
+
if (renderMode !== "clean" || !member || !group) {
|
|
829
|
+
return originalCustomRender.call(this, width);
|
|
830
|
+
}
|
|
831
|
+
activityUpdateComponents.set(member.id, this);
|
|
832
|
+
return renderActivityUpdateProjection(group, member, width);
|
|
833
|
+
};
|
|
834
|
+
const patchedCustomHandleMouse = function (this: any, event: any) {
|
|
835
|
+
if (releaseSemanticCustomMessage(this)) {
|
|
836
|
+
return originalCustomHandleMouse?.call(this, event);
|
|
837
|
+
}
|
|
838
|
+
const member = activityTimeline.memberForUpdate(customMessageKey(this.message));
|
|
839
|
+
const group = member ? activityTimeline.groupForMember(member.id) : undefined;
|
|
840
|
+
if (renderMode !== "clean" || !member || !group) {
|
|
841
|
+
return originalCustomHandleMouse?.call(this, event);
|
|
842
|
+
}
|
|
843
|
+
return handleActivityUpdateMouse(group, member, event);
|
|
844
|
+
};
|
|
845
|
+
customMessagePrototype[customMessagePatchKey] = {
|
|
846
|
+
originalRender: originalCustomRender,
|
|
847
|
+
patchedRender: patchedCustomRender,
|
|
848
|
+
originalHandleMouse: originalCustomHandleMouse,
|
|
849
|
+
patchedHandleMouse: patchedCustomHandleMouse,
|
|
850
|
+
};
|
|
851
|
+
customMessagePrototype.render = patchedCustomRender;
|
|
852
|
+
customMessagePrototype.handleMouse = patchedCustomHandleMouse;
|
|
853
|
+
pi.on("session_shutdown", () => {
|
|
854
|
+
const patch = customMessagePrototype[customMessagePatchKey];
|
|
855
|
+
if (!patch) return;
|
|
856
|
+
if (patch.patchedRender === customMessagePrototype.render) {
|
|
857
|
+
customMessagePrototype.render = patch.originalRender;
|
|
858
|
+
}
|
|
859
|
+
if (patch.patchedHandleMouse === customMessagePrototype.handleMouse) {
|
|
860
|
+
customMessagePrototype.handleMouse = patch.originalHandleMouse;
|
|
861
|
+
}
|
|
862
|
+
if (
|
|
863
|
+
customMessagePrototype.render === patch.originalRender &&
|
|
864
|
+
customMessagePrototype.handleMouse === patch.originalHandleMouse
|
|
865
|
+
) {
|
|
866
|
+
delete customMessagePrototype[customMessagePatchKey];
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Thinking and tools are peers in the transcript-first activity timeline.
|
|
872
|
+
// Assistant components keep Pi's native Markdown renderer, while clean mode
|
|
873
|
+
// projects each thinking run as a compact, independently expandable child.
|
|
300
874
|
const assistantPrototype = AssistantMessageComponent.prototype as any;
|
|
301
875
|
const thinkingPatchKey = Symbol.for("pretty-tui.clean-thinking");
|
|
302
876
|
if (!assistantPrototype[thinkingPatchKey]) {
|
|
303
877
|
const originalUpdateContent = assistantPrototype.updateContent;
|
|
304
878
|
const originalRender = assistantPrototype.render;
|
|
879
|
+
const originalHandleMouse = assistantPrototype.handleMouse;
|
|
305
880
|
const originalMessageKey = Symbol("pretty-tui.original-assistant-message");
|
|
306
881
|
const renderedModeKey = Symbol("pretty-tui.rendered-assistant-mode");
|
|
307
|
-
const renderedExpansionKey = Symbol("pretty-tui.rendered-
|
|
882
|
+
const renderedExpansionKey = Symbol("pretty-tui.rendered-assistant-expansion");
|
|
883
|
+
const cleanThinkingRenderCacheKey = Symbol("pretty-tui.clean-thinking-render-cache");
|
|
884
|
+
const cleanThinkingDetailKey = Symbol("pretty-tui.clean-thinking-detail");
|
|
885
|
+
|
|
886
|
+
const assistantTerminalUpdate = (message: any): ActivityMember | undefined => {
|
|
887
|
+
if (!assistantTerminalState(message)) return undefined;
|
|
888
|
+
const hasToolCalls = Array.isArray(message?.content) &&
|
|
889
|
+
message.content.some((item: any) => item?.type === "toolCall");
|
|
890
|
+
// Failed tools already preserve the error in their native result details;
|
|
891
|
+
// avoid duplicating that payload as a second durable member.
|
|
892
|
+
if (hasToolCalls) return undefined;
|
|
893
|
+
const key = `assistant-terminal:${assistantMessageKey(message)}`;
|
|
894
|
+
const existing = activityTimeline.memberForUpdate(key);
|
|
895
|
+
if (existing) return existing;
|
|
896
|
+
const stopReason = message?.stopReason;
|
|
897
|
+
const severity: ActivitySeverity = stopReason === "aborted" ? "warning" : "error";
|
|
898
|
+
const title = stopReason === "length"
|
|
899
|
+
? "Response truncated"
|
|
900
|
+
: stopReason === "aborted"
|
|
901
|
+
? "Operation aborted"
|
|
902
|
+
: "Error";
|
|
903
|
+
const content = String(message?.errorMessage ?? "").trim() ||
|
|
904
|
+
(stopReason === "length" ? "Response was truncated before completion." : "Unknown error");
|
|
905
|
+
activityTimeline.boundary();
|
|
906
|
+
const member = activityTimeline.addPendingUpdate(key, title, content, true, severity);
|
|
907
|
+
activityTimeline.boundary();
|
|
908
|
+
return member;
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
const eligibleThinking = (message: any) =>
|
|
912
|
+
Array.isArray(message?.content) &&
|
|
913
|
+
message.content.some(
|
|
914
|
+
(item: any) => item?.type === "thinking" &&
|
|
915
|
+
typeof item.thinking === "string" &&
|
|
916
|
+
item.thinking.length > 0,
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
const refreshGroup = (group: ActivityGroup) => {
|
|
920
|
+
for (const member of group.members) {
|
|
921
|
+
if (member.kind === "tool" && member.toolCallId) {
|
|
922
|
+
cleanToolComponents.get(member.toolCallId)?.updateDisplay?.();
|
|
923
|
+
} else if (member.kind === "thinking" && member.messageKey) {
|
|
924
|
+
thinkingComponents.get(member.messageKey)?.invalidate?.();
|
|
925
|
+
} else if (member.kind === "update") {
|
|
926
|
+
activityUpdateComponents.get(member.id)?.invalidate?.();
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
currentTui?.requestRender?.();
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
// Shared detail projection for a Thought, used both as a member of a tool
|
|
933
|
+
// group and as a standalone row. The cache records its theme because the
|
|
934
|
+
// same component can switch between the Markdown fallback and a real tool
|
|
935
|
+
// theme once the first tool of the group is constructed.
|
|
936
|
+
const thinkingDetailLines = (
|
|
937
|
+
owner: any,
|
|
938
|
+
text: string,
|
|
939
|
+
theme: any,
|
|
940
|
+
childWidth: number,
|
|
941
|
+
): string[] => {
|
|
942
|
+
let detail = owner[cleanThinkingDetailKey];
|
|
943
|
+
if (!detail || detail.text !== text || detail.theme !== theme) {
|
|
944
|
+
detail = {
|
|
945
|
+
text,
|
|
946
|
+
theme,
|
|
947
|
+
component: new Markdown(text, 0, 0, owner.markdownTheme, {
|
|
948
|
+
color: (line: string) => theme.fg("thinkingText", line),
|
|
949
|
+
italic: true,
|
|
950
|
+
}),
|
|
951
|
+
};
|
|
952
|
+
owner[cleanThinkingDetailKey] = detail;
|
|
953
|
+
}
|
|
954
|
+
const lines: string[] = detail.component.render(Math.max(1, childWidth));
|
|
955
|
+
return lines.map((line, index) =>
|
|
956
|
+
theme.fg("dim", index === lines.length - 1 ? " └ " : " │ ") + line
|
|
957
|
+
);
|
|
958
|
+
};
|
|
308
959
|
|
|
309
960
|
const patchedUpdateContent = function (this: any, message: any, isStreaming = this.isStreaming) {
|
|
961
|
+
this[originalMessageKey] = message;
|
|
962
|
+
this[renderedModeKey] = renderMode;
|
|
963
|
+
this[renderedExpansionKey] = cleanToolsExpanded;
|
|
964
|
+
this.lastMessage = message;
|
|
310
965
|
if (renderMode !== "clean") {
|
|
311
966
|
originalUpdateContent.call(this, message, isStreaming);
|
|
312
|
-
this
|
|
313
|
-
this[renderedModeKey] = renderMode;
|
|
314
|
-
this[renderedExpansionKey] = undefined;
|
|
967
|
+
this.lastMessage = message;
|
|
315
968
|
return;
|
|
316
969
|
}
|
|
970
|
+
const groupedThinking = eligibleThinking(message);
|
|
971
|
+
if (groupedThinking) {
|
|
972
|
+
const key = assistantMessageKey(message);
|
|
973
|
+
activityTimeline.addThinking(key, thinkingText(message));
|
|
974
|
+
thinkingComponents.set(key, this);
|
|
975
|
+
}
|
|
317
976
|
|
|
318
|
-
const showThinking = cleanToolsExpanded;
|
|
319
|
-
const content = Array.isArray(message?.content) ? message.content : [];
|
|
320
|
-
// In collapsed clean mode, omit Pi's built-in Thinking... placeholder
|
|
321
|
-
// entirely. The clean-mode Running(...) row owns the activity label;
|
|
322
|
-
// expanded mode still reveals the actual thinking content.
|
|
323
|
-
const displayMessage = showThinking
|
|
324
|
-
? message
|
|
325
|
-
: { ...message, content: content.filter((item: any) => item.type !== "thinking") };
|
|
326
977
|
const previousHideThinkingBlock = this.hideThinkingBlock;
|
|
327
978
|
this.hideThinkingBlock = false;
|
|
979
|
+
// Clean mode owns grouped thinking completely. Removing it from Pi's
|
|
980
|
+
// native content tree also removes the native MouseRegion that would
|
|
981
|
+
// otherwise toggle a second, unrelated `Thinking...` block.
|
|
982
|
+
const displayMessage =
|
|
983
|
+
groupedThinking && Array.isArray(message?.content)
|
|
984
|
+
? { ...message, content: message.content.filter((item: any) => item.type !== "thinking") }
|
|
985
|
+
: message;
|
|
328
986
|
try {
|
|
329
987
|
originalUpdateContent.call(this, displayMessage, isStreaming);
|
|
330
988
|
} finally {
|
|
331
989
|
this.hideThinkingBlock = previousHideThinkingBlock;
|
|
332
990
|
}
|
|
333
|
-
|
|
334
|
-
// Pi's invalidate()/setHideThinkingBlock() call updateContent with
|
|
335
|
-
// lastMessage, so retain the unfiltered message for later mode changes.
|
|
336
|
-
this[originalMessageKey] = message;
|
|
337
|
-
this[renderedModeKey] = renderMode;
|
|
338
|
-
this[renderedExpansionKey] = showThinking;
|
|
339
991
|
this.lastMessage = message;
|
|
340
992
|
};
|
|
341
993
|
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
994
|
+
const renderAssistantTranscript = function (this: any, width: number): string[] {
|
|
995
|
+
const message = this[originalMessageKey] ?? this.lastMessage;
|
|
996
|
+
if (
|
|
997
|
+
message &&
|
|
998
|
+
(this[renderedModeKey] !== renderMode || this[renderedExpansionKey] !== cleanToolsExpanded)
|
|
999
|
+
) {
|
|
1000
|
+
patchedUpdateContent.call(this, message, this.isStreaming);
|
|
1001
|
+
}
|
|
1002
|
+
if (renderMode !== "clean") return originalRender.call(this, width);
|
|
1003
|
+
const terminalMember = assistantTerminalUpdate(message);
|
|
1004
|
+
if (!eligibleThinking(message)) {
|
|
1005
|
+
if (!terminalMember) return originalRender.call(this, width);
|
|
1006
|
+
const terminalGroup = activityTimeline.groupForMember(terminalMember.id);
|
|
1007
|
+
if (!terminalGroup) return originalRender.call(this, width);
|
|
1008
|
+
activityUpdateComponents.set(terminalMember.id, this);
|
|
1009
|
+
return renderActivityUpdateProjection(terminalGroup, terminalMember, width);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const key = assistantMessageKey(message);
|
|
1013
|
+
const member = activityTimeline.memberForThinking(key);
|
|
1014
|
+
const group = member ? activityTimeline.groupForMember(member.id) : undefined;
|
|
1015
|
+
if (!member || !group) {
|
|
1016
|
+
// Timeline ownership can be briefly unavailable during transcript
|
|
1017
|
+
// rebuilds. Fail open to Pi's filtered native renderer so visible
|
|
1018
|
+
// assistant text is never lost.
|
|
1019
|
+
return originalRender.call(this, width);
|
|
1020
|
+
}
|
|
1021
|
+
thinkingComponents.set(key, this);
|
|
1022
|
+
if (!activityFallbackThemes.has(group.id)) {
|
|
1023
|
+
activityFallbackThemes.set(group.id, defaultActivityTheme());
|
|
1024
|
+
}
|
|
1025
|
+
const position = activityMemberPosition(group, member);
|
|
1026
|
+
const visibleLines = visibleAssistantText(message) ? originalRender.call(this, width) : [];
|
|
1027
|
+
// Pi's native renderer prints its own error line for messages with no
|
|
1028
|
+
// tool calls, so this component only adds the styled alert card when no
|
|
1029
|
+
// visible text can carry it.
|
|
1030
|
+
const terminalLines = terminalMember && visibleLines.length === 0
|
|
1031
|
+
? renderPendingActivityUpdate(terminalMember, width)
|
|
1032
|
+
: [];
|
|
1033
|
+
const appendTail = (activityLines: string[]) => [
|
|
1034
|
+
...activityLines,
|
|
1035
|
+
...terminalLines,
|
|
1036
|
+
...visibleLines,
|
|
1037
|
+
];
|
|
1038
|
+
const revealed = activityGroupRevealed(group);
|
|
1039
|
+
if (!revealed) {
|
|
1040
|
+
const activityLines = position.first
|
|
1041
|
+
? ["", ...renderActivityGroupSummary(group, width, true)]
|
|
1042
|
+
: [];
|
|
1043
|
+
return appendTail(activityLines);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const groupTheme = activityGroupTheme(group);
|
|
1047
|
+
const { prefix, continuation, childWidth } = activityTreeStyle(
|
|
1048
|
+
group,
|
|
1049
|
+
member,
|
|
1050
|
+
width,
|
|
1051
|
+
groupTheme,
|
|
1052
|
+
);
|
|
1053
|
+
// Clean mode owns thought disclosure: revealing a group shows the thought
|
|
1054
|
+
// member, while only a member click (or Ctrl+O) reveals its full text.
|
|
1055
|
+
const expanded = cleanToolsExpanded || expandedThinkingMembers.has(member.id);
|
|
1056
|
+
const settledOwner = position.first ? activityGroupOwner(group) : undefined;
|
|
1057
|
+
// A group without tools has no durable summary, so its own boundary is
|
|
1058
|
+
// the signal that it finished.
|
|
1059
|
+
const stableSummary = !position.first || (
|
|
1060
|
+
settledOwner
|
|
1061
|
+
? Boolean(settledSummaries.has(settledOwner) && !toolActivityHolds.has(settledOwner))
|
|
1062
|
+
: activityTimeline.isSettled(group.id)
|
|
346
1063
|
);
|
|
347
|
-
|
|
348
|
-
|
|
1064
|
+
const cacheable = !expanded && !this.isStreaming && stableSummary;
|
|
1065
|
+
const cached = cacheable ? this[cleanThinkingRenderCacheKey] : undefined;
|
|
1066
|
+
if (
|
|
1067
|
+
cached?.width === width &&
|
|
1068
|
+
cached?.message === message &&
|
|
1069
|
+
cached?.theme === groupTheme &&
|
|
1070
|
+
cached?.memberCount === group.members.length &&
|
|
1071
|
+
cached?.last === position.last
|
|
1072
|
+
) {
|
|
1073
|
+
// Only the stable activity projection is cached. Visible assistant
|
|
1074
|
+
// text belongs outside the collapsible group and must be appended on
|
|
1075
|
+
// every render, including cache hits.
|
|
1076
|
+
return appendTail(cached.lines);
|
|
1077
|
+
}
|
|
1078
|
+
const stateLabel = this.isStreaming ? "thinking" : "thought";
|
|
1079
|
+
const label = truncateToWidth(stateLabel, Math.max(1, childWidth - 2), "…");
|
|
1080
|
+
const header = groupTheme.fg("thinkingLow", "● ") +
|
|
1081
|
+
groupTheme.fg("toolTitle", groupTheme.bold(label));
|
|
1082
|
+
let contentLines: string[] = [header];
|
|
1083
|
+
if (expanded) {
|
|
1084
|
+
contentLines.push(...thinkingDetailLines(
|
|
1085
|
+
this,
|
|
1086
|
+
member.thinking ?? thinkingText(message),
|
|
1087
|
+
groupTheme,
|
|
1088
|
+
Math.max(1, childWidth - visibleWidth(" │ ")),
|
|
1089
|
+
));
|
|
1090
|
+
}
|
|
1091
|
+
const decorated = contentLines.map((line, index) =>
|
|
1092
|
+
truncateToWidth((index === 0 ? prefix : continuation) + line, Math.max(1, width), "")
|
|
1093
|
+
);
|
|
1094
|
+
const output = position.first
|
|
1095
|
+
? ["", ...renderActivityGroupSummary(group, width), ...decorated]
|
|
1096
|
+
: decorated;
|
|
1097
|
+
if (cacheable) {
|
|
1098
|
+
this[cleanThinkingRenderCacheKey] = {
|
|
1099
|
+
width,
|
|
1100
|
+
message,
|
|
1101
|
+
theme: groupTheme,
|
|
1102
|
+
memberCount: group.members.length,
|
|
1103
|
+
last: position.last,
|
|
1104
|
+
lines: output,
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
return appendTail(output);
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
const patchedRender = function (this: any, width: number): string[] {
|
|
1111
|
+
return withTranscriptMarkdown(() => renderAssistantTranscript.call(this, width));
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
const handleAssistantTranscriptMouse = function (this: any, event: any) {
|
|
1115
|
+
const message = this[originalMessageKey] ?? this.lastMessage;
|
|
1116
|
+
if (renderMode !== "clean") return originalHandleMouse.call(this, event);
|
|
1117
|
+
if (!eligibleThinking(message)) {
|
|
1118
|
+
const terminalMember = assistantTerminalUpdate(message);
|
|
1119
|
+
const terminalGroup = terminalMember
|
|
1120
|
+
? activityTimeline.groupForMember(terminalMember.id)
|
|
1121
|
+
: undefined;
|
|
1122
|
+
return terminalMember && terminalGroup && activityGroupCollapsible(terminalGroup)
|
|
1123
|
+
? handleActivityUpdateMouse(terminalGroup, terminalMember, event)
|
|
1124
|
+
: originalHandleMouse.call(this, event);
|
|
1125
|
+
}
|
|
1126
|
+
const key = assistantMessageKey(message);
|
|
1127
|
+
const member = activityTimeline.memberForThinking(key);
|
|
1128
|
+
const group = member ? activityTimeline.groupForMember(member.id) : undefined;
|
|
1129
|
+
if (!member || !group) return originalHandleMouse.call(this, event);
|
|
1130
|
+
const position = activityMemberPosition(group, member);
|
|
1131
|
+
const isLeftClick = event.type === "click" && event.button === "left";
|
|
1132
|
+
|
|
1133
|
+
// A completed assistant message can contain both the final Thought and
|
|
1134
|
+
// visible Markdown. The Markdown is rendered after the activity rows;
|
|
1135
|
+
// route clicks in that suffix back to Pi's native component so links,
|
|
1136
|
+
// text selection, and code-block Copy controls keep working.
|
|
1137
|
+
if (visibleAssistantText(message)) {
|
|
1138
|
+
const visibleLines = originalRender.call(this, event.width);
|
|
1139
|
+
const renderedLines = patchedRender.call(this, event.width);
|
|
1140
|
+
const activityHeight = Math.max(0, renderedLines.length - visibleLines.length);
|
|
1141
|
+
if (event.y >= activityHeight) {
|
|
1142
|
+
return originalHandleMouse.call(this, {
|
|
1143
|
+
...event,
|
|
1144
|
+
y: event.y - activityHeight,
|
|
1145
|
+
height: visibleLines.length,
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (!activityGroupRevealed(group)) {
|
|
1151
|
+
if (!isLeftClick) return undefined;
|
|
1152
|
+
if (position.first) {
|
|
1153
|
+
revealedActivityGroups.add(group.id);
|
|
1154
|
+
for (const toolCallId of group.toolCallIds) cleanCompactToolCallIds.add(toolCallId);
|
|
1155
|
+
refreshGroup(group);
|
|
1156
|
+
}
|
|
1157
|
+
return { handled: true };
|
|
1158
|
+
}
|
|
1159
|
+
const summaryHeight = position.first ? renderActivityGroupSummary(group, event.width).length : 0;
|
|
1160
|
+
if (position.first && event.y > 0 && event.y <= summaryHeight && isLeftClick) {
|
|
1161
|
+
revealedActivityGroups.delete(group.id);
|
|
1162
|
+
for (const toolCallId of group.toolCallIds) {
|
|
1163
|
+
cleanCompactToolCallIds.delete(toolCallId);
|
|
1164
|
+
const component = cleanToolComponents.get(toolCallId);
|
|
1165
|
+
if (component?.expanded) component.setExpanded(false);
|
|
1166
|
+
}
|
|
1167
|
+
for (const child of group.members) expandedThinkingMembers.delete(child.id);
|
|
1168
|
+
refreshGroup(group);
|
|
1169
|
+
return { handled: true };
|
|
349
1170
|
}
|
|
350
|
-
|
|
1171
|
+
if (isLeftClick) {
|
|
1172
|
+
if (expandedThinkingMembers.has(member.id)) expandedThinkingMembers.delete(member.id);
|
|
1173
|
+
else expandedThinkingMembers.add(member.id);
|
|
1174
|
+
refreshGroup(group);
|
|
1175
|
+
return { handled: true };
|
|
1176
|
+
}
|
|
1177
|
+
return undefined;
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
const patchedHandleMouse = function (this: any, event: any) {
|
|
1181
|
+
return withTranscriptMarkdown(() => handleAssistantTranscriptMouse.call(this, event));
|
|
351
1182
|
};
|
|
352
1183
|
|
|
353
1184
|
assistantPrototype[thinkingPatchKey] = {
|
|
@@ -355,9 +1186,12 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
355
1186
|
patchedUpdateContent,
|
|
356
1187
|
originalRender,
|
|
357
1188
|
patchedRender,
|
|
1189
|
+
originalHandleMouse,
|
|
1190
|
+
patchedHandleMouse,
|
|
358
1191
|
};
|
|
359
1192
|
assistantPrototype.updateContent = patchedUpdateContent;
|
|
360
1193
|
assistantPrototype.render = patchedRender;
|
|
1194
|
+
assistantPrototype.handleMouse = patchedHandleMouse;
|
|
361
1195
|
|
|
362
1196
|
pi.on("session_shutdown", () => {
|
|
363
1197
|
const patch = assistantPrototype[thinkingPatchKey];
|
|
@@ -368,9 +1202,13 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
368
1202
|
if (patch.patchedRender === assistantPrototype.render) {
|
|
369
1203
|
assistantPrototype.render = patch.originalRender;
|
|
370
1204
|
}
|
|
1205
|
+
if (patch.patchedHandleMouse === assistantPrototype.handleMouse) {
|
|
1206
|
+
assistantPrototype.handleMouse = patch.originalHandleMouse;
|
|
1207
|
+
}
|
|
371
1208
|
if (
|
|
372
1209
|
assistantPrototype.updateContent === patch.originalUpdateContent &&
|
|
373
|
-
assistantPrototype.render === patch.originalRender
|
|
1210
|
+
assistantPrototype.render === patch.originalRender &&
|
|
1211
|
+
assistantPrototype.handleMouse === patch.originalHandleMouse
|
|
374
1212
|
) {
|
|
375
1213
|
delete assistantPrototype[thinkingPatchKey];
|
|
376
1214
|
}
|
|
@@ -403,20 +1241,69 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
403
1241
|
];
|
|
404
1242
|
};
|
|
405
1243
|
|
|
1244
|
+
const appendRuntimeActivityUpdate = (
|
|
1245
|
+
mode: any,
|
|
1246
|
+
message: string,
|
|
1247
|
+
severity: ActivitySeverity,
|
|
1248
|
+
): boolean => {
|
|
1249
|
+
const [firstLine, ...remainingLines] = String(message).split("\n");
|
|
1250
|
+
const fallbackTitle = severity === "warning"
|
|
1251
|
+
? "Warning"
|
|
1252
|
+
: severity === "error"
|
|
1253
|
+
? "Error"
|
|
1254
|
+
: "Info";
|
|
1255
|
+
const title = firstLine || fallbackTitle;
|
|
1256
|
+
const content = remainingLines.join("\n");
|
|
1257
|
+
// Warnings and errors are standalone hard boundaries; only ordinary notices
|
|
1258
|
+
// join the work in progress.
|
|
1259
|
+
const standalone = severity === "warning" || severity === "error";
|
|
1260
|
+
const updateKey = `runtime:${severity}:${++activityUpdateSequence}`;
|
|
1261
|
+
if (standalone) closeCleanAtStandaloneAlert();
|
|
1262
|
+
const existingGroup = activityTimeline.currentGroup();
|
|
1263
|
+
const joinsWork = !standalone && hasWorkActivity(existingGroup);
|
|
1264
|
+
const member = joinsWork
|
|
1265
|
+
? activityTimeline.addUpdate(updateKey, title, content, false, severity) ??
|
|
1266
|
+
activityTimeline.addPendingUpdate(updateKey, title, content, false, severity)
|
|
1267
|
+
: activityTimeline.addPendingUpdate(updateKey, title, content, false, severity);
|
|
1268
|
+
if (!joinsWork) activityTimeline.boundary();
|
|
1269
|
+
const group = activityTimeline.groupForMember(member.id);
|
|
1270
|
+
if (!group || !mode.chatContainer?.addChild) return false;
|
|
1271
|
+
const component: Component = {
|
|
1272
|
+
render: (width: number) => renderActivityUpdateProjection(group, member, width),
|
|
1273
|
+
invalidate() {},
|
|
1274
|
+
handleMouse: (event: any) => handleActivityUpdateMouse(group, member, event),
|
|
1275
|
+
} as Component;
|
|
1276
|
+
activityUpdateComponents.set(member.id, component);
|
|
1277
|
+
mode.chatContainer.addChild(component);
|
|
1278
|
+
mode.ui?.requestRender?.();
|
|
1279
|
+
return true;
|
|
1280
|
+
};
|
|
1281
|
+
|
|
406
1282
|
const interactiveModePrototype = InteractiveMode.prototype as any;
|
|
407
1283
|
const toolsExpansionPatchKey = Symbol.for("pretty-tui.clean-tool-expansion");
|
|
408
1284
|
if (!interactiveModePrototype[toolsExpansionPatchKey]) {
|
|
409
1285
|
const originalSetToolsExpanded = interactiveModePrototype.setToolsExpanded;
|
|
410
1286
|
const originalRenderSessionEntries = interactiveModePrototype.renderSessionEntries;
|
|
411
1287
|
const originalSwitchTuiMode = interactiveModePrototype.switchTuiMode;
|
|
1288
|
+
const originalShowExtensionNotify = interactiveModePrototype.showExtensionNotify;
|
|
1289
|
+
const originalToggleThinkingBlockVisibility = interactiveModePrototype.toggleThinkingBlockVisibility;
|
|
1290
|
+
const originalShowStatus = interactiveModePrototype.showStatus;
|
|
1291
|
+
const originalShowWarning = interactiveModePrototype.showWarning;
|
|
1292
|
+
const originalShowError = interactiveModePrototype.showError;
|
|
412
1293
|
const patchedSetToolsExpanded = function (this: any, expanded: boolean) {
|
|
413
1294
|
cleanToolsExpanded = expanded;
|
|
414
1295
|
changingAllToolsExpansion = true;
|
|
415
|
-
if (!expanded)
|
|
1296
|
+
if (!expanded) {
|
|
1297
|
+
cleanCompactToolCallIds.clear();
|
|
1298
|
+
revealedActivityGroups.clear();
|
|
1299
|
+
expandedThinkingMembers.clear();
|
|
1300
|
+
}
|
|
416
1301
|
try {
|
|
417
1302
|
return originalSetToolsExpanded.call(this, expanded);
|
|
418
1303
|
} finally {
|
|
419
1304
|
changingAllToolsExpansion = false;
|
|
1305
|
+
for (const component of thinkingComponents.values()) component.invalidate?.();
|
|
1306
|
+
this.ui?.requestRender?.();
|
|
420
1307
|
}
|
|
421
1308
|
};
|
|
422
1309
|
|
|
@@ -426,11 +1313,8 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
426
1313
|
// buildContextEntries() prepends the latest compaction for model context,
|
|
427
1314
|
// while Pi's live compaction UI appends it chronologically. Keep reloads
|
|
428
1315
|
// and transcript rebuilds consistent with that live presentation.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
orderContextEntriesForTranscript(entries),
|
|
432
|
-
options,
|
|
433
|
-
);
|
|
1316
|
+
const orderedEntries = orderContextEntriesForTranscript(entries);
|
|
1317
|
+
return originalRenderSessionEntries.call(this, orderedEntries, options);
|
|
434
1318
|
};
|
|
435
1319
|
|
|
436
1320
|
const patchedSwitchTuiMode = function (this: any, ...args: any[]) {
|
|
@@ -440,6 +1324,80 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
440
1324
|
return result;
|
|
441
1325
|
};
|
|
442
1326
|
|
|
1327
|
+
// Pi's own showExtensionNotify only dispatches to showStatus/showWarning/
|
|
1328
|
+
// showError, and all three are replaced above. Delegating back to it would
|
|
1329
|
+
// therefore re-enter these patches instead of restoring native behaviour,
|
|
1330
|
+
// which sent info notifications into the transient UI flash. Route straight
|
|
1331
|
+
// to the original method that matches the severity instead.
|
|
1332
|
+
const nativeExtensionNotify = function (
|
|
1333
|
+
this: any,
|
|
1334
|
+
text: string,
|
|
1335
|
+
severity: ActivitySeverity,
|
|
1336
|
+
): void {
|
|
1337
|
+
const direct = severity === "error"
|
|
1338
|
+
? originalShowError
|
|
1339
|
+
: severity === "warning"
|
|
1340
|
+
? originalShowWarning
|
|
1341
|
+
: originalShowStatus;
|
|
1342
|
+
try {
|
|
1343
|
+
if (typeof direct === "function") direct.call(this, text);
|
|
1344
|
+
else originalShowExtensionNotify.call(this, text, severity === "info" ? undefined : severity);
|
|
1345
|
+
} catch {
|
|
1346
|
+
// This host has no transcript to write to. The notification is already
|
|
1347
|
+
// recorded in the activity timeline, and it does not belong in the
|
|
1348
|
+
// transient UI flash, which only carries Pi UI state messages.
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
const patchedShowExtensionNotify = function (
|
|
1353
|
+
this: any,
|
|
1354
|
+
message: string,
|
|
1355
|
+
type?: "info" | "warning" | "error",
|
|
1356
|
+
) {
|
|
1357
|
+
if (renderMode !== "clean") {
|
|
1358
|
+
return originalShowExtensionNotify.call(this, message, type);
|
|
1359
|
+
}
|
|
1360
|
+
const severity: ActivitySeverity = type ?? "info";
|
|
1361
|
+
if (!appendRuntimeActivityUpdate(this, message, severity)) {
|
|
1362
|
+
nativeExtensionNotify.call(this, message, severity);
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
const patchedToggleThinkingBlockVisibility = function (this: any) {
|
|
1367
|
+
if (renderMode !== "clean") {
|
|
1368
|
+
return originalToggleThinkingBlockVisibility.call(this);
|
|
1369
|
+
}
|
|
1370
|
+
// Do not change Pi's persisted hideThinkingBlock setting in clean mode.
|
|
1371
|
+
// Thought details use the activity hierarchy instead: click one thought,
|
|
1372
|
+
// or use Ctrl+O when every member should expand together.
|
|
1373
|
+
this.showStatus(
|
|
1374
|
+
"Thought details expand individually in clean mode\nClick a thought, or use Ctrl+O to expand all.",
|
|
1375
|
+
);
|
|
1376
|
+
};
|
|
1377
|
+
|
|
1378
|
+
const patchedShowStatus = function (this: any, message: string) {
|
|
1379
|
+
if (renderMode !== "clean") return originalShowStatus.call(this, message);
|
|
1380
|
+
const [firstLine, ...remainingLines] = String(message).split("\n");
|
|
1381
|
+
setLatestActivityStatus({
|
|
1382
|
+
title: firstLine || "Status",
|
|
1383
|
+
detail: remainingLines.join("\n"),
|
|
1384
|
+
severity: "status",
|
|
1385
|
+
});
|
|
1386
|
+
this.ui?.requestRender?.();
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
const patchedShowWarning = function (this: any, message: string) {
|
|
1390
|
+
if (renderMode !== "clean" || !appendRuntimeActivityUpdate(this, message, "warning")) {
|
|
1391
|
+
return originalShowWarning.call(this, message);
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
const patchedShowError = function (this: any, message: string) {
|
|
1396
|
+
if (renderMode !== "clean" || !appendRuntimeActivityUpdate(this, message, "error")) {
|
|
1397
|
+
return originalShowError.call(this, message);
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
|
|
443
1401
|
interactiveModePrototype[toolsExpansionPatchKey] = {
|
|
444
1402
|
originalSetToolsExpanded,
|
|
445
1403
|
patchedSetToolsExpanded,
|
|
@@ -447,14 +1405,30 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
447
1405
|
patchedRenderSessionEntries,
|
|
448
1406
|
originalSwitchTuiMode,
|
|
449
1407
|
patchedSwitchTuiMode,
|
|
1408
|
+
originalShowExtensionNotify,
|
|
1409
|
+
patchedShowExtensionNotify,
|
|
1410
|
+
originalToggleThinkingBlockVisibility,
|
|
1411
|
+
patchedToggleThinkingBlockVisibility,
|
|
1412
|
+
originalShowStatus,
|
|
1413
|
+
patchedShowStatus,
|
|
1414
|
+
originalShowWarning,
|
|
1415
|
+
patchedShowWarning,
|
|
1416
|
+
originalShowError,
|
|
1417
|
+
patchedShowError,
|
|
450
1418
|
};
|
|
451
1419
|
interactiveModePrototype.setToolsExpanded = patchedSetToolsExpanded;
|
|
452
1420
|
interactiveModePrototype.renderSessionEntries = patchedRenderSessionEntries;
|
|
453
1421
|
interactiveModePrototype.switchTuiMode = patchedSwitchTuiMode;
|
|
1422
|
+
interactiveModePrototype.showExtensionNotify = patchedShowExtensionNotify;
|
|
1423
|
+
interactiveModePrototype.toggleThinkingBlockVisibility = patchedToggleThinkingBlockVisibility;
|
|
1424
|
+
interactiveModePrototype.showStatus = patchedShowStatus;
|
|
1425
|
+
interactiveModePrototype.showWarning = patchedShowWarning;
|
|
1426
|
+
interactiveModePrototype.showError = patchedShowError;
|
|
454
1427
|
|
|
455
1428
|
pi.on("session_shutdown", () => {
|
|
456
1429
|
fullscreenTui = false;
|
|
457
1430
|
currentTui = undefined;
|
|
1431
|
+
clearLatestActivityStatus();
|
|
458
1432
|
currentExtensionUi = undefined;
|
|
459
1433
|
const patch = interactiveModePrototype[toolsExpansionPatchKey];
|
|
460
1434
|
if (!patch) return;
|
|
@@ -467,10 +1441,30 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
467
1441
|
if (patch.patchedSwitchTuiMode === interactiveModePrototype.switchTuiMode) {
|
|
468
1442
|
interactiveModePrototype.switchTuiMode = patch.originalSwitchTuiMode;
|
|
469
1443
|
}
|
|
1444
|
+
if (patch.patchedShowExtensionNotify === interactiveModePrototype.showExtensionNotify) {
|
|
1445
|
+
interactiveModePrototype.showExtensionNotify = patch.originalShowExtensionNotify;
|
|
1446
|
+
}
|
|
1447
|
+
if (patch.patchedToggleThinkingBlockVisibility === interactiveModePrototype.toggleThinkingBlockVisibility) {
|
|
1448
|
+
interactiveModePrototype.toggleThinkingBlockVisibility = patch.originalToggleThinkingBlockVisibility;
|
|
1449
|
+
}
|
|
1450
|
+
if (patch.patchedShowStatus === interactiveModePrototype.showStatus) {
|
|
1451
|
+
interactiveModePrototype.showStatus = patch.originalShowStatus;
|
|
1452
|
+
}
|
|
1453
|
+
if (patch.patchedShowWarning === interactiveModePrototype.showWarning) {
|
|
1454
|
+
interactiveModePrototype.showWarning = patch.originalShowWarning;
|
|
1455
|
+
}
|
|
1456
|
+
if (patch.patchedShowError === interactiveModePrototype.showError) {
|
|
1457
|
+
interactiveModePrototype.showError = patch.originalShowError;
|
|
1458
|
+
}
|
|
470
1459
|
if (
|
|
471
1460
|
interactiveModePrototype.setToolsExpanded === patch.originalSetToolsExpanded &&
|
|
472
1461
|
interactiveModePrototype.renderSessionEntries === patch.originalRenderSessionEntries &&
|
|
473
|
-
interactiveModePrototype.switchTuiMode === patch.originalSwitchTuiMode
|
|
1462
|
+
interactiveModePrototype.switchTuiMode === patch.originalSwitchTuiMode &&
|
|
1463
|
+
interactiveModePrototype.showExtensionNotify === patch.originalShowExtensionNotify &&
|
|
1464
|
+
interactiveModePrototype.toggleThinkingBlockVisibility === patch.originalToggleThinkingBlockVisibility &&
|
|
1465
|
+
interactiveModePrototype.showStatus === patch.originalShowStatus &&
|
|
1466
|
+
interactiveModePrototype.showWarning === patch.originalShowWarning &&
|
|
1467
|
+
interactiveModePrototype.showError === patch.originalShowError
|
|
474
1468
|
) {
|
|
475
1469
|
delete interactiveModePrototype[toolsExpansionPatchKey];
|
|
476
1470
|
}
|
|
@@ -546,6 +1540,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
546
1540
|
const codeBlockPatchKey = Symbol.for("pretty-tui.code-blocks");
|
|
547
1541
|
const codeBlockCollectionKey = Symbol("pretty-tui.code-block-collection");
|
|
548
1542
|
const codeBlockRegionsKey = Symbol("pretty-tui.code-block-regions");
|
|
1543
|
+
const transcriptMarkdownStateKey = Symbol("pretty-tui.transcript-markdown-state");
|
|
549
1544
|
if (!markdownPrototype[codeBlockPatchKey]) {
|
|
550
1545
|
const originalInvalidate = markdownPrototype.invalidate;
|
|
551
1546
|
const originalRender = markdownPrototype.render;
|
|
@@ -561,6 +1556,15 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
561
1556
|
};
|
|
562
1557
|
|
|
563
1558
|
const patchedRender = function (this: any, width: number): string[] {
|
|
1559
|
+
const scoped = transcriptMarkdownActive();
|
|
1560
|
+
if (this[transcriptMarkdownStateKey] !== scoped) {
|
|
1561
|
+
delete this[codeBlockCollectionKey];
|
|
1562
|
+
delete this[codeBlockRegionsKey];
|
|
1563
|
+
originalInvalidate.call(this);
|
|
1564
|
+
this[transcriptMarkdownStateKey] = scoped;
|
|
1565
|
+
}
|
|
1566
|
+
if (!scoped) return originalRender.call(this, width);
|
|
1567
|
+
|
|
564
1568
|
const needsRender =
|
|
565
1569
|
!this.cachedLines || this.cachedText !== this.text || this.cachedWidth !== width;
|
|
566
1570
|
if (needsRender) this[codeBlockCollectionKey] = [];
|
|
@@ -604,6 +1608,10 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
604
1608
|
nextTokenType?: string,
|
|
605
1609
|
styleContext?: any,
|
|
606
1610
|
): string[] {
|
|
1611
|
+
if (!transcriptMarkdownActive()) {
|
|
1612
|
+
return originalRenderToken.call(this, token, width, nextTokenType, styleContext);
|
|
1613
|
+
}
|
|
1614
|
+
|
|
607
1615
|
if (token?.type === "heading") {
|
|
608
1616
|
const level = Math.max(1, Math.min(6, Number(token.depth) || 1));
|
|
609
1617
|
const maxWidth = Math.max(1, width);
|
|
@@ -766,6 +1774,15 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
766
1774
|
};
|
|
767
1775
|
|
|
768
1776
|
const patchedHandleMouse = function (this: any, event: TuiMouseEvent) {
|
|
1777
|
+
// Fullscreen layout hit-testing may dispatch directly to this Markdown
|
|
1778
|
+
// leaf instead of traversing AssistantMessageComponent.handleMouse. The
|
|
1779
|
+
// most recent render therefore records whether this instance belongs to
|
|
1780
|
+
// the main transcript; dynamic render scope alone is not sufficient.
|
|
1781
|
+
const transcriptOwned = transcriptMarkdownActive() || this[transcriptMarkdownStateKey] === true;
|
|
1782
|
+
if (!transcriptOwned) {
|
|
1783
|
+
delete this[codeBlockRegionsKey];
|
|
1784
|
+
return originalHandleMouse?.call(this, event);
|
|
1785
|
+
}
|
|
769
1786
|
if (event.button === "left" && (event.type === "press" || event.type === "click")) {
|
|
770
1787
|
const region = (this[codeBlockRegionsKey] ?? []).find(
|
|
771
1788
|
(candidate: any) =>
|
|
@@ -782,8 +1799,14 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
782
1799
|
} else {
|
|
783
1800
|
// Defensive fallback for older Pi versions without the fullscreen helper.
|
|
784
1801
|
void copyToClipboard(region.code)
|
|
785
|
-
.then(() =>
|
|
786
|
-
|
|
1802
|
+
.then(() => setLatestActivityStatus({
|
|
1803
|
+
title: "Copied!",
|
|
1804
|
+
severity: "success",
|
|
1805
|
+
}))
|
|
1806
|
+
.catch(() => setLatestActivityStatus({
|
|
1807
|
+
title: "Copy failed",
|
|
1808
|
+
severity: "error",
|
|
1809
|
+
}));
|
|
787
1810
|
}
|
|
788
1811
|
}
|
|
789
1812
|
return { handled: true, render: false };
|
|
@@ -913,10 +1936,24 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
913
1936
|
context.state.compactToolStatus = status;
|
|
914
1937
|
};
|
|
915
1938
|
|
|
1939
|
+
const foregroundLuminance = (styled: string): number | undefined => {
|
|
1940
|
+
const trueColor = /\x1b\[38;2;(\d+);(\d+);(\d+)m/.exec(styled);
|
|
1941
|
+
if (!trueColor) return undefined;
|
|
1942
|
+
const [, red, green, blue] = trueColor.map(Number);
|
|
1943
|
+
return red * 0.2126 + green * 0.7152 + blue * 0.0722;
|
|
1944
|
+
};
|
|
1945
|
+
|
|
1946
|
+
const successMarkerColor = (theme: any): string => {
|
|
1947
|
+
const textLuminance = foregroundLuminance(theme.fg("text", "M"));
|
|
1948
|
+
return textLuminance !== undefined && textLuminance < 128
|
|
1949
|
+
? "success"
|
|
1950
|
+
: "syntaxComment";
|
|
1951
|
+
};
|
|
1952
|
+
|
|
916
1953
|
const callRow = (theme: any, name: string, detail: string, state: any): DisplayRow => ({
|
|
917
1954
|
prefix: () => {
|
|
918
1955
|
const status = (state.compactToolStatus ?? "running") as ToolStatus;
|
|
919
|
-
const color = status === "success" ?
|
|
1956
|
+
const color = status === "success" ? successMarkerColor(theme) : status === "error" ? "error" : "dim";
|
|
920
1957
|
return theme.fg(color, "● ");
|
|
921
1958
|
},
|
|
922
1959
|
continuation: " ",
|
|
@@ -930,7 +1967,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
930
1967
|
const call = (theme: any, name: string, detail: string, state: any) =>
|
|
931
1968
|
block([callRow(theme, name, detail, state)]);
|
|
932
1969
|
|
|
933
|
-
const writeCall = (theme: any, path: string, content: string, expanded: boolean, state: any) => {
|
|
1970
|
+
const writeCall = (theme: any, name: string, path: string, content: string, expanded: boolean, state: any) => {
|
|
934
1971
|
const lines = content.replace(/\t/g, " ").split("\n");
|
|
935
1972
|
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
936
1973
|
|
|
@@ -938,14 +1975,14 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
938
1975
|
const shown = lines.slice(0, expanded ? total : 10);
|
|
939
1976
|
const remaining = total - shown.length;
|
|
940
1977
|
const rows: DisplayRow[] = [
|
|
941
|
-
callRow(theme,
|
|
1978
|
+
callRow(theme, name, `${path} · ${total} ${total === 1 ? "line" : "lines"}`, state),
|
|
942
1979
|
];
|
|
943
1980
|
|
|
944
1981
|
for (let index = 0; index < shown.length; index++) {
|
|
945
1982
|
const isLast = index === shown.length - 1 && remaining === 0;
|
|
946
1983
|
rows.push({
|
|
947
1984
|
prefix: theme.fg("dim", isLast ? " └ " : " │ "),
|
|
948
|
-
continuation: theme.fg("dim", " │ "),
|
|
1985
|
+
continuation: isLast ? " " : theme.fg("dim", " │ "),
|
|
949
1986
|
content: theme.fg("toolOutput", shown[index] || " "),
|
|
950
1987
|
});
|
|
951
1988
|
}
|
|
@@ -978,19 +2015,22 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
978
2015
|
|
|
979
2016
|
if (expanded && output) {
|
|
980
2017
|
const lines = output.split("\n");
|
|
2018
|
+
while (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
981
2019
|
const shown = lines.slice(0, 40);
|
|
982
|
-
|
|
2020
|
+
const omitted = lines.length - shown.length;
|
|
2021
|
+
for (let index = 0; index < shown.length; index++) {
|
|
2022
|
+
const isLast = index === shown.length - 1 && omitted === 0;
|
|
983
2023
|
rows.push({
|
|
984
|
-
prefix: theme.fg("dim", " │ "),
|
|
985
|
-
continuation: theme.fg("dim", " │ "),
|
|
986
|
-
content: outputStyle(
|
|
2024
|
+
prefix: theme.fg("dim", isLast ? " └ " : " │ "),
|
|
2025
|
+
continuation: isLast ? " " : theme.fg("dim", " │ "),
|
|
2026
|
+
content: outputStyle(shown[index] || " "),
|
|
987
2027
|
});
|
|
988
2028
|
}
|
|
989
|
-
if (
|
|
2029
|
+
if (omitted > 0) {
|
|
990
2030
|
rows.push({
|
|
991
2031
|
prefix: theme.fg("muted", " └ "),
|
|
992
2032
|
continuation: " ",
|
|
993
|
-
content: theme.fg("muted", `… ${
|
|
2033
|
+
content: theme.fg("muted", `… ${omitted} more lines`),
|
|
994
2034
|
});
|
|
995
2035
|
}
|
|
996
2036
|
}
|
|
@@ -998,8 +2038,11 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
998
2038
|
return block(rows);
|
|
999
2039
|
};
|
|
1000
2040
|
|
|
1001
|
-
|
|
1002
|
-
|
|
2041
|
+
// Pi's render context always carries the authoritative failure flag
|
|
2042
|
+
// (tool-execution.js builds it from result.isError). A tool's own output text
|
|
2043
|
+
// must never decide this: `grep -n Error`, `cat error.log`, or a test runner
|
|
2044
|
+
// printing "failed 0" would otherwise mark a successful call as failed.
|
|
2045
|
+
const isError = (renderContext: any): boolean => Boolean(renderContext?.isError);
|
|
1003
2046
|
|
|
1004
2047
|
const partialResult = (context: any, theme: any, label: string): Component => {
|
|
1005
2048
|
setStatus(context, "running");
|
|
@@ -1064,7 +2107,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1064
2107
|
const isLast = index === shown.length - 1;
|
|
1065
2108
|
rows.push({
|
|
1066
2109
|
prefix: theme.fg("dim", isLast ? " └ " : " │ "),
|
|
1067
|
-
continuation: theme.fg("dim", " │ "),
|
|
2110
|
+
continuation: isLast ? " " : theme.fg("dim", " │ "),
|
|
1068
2111
|
content: theme.fg("toolOutput", shown[index] || " "),
|
|
1069
2112
|
});
|
|
1070
2113
|
}
|
|
@@ -1072,8 +2115,8 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1072
2115
|
return block(rows);
|
|
1073
2116
|
};
|
|
1074
2117
|
|
|
1075
|
-
const completeStatus = (context: any
|
|
1076
|
-
const failed = isError(context
|
|
2118
|
+
const completeStatus = (context: any): boolean => {
|
|
2119
|
+
const failed = isError(context);
|
|
1077
2120
|
setStatus(context, failed ? "error" : "success");
|
|
1078
2121
|
return failed;
|
|
1079
2122
|
};
|
|
@@ -1081,6 +2124,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1081
2124
|
type ToolSummaryGroup = {
|
|
1082
2125
|
count: number;
|
|
1083
2126
|
failed: number;
|
|
2127
|
+
thoughtCount?: number;
|
|
1084
2128
|
lastToolCallId: string;
|
|
1085
2129
|
toolCallIds?: string[];
|
|
1086
2130
|
activity?: string;
|
|
@@ -1088,12 +2132,12 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1088
2132
|
type ToolSummaryData = {
|
|
1089
2133
|
count?: number;
|
|
1090
2134
|
failed?: number;
|
|
2135
|
+
thoughtCount?: number;
|
|
1091
2136
|
/** Identifies the last tool component for older single-group entries. */
|
|
1092
2137
|
lastToolCallId?: string;
|
|
1093
2138
|
/** All groups from a run, used to restore summaries after reload. */
|
|
1094
2139
|
groups?: ToolSummaryGroup[];
|
|
1095
2140
|
};
|
|
1096
|
-
const supportedTools = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
|
|
1097
2141
|
type ToolActivityHold = {
|
|
1098
2142
|
toolCallId: string;
|
|
1099
2143
|
name: string;
|
|
@@ -1102,7 +2146,12 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1102
2146
|
started: boolean;
|
|
1103
2147
|
timer?: ReturnType<typeof setTimeout>;
|
|
1104
2148
|
};
|
|
1105
|
-
const settledSummaries = new Map<string, {
|
|
2149
|
+
const settledSummaries = new Map<string, {
|
|
2150
|
+
count: number;
|
|
2151
|
+
failed: number;
|
|
2152
|
+
thoughtCount: number;
|
|
2153
|
+
activity: DisplayValue;
|
|
2154
|
+
}>();
|
|
1106
2155
|
const toolActivityHolds = new Map<string, ToolActivityHold>();
|
|
1107
2156
|
const legacySummaryLastToolCallIds = new Map<string, string>();
|
|
1108
2157
|
const knownToolCallIds = new Set<string>();
|
|
@@ -1130,8 +2179,64 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1130
2179
|
settled: false,
|
|
1131
2180
|
};
|
|
1132
2181
|
|
|
1133
|
-
const
|
|
1134
|
-
|
|
2182
|
+
const toolLabel = (toolName: string, label?: string): string => label || toolName;
|
|
2183
|
+
|
|
2184
|
+
const conciseThirdPartyArgs = (args: any): string => {
|
|
2185
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return "";
|
|
2186
|
+
const sensitive = /(?:token|secret|password|authorization|credential|api[_-]?key)/iu;
|
|
2187
|
+
const preferred = ["path", "query", "url", "offset", "id"];
|
|
2188
|
+
const entries = Object.entries(args)
|
|
2189
|
+
.filter(([key, value]) => !sensitive.test(key) && ["string", "number", "boolean"].includes(typeof value))
|
|
2190
|
+
.sort(([left], [right]) => {
|
|
2191
|
+
const leftRank = preferred.indexOf(left);
|
|
2192
|
+
const rightRank = preferred.indexOf(right);
|
|
2193
|
+
return (leftRank < 0 ? preferred.length : leftRank) -
|
|
2194
|
+
(rightRank < 0 ? preferred.length : rightRank);
|
|
2195
|
+
})
|
|
2196
|
+
.slice(0, 2)
|
|
2197
|
+
.map(([key, value]) => {
|
|
2198
|
+
const safeValue = String(value)
|
|
2199
|
+
.replace(/(https?:\/\/)[^\s/@:]+:[^\s/@]+@/giu, "$1…:…@")
|
|
2200
|
+
.replace(/([?&](?:token|secret|password|api[_-]?key)=)[^&\s]+/giu, "$1…");
|
|
2201
|
+
const rendered = truncateToWidth(safeValue, 36, "…");
|
|
2202
|
+
return `${key}=${rendered}`;
|
|
2203
|
+
});
|
|
2204
|
+
return entries.join(" · ");
|
|
2205
|
+
};
|
|
2206
|
+
|
|
2207
|
+
const thirdPartyResultSummary = (component: any): string => {
|
|
2208
|
+
const detailsError = component.result?.details?.error;
|
|
2209
|
+
const firstText = component.result?.content?.find(
|
|
2210
|
+
(item: any) => item?.type === "text" && typeof item.text === "string" && item.text.length > 0,
|
|
2211
|
+
)?.text;
|
|
2212
|
+
const text = String(typeof detailsError === "string" ? detailsError : firstText ?? "");
|
|
2213
|
+
const boundedText = text.slice(0, 512);
|
|
2214
|
+
const newline = boundedText.indexOf("\n");
|
|
2215
|
+
const boundedFirstLine = boundedText.slice(0, newline < 0 ? boundedText.length : newline);
|
|
2216
|
+
const firstLine = stripTerminalSequences(boundedFirstLine).trim();
|
|
2217
|
+
if (firstLine) return truncateToWidth(firstLine, 72, "…");
|
|
2218
|
+
if (component.result) return component.result.isError ? "Failed" : "Done";
|
|
2219
|
+
return component.executionStarted ? "Running…" : "Pending";
|
|
2220
|
+
};
|
|
2221
|
+
|
|
2222
|
+
const renderThirdPartyCompact = (component: any, width: number, theme: any): string[] => {
|
|
2223
|
+
const label = toolLabel(component.toolName, component.toolDefinition?.label);
|
|
2224
|
+
const args = conciseThirdPartyArgs(component.args);
|
|
2225
|
+
const dotColor = component.result?.isError
|
|
2226
|
+
? "error"
|
|
2227
|
+
: component.result && !component.isPartial
|
|
2228
|
+
? successMarkerColor(theme)
|
|
2229
|
+
: "accent";
|
|
2230
|
+
const title = theme.fg(dotColor, "● ") +
|
|
2231
|
+
theme.fg("accent", theme.bold(label)) +
|
|
2232
|
+
(args ? theme.fg("muted", `(${args})`) : "");
|
|
2233
|
+
const result = thirdPartyResultSummary(component);
|
|
2234
|
+
const resultColor = component.result?.isError ? "error" : "muted";
|
|
2235
|
+
return [
|
|
2236
|
+
truncateToWidth(title, Math.max(1, width), "…"),
|
|
2237
|
+
theme.fg(resultColor, truncateToWidth(` └ ${result}`, Math.max(1, width), "…")),
|
|
2238
|
+
];
|
|
2239
|
+
};
|
|
1135
2240
|
|
|
1136
2241
|
const clearToolActivityHolds = () => {
|
|
1137
2242
|
for (const hold of toolActivityHolds.values()) {
|
|
@@ -1178,7 +2283,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1178
2283
|
toolCallId,
|
|
1179
2284
|
name,
|
|
1180
2285
|
until: startedAt + CLEAN_TOOL_ACTIVITY_MIN_MS,
|
|
1181
|
-
after: "thinking
|
|
2286
|
+
after: "thinking",
|
|
1182
2287
|
started,
|
|
1183
2288
|
});
|
|
1184
2289
|
cleanRun.activeToolName = name;
|
|
@@ -1193,14 +2298,14 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1193
2298
|
toolCallId,
|
|
1194
2299
|
name,
|
|
1195
2300
|
until: now + CLEAN_TOOL_ACTIVITY_MIN_MS,
|
|
1196
|
-
after: "thinking
|
|
2301
|
+
after: "thinking",
|
|
1197
2302
|
started: true,
|
|
1198
2303
|
};
|
|
1199
2304
|
toolActivityHolds.set(toolCallId, hold);
|
|
1200
2305
|
}
|
|
1201
2306
|
hold.name = name;
|
|
1202
2307
|
hold.started = true;
|
|
1203
|
-
hold.after = "thinking
|
|
2308
|
+
hold.after = "thinking";
|
|
1204
2309
|
scheduleToolActivityRelease(hold);
|
|
1205
2310
|
};
|
|
1206
2311
|
|
|
@@ -1214,15 +2319,18 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1214
2319
|
const liveCleanToolCount = (): number =>
|
|
1215
2320
|
Math.max(cleanRun.count, cleanRun.currentToolCallIds.length);
|
|
1216
2321
|
|
|
2322
|
+
const thoughtCountForTool = (toolCallId: string | undefined): number =>
|
|
2323
|
+
toolCallId ? activityTimeline.groupForTool(toolCallId)?.thoughtCount ?? 0 : 0;
|
|
2324
|
+
|
|
1217
2325
|
const currentCleanActivity = (): string => {
|
|
1218
2326
|
if (cleanRun.activeToolCallId) {
|
|
1219
|
-
return cleanRun.activeToolName ?? cleanRun.activity ?? "thinking
|
|
2327
|
+
return cleanRun.activeToolName ?? cleanRun.activity ?? "thinking";
|
|
1220
2328
|
}
|
|
1221
2329
|
if (cleanRun.lastCompletedToolCallId) {
|
|
1222
2330
|
const hold = toolActivityHolds.get(cleanRun.lastCompletedToolCallId);
|
|
1223
2331
|
if (hold) return heldActivity(hold);
|
|
1224
2332
|
}
|
|
1225
|
-
return cleanRun.activeToolName ?? cleanRun.activity ?? (cleanRun.active ? "thinking
|
|
2333
|
+
return cleanRun.activeToolName ?? cleanRun.activity ?? (cleanRun.active ? "thinking" : "done");
|
|
1226
2334
|
};
|
|
1227
2335
|
|
|
1228
2336
|
pi.on("session_shutdown", clearToolActivityHolds);
|
|
@@ -1238,116 +2346,198 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1238
2346
|
const originalToolRender = toolExecutionPrototype.render;
|
|
1239
2347
|
const originalToolHandleMouse = toolExecutionPrototype.handleMouse;
|
|
1240
2348
|
const renderedModeKey = Symbol("pretty-tui.tool-rendered-mode");
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1243
|
-
for (const
|
|
2349
|
+
const cleanChildRenderCacheKey = Symbol("pretty-tui.clean-child-render-cache");
|
|
2350
|
+
const refreshActivityGroup = (group: ActivityGroup) => {
|
|
2351
|
+
for (const member of group.members) {
|
|
2352
|
+
if (member.kind === "tool" && member.toolCallId) {
|
|
2353
|
+
cleanToolComponents.get(member.toolCallId)?.updateDisplay();
|
|
2354
|
+
} else if (member.kind === "thinking" && member.messageKey) {
|
|
2355
|
+
thinkingComponents.get(member.messageKey)?.invalidate?.();
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
};
|
|
2359
|
+
const revealCleanGroup = (group: ActivityGroup, ui?: any) => {
|
|
2360
|
+
revealedActivityGroups.add(group.id);
|
|
2361
|
+
for (const toolCallId of group.toolCallIds) cleanCompactToolCallIds.add(toolCallId);
|
|
2362
|
+
refreshActivityGroup(group);
|
|
2363
|
+
ui?.requestRender?.();
|
|
2364
|
+
};
|
|
2365
|
+
const collapseCleanGroup = (group: ActivityGroup, ui?: any) => {
|
|
2366
|
+
revealedActivityGroups.delete(group.id);
|
|
2367
|
+
for (const toolCallId of group.toolCallIds) {
|
|
2368
|
+
const component = cleanToolComponents.get(toolCallId);
|
|
2369
|
+
if (component?.expanded) originalSetExpanded.call(component, false);
|
|
2370
|
+
cleanCompactToolCallIds.delete(toolCallId);
|
|
2371
|
+
}
|
|
2372
|
+
for (const member of group.members) expandedThinkingMembers.delete(member.id);
|
|
2373
|
+
refreshActivityGroup(group);
|
|
1244
2374
|
ui?.requestRender?.();
|
|
1245
2375
|
};
|
|
1246
2376
|
const patchedMarkExecutionStarted = function (this: any) {
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
2377
|
+
activityTimeline.addTool(this.toolCallId, this.toolName);
|
|
2378
|
+
if (!cleanRun.currentToolCallIds.includes(this.toolCallId)) {
|
|
2379
|
+
cleanRun.currentToolCallIds.push(this.toolCallId);
|
|
2380
|
+
}
|
|
2381
|
+
cleanRun.activeToolCallIds.add(this.toolCallId);
|
|
2382
|
+
const displayName = toolLabel(this.toolName, this.toolDefinition?.label);
|
|
2383
|
+
cleanToolNames.set(this.toolCallId, displayName);
|
|
2384
|
+
setCleanGroupMembers(this.toolCallId, cleanRun.currentToolCallIds.slice());
|
|
2385
|
+
if (typeof this.ui?.requestRender === "function") {
|
|
2386
|
+
cleanRun.requestRender = () => this.ui.requestRender();
|
|
2387
|
+
}
|
|
2388
|
+
cleanRun.active = true;
|
|
2389
|
+
cleanRun.activeToolCallId = this.toolCallId;
|
|
2390
|
+
beginToolActivity(this.toolCallId, displayName, true);
|
|
1261
2391
|
return originalMarkExecutionStarted.call(this);
|
|
1262
2392
|
};
|
|
1263
2393
|
const patchedSetExpanded = function (this: any, expanded: boolean) {
|
|
1264
|
-
const
|
|
2394
|
+
const group = activityTimeline.groupForTool(this.toolCallId);
|
|
1265
2395
|
if (
|
|
1266
2396
|
renderMode === "clean" &&
|
|
1267
2397
|
!changingAllToolsExpansion &&
|
|
1268
2398
|
expanded &&
|
|
1269
2399
|
!this.expanded &&
|
|
1270
|
-
|
|
1271
|
-
|
|
2400
|
+
group &&
|
|
2401
|
+
!activityGroupRevealed(group)
|
|
1272
2402
|
) {
|
|
1273
|
-
revealCleanGroup(
|
|
2403
|
+
revealCleanGroup(group, this.ui);
|
|
1274
2404
|
return;
|
|
1275
2405
|
}
|
|
1276
2406
|
return originalSetExpanded.call(this, expanded);
|
|
1277
2407
|
};
|
|
1278
2408
|
const patchedToolRender = function (this: any, width: number): string[] {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
2409
|
+
cleanToolComponents.set(this.toolCallId, this);
|
|
2410
|
+
knownToolCallIds.add(this.toolCallId);
|
|
2411
|
+
const member = activityTimeline.addTool(this.toolCallId, this.toolName);
|
|
2412
|
+
cleanToolNames.set(
|
|
2413
|
+
this.toolCallId,
|
|
2414
|
+
toolLabel(this.toolName, this.toolDefinition?.label),
|
|
2415
|
+
);
|
|
1283
2416
|
if (this[renderedModeKey] !== renderMode) {
|
|
1284
2417
|
this[renderedModeKey] = renderMode;
|
|
1285
2418
|
this.updateDisplay();
|
|
1286
2419
|
}
|
|
1287
2420
|
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
2421
|
+
const group = activityTimeline.groupForMember(member.id);
|
|
2422
|
+
if (renderMode !== "clean" || !group) return originalToolRender.call(this, width);
|
|
2423
|
+
const position = activityMemberPosition(group, member);
|
|
2424
|
+
if (!activityGroupRevealed(group)) {
|
|
2425
|
+
return position.first
|
|
2426
|
+
? ["", ...renderActivityGroupSummary(group, width, true)]
|
|
2427
|
+
: [];
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
const childTheme = cleanThemeForToolCall(this.toolCallId) ?? activityGroupTheme(group);
|
|
2431
|
+
const {
|
|
2432
|
+
prefix: childPrefix,
|
|
2433
|
+
continuation,
|
|
2434
|
+
childWidth,
|
|
2435
|
+
} = activityTreeStyle(group, member, width, childTheme);
|
|
2436
|
+
const thirdParty = !SPECIALIZED_TOOL_NAMES.has(this.toolName);
|
|
2437
|
+
const cacheable = !this.expanded && Boolean(this.result) && !this.isPartial;
|
|
2438
|
+
const cached = cacheable ? this[cleanChildRenderCacheKey] : undefined;
|
|
2439
|
+
let decoratedContent: string[];
|
|
2440
|
+
if (
|
|
2441
|
+
cached?.width === width &&
|
|
2442
|
+
cached?.childWidth === childWidth &&
|
|
2443
|
+
cached?.result === this.result &&
|
|
2444
|
+
cached?.args === this.args &&
|
|
2445
|
+
cached?.callRenderer === this.callRendererComponent &&
|
|
2446
|
+
cached?.resultRenderer === this.resultRendererComponent &&
|
|
2447
|
+
cached?.theme === childTheme &&
|
|
2448
|
+
cached?.last === position.last &&
|
|
2449
|
+
cached?.memberCount === group.members.length
|
|
2450
|
+
) {
|
|
2451
|
+
decoratedContent = cached.lines;
|
|
2452
|
+
} else {
|
|
2453
|
+
let contentLines: string[];
|
|
2454
|
+
if (thirdParty) {
|
|
2455
|
+
const summaryLines = renderThirdPartyCompact(this, childWidth, childTheme);
|
|
2456
|
+
if (this.expanded) {
|
|
2457
|
+
const detailPrefix = " │ ";
|
|
2458
|
+
const nativeLines = originalToolRender.call(
|
|
2459
|
+
this,
|
|
2460
|
+
Math.max(1, childWidth - visibleWidth(detailPrefix)),
|
|
2461
|
+
);
|
|
2462
|
+
const nativeContentLines = nativeLines[0] === "" ? nativeLines.slice(1) : nativeLines;
|
|
2463
|
+
contentLines = [
|
|
2464
|
+
summaryLines[0],
|
|
2465
|
+
...nativeContentLines.map((line: string, index: number) =>
|
|
2466
|
+
childTheme.fg(
|
|
2467
|
+
"dim",
|
|
2468
|
+
index === nativeContentLines.length - 1 ? " └ " : detailPrefix,
|
|
2469
|
+
) + stripAnsiBackgrounds(line)
|
|
2470
|
+
),
|
|
2471
|
+
];
|
|
2472
|
+
} else {
|
|
2473
|
+
contentLines = summaryLines;
|
|
2474
|
+
}
|
|
2475
|
+
} else {
|
|
2476
|
+
const lines = originalToolRender.call(this, childWidth);
|
|
2477
|
+
if (lines.length === 0) return lines;
|
|
2478
|
+
contentLines = lines[0] === "" ? lines.slice(1) : lines;
|
|
2479
|
+
}
|
|
2480
|
+
decoratedContent = contentLines.map((line: string, index: number) =>
|
|
2481
|
+
truncateToWidth(
|
|
2482
|
+
(index === 0 ? childPrefix : continuation) + line,
|
|
2483
|
+
Math.max(1, width),
|
|
2484
|
+
"",
|
|
2485
|
+
)
|
|
2486
|
+
);
|
|
2487
|
+
if (cacheable) {
|
|
2488
|
+
this[cleanChildRenderCacheKey] = {
|
|
2489
|
+
width,
|
|
2490
|
+
childWidth,
|
|
2491
|
+
result: this.result,
|
|
2492
|
+
args: this.args,
|
|
2493
|
+
callRenderer: this.callRendererComponent,
|
|
2494
|
+
resultRenderer: this.resultRendererComponent,
|
|
2495
|
+
theme: childTheme,
|
|
2496
|
+
last: position.last,
|
|
2497
|
+
memberCount: group.members.length,
|
|
2498
|
+
lines: decoratedContent,
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
1294
2501
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
const rawContinuation = childIndex === groupToolCallIds.length - 1 ? " " : " │ ";
|
|
1298
|
-
const childTheme = cleanThemeForToolCall(this.toolCallId);
|
|
1299
|
-
const childPrefix = childTheme ? childTheme.fg("dim", rawChildPrefix) : rawChildPrefix;
|
|
1300
|
-
const continuation = childTheme ? childTheme.fg("dim", rawContinuation) : rawContinuation;
|
|
1301
|
-
const childWidth = Math.max(1, width - visibleWidth(rawChildPrefix));
|
|
1302
|
-
const lines = originalToolRender.call(this, childWidth);
|
|
1303
|
-
if (lines.length === 0) return lines;
|
|
1304
|
-
|
|
1305
|
-
const decoratedContent = lines.slice(1).map((line: string, index: number) =>
|
|
1306
|
-
(index === 0 ? childPrefix : continuation) + line
|
|
1307
|
-
);
|
|
1308
|
-
if (childIndex !== 0) return decoratedContent;
|
|
1309
|
-
return [lines[0], ...renderCleanGroupSummary(groupOwner, width), ...decoratedContent];
|
|
2502
|
+
if (!position.first) return decoratedContent;
|
|
2503
|
+
return ["", ...renderActivityGroupSummary(group, width), ...decoratedContent];
|
|
1310
2504
|
};
|
|
1311
2505
|
const patchedToolHandleMouse = function (this: any, event: any) {
|
|
1312
|
-
const
|
|
1313
|
-
const
|
|
1314
|
-
const childIndex = groupToolCallIds?.indexOf(this.toolCallId) ?? -1;
|
|
1315
|
-
const showAsChild = renderMode === "clean" && childIndex >= 0 && isCleanGroupRevealed(this.toolCallId);
|
|
2506
|
+
const member = activityTimeline.memberForTool(this.toolCallId);
|
|
2507
|
+
const group = member ? activityTimeline.groupForMember(member.id) : undefined;
|
|
1316
2508
|
const isLeftClick = event.type === "click" && event.button === "left";
|
|
2509
|
+
if (renderMode !== "clean" || !member || !group) {
|
|
2510
|
+
return originalToolHandleMouse.call(this, event);
|
|
2511
|
+
}
|
|
2512
|
+
const position = activityMemberPosition(group, member);
|
|
2513
|
+
const revealed = activityGroupRevealed(group);
|
|
1317
2514
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
// only; it does not affect or delay execution.
|
|
1321
|
-
if (
|
|
1322
|
-
renderMode === "clean" &&
|
|
1323
|
-
!showAsChild &&
|
|
1324
|
-
isLeftClick &&
|
|
1325
|
-
this.toolCallId === groupOwner &&
|
|
1326
|
-
groupToolCallIds?.length
|
|
1327
|
-
) {
|
|
1328
|
-
revealCleanGroup(groupToolCallIds, this.ui);
|
|
2515
|
+
if (!revealed && position.first && isLeftClick) {
|
|
2516
|
+
revealCleanGroup(group, this.ui);
|
|
1329
2517
|
return { handled: true };
|
|
1330
2518
|
}
|
|
1331
|
-
if (!
|
|
2519
|
+
if (!revealed) return isLeftClick ? { handled: true } : undefined;
|
|
1332
2520
|
|
|
1333
|
-
const summaryHeight =
|
|
1334
|
-
if (
|
|
1335
|
-
childIndex === 0 &&
|
|
1336
|
-
event.y > 0 &&
|
|
1337
|
-
event.y <= summaryHeight &&
|
|
1338
|
-
isLeftClick
|
|
1339
|
-
) {
|
|
2521
|
+
const summaryHeight = position.first ? renderActivityGroupSummary(group, event.width).length : 0;
|
|
2522
|
+
if (position.first && event.y > 0 && event.y <= summaryHeight && isLeftClick) {
|
|
1340
2523
|
changingAllToolsExpansion = true;
|
|
1341
2524
|
try {
|
|
1342
|
-
|
|
1343
|
-
const component = cleanToolComponents.get(toolCallId);
|
|
1344
|
-
if (component?.expanded) originalSetExpanded.call(component, false);
|
|
1345
|
-
cleanCompactToolCallIds.delete(toolCallId);
|
|
1346
|
-
}
|
|
1347
|
-
for (const toolCallId of groupToolCallIds) cleanToolComponents.get(toolCallId)?.updateDisplay();
|
|
2525
|
+
collapseCleanGroup(group, this.ui);
|
|
1348
2526
|
} finally {
|
|
1349
2527
|
changingAllToolsExpansion = false;
|
|
1350
2528
|
}
|
|
2529
|
+
return { handled: true };
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// Self-rendering tools reserve y=0 for Pi's leading spacer. Clean mode
|
|
2533
|
+
// replaces that spacer with the compact header for non-first members.
|
|
2534
|
+
const thirdPartyHeaderY = position.first ? summaryHeight + 1 : 0;
|
|
2535
|
+
if (
|
|
2536
|
+
!SPECIALIZED_TOOL_NAMES.has(this.toolName) &&
|
|
2537
|
+
isLeftClick &&
|
|
2538
|
+
event.y === thirdPartyHeaderY
|
|
2539
|
+
) {
|
|
2540
|
+
originalSetExpanded.call(this, !this.expanded);
|
|
1351
2541
|
this.ui?.requestRender?.();
|
|
1352
2542
|
return { handled: true };
|
|
1353
2543
|
}
|
|
@@ -1358,12 +2548,17 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1358
2548
|
return { handled: true };
|
|
1359
2549
|
}
|
|
1360
2550
|
|
|
1361
|
-
const
|
|
2551
|
+
const { prefixWidth } = activityTreeStyle(
|
|
2552
|
+
group,
|
|
2553
|
+
member,
|
|
2554
|
+
event.width,
|
|
2555
|
+
cleanThemeForToolCall(this.toolCallId),
|
|
2556
|
+
);
|
|
1362
2557
|
return originalToolHandleMouse.call(this, {
|
|
1363
2558
|
...event,
|
|
1364
|
-
x: Math.max(0, event.x -
|
|
1365
|
-
y: event.y - summaryHeight + (
|
|
1366
|
-
width: Math.max(1, event.width -
|
|
2559
|
+
x: Math.max(0, event.x - prefixWidth),
|
|
2560
|
+
y: event.y - summaryHeight + (position.first ? 0 : 1),
|
|
2561
|
+
width: Math.max(1, event.width - prefixWidth),
|
|
1367
2562
|
});
|
|
1368
2563
|
};
|
|
1369
2564
|
|
|
@@ -1413,6 +2608,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1413
2608
|
const group: ToolSummaryGroup = {
|
|
1414
2609
|
count: cleanRun.count,
|
|
1415
2610
|
failed: cleanRun.failed,
|
|
2611
|
+
thoughtCount: thoughtCountForTool(cleanRun.lastCompletedToolCallId),
|
|
1416
2612
|
lastToolCallId: cleanRun.lastCompletedToolCallId,
|
|
1417
2613
|
toolCallIds: cleanRun.currentToolCallIds.slice(),
|
|
1418
2614
|
activity,
|
|
@@ -1421,6 +2617,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1421
2617
|
settledSummaries.set(group.lastToolCallId, {
|
|
1422
2618
|
count: group.count,
|
|
1423
2619
|
failed: group.failed,
|
|
2620
|
+
thoughtCount: group.thoughtCount ?? 0,
|
|
1424
2621
|
activity: activityValueForHold(group.lastToolCallId, activity),
|
|
1425
2622
|
});
|
|
1426
2623
|
setCleanGroupMembers(group.lastToolCallId, group.toolCallIds ?? []);
|
|
@@ -1432,6 +2629,16 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1432
2629
|
cleanRun.lastCompletedToolCallId = undefined;
|
|
1433
2630
|
cleanRun.activeToolCallId = undefined;
|
|
1434
2631
|
cleanRun.activeToolName = undefined;
|
|
2632
|
+
activityTimeline.boundary();
|
|
2633
|
+
};
|
|
2634
|
+
|
|
2635
|
+
closeCleanAtNativeCustomBoundary = () => {
|
|
2636
|
+
finishCleanGroup("done");
|
|
2637
|
+
settleLastCleanGroup();
|
|
2638
|
+
};
|
|
2639
|
+
closeCleanAtStandaloneAlert = () => {
|
|
2640
|
+
finishCleanGroup("done");
|
|
2641
|
+
settleLastCleanGroup();
|
|
1435
2642
|
};
|
|
1436
2643
|
|
|
1437
2644
|
const settleLastCleanGroup = () => {
|
|
@@ -1441,30 +2648,46 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1441
2648
|
settledSummaries.set(group.lastToolCallId, {
|
|
1442
2649
|
count: group.count,
|
|
1443
2650
|
failed: group.failed,
|
|
2651
|
+
thoughtCount: group.thoughtCount ?? 0,
|
|
1444
2652
|
activity: activityValueForHold(group.lastToolCallId, "done"),
|
|
1445
2653
|
});
|
|
1446
2654
|
};
|
|
1447
2655
|
|
|
1448
|
-
const summaryText = (
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
2656
|
+
const summaryText = (
|
|
2657
|
+
count: number,
|
|
2658
|
+
_failed: number,
|
|
2659
|
+
thoughtCount: number,
|
|
2660
|
+
activity = "done",
|
|
2661
|
+
): string => {
|
|
2662
|
+
const rawActivityText = typeof activity === "string" ? activity : "done";
|
|
2663
|
+
const normalizedActivityText = rawActivityText.trim();
|
|
2664
|
+
const activityText = /^thinking(?:\.\.\.)?$/iu.test(normalizedActivityText)
|
|
2665
|
+
? "thinking"
|
|
2666
|
+
: rawActivityText;
|
|
2667
|
+
const countLabel = count > 0 ? `${count} tool ${count === 1 ? "call" : "calls"}` : "";
|
|
2668
|
+
const thoughtLabel = thoughtCount > 0
|
|
2669
|
+
? `${thoughtCount} ${thoughtCount === 1 ? "thought" : "thoughts"}`
|
|
1453
2670
|
: "";
|
|
1454
|
-
|
|
2671
|
+
// Without tools there is no current tool name to report, and the generic
|
|
2672
|
+
// "thinking" placeholder would only repeat the thought count.
|
|
2673
|
+
const activityLabel = count > 0 && activityText !== "done" && activityText.trim()
|
|
2674
|
+
? activityText
|
|
2675
|
+
: "";
|
|
2676
|
+
return [countLabel, thoughtLabel, activityLabel].filter(Boolean).join(" · ");
|
|
1455
2677
|
};
|
|
1456
2678
|
|
|
1457
2679
|
const summaryRow = (
|
|
1458
2680
|
theme: any,
|
|
1459
2681
|
count: number,
|
|
1460
2682
|
failed: number,
|
|
2683
|
+
thoughtCount: number,
|
|
1461
2684
|
activity: DisplayValue = "done",
|
|
1462
2685
|
collapsedDone = false,
|
|
1463
2686
|
): DisplayRow => ({
|
|
1464
2687
|
prefix: () => {
|
|
1465
2688
|
const currentActivity = typeof activity === "function" ? activity() : activity;
|
|
1466
2689
|
const color = currentActivity === "done"
|
|
1467
|
-
? collapsedDone ? "thinkingText" :
|
|
2690
|
+
? collapsedDone ? "thinkingText" : successMarkerColor(theme)
|
|
1468
2691
|
: "accent";
|
|
1469
2692
|
return theme.fg(color, "● ");
|
|
1470
2693
|
},
|
|
@@ -1473,12 +2696,13 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1473
2696
|
const currentActivity = typeof activity === "function" ? activity() : activity;
|
|
1474
2697
|
const label = currentActivity === "done" ? "Done" : "Running";
|
|
1475
2698
|
const color = label === "Done"
|
|
1476
|
-
? collapsedDone ? "thinkingText" :
|
|
2699
|
+
? collapsedDone ? "thinkingText" : successMarkerColor(theme)
|
|
1477
2700
|
: "accent";
|
|
1478
2701
|
const detailColor = label === "Done" && collapsedDone ? "thinkingText" : "text";
|
|
1479
2702
|
return theme.fg(color, theme.bold(label)) +
|
|
1480
2703
|
theme.fg("dim", "(") +
|
|
1481
|
-
theme.fg(detailColor, summaryText(count, failed, currentActivity)) +
|
|
2704
|
+
theme.fg(detailColor, summaryText(count, failed, thoughtCount, currentActivity)) +
|
|
2705
|
+
theme.fg("dim", ")");
|
|
1482
2706
|
},
|
|
1483
2707
|
});
|
|
1484
2708
|
|
|
@@ -1487,22 +2711,91 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1487
2711
|
if (!summaryTheme) return [];
|
|
1488
2712
|
const settled = settledSummaries.get(lastToolCallId);
|
|
1489
2713
|
if (settled) {
|
|
1490
|
-
return block([summaryRow(
|
|
2714
|
+
return block([summaryRow(
|
|
2715
|
+
summaryTheme,
|
|
2716
|
+
settled.count,
|
|
2717
|
+
settled.failed,
|
|
2718
|
+
settled.thoughtCount,
|
|
2719
|
+
settled.activity,
|
|
2720
|
+
)]).render(width);
|
|
1491
2721
|
}
|
|
1492
2722
|
if (cleanRun.activeToolCallId === lastToolCallId) {
|
|
1493
2723
|
return block([summaryRow(
|
|
1494
2724
|
summaryTheme,
|
|
1495
2725
|
liveCleanToolCount(),
|
|
1496
2726
|
cleanRun.failed,
|
|
2727
|
+
thoughtCountForTool(lastToolCallId),
|
|
1497
2728
|
() => cleanRun.activeToolName ?? currentCleanActivity(),
|
|
1498
2729
|
)]).render(width);
|
|
1499
2730
|
}
|
|
1500
2731
|
if (cleanRun.lastCompletedToolCallId === lastToolCallId && cleanRun.count > 0) {
|
|
1501
|
-
return block([summaryRow(
|
|
2732
|
+
return block([summaryRow(
|
|
2733
|
+
summaryTheme,
|
|
2734
|
+
cleanRun.count,
|
|
2735
|
+
cleanRun.failed,
|
|
2736
|
+
thoughtCountForTool(lastToolCallId),
|
|
2737
|
+
currentCleanActivity,
|
|
2738
|
+
)]).render(width);
|
|
1502
2739
|
}
|
|
1503
2740
|
return [];
|
|
1504
2741
|
};
|
|
1505
2742
|
|
|
2743
|
+
const renderActivityGroupSummary = (
|
|
2744
|
+
group: ActivityGroup,
|
|
2745
|
+
width: number,
|
|
2746
|
+
collapsedDone = false,
|
|
2747
|
+
): string[] => {
|
|
2748
|
+
const fit = (lines: string[]) => lines.map((line) => truncateToWidth(line, Math.max(1, width), ""));
|
|
2749
|
+
const summaryTheme = activityGroupTheme(group);
|
|
2750
|
+
if (!summaryTheme || !activityTimeline.hasWork(group)) return [];
|
|
2751
|
+
const owner = activityGroupOwner(group);
|
|
2752
|
+
const settled = owner ? settledSummaries.get(owner) : undefined;
|
|
2753
|
+
const toolCount = group.toolCallIds.length;
|
|
2754
|
+
const live = () => cleanRun.activeToolName ?? currentCleanActivity();
|
|
2755
|
+
if (settled) {
|
|
2756
|
+
return fit(block([summaryRow(
|
|
2757
|
+
summaryTheme,
|
|
2758
|
+
group.hardBoundarySplit ? toolCount : Math.max(settled.count, toolCount),
|
|
2759
|
+
group.hardBoundarySplit ? 0 : settled.failed,
|
|
2760
|
+
Math.max(settled.thoughtCount, group.thoughtCount),
|
|
2761
|
+
settled.activity,
|
|
2762
|
+
collapsedDone,
|
|
2763
|
+
)]).render(width));
|
|
2764
|
+
}
|
|
2765
|
+
if (owner && !cleanRun.settled) {
|
|
2766
|
+
// Live tool group: keep the current tool name and the running count.
|
|
2767
|
+
return fit(block([summaryRow(
|
|
2768
|
+
summaryTheme,
|
|
2769
|
+
Math.max(toolCount, liveCleanToolCount()),
|
|
2770
|
+
cleanRun.failed,
|
|
2771
|
+
group.thoughtCount,
|
|
2772
|
+
live,
|
|
2773
|
+
collapsedDone,
|
|
2774
|
+
)]).render(width));
|
|
2775
|
+
}
|
|
2776
|
+
if (!owner && !activityTimeline.isSettled(group.id)) {
|
|
2777
|
+
// Thought-only group while the model is still working.
|
|
2778
|
+
return fit(block([summaryRow(
|
|
2779
|
+
summaryTheme,
|
|
2780
|
+
toolCount,
|
|
2781
|
+
cleanRun.failed,
|
|
2782
|
+
group.thoughtCount,
|
|
2783
|
+
"thinking",
|
|
2784
|
+
collapsedDone,
|
|
2785
|
+
)]).render(width));
|
|
2786
|
+
}
|
|
2787
|
+
// Finished group with no durable summary: a completed Thought-only turn, or
|
|
2788
|
+
// a group split apart by a native custom renderer.
|
|
2789
|
+
return fit(block([summaryRow(
|
|
2790
|
+
summaryTheme,
|
|
2791
|
+
toolCount,
|
|
2792
|
+
0,
|
|
2793
|
+
group.thoughtCount,
|
|
2794
|
+
"done",
|
|
2795
|
+
collapsedDone,
|
|
2796
|
+
)]).render(width));
|
|
2797
|
+
};
|
|
2798
|
+
|
|
1506
2799
|
/**
|
|
1507
2800
|
* Clean mode keeps Running/Done rows in the existing tool components. This
|
|
1508
2801
|
* avoids waiting for agent_end (which can be followed by retry/compaction),
|
|
@@ -1511,6 +2804,8 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1511
2804
|
const cleanToolCall = (theme: any, name: string, toolCallId: string): Component => {
|
|
1512
2805
|
knownToolCallIds.add(toolCallId);
|
|
1513
2806
|
cleanToolThemes.set(toolCallId, theme);
|
|
2807
|
+
const activityGroup = activityTimeline.groupForTool(toolCallId);
|
|
2808
|
+
if (activityGroup) activityFallbackThemes.set(activityGroup.id, theme);
|
|
1514
2809
|
return {
|
|
1515
2810
|
render(width: number): string[] {
|
|
1516
2811
|
if (renderMode !== "clean") return [];
|
|
@@ -1521,6 +2816,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1521
2816
|
theme,
|
|
1522
2817
|
settledSummary.count,
|
|
1523
2818
|
settledSummary.failed,
|
|
2819
|
+
settledSummary.thoughtCount,
|
|
1524
2820
|
settledSummary.activity,
|
|
1525
2821
|
true,
|
|
1526
2822
|
)]).render(width);
|
|
@@ -1532,6 +2828,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1532
2828
|
theme,
|
|
1533
2829
|
liveCleanToolCount(),
|
|
1534
2830
|
cleanRun.failed,
|
|
2831
|
+
thoughtCountForTool(toolCallId),
|
|
1535
2832
|
() => cleanRun.activeToolName ?? name,
|
|
1536
2833
|
)]).render(width);
|
|
1537
2834
|
}
|
|
@@ -1543,7 +2840,13 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1543
2840
|
cleanRun.count > 0 &&
|
|
1544
2841
|
!cleanRun.activeToolCallId
|
|
1545
2842
|
) {
|
|
1546
|
-
return block([summaryRow(
|
|
2843
|
+
return block([summaryRow(
|
|
2844
|
+
theme,
|
|
2845
|
+
cleanRun.count,
|
|
2846
|
+
cleanRun.failed,
|
|
2847
|
+
thoughtCountForTool(toolCallId),
|
|
2848
|
+
currentCleanActivity,
|
|
2849
|
+
)]).render(width);
|
|
1547
2850
|
}
|
|
1548
2851
|
|
|
1549
2852
|
// Do not fall back to the per-component pending state here: Pi may
|
|
@@ -1558,12 +2861,12 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1558
2861
|
|
|
1559
2862
|
const hiddenToolResult = (context: any, options: any, output: string): Component => {
|
|
1560
2863
|
if (options.isPartial) setStatus(context, "running");
|
|
1561
|
-
else completeStatus(context
|
|
2864
|
+
else completeStatus(context);
|
|
1562
2865
|
return block([]);
|
|
1563
2866
|
};
|
|
1564
2867
|
|
|
1565
|
-
//
|
|
1566
|
-
//
|
|
2868
|
+
// Built-ins use specialized compact renderers, while the activity timeline
|
|
2869
|
+
// controls grouping for every tool, including third-party definitions.
|
|
1567
2870
|
const hideCleanTool = (
|
|
1568
2871
|
toolCallId: string,
|
|
1569
2872
|
expanded: boolean,
|
|
@@ -1586,6 +2889,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1586
2889
|
? [{
|
|
1587
2890
|
count: data.count ?? 0,
|
|
1588
2891
|
failed: data.failed ?? 0,
|
|
2892
|
+
thoughtCount: data.thoughtCount ?? 0,
|
|
1589
2893
|
lastToolCallId: data.lastToolCallId,
|
|
1590
2894
|
}]
|
|
1591
2895
|
: (() => {
|
|
@@ -1594,6 +2898,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1594
2898
|
? [{
|
|
1595
2899
|
count: data?.count ?? 0,
|
|
1596
2900
|
failed: data?.failed ?? 0,
|
|
2901
|
+
thoughtCount: data?.thoughtCount ?? 0,
|
|
1597
2902
|
lastToolCallId: summaryCallId,
|
|
1598
2903
|
}]
|
|
1599
2904
|
: [];
|
|
@@ -1603,11 +2908,10 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1603
2908
|
// absent from an uncompacted branch.
|
|
1604
2909
|
const missingGroups = groups.filter((group) => !knownToolCallIds.has(group.lastToolCallId));
|
|
1605
2910
|
if (missingGroups.length === 0) return [];
|
|
1606
|
-
// Persisted entries represent settled groups
|
|
1607
|
-
//
|
|
1608
|
-
// normalize their display to Done.
|
|
2911
|
+
// Persisted entries represent settled groups, so always normalize their
|
|
2912
|
+
// display to Done even when an older entry stored a transient activity.
|
|
1609
2913
|
return block(missingGroups.map((group) =>
|
|
1610
|
-
summaryRow(theme, group.count, group.failed, "done", true)
|
|
2914
|
+
summaryRow(theme, group.count, group.failed, group.thoughtCount ?? 0, "done", true)
|
|
1611
2915
|
)).render(width);
|
|
1612
2916
|
},
|
|
1613
2917
|
invalidate() {},
|
|
@@ -1625,9 +2929,20 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1625
2929
|
|
|
1626
2930
|
const restoreCleanSession = (ctx: any) => {
|
|
1627
2931
|
currentExtensionUi = ctx.ui;
|
|
2932
|
+
clearLatestActivityStatus();
|
|
1628
2933
|
cleanToolsExpanded = ctx.ui.getToolsExpanded();
|
|
1629
2934
|
settledSummaries.clear();
|
|
1630
2935
|
legacySummaryLastToolCallIds.clear();
|
|
2936
|
+
activityTimeline.clear();
|
|
2937
|
+
nativeCustomBoundaryKeys.clear();
|
|
2938
|
+
revealedActivityGroups.clear();
|
|
2939
|
+
expandedThinkingMembers.clear();
|
|
2940
|
+
thinkingComponents.clear();
|
|
2941
|
+
activityFallbackThemes.clear();
|
|
2942
|
+
activityUpdateComponents.clear();
|
|
2943
|
+
activityUpdateMarkdownComponents.clear();
|
|
2944
|
+
activityUpdateSequence = 0;
|
|
2945
|
+
assistantBoundaryKeys.clear();
|
|
1631
2946
|
knownToolCallIds.clear();
|
|
1632
2947
|
cleanCompactToolCallIds.clear();
|
|
1633
2948
|
cleanGroupToolCallIds.clear();
|
|
@@ -1659,9 +2974,15 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1659
2974
|
const explicitSummaryIds = new Set<string>();
|
|
1660
2975
|
|
|
1661
2976
|
const rememberGroup = (group: ToolSummaryGroup, entryId?: string) => {
|
|
2977
|
+
const thoughtCount = Math.max(
|
|
2978
|
+
group.thoughtCount ?? 0,
|
|
2979
|
+
thoughtCountForTool(group.lastToolCallId),
|
|
2980
|
+
);
|
|
2981
|
+
group.thoughtCount = thoughtCount;
|
|
1662
2982
|
settledSummaries.set(group.lastToolCallId, {
|
|
1663
2983
|
count: group.count,
|
|
1664
2984
|
failed: group.failed,
|
|
2985
|
+
thoughtCount,
|
|
1665
2986
|
activity: "done",
|
|
1666
2987
|
});
|
|
1667
2988
|
if (group.toolCallIds?.length) {
|
|
@@ -1673,11 +2994,22 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1673
2994
|
|
|
1674
2995
|
const finishGroup = () => {
|
|
1675
2996
|
if (lastToolCallId && count > 0) {
|
|
1676
|
-
lastFinishedGroup = {
|
|
2997
|
+
lastFinishedGroup = {
|
|
2998
|
+
count,
|
|
2999
|
+
failed,
|
|
3000
|
+
thoughtCount: thoughtCountForTool(lastToolCallId),
|
|
3001
|
+
lastToolCallId,
|
|
3002
|
+
toolCallIds: [...toolCalls],
|
|
3003
|
+
};
|
|
1677
3004
|
inferredGroups.push(lastFinishedGroup);
|
|
1678
3005
|
setCleanGroupMembers(lastToolCallId, [...toolCalls]);
|
|
1679
3006
|
if (!explicitSummaryIds.has(lastToolCallId)) {
|
|
1680
|
-
settledSummaries.set(lastToolCallId, {
|
|
3007
|
+
settledSummaries.set(lastToolCallId, {
|
|
3008
|
+
count,
|
|
3009
|
+
failed,
|
|
3010
|
+
thoughtCount: lastFinishedGroup.thoughtCount ?? 0,
|
|
3011
|
+
activity: "done",
|
|
3012
|
+
});
|
|
1681
3013
|
}
|
|
1682
3014
|
}
|
|
1683
3015
|
count = 0;
|
|
@@ -1685,13 +3017,20 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1685
3017
|
lastToolCallId = undefined;
|
|
1686
3018
|
toolCalls.clear();
|
|
1687
3019
|
completedToolCalls.clear();
|
|
3020
|
+
activityTimeline.boundary();
|
|
1688
3021
|
};
|
|
1689
3022
|
|
|
1690
3023
|
const inferredGroupsOverlapping = (group: ToolSummaryGroup): ToolSummaryGroup[] => {
|
|
1691
3024
|
if (!group.toolCallIds?.length) return [];
|
|
1692
3025
|
const persistedIds = new Set(group.toolCallIds);
|
|
1693
3026
|
const currentGroup = lastToolCallId && count > 0
|
|
1694
|
-
? [{
|
|
3027
|
+
? [{
|
|
3028
|
+
count,
|
|
3029
|
+
failed,
|
|
3030
|
+
thoughtCount: thoughtCountForTool(lastToolCallId),
|
|
3031
|
+
lastToolCallId,
|
|
3032
|
+
toolCallIds: [...toolCalls],
|
|
3033
|
+
}]
|
|
1695
3034
|
: [];
|
|
1696
3035
|
return [...inferredGroups, ...currentGroup].filter((inferred) =>
|
|
1697
3036
|
inferred.toolCallIds?.some((toolCallId) => persistedIds.has(toolCallId)),
|
|
@@ -1770,6 +3109,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1770
3109
|
const group = {
|
|
1771
3110
|
count: data.count ?? 0,
|
|
1772
3111
|
failed: data.failed ?? 0,
|
|
3112
|
+
thoughtCount: data.thoughtCount ?? 0,
|
|
1773
3113
|
lastToolCallId: data.lastToolCallId,
|
|
1774
3114
|
};
|
|
1775
3115
|
rememberGroup(group);
|
|
@@ -1781,6 +3121,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1781
3121
|
rememberGroup({
|
|
1782
3122
|
count: data?.count ?? lastFinishedGroup.count,
|
|
1783
3123
|
failed: data?.failed ?? lastFinishedGroup.failed,
|
|
3124
|
+
thoughtCount: data?.thoughtCount ?? lastFinishedGroup.thoughtCount ?? 0,
|
|
1784
3125
|
lastToolCallId: lastFinishedGroup.lastToolCallId,
|
|
1785
3126
|
}, entry.id);
|
|
1786
3127
|
} else if (lastToolCallId && count > 0) {
|
|
@@ -1788,9 +3129,16 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1788
3129
|
rememberGroup({
|
|
1789
3130
|
count: data?.count ?? count,
|
|
1790
3131
|
failed: data?.failed ?? failed,
|
|
3132
|
+
thoughtCount: data?.thoughtCount ?? thoughtCountForTool(lastToolCallId),
|
|
1791
3133
|
lastToolCallId,
|
|
1792
3134
|
}, entry.id);
|
|
1793
3135
|
}
|
|
3136
|
+
activityTimeline.boundary();
|
|
3137
|
+
continue;
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
if (entry.type === "custom_message") {
|
|
3141
|
+
if (entry.display !== false) addPersistentActivityUpdate(entry);
|
|
1794
3142
|
continue;
|
|
1795
3143
|
}
|
|
1796
3144
|
|
|
@@ -1803,12 +3151,23 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1803
3151
|
continue;
|
|
1804
3152
|
}
|
|
1805
3153
|
|
|
3154
|
+
if (message.role === "custom") {
|
|
3155
|
+
if (message.display !== false) addPersistentActivityUpdate(message);
|
|
3156
|
+
continue;
|
|
3157
|
+
}
|
|
3158
|
+
|
|
1806
3159
|
if (message.role === "assistant") {
|
|
3160
|
+
const thinking = thinkingText(message);
|
|
3161
|
+
if (thinking) {
|
|
3162
|
+
activityTimeline.addThinking(assistantMessageKey(message), thinking);
|
|
3163
|
+
}
|
|
1807
3164
|
if (messageHasVisibleText(message)) finishGroup();
|
|
1808
3165
|
for (const item of messageContentItems(message)) {
|
|
1809
|
-
if (item.type !== "toolCall" || !
|
|
3166
|
+
if (item.type !== "toolCall" || !item.id) continue;
|
|
1810
3167
|
toolCalls.add(item.id);
|
|
3168
|
+
activityTimeline.addTool(item.id, item.name ?? "tool");
|
|
1811
3169
|
}
|
|
3170
|
+
if (assistantTerminalState(message)) finishGroup();
|
|
1812
3171
|
continue;
|
|
1813
3172
|
}
|
|
1814
3173
|
|
|
@@ -1829,6 +3188,17 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1829
3188
|
|
|
1830
3189
|
pi.on("session_start", (_event, ctx) => restoreCleanSession(ctx));
|
|
1831
3190
|
pi.on("session_tree", (_event, ctx) => restoreCleanSession(ctx));
|
|
3191
|
+
pi.on("session_before_compact", () => {
|
|
3192
|
+
// Pi emits this for manual, threshold, and overflow-recovery compaction.
|
|
3193
|
+
// Seal the activity group before the compaction indicator is rendered so
|
|
3194
|
+
// completed work never remains labelled Running(... · thinking). A
|
|
3195
|
+
// lifecycle boundary overrides the minimum per-tool activity hold.
|
|
3196
|
+
clearToolActivityHolds();
|
|
3197
|
+
finishCleanGroup("done");
|
|
3198
|
+
settleLastCleanGroup();
|
|
3199
|
+
cleanRun.activity = "done";
|
|
3200
|
+
cleanRun.requestRender?.();
|
|
3201
|
+
});
|
|
1832
3202
|
pi.on("session_compact", () => {
|
|
1833
3203
|
// Compaction is a hard chronological boundary. Keep tools completed before
|
|
1834
3204
|
// it in their own group so expanded hierarchy lines never cross the summary.
|
|
@@ -1837,31 +3207,63 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1837
3207
|
// Pre-compaction tools have been summarized intentionally. Their durable
|
|
1838
3208
|
// fallback rows should not be replayed beside the compacted transcript.
|
|
1839
3209
|
cleanContextCompacted = true;
|
|
3210
|
+
cleanRun.requestRender?.();
|
|
3211
|
+
});
|
|
3212
|
+
pi.on("session_compact_failed", () => {
|
|
3213
|
+
// The pre-compaction work is still complete even when compaction is
|
|
3214
|
+
// cancelled or fails. Keep it settled and let a retry start a new group.
|
|
3215
|
+
settleLastCleanGroup();
|
|
3216
|
+
cleanRun.activity = "done";
|
|
3217
|
+
cleanRun.requestRender?.();
|
|
1840
3218
|
});
|
|
1841
3219
|
|
|
1842
3220
|
const hasVisibleAssistantText = (message: any): boolean =>
|
|
1843
3221
|
message?.role === "assistant" && messageHasVisibleText(message);
|
|
1844
3222
|
|
|
3223
|
+
const closeAtAssistantBoundary = (message: any, activity: string): boolean => {
|
|
3224
|
+
const key = assistantMessageKey(message);
|
|
3225
|
+
if (assistantBoundaryKeys.has(key)) return false;
|
|
3226
|
+
assistantBoundaryKeys.add(key);
|
|
3227
|
+
cleanRun.activity = activity;
|
|
3228
|
+
finishCleanGroup(activity);
|
|
3229
|
+
return true;
|
|
3230
|
+
};
|
|
3231
|
+
|
|
1845
3232
|
const pendingToolCalls = (message: any): any[] =>
|
|
1846
3233
|
messageContentItems(message).filter(
|
|
1847
|
-
(item: any) => item.type === "toolCall" &&
|
|
3234
|
+
(item: any) => item.type === "toolCall" && item.id,
|
|
1848
3235
|
);
|
|
1849
3236
|
|
|
3237
|
+
const conciseActivityText = (value: string): string =>
|
|
3238
|
+
value
|
|
3239
|
+
.split("\n")
|
|
3240
|
+
.map((line) => line.trim())
|
|
3241
|
+
.find(Boolean)
|
|
3242
|
+
?.replace(/^#{1,6}\s+/u, "")
|
|
3243
|
+
.replace(/^\*\*(.*?)\*\*$/u, "$1") ?? "";
|
|
3244
|
+
|
|
3245
|
+
const trackThinkingActivity = (message: any): void => {
|
|
3246
|
+
if (message?.role !== "assistant") return;
|
|
3247
|
+
const thinking = thinkingText(message);
|
|
3248
|
+
if (thinking) activityTimeline.addThinking(assistantMessageKey(message), thinking);
|
|
3249
|
+
};
|
|
3250
|
+
|
|
1850
3251
|
const trackPendingToolActivity = (message: any): boolean => {
|
|
1851
3252
|
const toolCalls = pendingToolCalls(message);
|
|
1852
3253
|
if (toolCalls.length === 0) return false;
|
|
1853
3254
|
for (const toolCall of toolCalls) {
|
|
3255
|
+
activityTimeline.addTool(toolCall.id, toolCall.name ?? "tool");
|
|
1854
3256
|
if (!cleanRun.currentToolCallIds.includes(toolCall.id)) {
|
|
1855
3257
|
cleanRun.currentToolCallIds.push(toolCall.id);
|
|
1856
3258
|
}
|
|
1857
|
-
cleanToolNames.set(toolCall.id,
|
|
3259
|
+
cleanToolNames.set(toolCall.id, cleanToolNames.get(toolCall.id) ?? toolCall.name);
|
|
1858
3260
|
}
|
|
1859
3261
|
const toolCall = toolCalls[toolCalls.length - 1];
|
|
1860
3262
|
if (cleanRun.lastCompletedToolCallId === toolCall.id) return false;
|
|
1861
3263
|
setCleanGroupMembers(toolCall.id, cleanRun.currentToolCallIds.slice());
|
|
1862
3264
|
cleanRun.active = true;
|
|
1863
3265
|
cleanRun.activeToolCallId = toolCall.id;
|
|
1864
|
-
beginToolActivity(toolCall.id,
|
|
3266
|
+
beginToolActivity(toolCall.id, cleanToolNames.get(toolCall.id) ?? toolCall.name);
|
|
1865
3267
|
return true;
|
|
1866
3268
|
};
|
|
1867
3269
|
|
|
@@ -1881,30 +3283,44 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1881
3283
|
// message_start runs before Pi adds the user component to the transcript,
|
|
1882
3284
|
// so settling here keeps the previous parent row above that message.
|
|
1883
3285
|
pi.on("message_start", (event) => {
|
|
3286
|
+
if (event.message.role === "custom" && event.message.display !== false) {
|
|
3287
|
+
addPersistentActivityUpdate(event.message);
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
1884
3290
|
if (event.message.role !== "user") return;
|
|
1885
3291
|
finishCleanGroup("done");
|
|
1886
3292
|
settleLastCleanGroup();
|
|
1887
3293
|
});
|
|
1888
3294
|
|
|
1889
|
-
// A visible assistant response is the boundary between tool groups.
|
|
1890
|
-
//
|
|
1891
|
-
//
|
|
3295
|
+
// A visible assistant response is the boundary between tool groups. Seal
|
|
3296
|
+
// the prior group directly as Done; the response text already communicates
|
|
3297
|
+
// current activity and does not need a duplicate response status.
|
|
1892
3298
|
pi.on("message_update", (event) => {
|
|
1893
3299
|
if (hasVisibleAssistantText(event.message)) {
|
|
1894
|
-
|
|
1895
|
-
|
|
3300
|
+
trackThinkingActivity(event.message);
|
|
3301
|
+
clearToolActivityHolds();
|
|
3302
|
+
closeAtAssistantBoundary(event.message, "done");
|
|
1896
3303
|
return;
|
|
1897
3304
|
}
|
|
3305
|
+
trackThinkingActivity(event.message);
|
|
1898
3306
|
trackPendingToolActivity(event.message);
|
|
1899
3307
|
});
|
|
1900
3308
|
pi.on("message_end", (event) => {
|
|
1901
3309
|
if (hasVisibleAssistantText(event.message)) {
|
|
3310
|
+
trackThinkingActivity(event.message);
|
|
3311
|
+
closeAtAssistantBoundary(event.message, "done");
|
|
1902
3312
|
cleanRun.activity = "done";
|
|
1903
|
-
finishCleanGroup("done");
|
|
1904
3313
|
settleLastCleanGroup();
|
|
1905
3314
|
return;
|
|
1906
3315
|
}
|
|
3316
|
+
trackThinkingActivity(event.message);
|
|
1907
3317
|
trackPendingToolActivity(event.message);
|
|
3318
|
+
if (assistantTerminalState(event.message)) {
|
|
3319
|
+
clearToolActivityHolds();
|
|
3320
|
+
finishCleanGroup("done");
|
|
3321
|
+
settleLastCleanGroup();
|
|
3322
|
+
cleanRun.activity = "done";
|
|
3323
|
+
}
|
|
1908
3324
|
});
|
|
1909
3325
|
|
|
1910
3326
|
pi.on("agent_start", () => {
|
|
@@ -1924,25 +3340,25 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1924
3340
|
cleanRun.activeToolCallIds.clear();
|
|
1925
3341
|
cleanRun.activeToolCallId = undefined;
|
|
1926
3342
|
cleanRun.activeToolName = undefined;
|
|
1927
|
-
cleanRun.activity = "thinking
|
|
3343
|
+
cleanRun.activity = "thinking";
|
|
1928
3344
|
});
|
|
1929
3345
|
pi.on("tool_execution_start", (event) => {
|
|
1930
|
-
|
|
3346
|
+
activityTimeline.addTool(event.toolCallId, event.toolName);
|
|
1931
3347
|
if (!cleanRun.currentToolCallIds.includes(event.toolCallId)) {
|
|
1932
3348
|
cleanRun.currentToolCallIds.push(event.toolCallId);
|
|
1933
3349
|
}
|
|
1934
3350
|
cleanRun.activeToolCallIds.add(event.toolCallId);
|
|
1935
|
-
cleanToolNames.
|
|
3351
|
+
const displayName = cleanToolNames.get(event.toolCallId) ?? event.toolName;
|
|
3352
|
+
cleanToolNames.set(event.toolCallId, displayName);
|
|
1936
3353
|
setCleanGroupMembers(event.toolCallId, cleanRun.currentToolCallIds.slice());
|
|
1937
3354
|
cleanRun.active = true;
|
|
1938
3355
|
cleanRun.activeToolCallId = event.toolCallId;
|
|
1939
|
-
beginToolActivity(event.toolCallId,
|
|
3356
|
+
beginToolActivity(event.toolCallId, displayName, true);
|
|
1940
3357
|
});
|
|
1941
3358
|
pi.on("tool_execution_end", (event) => {
|
|
1942
|
-
if (!supportedTools.has(event.toolName)) return;
|
|
1943
3359
|
const activityName = cleanRun.activeToolCallId === event.toolCallId
|
|
1944
|
-
? cleanRun.activeToolName ??
|
|
1945
|
-
:
|
|
3360
|
+
? cleanRun.activeToolName ?? cleanToolNames.get(event.toolCallId) ?? event.toolName
|
|
3361
|
+
: cleanToolNames.get(event.toolCallId) ?? event.toolName;
|
|
1946
3362
|
// Keep the tool activity visible until at least one second after start;
|
|
1947
3363
|
// this only delays the status transition, never the tool result itself.
|
|
1948
3364
|
holdToolActivity(event.toolCallId, activityName);
|
|
@@ -1957,10 +3373,12 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1957
3373
|
cleanRun.activeToolName = cleanRun.activeToolCallId
|
|
1958
3374
|
? cleanToolNames.get(cleanRun.activeToolCallId)
|
|
1959
3375
|
: undefined;
|
|
1960
|
-
cleanRun.activity = cleanRun.activeToolName ?? "thinking
|
|
3376
|
+
cleanRun.activity = cleanRun.activeToolName ?? "thinking";
|
|
1961
3377
|
}
|
|
1962
3378
|
const visibleSummaryToolCallId = cleanRun.activeToolCallId ?? cleanRun.lastCompletedToolCallId;
|
|
1963
3379
|
setCleanGroupMembers(visibleSummaryToolCallId, cleanRun.currentToolCallIds.slice());
|
|
3380
|
+
// Tool success and failure remain owned by the activity group. The bottom
|
|
3381
|
+
// line is reserved for transient UI status messages.
|
|
1964
3382
|
});
|
|
1965
3383
|
pi.on("agent_end", () => {
|
|
1966
3384
|
// No summary is appended here: this event may be followed by an automatic
|
|
@@ -1969,7 +3387,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1969
3387
|
cleanRun.activeToolCallIds.clear();
|
|
1970
3388
|
cleanRun.activeToolCallId = undefined;
|
|
1971
3389
|
cleanRun.activeToolName = undefined;
|
|
1972
|
-
cleanRun.activity = "thinking
|
|
3390
|
+
cleanRun.activity = "thinking";
|
|
1973
3391
|
});
|
|
1974
3392
|
pi.on("agent_settled", () => {
|
|
1975
3393
|
if (cleanRun.settled) return;
|
|
@@ -1982,6 +3400,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1982
3400
|
settledSummaries.set(group.lastToolCallId, {
|
|
1983
3401
|
count: group.count,
|
|
1984
3402
|
failed: group.failed,
|
|
3403
|
+
thoughtCount: group.thoughtCount ?? 0,
|
|
1985
3404
|
activity: activityValueForHold(group.lastToolCallId, "done"),
|
|
1986
3405
|
});
|
|
1987
3406
|
}
|
|
@@ -1997,14 +3416,17 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1997
3416
|
|
|
1998
3417
|
const count = groups.reduce((total, group) => total + group.count, 0);
|
|
1999
3418
|
const failed = groups.reduce((total, group) => total + group.failed, 0);
|
|
3419
|
+
const thoughtCount = groups.reduce((total, group) => total + (group.thoughtCount ?? 0), 0);
|
|
2000
3420
|
const lastToolCallId = groups[groups.length - 1]?.lastToolCallId;
|
|
2001
3421
|
pi.appendEntry<ToolSummaryData>("pretty-tui-tool-summary", {
|
|
2002
3422
|
count,
|
|
2003
3423
|
failed,
|
|
3424
|
+
thoughtCount,
|
|
2004
3425
|
lastToolCallId,
|
|
2005
|
-
groups: groups.map(({ count, failed, lastToolCallId, toolCallIds }) => ({
|
|
3426
|
+
groups: groups.map(({ count, failed, thoughtCount, lastToolCallId, toolCallIds }) => ({
|
|
2006
3427
|
count,
|
|
2007
3428
|
failed,
|
|
3429
|
+
thoughtCount,
|
|
2008
3430
|
lastToolCallId,
|
|
2009
3431
|
toolCallIds,
|
|
2010
3432
|
})),
|
|
@@ -2012,22 +3434,23 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2012
3434
|
});
|
|
2013
3435
|
|
|
2014
3436
|
const read = createReadTool(cwd);
|
|
3437
|
+
const readLabel = read.label || read.name;
|
|
2015
3438
|
pi.registerTool({
|
|
2016
3439
|
...read,
|
|
2017
3440
|
renderShell: "self",
|
|
2018
3441
|
renderCall(args: any, theme: any, context: any) {
|
|
2019
|
-
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme,
|
|
3442
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, readLabel, context.toolCallId);
|
|
2020
3443
|
const range = args.offset || args.limit
|
|
2021
3444
|
? ` · lines ${args.offset ?? 1}${args.limit ? `–${(args.offset ?? 1) + args.limit - 1}` : "+"}`
|
|
2022
3445
|
: "";
|
|
2023
|
-
return call(theme,
|
|
3446
|
+
return call(theme, readLabel, `${args.path}${range}`, context.state);
|
|
2024
3447
|
},
|
|
2025
3448
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
2026
3449
|
const output = textContent(toolResult);
|
|
2027
3450
|
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
2028
3451
|
if (options.isPartial) return partialResult(context, theme, "Reading…");
|
|
2029
3452
|
const image = toolResult.content?.find((item: any) => item.type === "image");
|
|
2030
|
-
const failed = completeStatus(context
|
|
3453
|
+
const failed = completeStatus(context);
|
|
2031
3454
|
if (image) return result(theme, "Read image", "", false, failed);
|
|
2032
3455
|
const lines = nonEmptyLines(output);
|
|
2033
3456
|
const truncated = toolResult.details?.truncation?.truncated ? " · truncated" : "";
|
|
@@ -2042,11 +3465,12 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2042
3465
|
} as any);
|
|
2043
3466
|
|
|
2044
3467
|
const bash = createBashTool(cwd);
|
|
3468
|
+
const bashLabel = bash.label || bash.name;
|
|
2045
3469
|
pi.registerTool({
|
|
2046
3470
|
...bash,
|
|
2047
3471
|
renderShell: "self",
|
|
2048
3472
|
renderCall(args: any, theme: any, context: any) {
|
|
2049
|
-
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme,
|
|
3473
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, bashLabel, context.toolCallId);
|
|
2050
3474
|
const command = typeof args.command === "string" ? args.command : "";
|
|
2051
3475
|
if (useCompactToolView(context.toolCallId, context.expanded)) {
|
|
2052
3476
|
const commandLines = command.split(/\r\n|\r|\n/);
|
|
@@ -2055,16 +3479,16 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2055
3479
|
const detail = omitted > 0
|
|
2056
3480
|
? `${firstLine} … (${omitted} more ${omitted === 1 ? "line" : "lines"})`
|
|
2057
3481
|
: firstLine;
|
|
2058
|
-
return call(theme,
|
|
3482
|
+
return call(theme, bashLabel, detail, context.state);
|
|
2059
3483
|
}
|
|
2060
|
-
return call(theme,
|
|
3484
|
+
return call(theme, bashLabel, command, context.state);
|
|
2061
3485
|
},
|
|
2062
3486
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
2063
3487
|
const output = textContent(toolResult);
|
|
2064
3488
|
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
2065
3489
|
if (useCompactToolView(context.toolCallId, options.expanded)) {
|
|
2066
3490
|
if (options.isPartial) return partialResult(context, theme, "Running…");
|
|
2067
|
-
const failed = completeStatus(context
|
|
3491
|
+
const failed = completeStatus(context);
|
|
2068
3492
|
return result(theme, failed ? "Command failed" : "Done", "", false, failed);
|
|
2069
3493
|
}
|
|
2070
3494
|
|
|
@@ -2072,7 +3496,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2072
3496
|
return bashResult(context, theme, "Running…", output, options.expanded, "running");
|
|
2073
3497
|
}
|
|
2074
3498
|
|
|
2075
|
-
const failed = isError(context
|
|
3499
|
+
const failed = isError(context);
|
|
2076
3500
|
const outputLines = terminalOutputLines(output);
|
|
2077
3501
|
const lineCount = outputLines.filter((line) => line.length > 0).length;
|
|
2078
3502
|
const summary = failed
|
|
@@ -2092,19 +3516,20 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2092
3516
|
} as any);
|
|
2093
3517
|
|
|
2094
3518
|
const edit = createEditTool(cwd);
|
|
3519
|
+
const editLabel = edit.label || edit.name;
|
|
2095
3520
|
pi.registerTool({
|
|
2096
3521
|
...edit,
|
|
2097
3522
|
renderShell: "self",
|
|
2098
3523
|
renderCall(args: any, theme: any, context: any) {
|
|
2099
|
-
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme,
|
|
3524
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, editLabel, context.toolCallId);
|
|
2100
3525
|
const count = Array.isArray(args.edits) ? ` · ${args.edits.length} changes` : "";
|
|
2101
|
-
return call(theme,
|
|
3526
|
+
return call(theme, editLabel, `${args.path}${count}`, context.state);
|
|
2102
3527
|
},
|
|
2103
3528
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
2104
3529
|
const output = textContent(toolResult);
|
|
2105
3530
|
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
2106
3531
|
if (options.isPartial) return partialResult(context, theme, "Editing…");
|
|
2107
|
-
const failed = completeStatus(context
|
|
3532
|
+
const failed = completeStatus(context);
|
|
2108
3533
|
const diff = toolResult.details?.diff ?? "";
|
|
2109
3534
|
const additions = diff.split("\n").filter((line: string) => line.startsWith("+") && !line.startsWith("+++")).length;
|
|
2110
3535
|
const removals = diff.split("\n").filter((line: string) => line.startsWith("-") && !line.startsWith("---")).length;
|
|
@@ -2133,26 +3558,27 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2133
3558
|
} as any);
|
|
2134
3559
|
|
|
2135
3560
|
const write = createWriteTool(cwd);
|
|
3561
|
+
const writeLabel = write.label || write.name;
|
|
2136
3562
|
pi.registerTool({
|
|
2137
3563
|
...write,
|
|
2138
3564
|
renderShell: "self",
|
|
2139
3565
|
renderCall(args: any, theme: any, context: any) {
|
|
2140
|
-
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme,
|
|
3566
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, writeLabel, context.toolCallId);
|
|
2141
3567
|
const content = typeof args.content === "string" ? args.content : "";
|
|
2142
3568
|
const path = String(args.path ?? "");
|
|
2143
3569
|
if (useCompactToolView(context.toolCallId, context.expanded)) {
|
|
2144
3570
|
const lines = content.replace(/\t/g, " ").split("\n");
|
|
2145
3571
|
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
2146
3572
|
const total = lines.length;
|
|
2147
|
-
return call(theme,
|
|
3573
|
+
return call(theme, writeLabel, `${path} · ${total} ${total === 1 ? "line" : "lines"}`, context.state);
|
|
2148
3574
|
}
|
|
2149
|
-
return writeCall(theme, path, content, context.expanded, context.state);
|
|
3575
|
+
return writeCall(theme, writeLabel, path, content, context.expanded, context.state);
|
|
2150
3576
|
},
|
|
2151
3577
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
2152
3578
|
const output = textContent(toolResult);
|
|
2153
3579
|
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
2154
3580
|
if (options.isPartial) return partialResult(context, theme, "Writing…");
|
|
2155
|
-
const failed = completeStatus(context
|
|
3581
|
+
const failed = completeStatus(context);
|
|
2156
3582
|
return result(
|
|
2157
3583
|
theme,
|
|
2158
3584
|
failed ? output.split("\n")[0] || "Write failed" : "Written",
|
|
@@ -2164,56 +3590,59 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2164
3590
|
} as any);
|
|
2165
3591
|
|
|
2166
3592
|
const grep = createGrepTool(cwd);
|
|
3593
|
+
const grepLabel = grep.label || grep.name;
|
|
2167
3594
|
pi.registerTool({
|
|
2168
3595
|
...grep,
|
|
2169
3596
|
renderShell: "self",
|
|
2170
3597
|
renderCall(args: any, theme: any, context: any) {
|
|
2171
|
-
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme,
|
|
3598
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, grepLabel, context.toolCallId);
|
|
2172
3599
|
const where = args.path ? ` · ${args.path}` : "";
|
|
2173
3600
|
const glob = args.glob ? ` · ${args.glob}` : "";
|
|
2174
|
-
return call(theme,
|
|
3601
|
+
return call(theme, grepLabel, `${args.pattern}${where}${glob}`, context.state);
|
|
2175
3602
|
},
|
|
2176
3603
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
2177
3604
|
const output = textContent(toolResult);
|
|
2178
3605
|
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
2179
3606
|
if (options.isPartial) return partialResult(context, theme, "Searching…");
|
|
2180
|
-
const failed = completeStatus(context
|
|
3607
|
+
const failed = completeStatus(context);
|
|
2181
3608
|
const matches = nonEmptyLines(output);
|
|
2182
3609
|
return result(theme, failed ? output.split("\n")[0] : `Found ${matches} matches`, output, options.expanded, failed);
|
|
2183
3610
|
},
|
|
2184
3611
|
} as any);
|
|
2185
3612
|
|
|
2186
3613
|
const find = createFindTool(cwd);
|
|
3614
|
+
const findLabel = find.label || find.name;
|
|
2187
3615
|
pi.registerTool({
|
|
2188
3616
|
...find,
|
|
2189
3617
|
renderShell: "self",
|
|
2190
3618
|
renderCall(args: any, theme: any, context: any) {
|
|
2191
|
-
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme,
|
|
2192
|
-
return call(theme,
|
|
3619
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, findLabel, context.toolCallId);
|
|
3620
|
+
return call(theme, findLabel, `${args.pattern}${args.path ? ` · ${args.path}` : ""}`, context.state);
|
|
2193
3621
|
},
|
|
2194
3622
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
2195
3623
|
const output = textContent(toolResult);
|
|
2196
3624
|
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
2197
3625
|
if (options.isPartial) return partialResult(context, theme, "Searching…");
|
|
2198
|
-
const failed = completeStatus(context
|
|
3626
|
+
const failed = completeStatus(context);
|
|
2199
3627
|
const matches = nonEmptyLines(output);
|
|
2200
3628
|
return result(theme, failed ? output.split("\n")[0] : `Found ${matches} paths`, output, options.expanded, failed);
|
|
2201
3629
|
},
|
|
2202
3630
|
} as any);
|
|
2203
3631
|
|
|
2204
3632
|
const ls = createLsTool(cwd);
|
|
3633
|
+
const lsLabel = ls.label || ls.name;
|
|
2205
3634
|
pi.registerTool({
|
|
2206
3635
|
...ls,
|
|
2207
3636
|
renderShell: "self",
|
|
2208
3637
|
renderCall(args: any, theme: any, context: any) {
|
|
2209
|
-
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme,
|
|
2210
|
-
return call(theme,
|
|
3638
|
+
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, lsLabel, context.toolCallId);
|
|
3639
|
+
return call(theme, lsLabel, args.path ?? ".", context.state);
|
|
2211
3640
|
},
|
|
2212
3641
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
2213
3642
|
const output = textContent(toolResult);
|
|
2214
3643
|
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
2215
3644
|
if (options.isPartial) return partialResult(context, theme, "Listing…");
|
|
2216
|
-
const failed = completeStatus(context
|
|
3645
|
+
const failed = completeStatus(context);
|
|
2217
3646
|
const entries = nonEmptyLines(output);
|
|
2218
3647
|
return result(theme, failed ? output.split("\n")[0] : `Listed ${entries} entries`, output, options.expanded, failed);
|
|
2219
3648
|
},
|