pi-pretty-tui 0.2.0 → 0.2.2
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 +12 -6
- package/README.zh-CN.md +12 -6
- package/extensions/activity-timeline.ts +77 -23
- package/extensions/index.ts +755 -265
- package/package.json +1 -1
package/extensions/index.ts
CHANGED
|
@@ -32,15 +32,18 @@ import { join } from "node:path";
|
|
|
32
32
|
import {
|
|
33
33
|
ActivityTimeline,
|
|
34
34
|
assistantMessageKey,
|
|
35
|
-
|
|
35
|
+
assistantTerminalState,
|
|
36
36
|
thinkingText,
|
|
37
37
|
visibleAssistantText,
|
|
38
38
|
type ActivityGroup,
|
|
39
39
|
type ActivityMember,
|
|
40
|
+
type ActivitySeverity,
|
|
40
41
|
} from "./activity-timeline.js";
|
|
41
42
|
|
|
42
43
|
type PrettyTuiMode = "full" | "compact" | "clean";
|
|
43
44
|
const CLEAN_TOOL_ACTIVITY_MIN_MS = 1000;
|
|
45
|
+
const TRANSIENT_UI_STATUS_MS = 10_000;
|
|
46
|
+
const RESPONSE_FOOTER_ENTRY_TYPE = "pretty-tui-response-footer";
|
|
44
47
|
const SPECIALIZED_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
|
|
45
48
|
|
|
46
49
|
const stripAnsiBackgrounds = (value: string): string =>
|
|
@@ -95,14 +98,131 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
95
98
|
let currentTui: any;
|
|
96
99
|
let currentExtensionUi: any;
|
|
97
100
|
let changingAllToolsExpansion = false;
|
|
101
|
+
const latestActivityWidgetKey = "pretty-tui-latest-activity";
|
|
102
|
+
type LatestActivityStatus = {
|
|
103
|
+
title: string;
|
|
104
|
+
detail?: string;
|
|
105
|
+
severity?: TransientStatusSeverity;
|
|
106
|
+
};
|
|
107
|
+
// The editor-adjacent flash reports Pi UI state ("status") and pretty-tui's
|
|
108
|
+
// own UI outcomes, so it has its own severities. Activity members only ever
|
|
109
|
+
// carry info, warning, or error.
|
|
110
|
+
type TransientStatusSeverity = "status" | "success" | "error";
|
|
111
|
+
let latestActivityStatus: LatestActivityStatus | undefined;
|
|
112
|
+
let latestActivityWidgetInstalled = false;
|
|
113
|
+
let latestActivityStatusTimer: ReturnType<typeof setTimeout> | undefined;
|
|
114
|
+
let latestActivityStatusGeneration = 0;
|
|
115
|
+
const renderLatestActivityStatus = (status: LatestActivityStatus, width: number, theme: any): string => {
|
|
116
|
+
const severity = status.severity ?? "status";
|
|
117
|
+
const icon = severity === "error" ? "✕" : severity === "success" ? "✓" : "◇";
|
|
118
|
+
const color = severity === "error"
|
|
119
|
+
? "error"
|
|
120
|
+
: severity === "success"
|
|
121
|
+
? "success"
|
|
122
|
+
: "dim";
|
|
123
|
+
const detail = status.detail?.trim() ? ` — ${status.detail.trim()}` : "";
|
|
124
|
+
return truncateToWidth(` ${theme.fg(color, `${icon} ${status.title}`)}${theme.fg("dim", detail)}`, Math.max(1, width), "…");
|
|
125
|
+
};
|
|
126
|
+
const refreshLatestActivityWidget = () => {
|
|
127
|
+
if (!currentExtensionUi?.setWidget) return;
|
|
128
|
+
if (renderMode !== "clean" || !latestActivityStatus) {
|
|
129
|
+
if (latestActivityWidgetInstalled) {
|
|
130
|
+
currentExtensionUi.setWidget(latestActivityWidgetKey, undefined);
|
|
131
|
+
latestActivityWidgetInstalled = false;
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (latestActivityWidgetInstalled) {
|
|
136
|
+
currentTui?.requestRender?.();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
currentExtensionUi.setWidget(
|
|
140
|
+
latestActivityWidgetKey,
|
|
141
|
+
(_tui: any, theme: any) => ({
|
|
142
|
+
render: (width: number) => latestActivityStatus
|
|
143
|
+
? [renderLatestActivityStatus(latestActivityStatus, width, theme)]
|
|
144
|
+
: [],
|
|
145
|
+
invalidate() {},
|
|
146
|
+
}),
|
|
147
|
+
{ placement: "aboveEditor" },
|
|
148
|
+
);
|
|
149
|
+
latestActivityWidgetInstalled = true;
|
|
150
|
+
};
|
|
151
|
+
const clearLatestActivityStatus = () => {
|
|
152
|
+
latestActivityStatusGeneration++;
|
|
153
|
+
if (latestActivityStatusTimer) clearTimeout(latestActivityStatusTimer);
|
|
154
|
+
latestActivityStatusTimer = undefined;
|
|
155
|
+
latestActivityStatus = undefined;
|
|
156
|
+
if (latestActivityWidgetInstalled) {
|
|
157
|
+
currentExtensionUi?.setWidget?.(latestActivityWidgetKey, undefined);
|
|
158
|
+
latestActivityWidgetInstalled = false;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const setLatestActivityStatus = (status: LatestActivityStatus) => {
|
|
162
|
+
const generation = ++latestActivityStatusGeneration;
|
|
163
|
+
if (latestActivityStatusTimer) clearTimeout(latestActivityStatusTimer);
|
|
164
|
+
latestActivityStatus = status;
|
|
165
|
+
refreshLatestActivityWidget();
|
|
166
|
+
latestActivityStatusTimer = setTimeout(() => {
|
|
167
|
+
if (generation !== latestActivityStatusGeneration) return;
|
|
168
|
+
clearLatestActivityStatus();
|
|
169
|
+
currentTui?.requestRender?.();
|
|
170
|
+
}, TRANSIENT_UI_STATUS_MS);
|
|
171
|
+
};
|
|
172
|
+
const copyTranscriptText = (text: string) => {
|
|
173
|
+
// Code-block copy must preserve leading/trailing whitespace and indentation.
|
|
174
|
+
// Answers are normalized separately by assistantAnswerText().
|
|
175
|
+
if (!text.trim()) return;
|
|
176
|
+
const copyText = text;
|
|
177
|
+
if (typeof currentTui?.copyTextToClipboard === "function") {
|
|
178
|
+
// Fullscreen uses Pi's native clipboard path and native flash feedback.
|
|
179
|
+
void Promise.resolve(currentTui.copyTextToClipboard(copyText)).catch(() => {
|
|
180
|
+
currentTui?.flash?.("Copy failed");
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
void copyToClipboard(copyText)
|
|
185
|
+
.then(() => setLatestActivityStatus({ title: "Copied!", severity: "success" }))
|
|
186
|
+
.catch(() => setLatestActivityStatus({ title: "Copy failed", severity: "error" }));
|
|
187
|
+
};
|
|
188
|
+
type ResponseFooterData = {
|
|
189
|
+
answerEntryId?: string;
|
|
190
|
+
durationMs: number;
|
|
191
|
+
outcome: "completed" | "stopped";
|
|
192
|
+
};
|
|
193
|
+
const responseAnswerTexts = new Map<string, string>();
|
|
194
|
+
let responseRunStartedAt: number | undefined;
|
|
195
|
+
let responseHasAssistantMessage = false;
|
|
196
|
+
const assistantAnswerText = (message: any): string => {
|
|
197
|
+
if (typeof message?.content === "string") return message.content.trim();
|
|
198
|
+
if (!Array.isArray(message?.content)) return "";
|
|
199
|
+
return message.content
|
|
200
|
+
.filter((item: any) => item?.type === "text" && typeof item.text === "string" && item.text.trim())
|
|
201
|
+
.map((item: any) => item.text.trim())
|
|
202
|
+
.join("\n\n")
|
|
203
|
+
.trim();
|
|
204
|
+
};
|
|
205
|
+
const formatResponseDuration = (durationMs: number): string => {
|
|
206
|
+
const totalSeconds = Math.max(1, Math.round(Math.max(0, durationMs) / 1000));
|
|
207
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
208
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
209
|
+
const seconds = totalSeconds % 60;
|
|
210
|
+
if (totalMinutes < 60) return `${totalMinutes}m ${seconds}s`;
|
|
211
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
212
|
+
const minutes = totalMinutes % 60;
|
|
213
|
+
return `${hours}h ${String(minutes).padStart(2, "0")}m`;
|
|
214
|
+
};
|
|
98
215
|
const activityTimeline = new ActivityTimeline();
|
|
99
216
|
const revealedActivityGroups = new Set<string>();
|
|
100
217
|
const expandedThinkingMembers = new Set<string>();
|
|
101
218
|
const thinkingComponents = new Map<string, any>();
|
|
102
219
|
const activityFallbackThemes = new Map<string, any>();
|
|
103
220
|
const activityUpdateComponents = new Map<string, Component>();
|
|
104
|
-
const
|
|
105
|
-
|
|
221
|
+
const activityUpdateMarkdownComponents = new Map<string, {
|
|
222
|
+
content: string;
|
|
223
|
+
theme: any;
|
|
224
|
+
component: Markdown;
|
|
225
|
+
}>();
|
|
106
226
|
let activityUpdateSequence = 0;
|
|
107
227
|
const assistantBoundaryKeys = new Set<string>();
|
|
108
228
|
const cleanCompactToolCallIds = new Set<string>();
|
|
@@ -113,6 +233,20 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
113
233
|
const cleanToolNames = new Map<string, string>();
|
|
114
234
|
let renderCleanGroupSummary = (_lastToolCallId: string, _width: number): string[] => [];
|
|
115
235
|
|
|
236
|
+
// Markdown enhancements are active only while pi-pretty-tui renders the main
|
|
237
|
+
// conversation transcript. Plugin overlays also use Pi's Markdown class, so
|
|
238
|
+
// a global always-on prototype patch would leak transcript styling into them.
|
|
239
|
+
let transcriptMarkdownDepth = 0;
|
|
240
|
+
const withTranscriptMarkdown = <T>(callback: () => T): T => {
|
|
241
|
+
transcriptMarkdownDepth++;
|
|
242
|
+
try {
|
|
243
|
+
return callback();
|
|
244
|
+
} finally {
|
|
245
|
+
transcriptMarkdownDepth--;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
const transcriptMarkdownActive = () => transcriptMarkdownDepth > 0;
|
|
249
|
+
|
|
116
250
|
const setCleanGroupMembers = (lastToolCallId: string, toolCallIds: string[]) => {
|
|
117
251
|
cleanGroupToolCallIds.set(lastToolCallId, toolCallIds);
|
|
118
252
|
for (const toolCallId of toolCallIds) cleanToolCallGroupOwners.set(toolCallId, lastToolCallId);
|
|
@@ -167,11 +301,17 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
167
301
|
};
|
|
168
302
|
|
|
169
303
|
const defaultActivityTheme = () => {
|
|
304
|
+
if (currentExtensionUi?.theme?.fg) return currentExtensionUi.theme;
|
|
170
305
|
const markdownTheme = getMarkdownTheme();
|
|
171
306
|
return {
|
|
172
307
|
fg: (color: string, text: string) => {
|
|
173
308
|
if (color === "accent") return markdownTheme.listBullet(text);
|
|
174
|
-
|
|
309
|
+
// Session UI normally supplies Pi's active Theme above. Keep portable
|
|
310
|
+
// semantic fallbacks for early startup and isolated renderer tests.
|
|
311
|
+
if (color === "warning") return `\x1b[33m${text}\x1b[39m`;
|
|
312
|
+
if (color === "error") return `\x1b[31m${text}\x1b[39m`;
|
|
313
|
+
if (color === "success") return `\x1b[32m${text}\x1b[39m`;
|
|
314
|
+
if (color === "syntaxComment") return markdownTheme.codeBlock(text);
|
|
175
315
|
if (color === "thinkingLow") return markdownTheme.link(text);
|
|
176
316
|
if (color === "dim" || color === "muted" || color === "thinkingText") {
|
|
177
317
|
return markdownTheme.quote(text);
|
|
@@ -204,6 +344,12 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
204
344
|
const activityGroupRevealed = (group: ActivityGroup): boolean =>
|
|
205
345
|
cleanToolsExpanded || revealedActivityGroups.has(group.id);
|
|
206
346
|
|
|
347
|
+
// A group is revealable once it owns anything renderable beneath its parent
|
|
348
|
+
// row. Thinking counts on its own: a tool-free turn still gets a parent row
|
|
349
|
+
// with its Thought as the child member.
|
|
350
|
+
const activityGroupCollapsible = (group: ActivityGroup): boolean =>
|
|
351
|
+
activityTimeline.hasWork(group);
|
|
352
|
+
|
|
207
353
|
const activityMemberPosition = (group: ActivityGroup, member: ActivityMember) => ({
|
|
208
354
|
first: group.members[0]?.id === member.id,
|
|
209
355
|
last: group.members[group.members.length - 1]?.id === member.id,
|
|
@@ -238,6 +384,50 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
238
384
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
239
385
|
.join(" ") || "Update";
|
|
240
386
|
|
|
387
|
+
const renderActivityUpdateMarkdown = (
|
|
388
|
+
member: ActivityMember,
|
|
389
|
+
width: number,
|
|
390
|
+
theme: any,
|
|
391
|
+
): string[] => {
|
|
392
|
+
const content = member.updateContent ?? "";
|
|
393
|
+
if (!content) return [];
|
|
394
|
+
let cached = activityUpdateMarkdownComponents.get(member.id);
|
|
395
|
+
if (!cached || cached.content !== content || cached.theme !== theme) {
|
|
396
|
+
cached = {
|
|
397
|
+
content,
|
|
398
|
+
theme,
|
|
399
|
+
component: new Markdown(
|
|
400
|
+
content,
|
|
401
|
+
0,
|
|
402
|
+
0,
|
|
403
|
+
getMarkdownTheme(),
|
|
404
|
+
{ color: (text: string) => theme.fg("muted", text) },
|
|
405
|
+
),
|
|
406
|
+
};
|
|
407
|
+
activityUpdateMarkdownComponents.set(member.id, cached);
|
|
408
|
+
}
|
|
409
|
+
const lines = withTranscriptMarkdown(() => cached.component.render(Math.max(1, width)));
|
|
410
|
+
while (lines.length > 0 && stripTerminalSequences(lines[lines.length - 1]).trim() === "") {
|
|
411
|
+
lines.pop();
|
|
412
|
+
}
|
|
413
|
+
return lines;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const activityUpdateStyle = (member: ActivityMember, theme: any) => {
|
|
417
|
+
const severity = member.updateSeverity ?? "info";
|
|
418
|
+
const styles: Record<ActivitySeverity, { icon: string; color: string }> = {
|
|
419
|
+
info: { icon: "◇", color: "accent" },
|
|
420
|
+
warning: { icon: "⚠", color: "warning" },
|
|
421
|
+
error: { icon: "✕", color: "error" },
|
|
422
|
+
};
|
|
423
|
+
const style = styles[severity];
|
|
424
|
+
return {
|
|
425
|
+
severity,
|
|
426
|
+
icon: theme.fg(style.color, `${style.icon} `),
|
|
427
|
+
title: (text: string) => theme.fg(style.color, theme.bold(text)),
|
|
428
|
+
};
|
|
429
|
+
};
|
|
430
|
+
|
|
241
431
|
const renderActivityUpdate = (
|
|
242
432
|
group: ActivityGroup,
|
|
243
433
|
member: ActivityMember,
|
|
@@ -247,10 +437,11 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
247
437
|
if (renderMode !== "clean" || !activityGroupRevealed(group)) return [];
|
|
248
438
|
const { prefix, continuation, childWidth } = activityTreeStyle(group, member, width, theme);
|
|
249
439
|
const title = truncateToWidth(member.updateTitle ?? "Update", Math.max(1, childWidth - 2), "…");
|
|
250
|
-
const
|
|
440
|
+
const style = activityUpdateStyle(member, theme);
|
|
441
|
+
const lines = [style.icon + style.title(title)];
|
|
251
442
|
if (member.updateContent) {
|
|
252
443
|
const detailWidth = Math.max(1, childWidth - visibleWidth(" │ "));
|
|
253
|
-
const detailLines =
|
|
444
|
+
const detailLines = renderActivityUpdateMarkdown(member, detailWidth, theme);
|
|
254
445
|
lines.push(...detailLines.map((line, index) =>
|
|
255
446
|
theme.fg("dim", index === detailLines.length - 1 ? " └ " : " │ ") + line
|
|
256
447
|
));
|
|
@@ -267,10 +458,11 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
267
458
|
const renderPendingActivityUpdate = (member: ActivityMember, width: number): string[] => {
|
|
268
459
|
const theme = defaultActivityTheme();
|
|
269
460
|
const title = truncateToWidth(member.updateTitle ?? "Update", Math.max(1, width - 2), "…");
|
|
270
|
-
const
|
|
461
|
+
const style = activityUpdateStyle(member, theme);
|
|
462
|
+
const lines = [style.icon + style.title(title)];
|
|
271
463
|
if (member.updateContent) {
|
|
272
464
|
const detailWidth = Math.max(1, width - visibleWidth(" │ "));
|
|
273
|
-
const details =
|
|
465
|
+
const details = renderActivityUpdateMarkdown(member, detailWidth, theme);
|
|
274
466
|
lines.push(...details.map((line, index) =>
|
|
275
467
|
theme.fg("dim", index === details.length - 1 ? " └ " : " │ ") + line
|
|
276
468
|
));
|
|
@@ -278,13 +470,28 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
278
470
|
return ["", ...lines.map((line) => truncateToWidth(line, Math.max(1, width), ""))];
|
|
279
471
|
};
|
|
280
472
|
|
|
473
|
+
const renderActivityUpdateProjection = (
|
|
474
|
+
group: ActivityGroup,
|
|
475
|
+
member: ActivityMember,
|
|
476
|
+
width: number,
|
|
477
|
+
): string[] => {
|
|
478
|
+
if (!activityGroupCollapsible(group)) return renderPendingActivityUpdate(member, width);
|
|
479
|
+
const position = activityMemberPosition(group, member);
|
|
480
|
+
if (!activityGroupRevealed(group)) {
|
|
481
|
+
return position.first
|
|
482
|
+
? ["", ...renderActivityGroupSummary(group, width, true)]
|
|
483
|
+
: [];
|
|
484
|
+
}
|
|
485
|
+
return renderActivityUpdate(group, member, width);
|
|
486
|
+
};
|
|
487
|
+
|
|
281
488
|
const handleActivityUpdateMouse = (
|
|
282
489
|
group: ActivityGroup,
|
|
283
490
|
member: ActivityMember,
|
|
284
491
|
event: any,
|
|
285
492
|
) => {
|
|
286
493
|
const isLeftClick = event.type === "click" && event.button === "left";
|
|
287
|
-
if (!isLeftClick || group
|
|
494
|
+
if (!isLeftClick || !activityGroupCollapsible(group)) return undefined;
|
|
288
495
|
const position = activityMemberPosition(group, member);
|
|
289
496
|
if (!activityGroupRevealed(group)) {
|
|
290
497
|
if (!position.first) return undefined;
|
|
@@ -306,6 +513,8 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
306
513
|
cleanToolComponents.get(child.toolCallId)?.updateDisplay?.();
|
|
307
514
|
} else if (child.kind === "thinking" && child.messageKey) {
|
|
308
515
|
thinkingComponents.get(child.messageKey)?.invalidate?.();
|
|
516
|
+
} else if (child.kind === "update") {
|
|
517
|
+
activityUpdateComponents.get(child.id)?.invalidate?.();
|
|
309
518
|
}
|
|
310
519
|
}
|
|
311
520
|
currentTui?.requestRender?.();
|
|
@@ -399,6 +608,8 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
399
608
|
revealedActivityGroups.clear();
|
|
400
609
|
expandedThinkingMembers.clear();
|
|
401
610
|
renderMode = requested;
|
|
611
|
+
if (renderMode === "clean") refreshLatestActivityWidget();
|
|
612
|
+
else clearLatestActivityStatus();
|
|
402
613
|
for (const component of cleanToolComponents.values()) component.updateDisplay?.();
|
|
403
614
|
for (const component of thinkingComponents.values()) component.invalidate?.();
|
|
404
615
|
try {
|
|
@@ -415,20 +626,6 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
415
626
|
// render patches or override built-in tools until the next enabled reload.
|
|
416
627
|
if (!activeForSession) return;
|
|
417
628
|
|
|
418
|
-
// Markdown enhancements are active only while pi-pretty-tui renders the main
|
|
419
|
-
// conversation transcript. Plugin overlays also use Pi's Markdown class, so
|
|
420
|
-
// a global always-on prototype patch would leak transcript styling into them.
|
|
421
|
-
let transcriptMarkdownDepth = 0;
|
|
422
|
-
const withTranscriptMarkdown = <T>(callback: () => T): T => {
|
|
423
|
-
transcriptMarkdownDepth++;
|
|
424
|
-
try {
|
|
425
|
-
return callback();
|
|
426
|
-
} finally {
|
|
427
|
-
transcriptMarkdownDepth--;
|
|
428
|
-
}
|
|
429
|
-
};
|
|
430
|
-
const transcriptMarkdownActive = () => transcriptMarkdownDepth > 0;
|
|
431
|
-
|
|
432
629
|
// Give Pi's main prompt editor a complete rounded frame. Render the native
|
|
433
630
|
// editor at a two-column narrower width so cursor layout, wrapping, IME, and
|
|
434
631
|
// autocomplete remain native, then add one themed border column per side.
|
|
@@ -499,8 +696,10 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
499
696
|
});
|
|
500
697
|
}
|
|
501
698
|
|
|
502
|
-
// Render user prompts as
|
|
503
|
-
//
|
|
699
|
+
// Render user prompts as right-aligned, content-sized chat bubbles while
|
|
700
|
+
// preserving Pi's Markdown component, wrapping, output padding, and OSC 133
|
|
701
|
+
// zones. Long messages stop at 75% of the terminal width; narrow terminals
|
|
702
|
+
// trade away the left gap before sacrificing readability.
|
|
504
703
|
const userMessagePrototype = UserMessageComponent.prototype as any;
|
|
505
704
|
const userMessagePatchKey = Symbol.for("pretty-tui.user-message-frame");
|
|
506
705
|
if (!userMessagePrototype[userMessagePatchKey]) {
|
|
@@ -510,66 +709,118 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
510
709
|
const content = this.children?.[0] as any;
|
|
511
710
|
if (!content) return;
|
|
512
711
|
|
|
513
|
-
//
|
|
514
|
-
|
|
712
|
+
// Remove Pi's full-width user-message background. The Markdown keeps its
|
|
713
|
+
// native foreground styling, while the surrounding bubble stays transparent.
|
|
515
714
|
content.paddingX = 0;
|
|
516
715
|
content.paddingY = 0;
|
|
517
716
|
content.setBgFn?.(undefined);
|
|
518
717
|
content.invalidate?.();
|
|
718
|
+
// Pi wraps the Markdown in a full-width Box. Render the Markdown child
|
|
719
|
+
// directly so short messages expose their natural visible width instead
|
|
720
|
+
// of inheriting the Box's width-filling padding.
|
|
721
|
+
const markdown = content.children?.[0] ?? content;
|
|
519
722
|
|
|
520
723
|
const outputPad = Math.max(0, Number(this.outputPad) || 0);
|
|
521
724
|
const markdownTheme = this.markdownTheme;
|
|
522
|
-
|
|
725
|
+
// Markdown deliberately pads every rendered line to the requested width.
|
|
726
|
+
// Remove only that trailing layout padding before measuring or framing;
|
|
727
|
+
// ANSI styling and the visible content remain untouched.
|
|
728
|
+
const renderMarkdown = (width: number): string[] =>
|
|
729
|
+
withTranscriptMarkdown(() => markdown.render(width))
|
|
730
|
+
.map((line: string) => line.replace(/ +$/u, ""));
|
|
731
|
+
const layout = (width: number) => {
|
|
732
|
+
const terminalWidth = Math.max(1, width);
|
|
733
|
+
const rightPad = Math.min(outputPad, Math.max(0, terminalWidth - 1));
|
|
734
|
+
const availableWidth = Math.max(1, terminalWidth - rightPad);
|
|
735
|
+
if (availableWidth < 4) {
|
|
736
|
+
return {
|
|
737
|
+
framed: false,
|
|
738
|
+
rightPad,
|
|
739
|
+
leftPad: 0,
|
|
740
|
+
frameWidth: availableWidth,
|
|
741
|
+
contentWidth: availableWidth,
|
|
742
|
+
body: renderMarkdown(availableWidth),
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const preferredMaximum = Math.max(4, Math.floor(terminalWidth * 0.75));
|
|
747
|
+
// Keep at least one blank column on the left whenever a framed bubble
|
|
748
|
+
// fits, even after a narrow terminal relaxes the usual 75% maximum.
|
|
749
|
+
const bubbleAvailable = availableWidth >= 5 ? availableWidth - 1 : availableWidth;
|
|
750
|
+
const narrowReadableMinimum = Math.min(bubbleAvailable, 24);
|
|
751
|
+
const frameMaximum = Math.min(
|
|
752
|
+
bubbleAvailable,
|
|
753
|
+
Math.max(preferredMaximum, narrowReadableMinimum),
|
|
754
|
+
);
|
|
755
|
+
const measuringWidth = Math.max(1, frameMaximum - 4);
|
|
756
|
+
const measuredBody = renderMarkdown(measuringWidth);
|
|
757
|
+
const naturalContentWidth = Math.max(
|
|
758
|
+
1,
|
|
759
|
+
...measuredBody.map((line: string) => visibleWidth(line)),
|
|
760
|
+
);
|
|
761
|
+
const frameWidth = Math.min(frameMaximum, naturalContentWidth + 4);
|
|
762
|
+
const contentWidth = Math.max(1, frameWidth - 4);
|
|
763
|
+
const body = contentWidth === measuringWidth
|
|
764
|
+
? measuredBody
|
|
765
|
+
: renderMarkdown(contentWidth);
|
|
766
|
+
return {
|
|
767
|
+
framed: true,
|
|
768
|
+
rightPad,
|
|
769
|
+
leftPad: Math.max(0, terminalWidth - rightPad - frameWidth),
|
|
770
|
+
frameWidth,
|
|
771
|
+
contentWidth,
|
|
772
|
+
body,
|
|
773
|
+
};
|
|
774
|
+
};
|
|
775
|
+
const border = (text: string) => currentExtensionUi?.theme?.fg
|
|
776
|
+
? currentExtensionUi.theme.fg("accent", text)
|
|
777
|
+
: markdownTheme.quoteBorder(text);
|
|
523
778
|
const frame: Component = {
|
|
524
779
|
render(width: number): string[] {
|
|
525
|
-
const
|
|
526
|
-
const
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
return userMessageBg ? userMessageBg(line + padding) : line + padding;
|
|
531
|
-
};
|
|
532
|
-
if (frameWidth < 4) {
|
|
533
|
-
return content.render(frameWidth).map((line: string) => outer + paintBackground(line));
|
|
780
|
+
const measured = layout(width);
|
|
781
|
+
const left = " ".repeat(measured.leftPad);
|
|
782
|
+
const right = " ".repeat(measured.rightPad);
|
|
783
|
+
if (!measured.framed) {
|
|
784
|
+
return measured.body.map((line: string) => left + line + right);
|
|
534
785
|
}
|
|
535
786
|
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
const bottom = border("╰" + "─".repeat(Math.max(0, frameWidth - 2)) + "╯");
|
|
541
|
-
const contentWidth = Math.max(1, frameWidth - 4);
|
|
542
|
-
const body = withTranscriptMarkdown(() => content.render(contentWidth)).map((line: string) => {
|
|
543
|
-
const padding = " ".repeat(Math.max(0, contentWidth - visibleWidth(line)));
|
|
787
|
+
const top = border("╭" + "─".repeat(Math.max(0, measured.frameWidth - 2)) + "╮");
|
|
788
|
+
const bottom = border("╰" + "─".repeat(Math.max(0, measured.frameWidth - 2)) + "╯");
|
|
789
|
+
const body = measured.body.map((line: string) => {
|
|
790
|
+
const padding = " ".repeat(Math.max(0, measured.contentWidth - visibleWidth(line)));
|
|
544
791
|
return border("│ ") + line + padding + border(" │");
|
|
545
792
|
});
|
|
546
|
-
return [top, ...body, bottom].map((line: string) =>
|
|
793
|
+
return [top, ...body, bottom].map((line: string) => left + line + right);
|
|
547
794
|
},
|
|
548
795
|
handleMouse(event: any) {
|
|
549
|
-
if (!
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
return withTranscriptMarkdown(() => content.handleMouse({
|
|
796
|
+
if (!markdown.handleMouse) return undefined;
|
|
797
|
+
const measured = layout(event.width);
|
|
798
|
+
if (!measured.framed) {
|
|
799
|
+
return withTranscriptMarkdown(() => markdown.handleMouse({
|
|
554
800
|
...event,
|
|
555
|
-
x: event.x -
|
|
556
|
-
width:
|
|
801
|
+
x: event.x - measured.leftPad,
|
|
802
|
+
width: measured.contentWidth,
|
|
557
803
|
}));
|
|
558
804
|
}
|
|
559
|
-
const
|
|
560
|
-
if (
|
|
805
|
+
const contentX = measured.leftPad + 2;
|
|
806
|
+
if (
|
|
807
|
+
event.y <= 0 ||
|
|
808
|
+
event.y > measured.body.length ||
|
|
809
|
+
event.x < contentX ||
|
|
810
|
+
event.x >= contentX + measured.contentWidth
|
|
811
|
+
) {
|
|
561
812
|
return undefined;
|
|
562
813
|
}
|
|
563
|
-
return withTranscriptMarkdown(() =>
|
|
814
|
+
return withTranscriptMarkdown(() => markdown.handleMouse({
|
|
564
815
|
...event,
|
|
565
|
-
x: event.x -
|
|
816
|
+
x: event.x - contentX,
|
|
566
817
|
y: event.y - 1,
|
|
567
|
-
width: contentWidth,
|
|
568
|
-
height:
|
|
818
|
+
width: measured.contentWidth,
|
|
819
|
+
height: measured.body.length,
|
|
569
820
|
}));
|
|
570
821
|
},
|
|
571
822
|
invalidate() {
|
|
572
|
-
|
|
823
|
+
markdown.invalidate?.();
|
|
573
824
|
},
|
|
574
825
|
};
|
|
575
826
|
|
|
@@ -615,14 +866,31 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
615
866
|
.join("\n")
|
|
616
867
|
: "";
|
|
617
868
|
|
|
869
|
+
// Delegates to the timeline so the precondition is stated once. The `??` is
|
|
870
|
+
// deliberate: the callee may still decline, and a dropped notification must
|
|
871
|
+
// never throw inside a third-party extension's ctx.ui.notify() call.
|
|
872
|
+
const hasWorkActivity = (group: ActivityGroup | undefined): boolean =>
|
|
873
|
+
activityTimeline.hasWork(group);
|
|
874
|
+
|
|
618
875
|
const addPersistentActivityUpdate = (message: any): ActivityMember => {
|
|
619
876
|
const key = customMessageKey(message);
|
|
620
877
|
const title = humanizeCustomType(message?.customType ?? "update");
|
|
621
878
|
const content = customMessageText(message);
|
|
622
|
-
|
|
623
|
-
|
|
879
|
+
const existingGroup = activityTimeline.currentGroup();
|
|
880
|
+
const joinsWork = hasWorkActivity(existingGroup);
|
|
881
|
+
const member = joinsWork
|
|
882
|
+
? activityTimeline.addUpdate(key, title, content, true, "info") ??
|
|
883
|
+
activityTimeline.addPendingUpdate(key, title, content, true, "info")
|
|
884
|
+
: activityTimeline.addPendingUpdate(key, title, content, true, "info");
|
|
885
|
+
// A displayed custom message without current model/tool work is an
|
|
886
|
+
// independent durable update, never a pending member of the next run.
|
|
887
|
+
if (!hasWorkActivity(existingGroup)) activityTimeline.boundary();
|
|
888
|
+
return member;
|
|
624
889
|
};
|
|
625
890
|
|
|
891
|
+
const nativeCustomBoundaryKeys = new Set<string>();
|
|
892
|
+
let closeCleanAtNativeCustomBoundary = () => activityTimeline.boundary();
|
|
893
|
+
let closeCleanAtStandaloneAlert = () => activityTimeline.boundary();
|
|
626
894
|
const customMessagePrototype = CustomMessageComponent.prototype as any;
|
|
627
895
|
const customMessagePatchKey = Symbol.for("pretty-tui.clean-custom-message");
|
|
628
896
|
if (!customMessagePrototype[customMessagePatchKey]) {
|
|
@@ -631,9 +899,13 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
631
899
|
const releaseSemanticCustomMessage = (component: any): boolean => {
|
|
632
900
|
if (!component.customRenderer) return false;
|
|
633
901
|
const key = customMessageKey(component.message);
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
902
|
+
if (!nativeCustomBoundaryKeys.has(key)) {
|
|
903
|
+
nativeCustomBoundaryKeys.add(key);
|
|
904
|
+
const member = activityTimeline.memberForUpdate(key);
|
|
905
|
+
if (member) activityUpdateComponents.delete(member.id);
|
|
906
|
+
activityTimeline.removeUpdate(key);
|
|
907
|
+
closeCleanAtNativeCustomBoundary();
|
|
908
|
+
}
|
|
637
909
|
return true;
|
|
638
910
|
};
|
|
639
911
|
const patchedCustomRender = function (this: any, width: number): string[] {
|
|
@@ -643,37 +915,19 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
643
915
|
const message = this.message;
|
|
644
916
|
const key = customMessageKey(message);
|
|
645
917
|
let member = activityTimeline.memberForUpdate(key);
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const attach = customUpdateBeforeToolHints.has(key)
|
|
653
|
-
? activityTimeline.addUpdateToGroupStart.bind(activityTimeline)
|
|
654
|
-
: activityTimeline.addUpdateToGroup.bind(activityTimeline);
|
|
655
|
-
member = attach(
|
|
656
|
-
hintedGroup.id,
|
|
657
|
-
key,
|
|
658
|
-
humanizeCustomType(message?.customType ?? "update"),
|
|
659
|
-
customMessageText(message),
|
|
660
|
-
true,
|
|
661
|
-
);
|
|
662
|
-
}
|
|
918
|
+
// Extension-injected custom messages do not always emit Pi's model
|
|
919
|
+
// message_start event. Claim them at the component boundary so live
|
|
920
|
+
// rendering matches transcript restoration instead of failing open to
|
|
921
|
+
// the purple native box until the next reload.
|
|
922
|
+
if (!member && renderMode === "clean") {
|
|
923
|
+
member = addPersistentActivityUpdate(message);
|
|
663
924
|
}
|
|
664
925
|
const group = member ? activityTimeline.groupForMember(member.id) : undefined;
|
|
665
926
|
if (renderMode !== "clean" || !member || !group) {
|
|
666
927
|
return originalCustomRender.call(this, width);
|
|
667
928
|
}
|
|
668
929
|
activityUpdateComponents.set(member.id, this);
|
|
669
|
-
|
|
670
|
-
const position = activityMemberPosition(group, member);
|
|
671
|
-
if (!activityGroupRevealed(group)) {
|
|
672
|
-
return position.first
|
|
673
|
-
? ["", ...renderActivityGroupSummary(group, width, true)]
|
|
674
|
-
: [];
|
|
675
|
-
}
|
|
676
|
-
return renderActivityUpdate(group, member, width);
|
|
930
|
+
return renderActivityUpdateProjection(group, member, width);
|
|
677
931
|
};
|
|
678
932
|
const patchedCustomHandleMouse = function (this: any, event: any) {
|
|
679
933
|
if (releaseSemanticCustomMessage(this)) {
|
|
@@ -684,7 +938,6 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
684
938
|
if (renderMode !== "clean" || !member || !group) {
|
|
685
939
|
return originalCustomHandleMouse?.call(this, event);
|
|
686
940
|
}
|
|
687
|
-
if (group.toolCallIds.length === 0) return undefined;
|
|
688
941
|
return handleActivityUpdateMouse(group, member, event);
|
|
689
942
|
};
|
|
690
943
|
customMessagePrototype[customMessagePatchKey] = {
|
|
@@ -728,14 +981,38 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
728
981
|
const cleanThinkingRenderCacheKey = Symbol("pretty-tui.clean-thinking-render-cache");
|
|
729
982
|
const cleanThinkingDetailKey = Symbol("pretty-tui.clean-thinking-detail");
|
|
730
983
|
|
|
984
|
+
const assistantTerminalUpdate = (message: any): ActivityMember | undefined => {
|
|
985
|
+
if (!assistantTerminalState(message)) return undefined;
|
|
986
|
+
const hasToolCalls = Array.isArray(message?.content) &&
|
|
987
|
+
message.content.some((item: any) => item?.type === "toolCall");
|
|
988
|
+
// Failed tools already preserve the error in their native result details;
|
|
989
|
+
// avoid duplicating that payload as a second durable member.
|
|
990
|
+
if (hasToolCalls) return undefined;
|
|
991
|
+
const key = `assistant-terminal:${assistantMessageKey(message)}`;
|
|
992
|
+
const existing = activityTimeline.memberForUpdate(key);
|
|
993
|
+
if (existing) return existing;
|
|
994
|
+
const stopReason = message?.stopReason;
|
|
995
|
+
const severity: ActivitySeverity = stopReason === "aborted" ? "warning" : "error";
|
|
996
|
+
const title = stopReason === "length"
|
|
997
|
+
? "Response truncated"
|
|
998
|
+
: stopReason === "aborted"
|
|
999
|
+
? "Operation aborted"
|
|
1000
|
+
: "Error";
|
|
1001
|
+
const content = String(message?.errorMessage ?? "").trim() ||
|
|
1002
|
+
(stopReason === "length" ? "Response was truncated before completion." : "Unknown error");
|
|
1003
|
+
activityTimeline.boundary();
|
|
1004
|
+
const member = activityTimeline.addPendingUpdate(key, title, content, true, severity);
|
|
1005
|
+
activityTimeline.boundary();
|
|
1006
|
+
return member;
|
|
1007
|
+
};
|
|
1008
|
+
|
|
731
1009
|
const eligibleThinking = (message: any) =>
|
|
732
1010
|
Array.isArray(message?.content) &&
|
|
733
1011
|
message.content.some(
|
|
734
1012
|
(item: any) => item?.type === "thinking" &&
|
|
735
1013
|
typeof item.thinking === "string" &&
|
|
736
1014
|
item.thinking.length > 0,
|
|
737
|
-
)
|
|
738
|
-
!assistantSystemBoundary(message);
|
|
1015
|
+
);
|
|
739
1016
|
|
|
740
1017
|
const refreshGroup = (group: ActivityGroup) => {
|
|
741
1018
|
for (const member of group.members) {
|
|
@@ -743,11 +1020,41 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
743
1020
|
cleanToolComponents.get(member.toolCallId)?.updateDisplay?.();
|
|
744
1021
|
} else if (member.kind === "thinking" && member.messageKey) {
|
|
745
1022
|
thinkingComponents.get(member.messageKey)?.invalidate?.();
|
|
1023
|
+
} else if (member.kind === "update") {
|
|
1024
|
+
activityUpdateComponents.get(member.id)?.invalidate?.();
|
|
746
1025
|
}
|
|
747
1026
|
}
|
|
748
1027
|
currentTui?.requestRender?.();
|
|
749
1028
|
};
|
|
750
1029
|
|
|
1030
|
+
// Shared detail projection for a Thought, used both as a member of a tool
|
|
1031
|
+
// group and as a standalone row. The cache records its theme because the
|
|
1032
|
+
// same component can switch between the Markdown fallback and a real tool
|
|
1033
|
+
// theme once the first tool of the group is constructed.
|
|
1034
|
+
const thinkingDetailLines = (
|
|
1035
|
+
owner: any,
|
|
1036
|
+
text: string,
|
|
1037
|
+
theme: any,
|
|
1038
|
+
childWidth: number,
|
|
1039
|
+
): string[] => {
|
|
1040
|
+
let detail = owner[cleanThinkingDetailKey];
|
|
1041
|
+
if (!detail || detail.text !== text || detail.theme !== theme) {
|
|
1042
|
+
detail = {
|
|
1043
|
+
text,
|
|
1044
|
+
theme,
|
|
1045
|
+
component: new Markdown(text, 0, 0, owner.markdownTheme, {
|
|
1046
|
+
color: (line: string) => theme.fg("thinkingText", line),
|
|
1047
|
+
italic: true,
|
|
1048
|
+
}),
|
|
1049
|
+
};
|
|
1050
|
+
owner[cleanThinkingDetailKey] = detail;
|
|
1051
|
+
}
|
|
1052
|
+
const lines: string[] = detail.component.render(Math.max(1, childWidth));
|
|
1053
|
+
return lines.map((line, index) =>
|
|
1054
|
+
theme.fg("dim", index === lines.length - 1 ? " └ " : " │ ") + line
|
|
1055
|
+
);
|
|
1056
|
+
};
|
|
1057
|
+
|
|
751
1058
|
const patchedUpdateContent = function (this: any, message: any, isStreaming = this.isStreaming) {
|
|
752
1059
|
this[originalMessageKey] = message;
|
|
753
1060
|
this[renderedModeKey] = renderMode;
|
|
@@ -790,8 +1097,14 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
790
1097
|
) {
|
|
791
1098
|
patchedUpdateContent.call(this, message, this.isStreaming);
|
|
792
1099
|
}
|
|
793
|
-
if (renderMode !== "clean"
|
|
794
|
-
|
|
1100
|
+
if (renderMode !== "clean") return originalRender.call(this, width);
|
|
1101
|
+
const terminalMember = assistantTerminalUpdate(message);
|
|
1102
|
+
if (!eligibleThinking(message)) {
|
|
1103
|
+
if (!terminalMember) return originalRender.call(this, width);
|
|
1104
|
+
const terminalGroup = activityTimeline.groupForMember(terminalMember.id);
|
|
1105
|
+
if (!terminalGroup) return originalRender.call(this, width);
|
|
1106
|
+
activityUpdateComponents.set(terminalMember.id, this);
|
|
1107
|
+
return renderActivityUpdateProjection(terminalGroup, terminalMember, width);
|
|
795
1108
|
}
|
|
796
1109
|
|
|
797
1110
|
const key = assistantMessageKey(message);
|
|
@@ -803,25 +1116,29 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
803
1116
|
// assistant text is never lost.
|
|
804
1117
|
return originalRender.call(this, width);
|
|
805
1118
|
}
|
|
806
|
-
if (group.toolCallIds.length === 0) {
|
|
807
|
-
// Activity groups are tool-oriented. A model may finish with Thinking
|
|
808
|
-
// and visible text without calling a tool; only the Thinking portion
|
|
809
|
-
// is filtered from Pi's native component, so always preserve the
|
|
810
|
-
// visible response here instead of returning an empty projection.
|
|
811
|
-
return visibleAssistantText(message) ? originalRender.call(this, width) : [];
|
|
812
|
-
}
|
|
813
1119
|
thinkingComponents.set(key, this);
|
|
814
1120
|
if (!activityFallbackThemes.has(group.id)) {
|
|
815
1121
|
activityFallbackThemes.set(group.id, defaultActivityTheme());
|
|
816
1122
|
}
|
|
817
1123
|
const position = activityMemberPosition(group, member);
|
|
818
1124
|
const visibleLines = visibleAssistantText(message) ? originalRender.call(this, width) : [];
|
|
1125
|
+
// Pi's native renderer prints its own error line for messages with no
|
|
1126
|
+
// tool calls, so this component only adds the styled alert card when no
|
|
1127
|
+
// visible text can carry it.
|
|
1128
|
+
const terminalLines = terminalMember && visibleLines.length === 0
|
|
1129
|
+
? renderPendingActivityUpdate(terminalMember, width)
|
|
1130
|
+
: [];
|
|
1131
|
+
const appendTail = (activityLines: string[]) => [
|
|
1132
|
+
...activityLines,
|
|
1133
|
+
...terminalLines,
|
|
1134
|
+
...visibleLines,
|
|
1135
|
+
];
|
|
819
1136
|
const revealed = activityGroupRevealed(group);
|
|
820
1137
|
if (!revealed) {
|
|
821
1138
|
const activityLines = position.first
|
|
822
1139
|
? ["", ...renderActivityGroupSummary(group, width, true)]
|
|
823
1140
|
: [];
|
|
824
|
-
return
|
|
1141
|
+
return appendTail(activityLines);
|
|
825
1142
|
}
|
|
826
1143
|
|
|
827
1144
|
const groupTheme = activityGroupTheme(group);
|
|
@@ -831,10 +1148,16 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
831
1148
|
width,
|
|
832
1149
|
groupTheme,
|
|
833
1150
|
);
|
|
1151
|
+
// Clean mode owns thought disclosure: revealing a group shows the thought
|
|
1152
|
+
// member, while only a member click (or Ctrl+O) reveals its full text.
|
|
834
1153
|
const expanded = cleanToolsExpanded || expandedThinkingMembers.has(member.id);
|
|
835
1154
|
const settledOwner = position.first ? activityGroupOwner(group) : undefined;
|
|
836
|
-
|
|
837
|
-
|
|
1155
|
+
// A group without tools has no durable summary, so its own boundary is
|
|
1156
|
+
// the signal that it finished.
|
|
1157
|
+
const stableSummary = !position.first || (
|
|
1158
|
+
settledOwner
|
|
1159
|
+
? Boolean(settledSummaries.has(settledOwner) && !toolActivityHolds.has(settledOwner))
|
|
1160
|
+
: activityTimeline.isSettled(group.id)
|
|
838
1161
|
);
|
|
839
1162
|
const cacheable = !expanded && !this.isStreaming && stableSummary;
|
|
840
1163
|
const cached = cacheable ? this[cleanThinkingRenderCacheKey] : undefined;
|
|
@@ -848,7 +1171,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
848
1171
|
// Only the stable activity projection is cached. Visible assistant
|
|
849
1172
|
// text belongs outside the collapsible group and must be appended on
|
|
850
1173
|
// every render, including cache hits.
|
|
851
|
-
return
|
|
1174
|
+
return appendTail(cached.lines);
|
|
852
1175
|
}
|
|
853
1176
|
const stateLabel = this.isStreaming ? "thinking" : "thought";
|
|
854
1177
|
const label = truncateToWidth(stateLabel, Math.max(1, childWidth - 2), "…");
|
|
@@ -856,28 +1179,11 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
856
1179
|
groupTheme.fg("toolTitle", groupTheme.bold(label));
|
|
857
1180
|
let contentLines: string[] = [header];
|
|
858
1181
|
if (expanded) {
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
text: detailText,
|
|
865
|
-
component: new Markdown(
|
|
866
|
-
detailText,
|
|
867
|
-
0,
|
|
868
|
-
0,
|
|
869
|
-
this.markdownTheme,
|
|
870
|
-
{
|
|
871
|
-
color: (text: string) => groupTheme.fg("thinkingText", text),
|
|
872
|
-
italic: true,
|
|
873
|
-
},
|
|
874
|
-
),
|
|
875
|
-
};
|
|
876
|
-
this[cleanThinkingDetailKey] = detail;
|
|
877
|
-
}
|
|
878
|
-
const detailLines = detail.component.render(detailWidth);
|
|
879
|
-
contentLines.push(...detailLines.map((line: string, index: number) =>
|
|
880
|
-
groupTheme.fg("dim", index === detailLines.length - 1 ? " └ " : " │ ") + line
|
|
1182
|
+
contentLines.push(...thinkingDetailLines(
|
|
1183
|
+
this,
|
|
1184
|
+
member.thinking ?? thinkingText(message),
|
|
1185
|
+
groupTheme,
|
|
1186
|
+
Math.max(1, childWidth - visibleWidth(" │ ")),
|
|
881
1187
|
));
|
|
882
1188
|
}
|
|
883
1189
|
const decorated = contentLines.map((line, index) =>
|
|
@@ -896,7 +1202,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
896
1202
|
lines: output,
|
|
897
1203
|
};
|
|
898
1204
|
}
|
|
899
|
-
return
|
|
1205
|
+
return appendTail(output);
|
|
900
1206
|
};
|
|
901
1207
|
|
|
902
1208
|
const patchedRender = function (this: any, width: number): string[] {
|
|
@@ -905,14 +1211,20 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
905
1211
|
|
|
906
1212
|
const handleAssistantTranscriptMouse = function (this: any, event: any) {
|
|
907
1213
|
const message = this[originalMessageKey] ?? this.lastMessage;
|
|
908
|
-
if (renderMode !== "clean"
|
|
909
|
-
|
|
1214
|
+
if (renderMode !== "clean") return originalHandleMouse.call(this, event);
|
|
1215
|
+
if (!eligibleThinking(message)) {
|
|
1216
|
+
const terminalMember = assistantTerminalUpdate(message);
|
|
1217
|
+
const terminalGroup = terminalMember
|
|
1218
|
+
? activityTimeline.groupForMember(terminalMember.id)
|
|
1219
|
+
: undefined;
|
|
1220
|
+
return terminalMember && terminalGroup && activityGroupCollapsible(terminalGroup)
|
|
1221
|
+
? handleActivityUpdateMouse(terminalGroup, terminalMember, event)
|
|
1222
|
+
: originalHandleMouse.call(this, event);
|
|
910
1223
|
}
|
|
911
1224
|
const key = assistantMessageKey(message);
|
|
912
1225
|
const member = activityTimeline.memberForThinking(key);
|
|
913
1226
|
const group = member ? activityTimeline.groupForMember(member.id) : undefined;
|
|
914
1227
|
if (!member || !group) return originalHandleMouse.call(this, event);
|
|
915
|
-
if (group.toolCallIds.length === 0) return originalHandleMouse.call(this, event);
|
|
916
1228
|
const position = activityMemberPosition(group, member);
|
|
917
1229
|
const isLeftClick = event.type === "click" && event.button === "left";
|
|
918
1230
|
|
|
@@ -1027,53 +1339,42 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1027
1339
|
];
|
|
1028
1340
|
};
|
|
1029
1341
|
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
if (visibleAssistantText(message) || assistantSystemBoundary(message)) {
|
|
1067
|
-
lastToolCallId = undefined;
|
|
1068
|
-
pendingCustomKeys = [];
|
|
1069
|
-
}
|
|
1070
|
-
for (const item of toolCalls) lastToolCallId = item.id;
|
|
1071
|
-
continue;
|
|
1072
|
-
}
|
|
1073
|
-
if (message?.role === "toolResult" && message.toolCallId) {
|
|
1074
|
-
lastToolCallId = message.toolCallId;
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1342
|
+
const appendRuntimeActivityUpdate = (
|
|
1343
|
+
mode: any,
|
|
1344
|
+
message: string,
|
|
1345
|
+
severity: ActivitySeverity,
|
|
1346
|
+
): boolean => {
|
|
1347
|
+
const [firstLine, ...remainingLines] = String(message).split("\n");
|
|
1348
|
+
const fallbackTitle = severity === "warning"
|
|
1349
|
+
? "Warning"
|
|
1350
|
+
: severity === "error"
|
|
1351
|
+
? "Error"
|
|
1352
|
+
: "Info";
|
|
1353
|
+
const title = firstLine || fallbackTitle;
|
|
1354
|
+
const content = remainingLines.join("\n");
|
|
1355
|
+
// Warnings and errors are standalone hard boundaries; only ordinary notices
|
|
1356
|
+
// join the work in progress.
|
|
1357
|
+
const standalone = severity === "warning" || severity === "error";
|
|
1358
|
+
const updateKey = `runtime:${severity}:${++activityUpdateSequence}`;
|
|
1359
|
+
if (standalone) closeCleanAtStandaloneAlert();
|
|
1360
|
+
const existingGroup = activityTimeline.currentGroup();
|
|
1361
|
+
const joinsWork = !standalone && hasWorkActivity(existingGroup);
|
|
1362
|
+
const member = joinsWork
|
|
1363
|
+
? activityTimeline.addUpdate(updateKey, title, content, false, severity) ??
|
|
1364
|
+
activityTimeline.addPendingUpdate(updateKey, title, content, false, severity)
|
|
1365
|
+
: activityTimeline.addPendingUpdate(updateKey, title, content, false, severity);
|
|
1366
|
+
if (!joinsWork) activityTimeline.boundary();
|
|
1367
|
+
const group = activityTimeline.groupForMember(member.id);
|
|
1368
|
+
if (!group || !mode.chatContainer?.addChild) return false;
|
|
1369
|
+
const component: Component = {
|
|
1370
|
+
render: (width: number) => renderActivityUpdateProjection(group, member, width),
|
|
1371
|
+
invalidate() {},
|
|
1372
|
+
handleMouse: (event: any) => handleActivityUpdateMouse(group, member, event),
|
|
1373
|
+
} as Component;
|
|
1374
|
+
activityUpdateComponents.set(member.id, component);
|
|
1375
|
+
mode.chatContainer.addChild(component);
|
|
1376
|
+
mode.ui?.requestRender?.();
|
|
1377
|
+
return true;
|
|
1077
1378
|
};
|
|
1078
1379
|
|
|
1079
1380
|
const interactiveModePrototype = InteractiveMode.prototype as any;
|
|
@@ -1083,6 +1384,10 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1083
1384
|
const originalRenderSessionEntries = interactiveModePrototype.renderSessionEntries;
|
|
1084
1385
|
const originalSwitchTuiMode = interactiveModePrototype.switchTuiMode;
|
|
1085
1386
|
const originalShowExtensionNotify = interactiveModePrototype.showExtensionNotify;
|
|
1387
|
+
const originalToggleThinkingBlockVisibility = interactiveModePrototype.toggleThinkingBlockVisibility;
|
|
1388
|
+
const originalShowStatus = interactiveModePrototype.showStatus;
|
|
1389
|
+
const originalShowWarning = interactiveModePrototype.showWarning;
|
|
1390
|
+
const originalShowError = interactiveModePrototype.showError;
|
|
1086
1391
|
const patchedSetToolsExpanded = function (this: any, expanded: boolean) {
|
|
1087
1392
|
cleanToolsExpanded = expanded;
|
|
1088
1393
|
changingAllToolsExpansion = true;
|
|
@@ -1107,7 +1412,6 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1107
1412
|
// while Pi's live compaction UI appends it chronologically. Keep reloads
|
|
1108
1413
|
// and transcript rebuilds consistent with that live presentation.
|
|
1109
1414
|
const orderedEntries = orderContextEntriesForTranscript(entries);
|
|
1110
|
-
indexDisplayedCustomUpdateHints(orderedEntries);
|
|
1111
1415
|
return originalRenderSessionEntries.call(this, orderedEntries, options);
|
|
1112
1416
|
};
|
|
1113
1417
|
|
|
@@ -1118,43 +1422,80 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1118
1422
|
return result;
|
|
1119
1423
|
};
|
|
1120
1424
|
|
|
1425
|
+
// Pi's own showExtensionNotify only dispatches to showStatus/showWarning/
|
|
1426
|
+
// showError, and all three are replaced above. Delegating back to it would
|
|
1427
|
+
// therefore re-enter these patches instead of restoring native behaviour,
|
|
1428
|
+
// which sent info notifications into the transient UI flash. Route straight
|
|
1429
|
+
// to the original method that matches the severity instead.
|
|
1430
|
+
const nativeExtensionNotify = function (
|
|
1431
|
+
this: any,
|
|
1432
|
+
text: string,
|
|
1433
|
+
severity: ActivitySeverity,
|
|
1434
|
+
): void {
|
|
1435
|
+
const direct = severity === "error"
|
|
1436
|
+
? originalShowError
|
|
1437
|
+
: severity === "warning"
|
|
1438
|
+
? originalShowWarning
|
|
1439
|
+
: originalShowStatus;
|
|
1440
|
+
try {
|
|
1441
|
+
if (typeof direct === "function") direct.call(this, text);
|
|
1442
|
+
else originalShowExtensionNotify.call(this, text, severity === "info" ? undefined : severity);
|
|
1443
|
+
} catch {
|
|
1444
|
+
// This host has no transcript to write to. The notification is already
|
|
1445
|
+
// recorded in the activity timeline, and it does not belong in the
|
|
1446
|
+
// transient UI flash, which only carries Pi UI state messages.
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1121
1450
|
const patchedShowExtensionNotify = function (
|
|
1122
1451
|
this: any,
|
|
1123
1452
|
message: string,
|
|
1124
1453
|
type?: "info" | "warning" | "error",
|
|
1125
1454
|
) {
|
|
1126
|
-
if (renderMode !== "clean"
|
|
1455
|
+
if (renderMode !== "clean") {
|
|
1127
1456
|
return originalShowExtensionNotify.call(this, message, type);
|
|
1128
1457
|
}
|
|
1129
|
-
const
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
const content = remainingLines.join("\n");
|
|
1133
|
-
const member = activityTimeline.addUpdate(updateKey, title, content, false) ??
|
|
1134
|
-
activityTimeline.addPendingUpdate(updateKey, title, content, false);
|
|
1135
|
-
const group = activityTimeline.groupForMember(member.id);
|
|
1136
|
-
if (!group || !this.chatContainer?.addChild) {
|
|
1137
|
-
return originalShowExtensionNotify.call(this, message, type);
|
|
1458
|
+
const severity: ActivitySeverity = type ?? "info";
|
|
1459
|
+
if (!appendRuntimeActivityUpdate(this, message, severity)) {
|
|
1460
|
+
nativeExtensionNotify.call(this, message, severity);
|
|
1138
1461
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1464
|
+
const patchedToggleThinkingBlockVisibility = function (this: any) {
|
|
1465
|
+
if (renderMode !== "clean") {
|
|
1466
|
+
return originalToggleThinkingBlockVisibility.call(this);
|
|
1467
|
+
}
|
|
1468
|
+
// Do not change Pi's persisted hideThinkingBlock setting in clean mode.
|
|
1469
|
+
// Thought details use the activity hierarchy instead: click one thought,
|
|
1470
|
+
// or use Ctrl+O when every member should expand together.
|
|
1471
|
+
this.showStatus(
|
|
1472
|
+
"Thought details expand individually in clean mode\nClick a thought, or use Ctrl+O to expand all.",
|
|
1473
|
+
);
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
const patchedShowStatus = function (this: any, message: string) {
|
|
1477
|
+
if (renderMode !== "clean") return originalShowStatus.call(this, message);
|
|
1478
|
+
const [firstLine, ...remainingLines] = String(message).split("\n");
|
|
1479
|
+
setLatestActivityStatus({
|
|
1480
|
+
title: firstLine || "Status",
|
|
1481
|
+
detail: remainingLines.join("\n"),
|
|
1482
|
+
severity: "status",
|
|
1483
|
+
});
|
|
1155
1484
|
this.ui?.requestRender?.();
|
|
1156
1485
|
};
|
|
1157
1486
|
|
|
1487
|
+
const patchedShowWarning = function (this: any, message: string) {
|
|
1488
|
+
if (renderMode !== "clean" || !appendRuntimeActivityUpdate(this, message, "warning")) {
|
|
1489
|
+
return originalShowWarning.call(this, message);
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
const patchedShowError = function (this: any, message: string) {
|
|
1494
|
+
if (renderMode !== "clean" || !appendRuntimeActivityUpdate(this, message, "error")) {
|
|
1495
|
+
return originalShowError.call(this, message);
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1498
|
+
|
|
1158
1499
|
interactiveModePrototype[toolsExpansionPatchKey] = {
|
|
1159
1500
|
originalSetToolsExpanded,
|
|
1160
1501
|
patchedSetToolsExpanded,
|
|
@@ -1164,15 +1505,28 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1164
1505
|
patchedSwitchTuiMode,
|
|
1165
1506
|
originalShowExtensionNotify,
|
|
1166
1507
|
patchedShowExtensionNotify,
|
|
1508
|
+
originalToggleThinkingBlockVisibility,
|
|
1509
|
+
patchedToggleThinkingBlockVisibility,
|
|
1510
|
+
originalShowStatus,
|
|
1511
|
+
patchedShowStatus,
|
|
1512
|
+
originalShowWarning,
|
|
1513
|
+
patchedShowWarning,
|
|
1514
|
+
originalShowError,
|
|
1515
|
+
patchedShowError,
|
|
1167
1516
|
};
|
|
1168
1517
|
interactiveModePrototype.setToolsExpanded = patchedSetToolsExpanded;
|
|
1169
1518
|
interactiveModePrototype.renderSessionEntries = patchedRenderSessionEntries;
|
|
1170
1519
|
interactiveModePrototype.switchTuiMode = patchedSwitchTuiMode;
|
|
1171
1520
|
interactiveModePrototype.showExtensionNotify = patchedShowExtensionNotify;
|
|
1521
|
+
interactiveModePrototype.toggleThinkingBlockVisibility = patchedToggleThinkingBlockVisibility;
|
|
1522
|
+
interactiveModePrototype.showStatus = patchedShowStatus;
|
|
1523
|
+
interactiveModePrototype.showWarning = patchedShowWarning;
|
|
1524
|
+
interactiveModePrototype.showError = patchedShowError;
|
|
1172
1525
|
|
|
1173
1526
|
pi.on("session_shutdown", () => {
|
|
1174
1527
|
fullscreenTui = false;
|
|
1175
1528
|
currentTui = undefined;
|
|
1529
|
+
clearLatestActivityStatus();
|
|
1176
1530
|
currentExtensionUi = undefined;
|
|
1177
1531
|
const patch = interactiveModePrototype[toolsExpansionPatchKey];
|
|
1178
1532
|
if (!patch) return;
|
|
@@ -1188,11 +1542,27 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1188
1542
|
if (patch.patchedShowExtensionNotify === interactiveModePrototype.showExtensionNotify) {
|
|
1189
1543
|
interactiveModePrototype.showExtensionNotify = patch.originalShowExtensionNotify;
|
|
1190
1544
|
}
|
|
1545
|
+
if (patch.patchedToggleThinkingBlockVisibility === interactiveModePrototype.toggleThinkingBlockVisibility) {
|
|
1546
|
+
interactiveModePrototype.toggleThinkingBlockVisibility = patch.originalToggleThinkingBlockVisibility;
|
|
1547
|
+
}
|
|
1548
|
+
if (patch.patchedShowStatus === interactiveModePrototype.showStatus) {
|
|
1549
|
+
interactiveModePrototype.showStatus = patch.originalShowStatus;
|
|
1550
|
+
}
|
|
1551
|
+
if (patch.patchedShowWarning === interactiveModePrototype.showWarning) {
|
|
1552
|
+
interactiveModePrototype.showWarning = patch.originalShowWarning;
|
|
1553
|
+
}
|
|
1554
|
+
if (patch.patchedShowError === interactiveModePrototype.showError) {
|
|
1555
|
+
interactiveModePrototype.showError = patch.originalShowError;
|
|
1556
|
+
}
|
|
1191
1557
|
if (
|
|
1192
1558
|
interactiveModePrototype.setToolsExpanded === patch.originalSetToolsExpanded &&
|
|
1193
1559
|
interactiveModePrototype.renderSessionEntries === patch.originalRenderSessionEntries &&
|
|
1194
1560
|
interactiveModePrototype.switchTuiMode === patch.originalSwitchTuiMode &&
|
|
1195
|
-
interactiveModePrototype.showExtensionNotify === patch.originalShowExtensionNotify
|
|
1561
|
+
interactiveModePrototype.showExtensionNotify === patch.originalShowExtensionNotify &&
|
|
1562
|
+
interactiveModePrototype.toggleThinkingBlockVisibility === patch.originalToggleThinkingBlockVisibility &&
|
|
1563
|
+
interactiveModePrototype.showStatus === patch.originalShowStatus &&
|
|
1564
|
+
interactiveModePrototype.showWarning === patch.originalShowWarning &&
|
|
1565
|
+
interactiveModePrototype.showError === patch.originalShowError
|
|
1196
1566
|
) {
|
|
1197
1567
|
delete interactiveModePrototype[toolsExpansionPatchKey];
|
|
1198
1568
|
}
|
|
@@ -1517,20 +1887,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1517
1887
|
event.y === candidate.y && event.x >= candidate.xStart && event.x < candidate.xEnd,
|
|
1518
1888
|
);
|
|
1519
1889
|
if (region) {
|
|
1520
|
-
if (event.type === "click")
|
|
1521
|
-
if (typeof currentTui?.copyTextToClipboard === "function") {
|
|
1522
|
-
// Match fullscreen selection exactly: use Pi TUI's clipboard path
|
|
1523
|
-
// and its transient "Copied!" / "Copy failed" flash feedback.
|
|
1524
|
-
void Promise.resolve(currentTui.copyTextToClipboard(region.code)).catch(() => {
|
|
1525
|
-
currentTui?.flash?.("Copy failed");
|
|
1526
|
-
});
|
|
1527
|
-
} else {
|
|
1528
|
-
// Defensive fallback for older Pi versions without the fullscreen helper.
|
|
1529
|
-
void copyToClipboard(region.code)
|
|
1530
|
-
.then(() => currentExtensionUi?.notify("Copied!", "info"))
|
|
1531
|
-
.catch(() => currentExtensionUi?.notify("Copy failed", "error"));
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1890
|
+
if (event.type === "click") copyTranscriptText(region.code);
|
|
1534
1891
|
return { handled: true, render: false };
|
|
1535
1892
|
}
|
|
1536
1893
|
}
|
|
@@ -1760,8 +2117,11 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1760
2117
|
return block(rows);
|
|
1761
2118
|
};
|
|
1762
2119
|
|
|
1763
|
-
|
|
1764
|
-
|
|
2120
|
+
// Pi's render context always carries the authoritative failure flag
|
|
2121
|
+
// (tool-execution.js builds it from result.isError). A tool's own output text
|
|
2122
|
+
// must never decide this: `grep -n Error`, `cat error.log`, or a test runner
|
|
2123
|
+
// printing "failed 0" would otherwise mark a successful call as failed.
|
|
2124
|
+
const isError = (renderContext: any): boolean => Boolean(renderContext?.isError);
|
|
1765
2125
|
|
|
1766
2126
|
const partialResult = (context: any, theme: any, label: string): Component => {
|
|
1767
2127
|
setStatus(context, "running");
|
|
@@ -1834,8 +2194,8 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
1834
2194
|
return block(rows);
|
|
1835
2195
|
};
|
|
1836
2196
|
|
|
1837
|
-
const completeStatus = (context: any
|
|
1838
|
-
const failed = isError(context
|
|
2197
|
+
const completeStatus = (context: any): boolean => {
|
|
2198
|
+
const failed = isError(context);
|
|
1839
2199
|
setStatus(context, failed ? "error" : "success");
|
|
1840
2200
|
return failed;
|
|
1841
2201
|
};
|
|
@@ -2351,6 +2711,15 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2351
2711
|
activityTimeline.boundary();
|
|
2352
2712
|
};
|
|
2353
2713
|
|
|
2714
|
+
closeCleanAtNativeCustomBoundary = () => {
|
|
2715
|
+
finishCleanGroup("done");
|
|
2716
|
+
settleLastCleanGroup();
|
|
2717
|
+
};
|
|
2718
|
+
closeCleanAtStandaloneAlert = () => {
|
|
2719
|
+
finishCleanGroup("done");
|
|
2720
|
+
settleLastCleanGroup();
|
|
2721
|
+
};
|
|
2722
|
+
|
|
2354
2723
|
const settleLastCleanGroup = () => {
|
|
2355
2724
|
const group = cleanRun.groups[cleanRun.groups.length - 1];
|
|
2356
2725
|
if (!group) return;
|
|
@@ -2374,14 +2743,16 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2374
2743
|
const activityText = /^thinking(?:\.\.\.)?$/iu.test(normalizedActivityText)
|
|
2375
2744
|
? "thinking"
|
|
2376
2745
|
: rawActivityText;
|
|
2377
|
-
const countLabel = `${count} tool ${count === 1 ? "call" : "calls"}
|
|
2746
|
+
const countLabel = count > 0 ? `${count} tool ${count === 1 ? "call" : "calls"}` : "";
|
|
2378
2747
|
const thoughtLabel = thoughtCount > 0
|
|
2379
|
-
?
|
|
2748
|
+
? `${thoughtCount} ${thoughtCount === 1 ? "thought" : "thoughts"}`
|
|
2380
2749
|
: "";
|
|
2381
|
-
|
|
2382
|
-
|
|
2750
|
+
// Without tools there is no current tool name to report, and the generic
|
|
2751
|
+
// "thinking" placeholder would only repeat the thought count.
|
|
2752
|
+
const activityLabel = count > 0 && activityText !== "done" && activityText.trim()
|
|
2753
|
+
? activityText
|
|
2383
2754
|
: "";
|
|
2384
|
-
return
|
|
2755
|
+
return [countLabel, thoughtLabel, activityLabel].filter(Boolean).join(" · ");
|
|
2385
2756
|
};
|
|
2386
2757
|
|
|
2387
2758
|
const summaryRow = (
|
|
@@ -2455,36 +2826,53 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2455
2826
|
): string[] => {
|
|
2456
2827
|
const fit = (lines: string[]) => lines.map((line) => truncateToWidth(line, Math.max(1, width), ""));
|
|
2457
2828
|
const summaryTheme = activityGroupTheme(group);
|
|
2829
|
+
if (!summaryTheme || !activityTimeline.hasWork(group)) return [];
|
|
2458
2830
|
const owner = activityGroupOwner(group);
|
|
2459
|
-
|
|
2460
|
-
const
|
|
2831
|
+
const settled = owner ? settledSummaries.get(owner) : undefined;
|
|
2832
|
+
const toolCount = group.toolCallIds.length;
|
|
2833
|
+
const live = () => cleanRun.activeToolName ?? currentCleanActivity();
|
|
2461
2834
|
if (settled) {
|
|
2462
2835
|
return fit(block([summaryRow(
|
|
2463
2836
|
summaryTheme,
|
|
2464
|
-
Math.max(settled.count,
|
|
2465
|
-
settled.failed,
|
|
2837
|
+
group.hardBoundarySplit ? toolCount : Math.max(settled.count, toolCount),
|
|
2838
|
+
group.hardBoundarySplit ? 0 : settled.failed,
|
|
2466
2839
|
Math.max(settled.thoughtCount, group.thoughtCount),
|
|
2467
2840
|
settled.activity,
|
|
2468
2841
|
collapsedDone,
|
|
2469
2842
|
)]).render(width));
|
|
2470
2843
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
: false;
|
|
2474
|
-
const lastBelongs = cleanRun.lastCompletedToolCallId
|
|
2475
|
-
? group.toolCallIds.includes(cleanRun.lastCompletedToolCallId)
|
|
2476
|
-
: false;
|
|
2477
|
-
if (currentBelongs || lastBelongs || !cleanRun.settled) {
|
|
2844
|
+
if (owner && !cleanRun.settled) {
|
|
2845
|
+
// Live tool group: keep the current tool name and the running count.
|
|
2478
2846
|
return fit(block([summaryRow(
|
|
2479
2847
|
summaryTheme,
|
|
2480
|
-
Math.max(
|
|
2848
|
+
Math.max(toolCount, liveCleanToolCount()),
|
|
2481
2849
|
cleanRun.failed,
|
|
2482
2850
|
group.thoughtCount,
|
|
2483
|
-
|
|
2851
|
+
live,
|
|
2484
2852
|
collapsedDone,
|
|
2485
2853
|
)]).render(width));
|
|
2486
2854
|
}
|
|
2487
|
-
|
|
2855
|
+
if (!owner && !activityTimeline.isSettled(group.id)) {
|
|
2856
|
+
// Thought-only group while the model is still working.
|
|
2857
|
+
return fit(block([summaryRow(
|
|
2858
|
+
summaryTheme,
|
|
2859
|
+
toolCount,
|
|
2860
|
+
cleanRun.failed,
|
|
2861
|
+
group.thoughtCount,
|
|
2862
|
+
"thinking",
|
|
2863
|
+
collapsedDone,
|
|
2864
|
+
)]).render(width));
|
|
2865
|
+
}
|
|
2866
|
+
// Finished group with no durable summary: a completed Thought-only turn, or
|
|
2867
|
+
// a group split apart by a native custom renderer.
|
|
2868
|
+
return fit(block([summaryRow(
|
|
2869
|
+
summaryTheme,
|
|
2870
|
+
toolCount,
|
|
2871
|
+
0,
|
|
2872
|
+
group.thoughtCount,
|
|
2873
|
+
"done",
|
|
2874
|
+
collapsedDone,
|
|
2875
|
+
)]).render(width));
|
|
2488
2876
|
};
|
|
2489
2877
|
|
|
2490
2878
|
/**
|
|
@@ -2552,7 +2940,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2552
2940
|
|
|
2553
2941
|
const hiddenToolResult = (context: any, options: any, output: string): Component => {
|
|
2554
2942
|
if (options.isPartial) setStatus(context, "running");
|
|
2555
|
-
else completeStatus(context
|
|
2943
|
+
else completeStatus(context);
|
|
2556
2944
|
return block([]);
|
|
2557
2945
|
};
|
|
2558
2946
|
|
|
@@ -2570,6 +2958,62 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2570
2958
|
(renderMode === "clean" && isCleanGroupRevealed(toolCallId))
|
|
2571
2959
|
);
|
|
2572
2960
|
|
|
2961
|
+
pi.registerEntryRenderer<ResponseFooterData>(RESPONSE_FOOTER_ENTRY_TYPE, (entry, _options, theme) => {
|
|
2962
|
+
let copyStart = -1;
|
|
2963
|
+
let copyWidth = 0;
|
|
2964
|
+
return {
|
|
2965
|
+
render(width: number): string[] {
|
|
2966
|
+
const data = entry.data;
|
|
2967
|
+
if (!data || width <= 0) return [];
|
|
2968
|
+
copyStart = -1;
|
|
2969
|
+
copyWidth = 0;
|
|
2970
|
+
const answer = data.answerEntryId ? responseAnswerTexts.get(data.answerEntryId) ?? "" : "";
|
|
2971
|
+
const copyLabel = "[Copy]";
|
|
2972
|
+
const showCopy = fullscreenTui && Boolean(answer.trim()) && width >= 30;
|
|
2973
|
+
const duration = formatResponseDuration(data.durationMs);
|
|
2974
|
+
const statusText = data.outcome === "stopped"
|
|
2975
|
+
? `⚠ Stopped after ${duration}`
|
|
2976
|
+
: `✓ Completed in ${duration}`;
|
|
2977
|
+
const leading = "── ";
|
|
2978
|
+
const trailing = showCopy ? ` ${copyLabel} ──` : "──";
|
|
2979
|
+
const fixedWidth = visibleWidth(leading) + visibleWidth(trailing) + 1;
|
|
2980
|
+
if (width <= fixedWidth) return [theme.fg("dim", "─".repeat(width))];
|
|
2981
|
+
|
|
2982
|
+
const statusWidth = Math.max(1, width - fixedWidth);
|
|
2983
|
+
const visibleStatus = truncateToWidth(statusText, statusWidth, "…");
|
|
2984
|
+
const prefix = leading + visibleStatus + " ";
|
|
2985
|
+
const fill = "─".repeat(Math.max(0, width - visibleWidth(prefix) - visibleWidth(trailing)));
|
|
2986
|
+
const line = prefix + fill + trailing;
|
|
2987
|
+
if (showCopy) {
|
|
2988
|
+
const copyIndex = line.indexOf(copyLabel);
|
|
2989
|
+
copyStart = visibleWidth(line.slice(0, copyIndex));
|
|
2990
|
+
copyWidth = visibleWidth(copyLabel);
|
|
2991
|
+
}
|
|
2992
|
+
// The divider, completion text, and Copy control intentionally share
|
|
2993
|
+
// one quiet gray treatment; position alone communicates the hierarchy.
|
|
2994
|
+
return [theme.fg("dim", line)];
|
|
2995
|
+
},
|
|
2996
|
+
handleMouse(event: TuiMouseEvent) {
|
|
2997
|
+
if (
|
|
2998
|
+
copyStart < 0 ||
|
|
2999
|
+
event.button !== "left" ||
|
|
3000
|
+
(event.type !== "press" && event.type !== "click") ||
|
|
3001
|
+
event.y !== 0 ||
|
|
3002
|
+
event.x < copyStart ||
|
|
3003
|
+
event.x >= copyStart + copyWidth
|
|
3004
|
+
) {
|
|
3005
|
+
return undefined;
|
|
3006
|
+
}
|
|
3007
|
+
if (event.type === "click" && entry.data?.answerEntryId) {
|
|
3008
|
+
const answer = responseAnswerTexts.get(entry.data.answerEntryId);
|
|
3009
|
+
if (answer) copyTranscriptText(answer);
|
|
3010
|
+
}
|
|
3011
|
+
return { handled: true, render: false };
|
|
3012
|
+
},
|
|
3013
|
+
invalidate() {},
|
|
3014
|
+
};
|
|
3015
|
+
});
|
|
3016
|
+
|
|
2573
3017
|
pi.registerEntryRenderer<ToolSummaryData>("pretty-tui-tool-summary", (entry, { expanded }, theme) => ({
|
|
2574
3018
|
render(width: number): string[] {
|
|
2575
3019
|
if (renderMode !== "clean" || expanded || cleanContextCompacted) return [];
|
|
@@ -2620,17 +3064,21 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2620
3064
|
|
|
2621
3065
|
const restoreCleanSession = (ctx: any) => {
|
|
2622
3066
|
currentExtensionUi = ctx.ui;
|
|
3067
|
+
clearLatestActivityStatus();
|
|
2623
3068
|
cleanToolsExpanded = ctx.ui.getToolsExpanded();
|
|
2624
3069
|
settledSummaries.clear();
|
|
2625
3070
|
legacySummaryLastToolCallIds.clear();
|
|
2626
3071
|
activityTimeline.clear();
|
|
3072
|
+
responseAnswerTexts.clear();
|
|
3073
|
+
responseRunStartedAt = undefined;
|
|
3074
|
+
responseHasAssistantMessage = false;
|
|
3075
|
+
nativeCustomBoundaryKeys.clear();
|
|
2627
3076
|
revealedActivityGroups.clear();
|
|
2628
3077
|
expandedThinkingMembers.clear();
|
|
2629
3078
|
thinkingComponents.clear();
|
|
2630
3079
|
activityFallbackThemes.clear();
|
|
2631
3080
|
activityUpdateComponents.clear();
|
|
2632
|
-
|
|
2633
|
-
customUpdateBeforeToolHints.clear();
|
|
3081
|
+
activityUpdateMarkdownComponents.clear();
|
|
2634
3082
|
activityUpdateSequence = 0;
|
|
2635
3083
|
assistantBoundaryKeys.clear();
|
|
2636
3084
|
knownToolCallIds.clear();
|
|
@@ -2847,8 +3295,10 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2847
3295
|
}
|
|
2848
3296
|
|
|
2849
3297
|
if (message.role === "assistant") {
|
|
3298
|
+
const answerText = assistantAnswerText(message);
|
|
3299
|
+
if (answerText) responseAnswerTexts.set(entry.id, answerText);
|
|
2850
3300
|
const thinking = thinkingText(message);
|
|
2851
|
-
if (thinking
|
|
3301
|
+
if (thinking) {
|
|
2852
3302
|
activityTimeline.addThinking(assistantMessageKey(message), thinking);
|
|
2853
3303
|
}
|
|
2854
3304
|
if (messageHasVisibleText(message)) finishGroup();
|
|
@@ -2857,7 +3307,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2857
3307
|
toolCalls.add(item.id);
|
|
2858
3308
|
activityTimeline.addTool(item.id, item.name ?? "tool");
|
|
2859
3309
|
}
|
|
2860
|
-
if (
|
|
3310
|
+
if (assistantTerminalState(message)) finishGroup();
|
|
2861
3311
|
continue;
|
|
2862
3312
|
}
|
|
2863
3313
|
|
|
@@ -2924,8 +3374,16 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2924
3374
|
(item: any) => item.type === "toolCall" && item.id,
|
|
2925
3375
|
);
|
|
2926
3376
|
|
|
3377
|
+
const conciseActivityText = (value: string): string =>
|
|
3378
|
+
value
|
|
3379
|
+
.split("\n")
|
|
3380
|
+
.map((line) => line.trim())
|
|
3381
|
+
.find(Boolean)
|
|
3382
|
+
?.replace(/^#{1,6}\s+/u, "")
|
|
3383
|
+
.replace(/^\*\*(.*?)\*\*$/u, "$1") ?? "";
|
|
3384
|
+
|
|
2927
3385
|
const trackThinkingActivity = (message: any): void => {
|
|
2928
|
-
if (message?.role !== "assistant"
|
|
3386
|
+
if (message?.role !== "assistant") return;
|
|
2929
3387
|
const thinking = thinkingText(message);
|
|
2930
3388
|
if (thinking) activityTimeline.addThinking(assistantMessageKey(message), thinking);
|
|
2931
3389
|
};
|
|
@@ -2984,16 +3442,12 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2984
3442
|
closeAtAssistantBoundary(event.message, "done");
|
|
2985
3443
|
return;
|
|
2986
3444
|
}
|
|
2987
|
-
if (assistantSystemBoundary(event.message)) {
|
|
2988
|
-
closeAtAssistantBoundary(event.message, "done");
|
|
2989
|
-
settleLastCleanGroup();
|
|
2990
|
-
return;
|
|
2991
|
-
}
|
|
2992
3445
|
trackThinkingActivity(event.message);
|
|
2993
3446
|
trackPendingToolActivity(event.message);
|
|
2994
3447
|
});
|
|
2995
3448
|
pi.on("message_end", (event) => {
|
|
2996
|
-
if (
|
|
3449
|
+
if (event.message?.role === "assistant") responseHasAssistantMessage = true;
|
|
3450
|
+
if (hasVisibleAssistantText(event.message)) {
|
|
2997
3451
|
trackThinkingActivity(event.message);
|
|
2998
3452
|
closeAtAssistantBoundary(event.message, "done");
|
|
2999
3453
|
cleanRun.activity = "done";
|
|
@@ -3002,6 +3456,12 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3002
3456
|
}
|
|
3003
3457
|
trackThinkingActivity(event.message);
|
|
3004
3458
|
trackPendingToolActivity(event.message);
|
|
3459
|
+
if (assistantTerminalState(event.message)) {
|
|
3460
|
+
clearToolActivityHolds();
|
|
3461
|
+
finishCleanGroup("done");
|
|
3462
|
+
settleLastCleanGroup();
|
|
3463
|
+
cleanRun.activity = "done";
|
|
3464
|
+
}
|
|
3005
3465
|
});
|
|
3006
3466
|
|
|
3007
3467
|
pi.on("agent_start", () => {
|
|
@@ -3009,6 +3469,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3009
3469
|
// count until the whole run reaches agent_settled.
|
|
3010
3470
|
if (!cleanRun.active) {
|
|
3011
3471
|
clearToolActivityHolds();
|
|
3472
|
+
responseRunStartedAt = Date.now();
|
|
3012
3473
|
cleanRun.count = 0;
|
|
3013
3474
|
cleanRun.failed = 0;
|
|
3014
3475
|
cleanRun.currentToolCallIds = [];
|
|
@@ -3017,6 +3478,9 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3017
3478
|
cleanRun.lastCompletedToolCallId = undefined;
|
|
3018
3479
|
cleanRun.settled = false;
|
|
3019
3480
|
}
|
|
3481
|
+
// Retries share the original start time but only the newest low-level
|
|
3482
|
+
// run may supply a candidate final answer.
|
|
3483
|
+
responseHasAssistantMessage = false;
|
|
3020
3484
|
cleanRun.active = true;
|
|
3021
3485
|
cleanRun.activeToolCallIds.clear();
|
|
3022
3486
|
cleanRun.activeToolCallId = undefined;
|
|
@@ -3058,6 +3522,8 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3058
3522
|
}
|
|
3059
3523
|
const visibleSummaryToolCallId = cleanRun.activeToolCallId ?? cleanRun.lastCompletedToolCallId;
|
|
3060
3524
|
setCleanGroupMembers(visibleSummaryToolCallId, cleanRun.currentToolCallIds.slice());
|
|
3525
|
+
// Tool success and failure remain owned by the activity group. The bottom
|
|
3526
|
+
// line is reserved for transient UI status messages.
|
|
3061
3527
|
});
|
|
3062
3528
|
pi.on("agent_end", () => {
|
|
3063
3529
|
// No summary is appended here: this event may be followed by an automatic
|
|
@@ -3068,7 +3534,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3068
3534
|
cleanRun.activeToolName = undefined;
|
|
3069
3535
|
cleanRun.activity = "thinking";
|
|
3070
3536
|
});
|
|
3071
|
-
pi.on("agent_settled", () => {
|
|
3537
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
3072
3538
|
if (cleanRun.settled) return;
|
|
3073
3539
|
|
|
3074
3540
|
clearPendingToolActivities();
|
|
@@ -3090,6 +3556,30 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3090
3556
|
cleanRun.activity = "done";
|
|
3091
3557
|
cleanRun.settled = true;
|
|
3092
3558
|
|
|
3559
|
+
if (responseRunStartedAt !== undefined) {
|
|
3560
|
+
// message_end runs before later extensions can replace the message and
|
|
3561
|
+
// before Pi persists it. Read the committed branch at the final boundary
|
|
3562
|
+
// so Copy matches the transcript even when another extension redacts it.
|
|
3563
|
+
const finalEntry = responseHasAssistantMessage
|
|
3564
|
+
? ctx.sessionManager.getBranch().reverse().find(
|
|
3565
|
+
(entry) => entry.type === "message" && entry.message.role === "assistant",
|
|
3566
|
+
)
|
|
3567
|
+
: undefined;
|
|
3568
|
+
const answer = finalEntry && finalEntry.type === "message"
|
|
3569
|
+
? assistantAnswerText(finalEntry.message)
|
|
3570
|
+
: "";
|
|
3571
|
+
if (answer && finalEntry) responseAnswerTexts.set(finalEntry.id, answer);
|
|
3572
|
+
pi.appendEntry<ResponseFooterData>(RESPONSE_FOOTER_ENTRY_TYPE, {
|
|
3573
|
+
answerEntryId: answer ? finalEntry?.id : undefined,
|
|
3574
|
+
durationMs: Math.max(0, Date.now() - responseRunStartedAt),
|
|
3575
|
+
outcome: finalEntry?.type === "message" && assistantTerminalState(finalEntry.message)
|
|
3576
|
+
? "stopped"
|
|
3577
|
+
: "completed",
|
|
3578
|
+
});
|
|
3579
|
+
responseRunStartedAt = undefined;
|
|
3580
|
+
responseHasAssistantMessage = false;
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3093
3583
|
const groups = cleanRun.groups.slice();
|
|
3094
3584
|
if (renderMode !== "clean" || groups.length === 0) return;
|
|
3095
3585
|
|
|
@@ -3129,7 +3619,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3129
3619
|
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
3130
3620
|
if (options.isPartial) return partialResult(context, theme, "Reading…");
|
|
3131
3621
|
const image = toolResult.content?.find((item: any) => item.type === "image");
|
|
3132
|
-
const failed = completeStatus(context
|
|
3622
|
+
const failed = completeStatus(context);
|
|
3133
3623
|
if (image) return result(theme, "Read image", "", false, failed);
|
|
3134
3624
|
const lines = nonEmptyLines(output);
|
|
3135
3625
|
const truncated = toolResult.details?.truncation?.truncated ? " · truncated" : "";
|
|
@@ -3167,7 +3657,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3167
3657
|
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
3168
3658
|
if (useCompactToolView(context.toolCallId, options.expanded)) {
|
|
3169
3659
|
if (options.isPartial) return partialResult(context, theme, "Running…");
|
|
3170
|
-
const failed = completeStatus(context
|
|
3660
|
+
const failed = completeStatus(context);
|
|
3171
3661
|
return result(theme, failed ? "Command failed" : "Done", "", false, failed);
|
|
3172
3662
|
}
|
|
3173
3663
|
|
|
@@ -3175,7 +3665,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3175
3665
|
return bashResult(context, theme, "Running…", output, options.expanded, "running");
|
|
3176
3666
|
}
|
|
3177
3667
|
|
|
3178
|
-
const failed = isError(context
|
|
3668
|
+
const failed = isError(context);
|
|
3179
3669
|
const outputLines = terminalOutputLines(output);
|
|
3180
3670
|
const lineCount = outputLines.filter((line) => line.length > 0).length;
|
|
3181
3671
|
const summary = failed
|
|
@@ -3208,7 +3698,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3208
3698
|
const output = textContent(toolResult);
|
|
3209
3699
|
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
3210
3700
|
if (options.isPartial) return partialResult(context, theme, "Editing…");
|
|
3211
|
-
const failed = completeStatus(context
|
|
3701
|
+
const failed = completeStatus(context);
|
|
3212
3702
|
const diff = toolResult.details?.diff ?? "";
|
|
3213
3703
|
const additions = diff.split("\n").filter((line: string) => line.startsWith("+") && !line.startsWith("+++")).length;
|
|
3214
3704
|
const removals = diff.split("\n").filter((line: string) => line.startsWith("-") && !line.startsWith("---")).length;
|
|
@@ -3257,7 +3747,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3257
3747
|
const output = textContent(toolResult);
|
|
3258
3748
|
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
3259
3749
|
if (options.isPartial) return partialResult(context, theme, "Writing…");
|
|
3260
|
-
const failed = completeStatus(context
|
|
3750
|
+
const failed = completeStatus(context);
|
|
3261
3751
|
return result(
|
|
3262
3752
|
theme,
|
|
3263
3753
|
failed ? output.split("\n")[0] || "Write failed" : "Written",
|
|
@@ -3283,7 +3773,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3283
3773
|
const output = textContent(toolResult);
|
|
3284
3774
|
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
3285
3775
|
if (options.isPartial) return partialResult(context, theme, "Searching…");
|
|
3286
|
-
const failed = completeStatus(context
|
|
3776
|
+
const failed = completeStatus(context);
|
|
3287
3777
|
const matches = nonEmptyLines(output);
|
|
3288
3778
|
return result(theme, failed ? output.split("\n")[0] : `Found ${matches} matches`, output, options.expanded, failed);
|
|
3289
3779
|
},
|
|
@@ -3302,7 +3792,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3302
3792
|
const output = textContent(toolResult);
|
|
3303
3793
|
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
3304
3794
|
if (options.isPartial) return partialResult(context, theme, "Searching…");
|
|
3305
|
-
const failed = completeStatus(context
|
|
3795
|
+
const failed = completeStatus(context);
|
|
3306
3796
|
const matches = nonEmptyLines(output);
|
|
3307
3797
|
return result(theme, failed ? output.split("\n")[0] : `Found ${matches} paths`, output, options.expanded, failed);
|
|
3308
3798
|
},
|
|
@@ -3321,7 +3811,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3321
3811
|
const output = textContent(toolResult);
|
|
3322
3812
|
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
3323
3813
|
if (options.isPartial) return partialResult(context, theme, "Listing…");
|
|
3324
|
-
const failed = completeStatus(context
|
|
3814
|
+
const failed = completeStatus(context);
|
|
3325
3815
|
const entries = nonEmptyLines(output);
|
|
3326
3816
|
return result(theme, failed ? output.split("\n")[0] : `Listed ${entries} entries`, output, options.expanded, failed);
|
|
3327
3817
|
},
|