pi-pretty-tui 0.1.8 → 0.2.0

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.
@@ -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,40 @@ 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
+ assistantSystemBoundary,
36
+ thinkingText,
37
+ visibleAssistantText,
38
+ type ActivityGroup,
39
+ type ActivityMember,
40
+ } from "./activity-timeline.js";
30
41
 
31
42
  type PrettyTuiMode = "full" | "compact" | "clean";
32
43
  const CLEAN_TOOL_ACTIVITY_MIN_MS = 1000;
44
+ const SPECIALIZED_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
45
+
46
+ const stripAnsiBackgrounds = (value: string): string =>
47
+ value.replace(/\x1b\[([0-9:;]*)m/g, (sequence, raw: string) => {
48
+ const fields = raw === "" ? [0] : raw.split(";").map((field) => Number(field));
49
+ const kept: number[] = [];
50
+ for (let index = 0; index < fields.length; index++) {
51
+ const field = fields[index];
52
+ if ((field >= 40 && field <= 49) || (field >= 100 && field <= 107)) continue;
53
+ if (field === 48) {
54
+ const mode = fields[index + 1];
55
+ if (mode === 5) index += 2;
56
+ else if (mode === 2) index += 4;
57
+ continue;
58
+ }
59
+ kept.push(field);
60
+ }
61
+ if (raw.includes(":") && /(?:^|;)48:/u.test(raw)) return "";
62
+ return kept.length > 0 ? `\x1b[${kept.join(";")}m` : "";
63
+ });
33
64
  type PrettyTuiConfig = {
65
+ enabled?: boolean;
34
66
  mode?: PrettyTuiMode;
35
67
  /** Legacy location used by the first mode implementation. */
36
68
  bash?: {
@@ -52,6 +84,7 @@ export default function prettyTui(pi: ExtensionAPI) {
52
84
  const cwd = process.cwd();
53
85
  const configPath = join(getAgentDir(), "pretty-tui.json");
54
86
  let config = loadConfig(configPath);
87
+ const activeForSession = config.enabled !== false;
55
88
  const configuredMode = config.mode ?? config.bash?.mode;
56
89
  let renderMode: PrettyTuiMode = configuredMode === "full" || configuredMode === "compact" || configuredMode === "clean"
57
90
  ? configuredMode
@@ -62,6 +95,16 @@ export default function prettyTui(pi: ExtensionAPI) {
62
95
  let currentTui: any;
63
96
  let currentExtensionUi: any;
64
97
  let changingAllToolsExpansion = false;
98
+ const activityTimeline = new ActivityTimeline();
99
+ const revealedActivityGroups = new Set<string>();
100
+ const expandedThinkingMembers = new Set<string>();
101
+ const thinkingComponents = new Map<string, any>();
102
+ const activityFallbackThemes = new Map<string, any>();
103
+ const activityUpdateComponents = new Map<string, Component>();
104
+ const customUpdateToolHints = new Map<string, string>();
105
+ const customUpdateBeforeToolHints = new Set<string>();
106
+ let activityUpdateSequence = 0;
107
+ const assistantBoundaryKeys = new Set<string>();
65
108
  const cleanCompactToolCallIds = new Set<string>();
66
109
  const cleanGroupToolCallIds = new Map<string, string[]>();
67
110
  const cleanToolCallGroupOwners = new Map<string, string>();
@@ -77,31 +120,213 @@ export default function prettyTui(pi: ExtensionAPI) {
77
120
 
78
121
  const isCleanGroupRevealed = (toolCallId: string): boolean => {
79
122
  if (cleanCompactToolCallIds.has(toolCallId)) return true;
80
- const groupOwner = cleanToolCallGroupOwners.get(toolCallId);
81
- const groupToolCallIds = groupOwner ? cleanGroupToolCallIds.get(groupOwner) : undefined;
82
- return groupToolCallIds?.some((id) => cleanCompactToolCallIds.has(id)) ?? false;
123
+ const activityGroup = activityTimeline.groupForTool(toolCallId);
124
+ if (activityGroup && activityGroupRevealed(activityGroup)) return true;
125
+ const legacyOwner = cleanToolCallGroupOwners.get(toolCallId);
126
+ const legacyIds = legacyOwner ? cleanGroupToolCallIds.get(legacyOwner) : undefined;
127
+ return legacyIds?.some((id) => cleanCompactToolCallIds.has(id)) ?? false;
83
128
  };
84
129
 
85
130
  const cleanThemeForToolCall = (toolCallId: string): any => {
86
131
  const directTheme = cleanToolThemes.get(toolCallId);
87
132
  if (directTheme) return directTheme;
88
- const groupOwner = cleanToolCallGroupOwners.get(toolCallId);
89
- const groupToolCallIds = groupOwner ? cleanGroupToolCallIds.get(groupOwner) : undefined;
90
- return groupToolCallIds
91
- ?.map((id) => cleanToolThemes.get(id))
92
- .find(Boolean);
133
+ const activityGroup = activityTimeline.groupForTool(toolCallId);
134
+ // Prefer a real tool renderer theme over the Markdown-derived fallback.
135
+ // A group that starts with Thought installs the fallback first; returning
136
+ // it here made later third-party success dots use the heading color.
137
+ if (activityGroup) {
138
+ for (const id of activityGroup.toolCallIds) {
139
+ const activityTheme = cleanToolThemes.get(id);
140
+ if (activityTheme) {
141
+ activityFallbackThemes.set(activityGroup.id, activityTheme);
142
+ return activityTheme;
143
+ }
144
+ }
145
+ }
146
+ const cachedActivityTheme = activityGroup
147
+ ? activityFallbackThemes.get(activityGroup.id)
148
+ : undefined;
149
+ if (cachedActivityTheme) return cachedActivityTheme;
150
+ const legacyOwner = cleanToolCallGroupOwners.get(toolCallId);
151
+ const legacyIds = legacyOwner ? cleanGroupToolCallIds.get(legacyOwner) : undefined;
152
+ return legacyIds?.map((id) => cleanToolThemes.get(id)).find(Boolean);
153
+ };
154
+
155
+ const activityGroupOwner = (group: ActivityGroup): string | undefined => {
156
+ for (let index = group.toolCallIds.length - 1; index >= 0; index--) {
157
+ const toolCallId = group.toolCallIds[index];
158
+ if (settledSummaries.has(toolCallId)) return toolCallId;
159
+ }
160
+ if (cleanRun.activeToolCallId && group.toolCallIds.includes(cleanRun.activeToolCallId)) {
161
+ return cleanRun.activeToolCallId;
162
+ }
163
+ if (cleanRun.lastCompletedToolCallId && group.toolCallIds.includes(cleanRun.lastCompletedToolCallId)) {
164
+ return cleanRun.lastCompletedToolCallId;
165
+ }
166
+ return group.toolCallIds[group.toolCallIds.length - 1];
167
+ };
168
+
169
+ const defaultActivityTheme = () => {
170
+ const markdownTheme = getMarkdownTheme();
171
+ return {
172
+ fg: (color: string, text: string) => {
173
+ if (color === "accent") return markdownTheme.listBullet(text);
174
+ if (color === "success" || color === "syntaxComment") return markdownTheme.codeBlock(text);
175
+ if (color === "thinkingLow") return markdownTheme.link(text);
176
+ if (color === "dim" || color === "muted" || color === "thinkingText") {
177
+ return markdownTheme.quote(text);
178
+ }
179
+ return text;
180
+ },
181
+ bold: markdownTheme.bold,
182
+ italic: markdownTheme.italic,
183
+ };
184
+ };
185
+
186
+ const activityGroupTheme = (group: ActivityGroup): any => {
187
+ // A Thought can render before its tools are constructed and install a
188
+ // Markdown fallback. Always promote a real tool theme once available so
189
+ // light mode receives explicit, high-contrast text colors.
190
+ for (const toolCallId of group.toolCallIds) {
191
+ const toolTheme = cleanToolThemes.get(toolCallId);
192
+ if (toolTheme) {
193
+ activityFallbackThemes.set(group.id, toolTheme);
194
+ return toolTheme;
195
+ }
196
+ }
197
+ const cached = activityFallbackThemes.get(group.id);
198
+ if (cached) return cached;
199
+ const fallback = defaultActivityTheme();
200
+ activityFallbackThemes.set(group.id, fallback);
201
+ return fallback;
202
+ };
203
+
204
+ const activityGroupRevealed = (group: ActivityGroup): boolean =>
205
+ cleanToolsExpanded || revealedActivityGroups.has(group.id);
206
+
207
+ const activityMemberPosition = (group: ActivityGroup, member: ActivityMember) => ({
208
+ first: group.members[0]?.id === member.id,
209
+ last: group.members[group.members.length - 1]?.id === member.id,
210
+ });
211
+
212
+ const activityTreeStyle = (
213
+ group: ActivityGroup,
214
+ member: ActivityMember,
215
+ width: number,
216
+ memberTheme?: any,
217
+ ) => {
218
+ const { last } = activityMemberPosition(group, member);
219
+ const rawPrefix = last ? " └─ " : " ├─ ";
220
+ const rawContinuation = last ? " " : " │ ";
221
+ const prefixWidth = visibleWidth(rawPrefix);
222
+ if (width <= prefixWidth) {
223
+ return { prefix: "", continuation: "", childWidth: Math.max(1, width), prefixWidth: 0 };
224
+ }
225
+ const resolvedTheme = memberTheme ?? activityGroupTheme(group);
226
+ return {
227
+ prefix: resolvedTheme ? resolvedTheme.fg("dim", rawPrefix) : rawPrefix,
228
+ continuation: resolvedTheme ? resolvedTheme.fg("dim", rawContinuation) : rawContinuation,
229
+ childWidth: Math.max(1, width - prefixWidth),
230
+ prefixWidth,
231
+ };
232
+ };
233
+
234
+ const humanizeCustomType = (value: string): string =>
235
+ value
236
+ .split(/[-_\s]+/u)
237
+ .filter(Boolean)
238
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
239
+ .join(" ") || "Update";
240
+
241
+ const renderActivityUpdate = (
242
+ group: ActivityGroup,
243
+ member: ActivityMember,
244
+ width: number,
245
+ theme = activityGroupTheme(group),
246
+ ): string[] => {
247
+ if (renderMode !== "clean" || !activityGroupRevealed(group)) return [];
248
+ const { prefix, continuation, childWidth } = activityTreeStyle(group, member, width, theme);
249
+ const title = truncateToWidth(member.updateTitle ?? "Update", Math.max(1, childWidth - 2), "…");
250
+ const lines = [theme.fg("accent", "◇ ") + theme.fg("toolTitle", theme.bold(title))];
251
+ if (member.updateContent) {
252
+ const detailWidth = Math.max(1, childWidth - visibleWidth(" │ "));
253
+ const detailLines = wrapTextWithAnsi(theme.fg("muted", member.updateContent), detailWidth);
254
+ lines.push(...detailLines.map((line, index) =>
255
+ theme.fg("dim", index === detailLines.length - 1 ? " └ " : " │ ") + line
256
+ ));
257
+ }
258
+ const decorated = lines.map((line, index) =>
259
+ truncateToWidth((index === 0 ? prefix : continuation) + line, Math.max(1, width), "")
260
+ );
261
+ const position = activityMemberPosition(group, member);
262
+ return position.first
263
+ ? ["", ...renderActivityGroupSummary(group, width), ...decorated]
264
+ : decorated;
93
265
  };
94
266
 
95
- const saveRenderMode = (mode: PrettyTuiMode) => {
96
- config = { ...config, mode };
267
+ const renderPendingActivityUpdate = (member: ActivityMember, width: number): string[] => {
268
+ const theme = defaultActivityTheme();
269
+ const title = truncateToWidth(member.updateTitle ?? "Update", Math.max(1, width - 2), "…");
270
+ const lines = [theme.fg("accent", "◇ ") + theme.fg("toolTitle", theme.bold(title))];
271
+ if (member.updateContent) {
272
+ const detailWidth = Math.max(1, width - visibleWidth(" │ "));
273
+ const details = wrapTextWithAnsi(theme.fg("muted", member.updateContent), detailWidth);
274
+ lines.push(...details.map((line, index) =>
275
+ theme.fg("dim", index === details.length - 1 ? " └ " : " │ ") + line
276
+ ));
277
+ }
278
+ return ["", ...lines.map((line) => truncateToWidth(line, Math.max(1, width), ""))];
279
+ };
280
+
281
+ const handleActivityUpdateMouse = (
282
+ group: ActivityGroup,
283
+ member: ActivityMember,
284
+ event: any,
285
+ ) => {
286
+ const isLeftClick = event.type === "click" && event.button === "left";
287
+ if (!isLeftClick || group.toolCallIds.length === 0) return undefined;
288
+ const position = activityMemberPosition(group, member);
289
+ if (!activityGroupRevealed(group)) {
290
+ if (!position.first) return undefined;
291
+ revealedActivityGroups.add(group.id);
292
+ for (const toolCallId of group.toolCallIds) cleanCompactToolCallIds.add(toolCallId);
293
+ } else {
294
+ const summaryHeight = position.first ? renderActivityGroupSummary(group, event.width).length : 0;
295
+ if (!position.first || event.y <= 0 || event.y > summaryHeight) return undefined;
296
+ revealedActivityGroups.delete(group.id);
297
+ for (const toolCallId of group.toolCallIds) {
298
+ cleanCompactToolCallIds.delete(toolCallId);
299
+ const component = cleanToolComponents.get(toolCallId);
300
+ if (component?.expanded) component.setExpanded(false);
301
+ }
302
+ for (const child of group.members) expandedThinkingMembers.delete(child.id);
303
+ }
304
+ for (const child of group.members) {
305
+ if (child.kind === "tool" && child.toolCallId) {
306
+ cleanToolComponents.get(child.toolCallId)?.updateDisplay?.();
307
+ } else if (child.kind === "thinking" && child.messageKey) {
308
+ thinkingComponents.get(child.messageKey)?.invalidate?.();
309
+ }
310
+ }
311
+ currentTui?.requestRender?.();
312
+ return { handled: true };
313
+ };
314
+
315
+ const saveConfig = (nextConfig: Record<string, any>) => {
316
+ config = nextConfig;
97
317
  mkdirSync(getAgentDir(), { recursive: true });
98
318
  writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
99
319
  };
100
320
 
321
+ const saveRenderMode = (mode: PrettyTuiMode) => saveConfig({ ...config, mode });
322
+ const saveEnabled = (enabled: boolean) => saveConfig({ ...config, enabled });
323
+
101
324
  pi.registerCommand("pretty-tui", {
102
- description: "Configure pi-pretty-tui rendering mode",
325
+ description: "Configure pi-pretty-tui",
103
326
  getArgumentCompletions: (prefix: string) => {
104
327
  const items = [
328
+ { value: "enable", label: "enable", description: "Enable the extension and reload automatically" },
329
+ { value: "disable", label: "disable", description: "Disable the extension and reload automatically" },
105
330
  { value: "full", label: "full", description: "Full tool details and output" },
106
331
  { value: "compact", label: "compact", description: "Concise summaries for all built-in tools" },
107
332
  { value: "clean", label: "clean", description: "Group supported tools into Running/Done status" },
@@ -115,29 +340,67 @@ export default function prettyTui(pi: ExtensionAPI) {
115
340
 
116
341
  if (!requested) {
117
342
  if (!ctx.hasUI) {
118
- ctx.ui.notify(`pi-pretty-tui mode: ${renderMode}`, "info");
343
+ ctx.ui.notify(
344
+ `pi-pretty-tui: ${config.enabled !== false ? "enabled" : "disabled"} · mode: ${renderMode}`,
345
+ "info",
346
+ );
119
347
  return;
120
348
  }
349
+ const enabled = `Enabled — ${config.enabled !== false ? "on" : "off"}`;
121
350
  const full = `Full — full tool details and output${renderMode === "full" ? " (current)" : ""}`;
122
351
  const compact = `Compact — concise summaries for all built-in tools${renderMode === "compact" ? " (current)" : ""}`;
123
352
  const clean = `Clean — group supported tools into Running/Done status${renderMode === "clean" ? " (current)" : ""}`;
124
- const selected = await ctx.ui.select("pi-pretty-tui rendering mode", [full, compact, clean]);
353
+ const selected = await ctx.ui.select("pi-pretty-tui settings", [enabled, full, compact, clean]);
125
354
  if (!selected) return;
126
- requested = selected === full ? "full" : selected === compact ? "compact" : "clean";
355
+ requested = selected === enabled
356
+ ? config.enabled !== false ? "disable" : "enable"
357
+ : selected === full
358
+ ? "full"
359
+ : selected === compact
360
+ ? "compact"
361
+ : "clean";
127
362
  }
128
363
 
129
364
  if (requested === "status") {
130
- ctx.ui.notify(`pi-pretty-tui mode: ${renderMode}`, "info");
365
+ ctx.ui.notify(
366
+ `pi-pretty-tui: ${config.enabled !== false ? "enabled" : "disabled"} · mode: ${renderMode}`,
367
+ "info",
368
+ );
131
369
  return;
132
370
  }
371
+ if (requested === "enable" || requested === "disable") {
372
+ const enabled = requested === "enable";
373
+ try {
374
+ saveEnabled(enabled);
375
+ } catch (error) {
376
+ const message = error instanceof Error ? error.message : String(error);
377
+ ctx.ui.notify(`Could not save pi-pretty-tui setting: ${message}`, "error");
378
+ return;
379
+ }
380
+ ctx.ui.notify(
381
+ `pi-pretty-tui ${enabled ? "enabled" : "disabled"}; reloading…`,
382
+ "info",
383
+ );
384
+ try {
385
+ await ctx.reload();
386
+ return;
387
+ } catch (error) {
388
+ const message = error instanceof Error ? error.message : String(error);
389
+ ctx.ui.notify(`Setting saved, but automatic reload failed: ${message}`, "warning");
390
+ return;
391
+ }
392
+ }
133
393
  if (requested !== "full" && requested !== "compact" && requested !== "clean") {
134
- ctx.ui.notify("Usage: /pretty-tui [full|compact|clean|status]", "error");
394
+ ctx.ui.notify("Usage: /pretty-tui [enable|disable|full|compact|clean|status]", "error");
135
395
  return;
136
396
  }
137
397
 
138
398
  cleanCompactToolCallIds.clear();
399
+ revealedActivityGroups.clear();
400
+ expandedThinkingMembers.clear();
139
401
  renderMode = requested;
140
402
  for (const component of cleanToolComponents.values()) component.updateDisplay?.();
403
+ for (const component of thinkingComponents.values()) component.invalidate?.();
141
404
  try {
142
405
  saveRenderMode(renderMode);
143
406
  ctx.ui.notify(`pi-pretty-tui mode set to: ${renderMode}`, "info");
@@ -148,6 +411,24 @@ export default function prettyTui(pi: ExtensionAPI) {
148
411
  },
149
412
  });
150
413
 
414
+ // Keep the settings command available while disabled, but do not install
415
+ // render patches or override built-in tools until the next enabled reload.
416
+ if (!activeForSession) return;
417
+
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
+
151
432
  // Give Pi's main prompt editor a complete rounded frame. Render the native
152
433
  // editor at a two-column narrower width so cursor layout, wrapping, IME, and
153
434
  // autocomplete remain native, then add one themed border column per side.
@@ -258,12 +539,35 @@ export default function prettyTui(pi: ExtensionAPI) {
258
539
  const top = border(topStart + markdownTheme.bold(title) + topTail + "╮");
259
540
  const bottom = border("╰" + "─".repeat(Math.max(0, frameWidth - 2)) + "╯");
260
541
  const contentWidth = Math.max(1, frameWidth - 4);
261
- const body = content.render(contentWidth).map((line: string) => {
542
+ const body = withTranscriptMarkdown(() => content.render(contentWidth)).map((line: string) => {
262
543
  const padding = " ".repeat(Math.max(0, contentWidth - visibleWidth(line)));
263
544
  return border("│ ") + line + padding + border(" │");
264
545
  });
265
546
  return [top, ...body, bottom].map((line: string) => outer + paintBackground(line));
266
547
  },
548
+ handleMouse(event: any) {
549
+ if (!content.handleMouse) return undefined;
550
+ const sidePad = Math.min(outputPad, Math.max(0, Math.floor((event.width - 1) / 2)));
551
+ const frameWidth = Math.max(1, event.width - sidePad * 2);
552
+ if (frameWidth < 4) {
553
+ return withTranscriptMarkdown(() => content.handleMouse({
554
+ ...event,
555
+ x: event.x - sidePad,
556
+ width: frameWidth,
557
+ }));
558
+ }
559
+ const contentWidth = Math.max(1, frameWidth - 4);
560
+ if (event.y <= 0 || event.x < sidePad + 2 || event.x >= sidePad + 2 + contentWidth) {
561
+ return undefined;
562
+ }
563
+ return withTranscriptMarkdown(() => content.handleMouse({
564
+ ...event,
565
+ x: event.x - sidePad - 2,
566
+ y: event.y - 1,
567
+ width: contentWidth,
568
+ height: Math.max(0, event.height - 2),
569
+ }));
570
+ },
267
571
  invalidate() {
268
572
  content.invalidate?.();
269
573
  },
@@ -291,63 +595,376 @@ export default function prettyTui(pi: ExtensionAPI) {
291
595
  });
292
596
  }
293
597
 
294
- // Pi's built-in assistant component turns hidden thinking into a static
295
- // "Thinking..." label. In collapsed clean mode, omit that label entirely;
296
- // the clean-mode Running(...) row owns activity, while Ctrl+O still reveals
297
- // the thinking content. The component is exported by Pi specifically for
298
- // extension-level rendering customizations, so patch its public methods
299
- // rather than Pi's source.
598
+ const customMessageKey = (message: any): string => {
599
+ if (message?.timestamp !== undefined) {
600
+ const parsed = typeof message.timestamp === "string"
601
+ ? Date.parse(message.timestamp)
602
+ : Number(message.timestamp);
603
+ return Number.isFinite(parsed) ? String(parsed) : String(message.timestamp);
604
+ }
605
+ return `${message?.customType ?? "custom"}:${JSON.stringify(message?.content ?? "")}`;
606
+ };
607
+
608
+ const customMessageText = (message: any): string =>
609
+ typeof message?.content === "string"
610
+ ? message.content
611
+ : Array.isArray(message?.content)
612
+ ? message.content
613
+ .filter((item: any) => item?.type === "text" && typeof item.text === "string")
614
+ .map((item: any) => item.text)
615
+ .join("\n")
616
+ : "";
617
+
618
+ const addPersistentActivityUpdate = (message: any): ActivityMember => {
619
+ const key = customMessageKey(message);
620
+ const title = humanizeCustomType(message?.customType ?? "update");
621
+ const content = customMessageText(message);
622
+ return activityTimeline.addUpdate(key, title, content, true) ??
623
+ activityTimeline.addPendingUpdate(key, title, content, true);
624
+ };
625
+
626
+ const customMessagePrototype = CustomMessageComponent.prototype as any;
627
+ const customMessagePatchKey = Symbol.for("pretty-tui.clean-custom-message");
628
+ if (!customMessagePrototype[customMessagePatchKey]) {
629
+ const originalCustomRender = customMessagePrototype.render;
630
+ const originalCustomHandleMouse = customMessagePrototype.handleMouse;
631
+ const releaseSemanticCustomMessage = (component: any): boolean => {
632
+ if (!component.customRenderer) return false;
633
+ const key = customMessageKey(component.message);
634
+ const member = activityTimeline.memberForUpdate(key);
635
+ if (member) activityUpdateComponents.delete(member.id);
636
+ activityTimeline.removeUpdate(key);
637
+ return true;
638
+ };
639
+ const patchedCustomRender = function (this: any, width: number): string[] {
640
+ if (releaseSemanticCustomMessage(this)) {
641
+ return originalCustomRender.call(this, width);
642
+ }
643
+ const message = this.message;
644
+ const key = customMessageKey(message);
645
+ let member = activityTimeline.memberForUpdate(key);
646
+ if (!member) {
647
+ const hintedToolCallId = customUpdateToolHints.get(key);
648
+ const hintedGroup = hintedToolCallId
649
+ ? activityTimeline.groupForTool(hintedToolCallId)
650
+ : undefined;
651
+ if (hintedGroup) {
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
+ }
663
+ }
664
+ const group = member ? activityTimeline.groupForMember(member.id) : undefined;
665
+ if (renderMode !== "clean" || !member || !group) {
666
+ return originalCustomRender.call(this, width);
667
+ }
668
+ activityUpdateComponents.set(member.id, this);
669
+ if (group.toolCallIds.length === 0) return renderPendingActivityUpdate(member, width);
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);
677
+ };
678
+ const patchedCustomHandleMouse = function (this: any, event: any) {
679
+ if (releaseSemanticCustomMessage(this)) {
680
+ return originalCustomHandleMouse?.call(this, event);
681
+ }
682
+ const member = activityTimeline.memberForUpdate(customMessageKey(this.message));
683
+ const group = member ? activityTimeline.groupForMember(member.id) : undefined;
684
+ if (renderMode !== "clean" || !member || !group) {
685
+ return originalCustomHandleMouse?.call(this, event);
686
+ }
687
+ if (group.toolCallIds.length === 0) return undefined;
688
+ return handleActivityUpdateMouse(group, member, event);
689
+ };
690
+ customMessagePrototype[customMessagePatchKey] = {
691
+ originalRender: originalCustomRender,
692
+ patchedRender: patchedCustomRender,
693
+ originalHandleMouse: originalCustomHandleMouse,
694
+ patchedHandleMouse: patchedCustomHandleMouse,
695
+ };
696
+ customMessagePrototype.render = patchedCustomRender;
697
+ customMessagePrototype.handleMouse = patchedCustomHandleMouse;
698
+ pi.on("session_shutdown", () => {
699
+ const patch = customMessagePrototype[customMessagePatchKey];
700
+ if (!patch) return;
701
+ if (patch.patchedRender === customMessagePrototype.render) {
702
+ customMessagePrototype.render = patch.originalRender;
703
+ }
704
+ if (patch.patchedHandleMouse === customMessagePrototype.handleMouse) {
705
+ customMessagePrototype.handleMouse = patch.originalHandleMouse;
706
+ }
707
+ if (
708
+ customMessagePrototype.render === patch.originalRender &&
709
+ customMessagePrototype.handleMouse === patch.originalHandleMouse
710
+ ) {
711
+ delete customMessagePrototype[customMessagePatchKey];
712
+ }
713
+ });
714
+ }
715
+
716
+ // Thinking and tools are peers in the transcript-first activity timeline.
717
+ // Assistant components keep Pi's native Markdown renderer, while clean mode
718
+ // projects each thinking run as a compact, independently expandable child.
300
719
  const assistantPrototype = AssistantMessageComponent.prototype as any;
301
720
  const thinkingPatchKey = Symbol.for("pretty-tui.clean-thinking");
302
721
  if (!assistantPrototype[thinkingPatchKey]) {
303
722
  const originalUpdateContent = assistantPrototype.updateContent;
304
723
  const originalRender = assistantPrototype.render;
724
+ const originalHandleMouse = assistantPrototype.handleMouse;
305
725
  const originalMessageKey = Symbol("pretty-tui.original-assistant-message");
306
726
  const renderedModeKey = Symbol("pretty-tui.rendered-assistant-mode");
307
- const renderedExpansionKey = Symbol("pretty-tui.rendered-thinking-expansion");
727
+ const renderedExpansionKey = Symbol("pretty-tui.rendered-assistant-expansion");
728
+ const cleanThinkingRenderCacheKey = Symbol("pretty-tui.clean-thinking-render-cache");
729
+ const cleanThinkingDetailKey = Symbol("pretty-tui.clean-thinking-detail");
730
+
731
+ const eligibleThinking = (message: any) =>
732
+ Array.isArray(message?.content) &&
733
+ message.content.some(
734
+ (item: any) => item?.type === "thinking" &&
735
+ typeof item.thinking === "string" &&
736
+ item.thinking.length > 0,
737
+ ) &&
738
+ !assistantSystemBoundary(message);
739
+
740
+ const refreshGroup = (group: ActivityGroup) => {
741
+ for (const member of group.members) {
742
+ if (member.kind === "tool" && member.toolCallId) {
743
+ cleanToolComponents.get(member.toolCallId)?.updateDisplay?.();
744
+ } else if (member.kind === "thinking" && member.messageKey) {
745
+ thinkingComponents.get(member.messageKey)?.invalidate?.();
746
+ }
747
+ }
748
+ currentTui?.requestRender?.();
749
+ };
308
750
 
309
751
  const patchedUpdateContent = function (this: any, message: any, isStreaming = this.isStreaming) {
752
+ this[originalMessageKey] = message;
753
+ this[renderedModeKey] = renderMode;
754
+ this[renderedExpansionKey] = cleanToolsExpanded;
755
+ this.lastMessage = message;
310
756
  if (renderMode !== "clean") {
311
757
  originalUpdateContent.call(this, message, isStreaming);
312
- this[originalMessageKey] = message;
313
- this[renderedModeKey] = renderMode;
314
- this[renderedExpansionKey] = undefined;
758
+ this.lastMessage = message;
315
759
  return;
316
760
  }
761
+ const groupedThinking = eligibleThinking(message);
762
+ if (groupedThinking) {
763
+ const key = assistantMessageKey(message);
764
+ activityTimeline.addThinking(key, thinkingText(message));
765
+ thinkingComponents.set(key, this);
766
+ }
317
767
 
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
768
  const previousHideThinkingBlock = this.hideThinkingBlock;
327
769
  this.hideThinkingBlock = false;
770
+ // Clean mode owns grouped thinking completely. Removing it from Pi's
771
+ // native content tree also removes the native MouseRegion that would
772
+ // otherwise toggle a second, unrelated `Thinking...` block.
773
+ const displayMessage =
774
+ groupedThinking && Array.isArray(message?.content)
775
+ ? { ...message, content: message.content.filter((item: any) => item.type !== "thinking") }
776
+ : message;
328
777
  try {
329
778
  originalUpdateContent.call(this, displayMessage, isStreaming);
330
779
  } finally {
331
780
  this.hideThinkingBlock = previousHideThinkingBlock;
332
781
  }
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
782
  this.lastMessage = message;
340
783
  };
341
784
 
342
- const patchedRender = function (this: any, width: number): string[] {
343
- const needsRefresh = this[originalMessageKey] && (
344
- this[renderedModeKey] !== renderMode ||
345
- (renderMode === "clean" && this[renderedExpansionKey] !== cleanToolsExpanded)
785
+ const renderAssistantTranscript = function (this: any, width: number): string[] {
786
+ const message = this[originalMessageKey] ?? this.lastMessage;
787
+ if (
788
+ message &&
789
+ (this[renderedModeKey] !== renderMode || this[renderedExpansionKey] !== cleanToolsExpanded)
790
+ ) {
791
+ patchedUpdateContent.call(this, message, this.isStreaming);
792
+ }
793
+ if (renderMode !== "clean" || !eligibleThinking(message)) {
794
+ return originalRender.call(this, width);
795
+ }
796
+
797
+ const key = assistantMessageKey(message);
798
+ const member = activityTimeline.memberForThinking(key);
799
+ const group = member ? activityTimeline.groupForMember(member.id) : undefined;
800
+ if (!member || !group) {
801
+ // Timeline ownership can be briefly unavailable during transcript
802
+ // rebuilds. Fail open to Pi's filtered native renderer so visible
803
+ // assistant text is never lost.
804
+ return originalRender.call(this, width);
805
+ }
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
+ thinkingComponents.set(key, this);
814
+ if (!activityFallbackThemes.has(group.id)) {
815
+ activityFallbackThemes.set(group.id, defaultActivityTheme());
816
+ }
817
+ const position = activityMemberPosition(group, member);
818
+ const visibleLines = visibleAssistantText(message) ? originalRender.call(this, width) : [];
819
+ const revealed = activityGroupRevealed(group);
820
+ if (!revealed) {
821
+ const activityLines = position.first
822
+ ? ["", ...renderActivityGroupSummary(group, width, true)]
823
+ : [];
824
+ return [...activityLines, ...visibleLines];
825
+ }
826
+
827
+ const groupTheme = activityGroupTheme(group);
828
+ const { prefix, continuation, childWidth } = activityTreeStyle(
829
+ group,
830
+ member,
831
+ width,
832
+ groupTheme,
833
+ );
834
+ const expanded = cleanToolsExpanded || expandedThinkingMembers.has(member.id);
835
+ const settledOwner = position.first ? activityGroupOwner(group) : undefined;
836
+ const stableSummary = !position.first || Boolean(
837
+ settledOwner && settledSummaries.has(settledOwner) && !toolActivityHolds.has(settledOwner),
838
+ );
839
+ const cacheable = !expanded && !this.isStreaming && stableSummary;
840
+ const cached = cacheable ? this[cleanThinkingRenderCacheKey] : undefined;
841
+ if (
842
+ cached?.width === width &&
843
+ cached?.message === message &&
844
+ cached?.theme === groupTheme &&
845
+ cached?.memberCount === group.members.length &&
846
+ cached?.last === position.last
847
+ ) {
848
+ // Only the stable activity projection is cached. Visible assistant
849
+ // text belongs outside the collapsible group and must be appended on
850
+ // every render, including cache hits.
851
+ return [...cached.lines, ...visibleLines];
852
+ }
853
+ const stateLabel = this.isStreaming ? "thinking" : "thought";
854
+ const label = truncateToWidth(stateLabel, Math.max(1, childWidth - 2), "…");
855
+ const header = groupTheme.fg("thinkingLow", "● ") +
856
+ groupTheme.fg("toolTitle", groupTheme.bold(label));
857
+ let contentLines: string[] = [header];
858
+ if (expanded) {
859
+ const detailWidth = Math.max(1, childWidth - visibleWidth(" │ "));
860
+ const detailText = member.thinking ?? thinkingText(message);
861
+ let detail = this[cleanThinkingDetailKey];
862
+ if (!detail || detail.text !== detailText) {
863
+ detail = {
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
881
+ ));
882
+ }
883
+ const decorated = contentLines.map((line, index) =>
884
+ truncateToWidth((index === 0 ? prefix : continuation) + line, Math.max(1, width), "")
346
885
  );
347
- if (needsRefresh) {
348
- patchedUpdateContent.call(this, this[originalMessageKey], this.isStreaming);
886
+ const output = position.first
887
+ ? ["", ...renderActivityGroupSummary(group, width), ...decorated]
888
+ : decorated;
889
+ if (cacheable) {
890
+ this[cleanThinkingRenderCacheKey] = {
891
+ width,
892
+ message,
893
+ theme: groupTheme,
894
+ memberCount: group.members.length,
895
+ last: position.last,
896
+ lines: output,
897
+ };
349
898
  }
350
- return originalRender.call(this, width);
899
+ return [...output, ...visibleLines];
900
+ };
901
+
902
+ const patchedRender = function (this: any, width: number): string[] {
903
+ return withTranscriptMarkdown(() => renderAssistantTranscript.call(this, width));
904
+ };
905
+
906
+ const handleAssistantTranscriptMouse = function (this: any, event: any) {
907
+ const message = this[originalMessageKey] ?? this.lastMessage;
908
+ if (renderMode !== "clean" || !eligibleThinking(message)) {
909
+ return originalHandleMouse.call(this, event);
910
+ }
911
+ const key = assistantMessageKey(message);
912
+ const member = activityTimeline.memberForThinking(key);
913
+ const group = member ? activityTimeline.groupForMember(member.id) : undefined;
914
+ if (!member || !group) return originalHandleMouse.call(this, event);
915
+ if (group.toolCallIds.length === 0) return originalHandleMouse.call(this, event);
916
+ const position = activityMemberPosition(group, member);
917
+ const isLeftClick = event.type === "click" && event.button === "left";
918
+
919
+ // A completed assistant message can contain both the final Thought and
920
+ // visible Markdown. The Markdown is rendered after the activity rows;
921
+ // route clicks in that suffix back to Pi's native component so links,
922
+ // text selection, and code-block Copy controls keep working.
923
+ if (visibleAssistantText(message)) {
924
+ const visibleLines = originalRender.call(this, event.width);
925
+ const renderedLines = patchedRender.call(this, event.width);
926
+ const activityHeight = Math.max(0, renderedLines.length - visibleLines.length);
927
+ if (event.y >= activityHeight) {
928
+ return originalHandleMouse.call(this, {
929
+ ...event,
930
+ y: event.y - activityHeight,
931
+ height: visibleLines.length,
932
+ });
933
+ }
934
+ }
935
+
936
+ if (!activityGroupRevealed(group)) {
937
+ if (!isLeftClick) return undefined;
938
+ if (position.first) {
939
+ revealedActivityGroups.add(group.id);
940
+ for (const toolCallId of group.toolCallIds) cleanCompactToolCallIds.add(toolCallId);
941
+ refreshGroup(group);
942
+ }
943
+ return { handled: true };
944
+ }
945
+ const summaryHeight = position.first ? renderActivityGroupSummary(group, event.width).length : 0;
946
+ if (position.first && event.y > 0 && event.y <= summaryHeight && isLeftClick) {
947
+ revealedActivityGroups.delete(group.id);
948
+ for (const toolCallId of group.toolCallIds) {
949
+ cleanCompactToolCallIds.delete(toolCallId);
950
+ const component = cleanToolComponents.get(toolCallId);
951
+ if (component?.expanded) component.setExpanded(false);
952
+ }
953
+ for (const child of group.members) expandedThinkingMembers.delete(child.id);
954
+ refreshGroup(group);
955
+ return { handled: true };
956
+ }
957
+ if (isLeftClick) {
958
+ if (expandedThinkingMembers.has(member.id)) expandedThinkingMembers.delete(member.id);
959
+ else expandedThinkingMembers.add(member.id);
960
+ refreshGroup(group);
961
+ return { handled: true };
962
+ }
963
+ return undefined;
964
+ };
965
+
966
+ const patchedHandleMouse = function (this: any, event: any) {
967
+ return withTranscriptMarkdown(() => handleAssistantTranscriptMouse.call(this, event));
351
968
  };
352
969
 
353
970
  assistantPrototype[thinkingPatchKey] = {
@@ -355,9 +972,12 @@ export default function prettyTui(pi: ExtensionAPI) {
355
972
  patchedUpdateContent,
356
973
  originalRender,
357
974
  patchedRender,
975
+ originalHandleMouse,
976
+ patchedHandleMouse,
358
977
  };
359
978
  assistantPrototype.updateContent = patchedUpdateContent;
360
979
  assistantPrototype.render = patchedRender;
980
+ assistantPrototype.handleMouse = patchedHandleMouse;
361
981
 
362
982
  pi.on("session_shutdown", () => {
363
983
  const patch = assistantPrototype[thinkingPatchKey];
@@ -368,9 +988,13 @@ export default function prettyTui(pi: ExtensionAPI) {
368
988
  if (patch.patchedRender === assistantPrototype.render) {
369
989
  assistantPrototype.render = patch.originalRender;
370
990
  }
991
+ if (patch.patchedHandleMouse === assistantPrototype.handleMouse) {
992
+ assistantPrototype.handleMouse = patch.originalHandleMouse;
993
+ }
371
994
  if (
372
995
  assistantPrototype.updateContent === patch.originalUpdateContent &&
373
- assistantPrototype.render === patch.originalRender
996
+ assistantPrototype.render === patch.originalRender &&
997
+ assistantPrototype.handleMouse === patch.originalHandleMouse
374
998
  ) {
375
999
  delete assistantPrototype[thinkingPatchKey];
376
1000
  }
@@ -403,20 +1027,76 @@ export default function prettyTui(pi: ExtensionAPI) {
403
1027
  ];
404
1028
  };
405
1029
 
1030
+ const indexDisplayedCustomUpdateHints = (entries: any[]) => {
1031
+ customUpdateToolHints.clear();
1032
+ customUpdateBeforeToolHints.clear();
1033
+ let lastToolCallId: string | undefined;
1034
+ let pendingCustomKeys: string[] = [];
1035
+ for (const entry of entries) {
1036
+ if (entry?.type === "compaction") {
1037
+ lastToolCallId = undefined;
1038
+ pendingCustomKeys = [];
1039
+ continue;
1040
+ }
1041
+ if (entry?.type === "custom_message") {
1042
+ if (entry.display !== false) {
1043
+ const key = customMessageKey(entry);
1044
+ if (lastToolCallId) customUpdateToolHints.set(key, lastToolCallId);
1045
+ else pendingCustomKeys.push(key);
1046
+ }
1047
+ continue;
1048
+ }
1049
+ if (entry?.type !== "message") continue;
1050
+ const message = entry.message;
1051
+ if (message?.role === "user") {
1052
+ lastToolCallId = undefined;
1053
+ pendingCustomKeys = [];
1054
+ continue;
1055
+ }
1056
+ if (message?.role === "assistant") {
1057
+ const toolCalls = (Array.isArray(message.content) ? message.content : [])
1058
+ .filter((item: any) => item?.type === "toolCall" && item.id);
1059
+ if (toolCalls.length > 0 && pendingCustomKeys.length > 0) {
1060
+ for (const key of pendingCustomKeys) {
1061
+ customUpdateToolHints.set(key, toolCalls[0].id);
1062
+ customUpdateBeforeToolHints.add(key);
1063
+ }
1064
+ pendingCustomKeys = [];
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
+ }
1077
+ };
1078
+
406
1079
  const interactiveModePrototype = InteractiveMode.prototype as any;
407
1080
  const toolsExpansionPatchKey = Symbol.for("pretty-tui.clean-tool-expansion");
408
1081
  if (!interactiveModePrototype[toolsExpansionPatchKey]) {
409
1082
  const originalSetToolsExpanded = interactiveModePrototype.setToolsExpanded;
410
1083
  const originalRenderSessionEntries = interactiveModePrototype.renderSessionEntries;
411
1084
  const originalSwitchTuiMode = interactiveModePrototype.switchTuiMode;
1085
+ const originalShowExtensionNotify = interactiveModePrototype.showExtensionNotify;
412
1086
  const patchedSetToolsExpanded = function (this: any, expanded: boolean) {
413
1087
  cleanToolsExpanded = expanded;
414
1088
  changingAllToolsExpansion = true;
415
- if (!expanded) cleanCompactToolCallIds.clear();
1089
+ if (!expanded) {
1090
+ cleanCompactToolCallIds.clear();
1091
+ revealedActivityGroups.clear();
1092
+ expandedThinkingMembers.clear();
1093
+ }
416
1094
  try {
417
1095
  return originalSetToolsExpanded.call(this, expanded);
418
1096
  } finally {
419
1097
  changingAllToolsExpansion = false;
1098
+ for (const component of thinkingComponents.values()) component.invalidate?.();
1099
+ this.ui?.requestRender?.();
420
1100
  }
421
1101
  };
422
1102
 
@@ -426,11 +1106,9 @@ export default function prettyTui(pi: ExtensionAPI) {
426
1106
  // buildContextEntries() prepends the latest compaction for model context,
427
1107
  // while Pi's live compaction UI appends it chronologically. Keep reloads
428
1108
  // and transcript rebuilds consistent with that live presentation.
429
- return originalRenderSessionEntries.call(
430
- this,
431
- orderContextEntriesForTranscript(entries),
432
- options,
433
- );
1109
+ const orderedEntries = orderContextEntriesForTranscript(entries);
1110
+ indexDisplayedCustomUpdateHints(orderedEntries);
1111
+ return originalRenderSessionEntries.call(this, orderedEntries, options);
434
1112
  };
435
1113
 
436
1114
  const patchedSwitchTuiMode = function (this: any, ...args: any[]) {
@@ -440,6 +1118,43 @@ export default function prettyTui(pi: ExtensionAPI) {
440
1118
  return result;
441
1119
  };
442
1120
 
1121
+ const patchedShowExtensionNotify = function (
1122
+ this: any,
1123
+ message: string,
1124
+ type?: "info" | "warning" | "error",
1125
+ ) {
1126
+ if (renderMode !== "clean" || (type !== undefined && type !== "info")) {
1127
+ return originalShowExtensionNotify.call(this, message, type);
1128
+ }
1129
+ const [firstLine, ...remainingLines] = String(message).split("\n");
1130
+ const updateKey = `info:${++activityUpdateSequence}`;
1131
+ const title = firstLine || "Info";
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);
1138
+ }
1139
+ const component: Component = {
1140
+ render: (width: number) => {
1141
+ if (group.toolCallIds.length === 0) return renderPendingActivityUpdate(member, width);
1142
+ const position = activityMemberPosition(group, member);
1143
+ if (!activityGroupRevealed(group)) {
1144
+ return position.first
1145
+ ? ["", ...renderActivityGroupSummary(group, width, true)]
1146
+ : [];
1147
+ }
1148
+ return renderActivityUpdate(group, member, width);
1149
+ },
1150
+ invalidate() {},
1151
+ handleMouse: (event: any) => handleActivityUpdateMouse(group, member, event),
1152
+ } as Component;
1153
+ activityUpdateComponents.set(member.id, component);
1154
+ this.chatContainer.addChild(component);
1155
+ this.ui?.requestRender?.();
1156
+ };
1157
+
443
1158
  interactiveModePrototype[toolsExpansionPatchKey] = {
444
1159
  originalSetToolsExpanded,
445
1160
  patchedSetToolsExpanded,
@@ -447,10 +1162,13 @@ export default function prettyTui(pi: ExtensionAPI) {
447
1162
  patchedRenderSessionEntries,
448
1163
  originalSwitchTuiMode,
449
1164
  patchedSwitchTuiMode,
1165
+ originalShowExtensionNotify,
1166
+ patchedShowExtensionNotify,
450
1167
  };
451
1168
  interactiveModePrototype.setToolsExpanded = patchedSetToolsExpanded;
452
1169
  interactiveModePrototype.renderSessionEntries = patchedRenderSessionEntries;
453
1170
  interactiveModePrototype.switchTuiMode = patchedSwitchTuiMode;
1171
+ interactiveModePrototype.showExtensionNotify = patchedShowExtensionNotify;
454
1172
 
455
1173
  pi.on("session_shutdown", () => {
456
1174
  fullscreenTui = false;
@@ -467,10 +1185,14 @@ export default function prettyTui(pi: ExtensionAPI) {
467
1185
  if (patch.patchedSwitchTuiMode === interactiveModePrototype.switchTuiMode) {
468
1186
  interactiveModePrototype.switchTuiMode = patch.originalSwitchTuiMode;
469
1187
  }
1188
+ if (patch.patchedShowExtensionNotify === interactiveModePrototype.showExtensionNotify) {
1189
+ interactiveModePrototype.showExtensionNotify = patch.originalShowExtensionNotify;
1190
+ }
470
1191
  if (
471
1192
  interactiveModePrototype.setToolsExpanded === patch.originalSetToolsExpanded &&
472
1193
  interactiveModePrototype.renderSessionEntries === patch.originalRenderSessionEntries &&
473
- interactiveModePrototype.switchTuiMode === patch.originalSwitchTuiMode
1194
+ interactiveModePrototype.switchTuiMode === patch.originalSwitchTuiMode &&
1195
+ interactiveModePrototype.showExtensionNotify === patch.originalShowExtensionNotify
474
1196
  ) {
475
1197
  delete interactiveModePrototype[toolsExpansionPatchKey];
476
1198
  }
@@ -546,6 +1268,7 @@ export default function prettyTui(pi: ExtensionAPI) {
546
1268
  const codeBlockPatchKey = Symbol.for("pretty-tui.code-blocks");
547
1269
  const codeBlockCollectionKey = Symbol("pretty-tui.code-block-collection");
548
1270
  const codeBlockRegionsKey = Symbol("pretty-tui.code-block-regions");
1271
+ const transcriptMarkdownStateKey = Symbol("pretty-tui.transcript-markdown-state");
549
1272
  if (!markdownPrototype[codeBlockPatchKey]) {
550
1273
  const originalInvalidate = markdownPrototype.invalidate;
551
1274
  const originalRender = markdownPrototype.render;
@@ -561,6 +1284,15 @@ export default function prettyTui(pi: ExtensionAPI) {
561
1284
  };
562
1285
 
563
1286
  const patchedRender = function (this: any, width: number): string[] {
1287
+ const scoped = transcriptMarkdownActive();
1288
+ if (this[transcriptMarkdownStateKey] !== scoped) {
1289
+ delete this[codeBlockCollectionKey];
1290
+ delete this[codeBlockRegionsKey];
1291
+ originalInvalidate.call(this);
1292
+ this[transcriptMarkdownStateKey] = scoped;
1293
+ }
1294
+ if (!scoped) return originalRender.call(this, width);
1295
+
564
1296
  const needsRender =
565
1297
  !this.cachedLines || this.cachedText !== this.text || this.cachedWidth !== width;
566
1298
  if (needsRender) this[codeBlockCollectionKey] = [];
@@ -604,6 +1336,10 @@ export default function prettyTui(pi: ExtensionAPI) {
604
1336
  nextTokenType?: string,
605
1337
  styleContext?: any,
606
1338
  ): string[] {
1339
+ if (!transcriptMarkdownActive()) {
1340
+ return originalRenderToken.call(this, token, width, nextTokenType, styleContext);
1341
+ }
1342
+
607
1343
  if (token?.type === "heading") {
608
1344
  const level = Math.max(1, Math.min(6, Number(token.depth) || 1));
609
1345
  const maxWidth = Math.max(1, width);
@@ -766,6 +1502,15 @@ export default function prettyTui(pi: ExtensionAPI) {
766
1502
  };
767
1503
 
768
1504
  const patchedHandleMouse = function (this: any, event: TuiMouseEvent) {
1505
+ // Fullscreen layout hit-testing may dispatch directly to this Markdown
1506
+ // leaf instead of traversing AssistantMessageComponent.handleMouse. The
1507
+ // most recent render therefore records whether this instance belongs to
1508
+ // the main transcript; dynamic render scope alone is not sufficient.
1509
+ const transcriptOwned = transcriptMarkdownActive() || this[transcriptMarkdownStateKey] === true;
1510
+ if (!transcriptOwned) {
1511
+ delete this[codeBlockRegionsKey];
1512
+ return originalHandleMouse?.call(this, event);
1513
+ }
769
1514
  if (event.button === "left" && (event.type === "press" || event.type === "click")) {
770
1515
  const region = (this[codeBlockRegionsKey] ?? []).find(
771
1516
  (candidate: any) =>
@@ -913,10 +1658,24 @@ export default function prettyTui(pi: ExtensionAPI) {
913
1658
  context.state.compactToolStatus = status;
914
1659
  };
915
1660
 
1661
+ const foregroundLuminance = (styled: string): number | undefined => {
1662
+ const trueColor = /\x1b\[38;2;(\d+);(\d+);(\d+)m/.exec(styled);
1663
+ if (!trueColor) return undefined;
1664
+ const [, red, green, blue] = trueColor.map(Number);
1665
+ return red * 0.2126 + green * 0.7152 + blue * 0.0722;
1666
+ };
1667
+
1668
+ const successMarkerColor = (theme: any): string => {
1669
+ const textLuminance = foregroundLuminance(theme.fg("text", "M"));
1670
+ return textLuminance !== undefined && textLuminance < 128
1671
+ ? "success"
1672
+ : "syntaxComment";
1673
+ };
1674
+
916
1675
  const callRow = (theme: any, name: string, detail: string, state: any): DisplayRow => ({
917
1676
  prefix: () => {
918
1677
  const status = (state.compactToolStatus ?? "running") as ToolStatus;
919
- const color = status === "success" ? "success" : status === "error" ? "error" : "dim";
1678
+ const color = status === "success" ? successMarkerColor(theme) : status === "error" ? "error" : "dim";
920
1679
  return theme.fg(color, "● ");
921
1680
  },
922
1681
  continuation: " ",
@@ -930,7 +1689,7 @@ export default function prettyTui(pi: ExtensionAPI) {
930
1689
  const call = (theme: any, name: string, detail: string, state: any) =>
931
1690
  block([callRow(theme, name, detail, state)]);
932
1691
 
933
- const writeCall = (theme: any, path: string, content: string, expanded: boolean, state: any) => {
1692
+ const writeCall = (theme: any, name: string, path: string, content: string, expanded: boolean, state: any) => {
934
1693
  const lines = content.replace(/\t/g, " ").split("\n");
935
1694
  while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
936
1695
 
@@ -938,14 +1697,14 @@ export default function prettyTui(pi: ExtensionAPI) {
938
1697
  const shown = lines.slice(0, expanded ? total : 10);
939
1698
  const remaining = total - shown.length;
940
1699
  const rows: DisplayRow[] = [
941
- callRow(theme, "Write", `${path} · ${total} ${total === 1 ? "line" : "lines"}`, state),
1700
+ callRow(theme, name, `${path} · ${total} ${total === 1 ? "line" : "lines"}`, state),
942
1701
  ];
943
1702
 
944
1703
  for (let index = 0; index < shown.length; index++) {
945
1704
  const isLast = index === shown.length - 1 && remaining === 0;
946
1705
  rows.push({
947
1706
  prefix: theme.fg("dim", isLast ? " └ " : " │ "),
948
- continuation: theme.fg("dim", " │ "),
1707
+ continuation: isLast ? " " : theme.fg("dim", " │ "),
949
1708
  content: theme.fg("toolOutput", shown[index] || " "),
950
1709
  });
951
1710
  }
@@ -978,19 +1737,22 @@ export default function prettyTui(pi: ExtensionAPI) {
978
1737
 
979
1738
  if (expanded && output) {
980
1739
  const lines = output.split("\n");
1740
+ while (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
981
1741
  const shown = lines.slice(0, 40);
982
- for (const line of shown) {
1742
+ const omitted = lines.length - shown.length;
1743
+ for (let index = 0; index < shown.length; index++) {
1744
+ const isLast = index === shown.length - 1 && omitted === 0;
983
1745
  rows.push({
984
- prefix: theme.fg("dim", " │ "),
985
- continuation: theme.fg("dim", " │ "),
986
- content: outputStyle(line || " "),
1746
+ prefix: theme.fg("dim", isLast ? " └ " : " │ "),
1747
+ continuation: isLast ? " " : theme.fg("dim", " │ "),
1748
+ content: outputStyle(shown[index] || " "),
987
1749
  });
988
1750
  }
989
- if (lines.length > shown.length) {
1751
+ if (omitted > 0) {
990
1752
  rows.push({
991
1753
  prefix: theme.fg("muted", " └ "),
992
1754
  continuation: " ",
993
- content: theme.fg("muted", `… ${lines.length - shown.length} more lines`),
1755
+ content: theme.fg("muted", `… ${omitted} more lines`),
994
1756
  });
995
1757
  }
996
1758
  }
@@ -1064,7 +1826,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1064
1826
  const isLast = index === shown.length - 1;
1065
1827
  rows.push({
1066
1828
  prefix: theme.fg("dim", isLast ? " └ " : " │ "),
1067
- continuation: theme.fg("dim", " │ "),
1829
+ continuation: isLast ? " " : theme.fg("dim", " │ "),
1068
1830
  content: theme.fg("toolOutput", shown[index] || " "),
1069
1831
  });
1070
1832
  }
@@ -1081,6 +1843,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1081
1843
  type ToolSummaryGroup = {
1082
1844
  count: number;
1083
1845
  failed: number;
1846
+ thoughtCount?: number;
1084
1847
  lastToolCallId: string;
1085
1848
  toolCallIds?: string[];
1086
1849
  activity?: string;
@@ -1088,12 +1851,12 @@ export default function prettyTui(pi: ExtensionAPI) {
1088
1851
  type ToolSummaryData = {
1089
1852
  count?: number;
1090
1853
  failed?: number;
1854
+ thoughtCount?: number;
1091
1855
  /** Identifies the last tool component for older single-group entries. */
1092
1856
  lastToolCallId?: string;
1093
1857
  /** All groups from a run, used to restore summaries after reload. */
1094
1858
  groups?: ToolSummaryGroup[];
1095
1859
  };
1096
- const supportedTools = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
1097
1860
  type ToolActivityHold = {
1098
1861
  toolCallId: string;
1099
1862
  name: string;
@@ -1102,7 +1865,12 @@ export default function prettyTui(pi: ExtensionAPI) {
1102
1865
  started: boolean;
1103
1866
  timer?: ReturnType<typeof setTimeout>;
1104
1867
  };
1105
- const settledSummaries = new Map<string, { count: number; failed: number; activity: DisplayValue }>();
1868
+ const settledSummaries = new Map<string, {
1869
+ count: number;
1870
+ failed: number;
1871
+ thoughtCount: number;
1872
+ activity: DisplayValue;
1873
+ }>();
1106
1874
  const toolActivityHolds = new Map<string, ToolActivityHold>();
1107
1875
  const legacySummaryLastToolCallIds = new Map<string, string>();
1108
1876
  const knownToolCallIds = new Set<string>();
@@ -1130,8 +1898,64 @@ export default function prettyTui(pi: ExtensionAPI) {
1130
1898
  settled: false,
1131
1899
  };
1132
1900
 
1133
- const toolDisplayName = (name: string): string =>
1134
- name === "ls" ? "List" : name.charAt(0).toUpperCase() + name.slice(1);
1901
+ const toolLabel = (toolName: string, label?: string): string => label || toolName;
1902
+
1903
+ const conciseThirdPartyArgs = (args: any): string => {
1904
+ if (!args || typeof args !== "object" || Array.isArray(args)) return "";
1905
+ const sensitive = /(?:token|secret|password|authorization|credential|api[_-]?key)/iu;
1906
+ const preferred = ["path", "query", "url", "offset", "id"];
1907
+ const entries = Object.entries(args)
1908
+ .filter(([key, value]) => !sensitive.test(key) && ["string", "number", "boolean"].includes(typeof value))
1909
+ .sort(([left], [right]) => {
1910
+ const leftRank = preferred.indexOf(left);
1911
+ const rightRank = preferred.indexOf(right);
1912
+ return (leftRank < 0 ? preferred.length : leftRank) -
1913
+ (rightRank < 0 ? preferred.length : rightRank);
1914
+ })
1915
+ .slice(0, 2)
1916
+ .map(([key, value]) => {
1917
+ const safeValue = String(value)
1918
+ .replace(/(https?:\/\/)[^\s/@:]+:[^\s/@]+@/giu, "$1…:…@")
1919
+ .replace(/([?&](?:token|secret|password|api[_-]?key)=)[^&\s]+/giu, "$1…");
1920
+ const rendered = truncateToWidth(safeValue, 36, "…");
1921
+ return `${key}=${rendered}`;
1922
+ });
1923
+ return entries.join(" · ");
1924
+ };
1925
+
1926
+ const thirdPartyResultSummary = (component: any): string => {
1927
+ const detailsError = component.result?.details?.error;
1928
+ const firstText = component.result?.content?.find(
1929
+ (item: any) => item?.type === "text" && typeof item.text === "string" && item.text.length > 0,
1930
+ )?.text;
1931
+ const text = String(typeof detailsError === "string" ? detailsError : firstText ?? "");
1932
+ const boundedText = text.slice(0, 512);
1933
+ const newline = boundedText.indexOf("\n");
1934
+ const boundedFirstLine = boundedText.slice(0, newline < 0 ? boundedText.length : newline);
1935
+ const firstLine = stripTerminalSequences(boundedFirstLine).trim();
1936
+ if (firstLine) return truncateToWidth(firstLine, 72, "…");
1937
+ if (component.result) return component.result.isError ? "Failed" : "Done";
1938
+ return component.executionStarted ? "Running…" : "Pending";
1939
+ };
1940
+
1941
+ const renderThirdPartyCompact = (component: any, width: number, theme: any): string[] => {
1942
+ const label = toolLabel(component.toolName, component.toolDefinition?.label);
1943
+ const args = conciseThirdPartyArgs(component.args);
1944
+ const dotColor = component.result?.isError
1945
+ ? "error"
1946
+ : component.result && !component.isPartial
1947
+ ? successMarkerColor(theme)
1948
+ : "accent";
1949
+ const title = theme.fg(dotColor, "● ") +
1950
+ theme.fg("accent", theme.bold(label)) +
1951
+ (args ? theme.fg("muted", `(${args})`) : "");
1952
+ const result = thirdPartyResultSummary(component);
1953
+ const resultColor = component.result?.isError ? "error" : "muted";
1954
+ return [
1955
+ truncateToWidth(title, Math.max(1, width), "…"),
1956
+ theme.fg(resultColor, truncateToWidth(` └ ${result}`, Math.max(1, width), "…")),
1957
+ ];
1958
+ };
1135
1959
 
1136
1960
  const clearToolActivityHolds = () => {
1137
1961
  for (const hold of toolActivityHolds.values()) {
@@ -1178,7 +2002,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1178
2002
  toolCallId,
1179
2003
  name,
1180
2004
  until: startedAt + CLEAN_TOOL_ACTIVITY_MIN_MS,
1181
- after: "thinking...",
2005
+ after: "thinking",
1182
2006
  started,
1183
2007
  });
1184
2008
  cleanRun.activeToolName = name;
@@ -1193,14 +2017,14 @@ export default function prettyTui(pi: ExtensionAPI) {
1193
2017
  toolCallId,
1194
2018
  name,
1195
2019
  until: now + CLEAN_TOOL_ACTIVITY_MIN_MS,
1196
- after: "thinking...",
2020
+ after: "thinking",
1197
2021
  started: true,
1198
2022
  };
1199
2023
  toolActivityHolds.set(toolCallId, hold);
1200
2024
  }
1201
2025
  hold.name = name;
1202
2026
  hold.started = true;
1203
- hold.after = "thinking...";
2027
+ hold.after = "thinking";
1204
2028
  scheduleToolActivityRelease(hold);
1205
2029
  };
1206
2030
 
@@ -1214,15 +2038,18 @@ export default function prettyTui(pi: ExtensionAPI) {
1214
2038
  const liveCleanToolCount = (): number =>
1215
2039
  Math.max(cleanRun.count, cleanRun.currentToolCallIds.length);
1216
2040
 
2041
+ const thoughtCountForTool = (toolCallId: string | undefined): number =>
2042
+ toolCallId ? activityTimeline.groupForTool(toolCallId)?.thoughtCount ?? 0 : 0;
2043
+
1217
2044
  const currentCleanActivity = (): string => {
1218
2045
  if (cleanRun.activeToolCallId) {
1219
- return cleanRun.activeToolName ?? cleanRun.activity ?? "thinking...";
2046
+ return cleanRun.activeToolName ?? cleanRun.activity ?? "thinking";
1220
2047
  }
1221
2048
  if (cleanRun.lastCompletedToolCallId) {
1222
2049
  const hold = toolActivityHolds.get(cleanRun.lastCompletedToolCallId);
1223
2050
  if (hold) return heldActivity(hold);
1224
2051
  }
1225
- return cleanRun.activeToolName ?? cleanRun.activity ?? (cleanRun.active ? "thinking..." : "done");
2052
+ return cleanRun.activeToolName ?? cleanRun.activity ?? (cleanRun.active ? "thinking" : "done");
1226
2053
  };
1227
2054
 
1228
2055
  pi.on("session_shutdown", clearToolActivityHolds);
@@ -1238,116 +2065,198 @@ export default function prettyTui(pi: ExtensionAPI) {
1238
2065
  const originalToolRender = toolExecutionPrototype.render;
1239
2066
  const originalToolHandleMouse = toolExecutionPrototype.handleMouse;
1240
2067
  const renderedModeKey = Symbol("pretty-tui.tool-rendered-mode");
1241
- const revealCleanGroup = (groupToolCallIds: string[], ui?: any) => {
1242
- for (const toolCallId of groupToolCallIds) cleanCompactToolCallIds.add(toolCallId);
1243
- for (const toolCallId of groupToolCallIds) cleanToolComponents.get(toolCallId)?.updateDisplay();
2068
+ const cleanChildRenderCacheKey = Symbol("pretty-tui.clean-child-render-cache");
2069
+ const refreshActivityGroup = (group: ActivityGroup) => {
2070
+ for (const member of group.members) {
2071
+ if (member.kind === "tool" && member.toolCallId) {
2072
+ cleanToolComponents.get(member.toolCallId)?.updateDisplay();
2073
+ } else if (member.kind === "thinking" && member.messageKey) {
2074
+ thinkingComponents.get(member.messageKey)?.invalidate?.();
2075
+ }
2076
+ }
2077
+ };
2078
+ const revealCleanGroup = (group: ActivityGroup, ui?: any) => {
2079
+ revealedActivityGroups.add(group.id);
2080
+ for (const toolCallId of group.toolCallIds) cleanCompactToolCallIds.add(toolCallId);
2081
+ refreshActivityGroup(group);
2082
+ ui?.requestRender?.();
2083
+ };
2084
+ const collapseCleanGroup = (group: ActivityGroup, ui?: any) => {
2085
+ revealedActivityGroups.delete(group.id);
2086
+ for (const toolCallId of group.toolCallIds) {
2087
+ const component = cleanToolComponents.get(toolCallId);
2088
+ if (component?.expanded) originalSetExpanded.call(component, false);
2089
+ cleanCompactToolCallIds.delete(toolCallId);
2090
+ }
2091
+ for (const member of group.members) expandedThinkingMembers.delete(member.id);
2092
+ refreshActivityGroup(group);
1244
2093
  ui?.requestRender?.();
1245
2094
  };
1246
2095
  const patchedMarkExecutionStarted = function (this: any) {
1247
- if (supportedTools.has(this.toolName)) {
1248
- if (!cleanRun.currentToolCallIds.includes(this.toolCallId)) {
1249
- cleanRun.currentToolCallIds.push(this.toolCallId);
1250
- }
1251
- cleanRun.activeToolCallIds.add(this.toolCallId);
1252
- cleanToolNames.set(this.toolCallId, toolDisplayName(this.toolName));
1253
- setCleanGroupMembers(this.toolCallId, cleanRun.currentToolCallIds.slice());
1254
- if (typeof this.ui?.requestRender === "function") {
1255
- cleanRun.requestRender = () => this.ui.requestRender();
1256
- }
1257
- cleanRun.active = true;
1258
- cleanRun.activeToolCallId = this.toolCallId;
1259
- beginToolActivity(this.toolCallId, toolDisplayName(this.toolName), true);
1260
- }
2096
+ activityTimeline.addTool(this.toolCallId, this.toolName);
2097
+ if (!cleanRun.currentToolCallIds.includes(this.toolCallId)) {
2098
+ cleanRun.currentToolCallIds.push(this.toolCallId);
2099
+ }
2100
+ cleanRun.activeToolCallIds.add(this.toolCallId);
2101
+ const displayName = toolLabel(this.toolName, this.toolDefinition?.label);
2102
+ cleanToolNames.set(this.toolCallId, displayName);
2103
+ setCleanGroupMembers(this.toolCallId, cleanRun.currentToolCallIds.slice());
2104
+ if (typeof this.ui?.requestRender === "function") {
2105
+ cleanRun.requestRender = () => this.ui.requestRender();
2106
+ }
2107
+ cleanRun.active = true;
2108
+ cleanRun.activeToolCallId = this.toolCallId;
2109
+ beginToolActivity(this.toolCallId, displayName, true);
1261
2110
  return originalMarkExecutionStarted.call(this);
1262
2111
  };
1263
2112
  const patchedSetExpanded = function (this: any, expanded: boolean) {
1264
- const groupToolCallIds = cleanGroupToolCallIds.get(this.toolCallId);
2113
+ const group = activityTimeline.groupForTool(this.toolCallId);
1265
2114
  if (
1266
2115
  renderMode === "clean" &&
1267
2116
  !changingAllToolsExpansion &&
1268
2117
  expanded &&
1269
2118
  !this.expanded &&
1270
- !isCleanGroupRevealed(this.toolCallId) &&
1271
- groupToolCallIds?.length
2119
+ group &&
2120
+ !activityGroupRevealed(group)
1272
2121
  ) {
1273
- revealCleanGroup(groupToolCallIds, this.ui);
2122
+ revealCleanGroup(group, this.ui);
1274
2123
  return;
1275
2124
  }
1276
2125
  return originalSetExpanded.call(this, expanded);
1277
2126
  };
1278
2127
  const patchedToolRender = function (this: any, width: number): string[] {
1279
- if (supportedTools.has(this.toolName)) {
1280
- cleanToolComponents.set(this.toolCallId, this);
1281
- knownToolCallIds.add(this.toolCallId);
1282
- }
2128
+ cleanToolComponents.set(this.toolCallId, this);
2129
+ knownToolCallIds.add(this.toolCallId);
2130
+ const member = activityTimeline.addTool(this.toolCallId, this.toolName);
2131
+ cleanToolNames.set(
2132
+ this.toolCallId,
2133
+ toolLabel(this.toolName, this.toolDefinition?.label),
2134
+ );
1283
2135
  if (this[renderedModeKey] !== renderMode) {
1284
2136
  this[renderedModeKey] = renderMode;
1285
2137
  this.updateDisplay();
1286
2138
  }
1287
2139
 
1288
- const groupOwner = cleanToolCallGroupOwners.get(this.toolCallId);
1289
- const groupToolCallIds = groupOwner ? cleanGroupToolCallIds.get(groupOwner) : undefined;
1290
- const childIndex = groupToolCallIds?.indexOf(this.toolCallId) ?? -1;
1291
- const showAsChild = renderMode === "clean" && childIndex >= 0 && isCleanGroupRevealed(this.toolCallId);
1292
- if (!showAsChild || !groupOwner || !groupToolCallIds) {
1293
- return originalToolRender.call(this, width);
2140
+ const group = activityTimeline.groupForMember(member.id);
2141
+ if (renderMode !== "clean" || !group) return originalToolRender.call(this, width);
2142
+ const position = activityMemberPosition(group, member);
2143
+ if (!activityGroupRevealed(group)) {
2144
+ return position.first
2145
+ ? ["", ...renderActivityGroupSummary(group, width, true)]
2146
+ : [];
2147
+ }
2148
+
2149
+ const childTheme = cleanThemeForToolCall(this.toolCallId) ?? activityGroupTheme(group);
2150
+ const {
2151
+ prefix: childPrefix,
2152
+ continuation,
2153
+ childWidth,
2154
+ } = activityTreeStyle(group, member, width, childTheme);
2155
+ const thirdParty = !SPECIALIZED_TOOL_NAMES.has(this.toolName);
2156
+ const cacheable = !this.expanded && Boolean(this.result) && !this.isPartial;
2157
+ const cached = cacheable ? this[cleanChildRenderCacheKey] : undefined;
2158
+ let decoratedContent: string[];
2159
+ if (
2160
+ cached?.width === width &&
2161
+ cached?.childWidth === childWidth &&
2162
+ cached?.result === this.result &&
2163
+ cached?.args === this.args &&
2164
+ cached?.callRenderer === this.callRendererComponent &&
2165
+ cached?.resultRenderer === this.resultRendererComponent &&
2166
+ cached?.theme === childTheme &&
2167
+ cached?.last === position.last &&
2168
+ cached?.memberCount === group.members.length
2169
+ ) {
2170
+ decoratedContent = cached.lines;
2171
+ } else {
2172
+ let contentLines: string[];
2173
+ if (thirdParty) {
2174
+ const summaryLines = renderThirdPartyCompact(this, childWidth, childTheme);
2175
+ if (this.expanded) {
2176
+ const detailPrefix = " │ ";
2177
+ const nativeLines = originalToolRender.call(
2178
+ this,
2179
+ Math.max(1, childWidth - visibleWidth(detailPrefix)),
2180
+ );
2181
+ const nativeContentLines = nativeLines[0] === "" ? nativeLines.slice(1) : nativeLines;
2182
+ contentLines = [
2183
+ summaryLines[0],
2184
+ ...nativeContentLines.map((line: string, index: number) =>
2185
+ childTheme.fg(
2186
+ "dim",
2187
+ index === nativeContentLines.length - 1 ? " └ " : detailPrefix,
2188
+ ) + stripAnsiBackgrounds(line)
2189
+ ),
2190
+ ];
2191
+ } else {
2192
+ contentLines = summaryLines;
2193
+ }
2194
+ } else {
2195
+ const lines = originalToolRender.call(this, childWidth);
2196
+ if (lines.length === 0) return lines;
2197
+ contentLines = lines[0] === "" ? lines.slice(1) : lines;
2198
+ }
2199
+ decoratedContent = contentLines.map((line: string, index: number) =>
2200
+ truncateToWidth(
2201
+ (index === 0 ? childPrefix : continuation) + line,
2202
+ Math.max(1, width),
2203
+ "",
2204
+ )
2205
+ );
2206
+ if (cacheable) {
2207
+ this[cleanChildRenderCacheKey] = {
2208
+ width,
2209
+ childWidth,
2210
+ result: this.result,
2211
+ args: this.args,
2212
+ callRenderer: this.callRendererComponent,
2213
+ resultRenderer: this.resultRendererComponent,
2214
+ theme: childTheme,
2215
+ last: position.last,
2216
+ memberCount: group.members.length,
2217
+ lines: decoratedContent,
2218
+ };
2219
+ }
1294
2220
  }
1295
-
1296
- const rawChildPrefix = childIndex === groupToolCallIds.length - 1 ? " └─ " : " ├─ ";
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];
2221
+ if (!position.first) return decoratedContent;
2222
+ return ["", ...renderActivityGroupSummary(group, width), ...decoratedContent];
1310
2223
  };
1311
2224
  const patchedToolHandleMouse = function (this: any, event: any) {
1312
- const groupOwner = cleanToolCallGroupOwners.get(this.toolCallId);
1313
- const groupToolCallIds = groupOwner ? cleanGroupToolCallIds.get(groupOwner) : undefined;
1314
- const childIndex = groupToolCallIds?.indexOf(this.toolCallId) ?? -1;
1315
- const showAsChild = renderMode === "clean" && childIndex >= 0 && isCleanGroupRevealed(this.toolCallId);
2225
+ const member = activityTimeline.memberForTool(this.toolCallId);
2226
+ const group = member ? activityTimeline.groupForMember(member.id) : undefined;
1316
2227
  const isLeftClick = event.type === "click" && event.button === "left";
2228
+ if (renderMode !== "clean" || !member || !group) {
2229
+ return originalToolHandleMouse.call(this, event);
2230
+ }
2231
+ const position = activityMemberPosition(group, member);
2232
+ const revealed = activityGroupRevealed(group);
1317
2233
 
1318
- // Pi normally ignores tool clicks until a result exists. Clean mode can
1319
- // still reveal the active call safely because this changes presentation
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);
2234
+ if (!revealed && position.first && isLeftClick) {
2235
+ revealCleanGroup(group, this.ui);
1329
2236
  return { handled: true };
1330
2237
  }
1331
- if (!showAsChild || !groupToolCallIds) return originalToolHandleMouse.call(this, event);
2238
+ if (!revealed) return isLeftClick ? { handled: true } : undefined;
1332
2239
 
1333
- const summaryHeight = childIndex === 0 ? renderCleanGroupSummary(groupOwner!, event.width).length : 0;
1334
- if (
1335
- childIndex === 0 &&
1336
- event.y > 0 &&
1337
- event.y <= summaryHeight &&
1338
- isLeftClick
1339
- ) {
2240
+ const summaryHeight = position.first ? renderActivityGroupSummary(group, event.width).length : 0;
2241
+ if (position.first && event.y > 0 && event.y <= summaryHeight && isLeftClick) {
1340
2242
  changingAllToolsExpansion = true;
1341
2243
  try {
1342
- for (const toolCallId of groupToolCallIds) {
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();
2244
+ collapseCleanGroup(group, this.ui);
1348
2245
  } finally {
1349
2246
  changingAllToolsExpansion = false;
1350
2247
  }
2248
+ return { handled: true };
2249
+ }
2250
+
2251
+ // Self-rendering tools reserve y=0 for Pi's leading spacer. Clean mode
2252
+ // replaces that spacer with the compact header for non-first members.
2253
+ const thirdPartyHeaderY = position.first ? summaryHeight + 1 : 0;
2254
+ if (
2255
+ !SPECIALIZED_TOOL_NAMES.has(this.toolName) &&
2256
+ isLeftClick &&
2257
+ event.y === thirdPartyHeaderY
2258
+ ) {
2259
+ originalSetExpanded.call(this, !this.expanded);
1351
2260
  this.ui?.requestRender?.();
1352
2261
  return { handled: true };
1353
2262
  }
@@ -1358,12 +2267,17 @@ export default function prettyTui(pi: ExtensionAPI) {
1358
2267
  return { handled: true };
1359
2268
  }
1360
2269
 
1361
- const childPrefixWidth = visibleWidth(" ├─ ");
2270
+ const { prefixWidth } = activityTreeStyle(
2271
+ group,
2272
+ member,
2273
+ event.width,
2274
+ cleanThemeForToolCall(this.toolCallId),
2275
+ );
1362
2276
  return originalToolHandleMouse.call(this, {
1363
2277
  ...event,
1364
- x: Math.max(0, event.x - childPrefixWidth),
1365
- y: event.y - summaryHeight + (childIndex === 0 ? 0 : 1),
1366
- width: Math.max(1, event.width - childPrefixWidth),
2278
+ x: Math.max(0, event.x - prefixWidth),
2279
+ y: event.y - summaryHeight + (position.first ? 0 : 1),
2280
+ width: Math.max(1, event.width - prefixWidth),
1367
2281
  });
1368
2282
  };
1369
2283
 
@@ -1413,6 +2327,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1413
2327
  const group: ToolSummaryGroup = {
1414
2328
  count: cleanRun.count,
1415
2329
  failed: cleanRun.failed,
2330
+ thoughtCount: thoughtCountForTool(cleanRun.lastCompletedToolCallId),
1416
2331
  lastToolCallId: cleanRun.lastCompletedToolCallId,
1417
2332
  toolCallIds: cleanRun.currentToolCallIds.slice(),
1418
2333
  activity,
@@ -1421,6 +2336,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1421
2336
  settledSummaries.set(group.lastToolCallId, {
1422
2337
  count: group.count,
1423
2338
  failed: group.failed,
2339
+ thoughtCount: group.thoughtCount ?? 0,
1424
2340
  activity: activityValueForHold(group.lastToolCallId, activity),
1425
2341
  });
1426
2342
  setCleanGroupMembers(group.lastToolCallId, group.toolCallIds ?? []);
@@ -1432,6 +2348,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1432
2348
  cleanRun.lastCompletedToolCallId = undefined;
1433
2349
  cleanRun.activeToolCallId = undefined;
1434
2350
  cleanRun.activeToolName = undefined;
2351
+ activityTimeline.boundary();
1435
2352
  };
1436
2353
 
1437
2354
  const settleLastCleanGroup = () => {
@@ -1441,30 +2358,44 @@ export default function prettyTui(pi: ExtensionAPI) {
1441
2358
  settledSummaries.set(group.lastToolCallId, {
1442
2359
  count: group.count,
1443
2360
  failed: group.failed,
2361
+ thoughtCount: group.thoughtCount ?? 0,
1444
2362
  activity: activityValueForHold(group.lastToolCallId, "done"),
1445
2363
  });
1446
2364
  };
1447
2365
 
1448
- const summaryText = (count: number, _failed: number, activity = "done"): string => {
1449
- const activityText = typeof activity === "string" ? activity : "done";
2366
+ const summaryText = (
2367
+ count: number,
2368
+ _failed: number,
2369
+ thoughtCount: number,
2370
+ activity = "done",
2371
+ ): string => {
2372
+ const rawActivityText = typeof activity === "string" ? activity : "done";
2373
+ const normalizedActivityText = rawActivityText.trim();
2374
+ const activityText = /^thinking(?:\.\.\.)?$/iu.test(normalizedActivityText)
2375
+ ? "thinking"
2376
+ : rawActivityText;
1450
2377
  const countLabel = `${count} tool ${count === 1 ? "call" : "calls"}`;
2378
+ const thoughtLabel = thoughtCount > 0
2379
+ ? ` · ${thoughtCount} ${thoughtCount === 1 ? "thought" : "thoughts"}`
2380
+ : "";
1451
2381
  const activityLabel = activityText !== "done" && activityText.trim()
1452
2382
  ? ` · ${activityText}`
1453
2383
  : "";
1454
- return `${countLabel}${activityLabel}`;
2384
+ return `${countLabel}${thoughtLabel}${activityLabel}`;
1455
2385
  };
1456
2386
 
1457
2387
  const summaryRow = (
1458
2388
  theme: any,
1459
2389
  count: number,
1460
2390
  failed: number,
2391
+ thoughtCount: number,
1461
2392
  activity: DisplayValue = "done",
1462
2393
  collapsedDone = false,
1463
2394
  ): DisplayRow => ({
1464
2395
  prefix: () => {
1465
2396
  const currentActivity = typeof activity === "function" ? activity() : activity;
1466
2397
  const color = currentActivity === "done"
1467
- ? collapsedDone ? "thinkingText" : "success"
2398
+ ? collapsedDone ? "thinkingText" : successMarkerColor(theme)
1468
2399
  : "accent";
1469
2400
  return theme.fg(color, "● ");
1470
2401
  },
@@ -1473,12 +2404,13 @@ export default function prettyTui(pi: ExtensionAPI) {
1473
2404
  const currentActivity = typeof activity === "function" ? activity() : activity;
1474
2405
  const label = currentActivity === "done" ? "Done" : "Running";
1475
2406
  const color = label === "Done"
1476
- ? collapsedDone ? "thinkingText" : "success"
2407
+ ? collapsedDone ? "thinkingText" : successMarkerColor(theme)
1477
2408
  : "accent";
1478
2409
  const detailColor = label === "Done" && collapsedDone ? "thinkingText" : "text";
1479
2410
  return theme.fg(color, theme.bold(label)) +
1480
2411
  theme.fg("dim", "(") +
1481
- theme.fg(detailColor, summaryText(count, failed, currentActivity)) + theme.fg("dim", ")");
2412
+ theme.fg(detailColor, summaryText(count, failed, thoughtCount, currentActivity)) +
2413
+ theme.fg("dim", ")");
1482
2414
  },
1483
2415
  });
1484
2416
 
@@ -1487,18 +2419,70 @@ export default function prettyTui(pi: ExtensionAPI) {
1487
2419
  if (!summaryTheme) return [];
1488
2420
  const settled = settledSummaries.get(lastToolCallId);
1489
2421
  if (settled) {
1490
- return block([summaryRow(summaryTheme, settled.count, settled.failed, settled.activity)]).render(width);
2422
+ return block([summaryRow(
2423
+ summaryTheme,
2424
+ settled.count,
2425
+ settled.failed,
2426
+ settled.thoughtCount,
2427
+ settled.activity,
2428
+ )]).render(width);
1491
2429
  }
1492
2430
  if (cleanRun.activeToolCallId === lastToolCallId) {
1493
2431
  return block([summaryRow(
1494
2432
  summaryTheme,
1495
2433
  liveCleanToolCount(),
1496
2434
  cleanRun.failed,
2435
+ thoughtCountForTool(lastToolCallId),
1497
2436
  () => cleanRun.activeToolName ?? currentCleanActivity(),
1498
2437
  )]).render(width);
1499
2438
  }
1500
2439
  if (cleanRun.lastCompletedToolCallId === lastToolCallId && cleanRun.count > 0) {
1501
- return block([summaryRow(summaryTheme, cleanRun.count, cleanRun.failed, currentCleanActivity)]).render(width);
2440
+ return block([summaryRow(
2441
+ summaryTheme,
2442
+ cleanRun.count,
2443
+ cleanRun.failed,
2444
+ thoughtCountForTool(lastToolCallId),
2445
+ currentCleanActivity,
2446
+ )]).render(width);
2447
+ }
2448
+ return [];
2449
+ };
2450
+
2451
+ const renderActivityGroupSummary = (
2452
+ group: ActivityGroup,
2453
+ width: number,
2454
+ collapsedDone = false,
2455
+ ): string[] => {
2456
+ const fit = (lines: string[]) => lines.map((line) => truncateToWidth(line, Math.max(1, width), ""));
2457
+ const summaryTheme = activityGroupTheme(group);
2458
+ const owner = activityGroupOwner(group);
2459
+ if (!summaryTheme || !owner || group.toolCallIds.length === 0) return [];
2460
+ const settled = settledSummaries.get(owner);
2461
+ if (settled) {
2462
+ return fit(block([summaryRow(
2463
+ summaryTheme,
2464
+ Math.max(settled.count, group.toolCallIds.length),
2465
+ settled.failed,
2466
+ Math.max(settled.thoughtCount, group.thoughtCount),
2467
+ settled.activity,
2468
+ collapsedDone,
2469
+ )]).render(width));
2470
+ }
2471
+ const currentBelongs = cleanRun.activeToolCallId
2472
+ ? group.toolCallIds.includes(cleanRun.activeToolCallId)
2473
+ : false;
2474
+ const lastBelongs = cleanRun.lastCompletedToolCallId
2475
+ ? group.toolCallIds.includes(cleanRun.lastCompletedToolCallId)
2476
+ : false;
2477
+ if (currentBelongs || lastBelongs || !cleanRun.settled) {
2478
+ return fit(block([summaryRow(
2479
+ summaryTheme,
2480
+ Math.max(group.toolCallIds.length, liveCleanToolCount()),
2481
+ cleanRun.failed,
2482
+ group.thoughtCount,
2483
+ () => cleanRun.activeToolName ?? currentCleanActivity(),
2484
+ collapsedDone,
2485
+ )]).render(width));
1502
2486
  }
1503
2487
  return [];
1504
2488
  };
@@ -1511,6 +2495,8 @@ export default function prettyTui(pi: ExtensionAPI) {
1511
2495
  const cleanToolCall = (theme: any, name: string, toolCallId: string): Component => {
1512
2496
  knownToolCallIds.add(toolCallId);
1513
2497
  cleanToolThemes.set(toolCallId, theme);
2498
+ const activityGroup = activityTimeline.groupForTool(toolCallId);
2499
+ if (activityGroup) activityFallbackThemes.set(activityGroup.id, theme);
1514
2500
  return {
1515
2501
  render(width: number): string[] {
1516
2502
  if (renderMode !== "clean") return [];
@@ -1521,6 +2507,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1521
2507
  theme,
1522
2508
  settledSummary.count,
1523
2509
  settledSummary.failed,
2510
+ settledSummary.thoughtCount,
1524
2511
  settledSummary.activity,
1525
2512
  true,
1526
2513
  )]).render(width);
@@ -1532,6 +2519,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1532
2519
  theme,
1533
2520
  liveCleanToolCount(),
1534
2521
  cleanRun.failed,
2522
+ thoughtCountForTool(toolCallId),
1535
2523
  () => cleanRun.activeToolName ?? name,
1536
2524
  )]).render(width);
1537
2525
  }
@@ -1543,7 +2531,13 @@ export default function prettyTui(pi: ExtensionAPI) {
1543
2531
  cleanRun.count > 0 &&
1544
2532
  !cleanRun.activeToolCallId
1545
2533
  ) {
1546
- return block([summaryRow(theme, cleanRun.count, cleanRun.failed, currentCleanActivity)]).render(width);
2534
+ return block([summaryRow(
2535
+ theme,
2536
+ cleanRun.count,
2537
+ cleanRun.failed,
2538
+ thoughtCountForTool(toolCallId),
2539
+ currentCleanActivity,
2540
+ )]).render(width);
1547
2541
  }
1548
2542
 
1549
2543
  // Do not fall back to the per-component pending state here: Pi may
@@ -1562,8 +2556,8 @@ export default function prettyTui(pi: ExtensionAPI) {
1562
2556
  return block([]);
1563
2557
  };
1564
2558
 
1565
- // In collapsed clean mode, every supported tool call is represented by its
1566
- // Running row. Ctrl+O bypasses this and restores the normal full renderer.
2559
+ // Built-ins use specialized compact renderers, while the activity timeline
2560
+ // controls grouping for every tool, including third-party definitions.
1567
2561
  const hideCleanTool = (
1568
2562
  toolCallId: string,
1569
2563
  expanded: boolean,
@@ -1586,6 +2580,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1586
2580
  ? [{
1587
2581
  count: data.count ?? 0,
1588
2582
  failed: data.failed ?? 0,
2583
+ thoughtCount: data.thoughtCount ?? 0,
1589
2584
  lastToolCallId: data.lastToolCallId,
1590
2585
  }]
1591
2586
  : (() => {
@@ -1594,6 +2589,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1594
2589
  ? [{
1595
2590
  count: data?.count ?? 0,
1596
2591
  failed: data?.failed ?? 0,
2592
+ thoughtCount: data?.thoughtCount ?? 0,
1597
2593
  lastToolCallId: summaryCallId,
1598
2594
  }]
1599
2595
  : [];
@@ -1603,11 +2599,10 @@ export default function prettyTui(pi: ExtensionAPI) {
1603
2599
  // absent from an uncompacted branch.
1604
2600
  const missingGroups = groups.filter((group) => !knownToolCallIds.has(group.lastToolCallId));
1605
2601
  if (missingGroups.length === 0) return [];
1606
- // Persisted entries represent settled groups. Older versions could
1607
- // accidentally store the transient responding... activity, so always
1608
- // normalize their display to Done.
2602
+ // Persisted entries represent settled groups, so always normalize their
2603
+ // display to Done even when an older entry stored a transient activity.
1609
2604
  return block(missingGroups.map((group) =>
1610
- summaryRow(theme, group.count, group.failed, "done", true)
2605
+ summaryRow(theme, group.count, group.failed, group.thoughtCount ?? 0, "done", true)
1611
2606
  )).render(width);
1612
2607
  },
1613
2608
  invalidate() {},
@@ -1628,6 +2623,16 @@ export default function prettyTui(pi: ExtensionAPI) {
1628
2623
  cleanToolsExpanded = ctx.ui.getToolsExpanded();
1629
2624
  settledSummaries.clear();
1630
2625
  legacySummaryLastToolCallIds.clear();
2626
+ activityTimeline.clear();
2627
+ revealedActivityGroups.clear();
2628
+ expandedThinkingMembers.clear();
2629
+ thinkingComponents.clear();
2630
+ activityFallbackThemes.clear();
2631
+ activityUpdateComponents.clear();
2632
+ customUpdateToolHints.clear();
2633
+ customUpdateBeforeToolHints.clear();
2634
+ activityUpdateSequence = 0;
2635
+ assistantBoundaryKeys.clear();
1631
2636
  knownToolCallIds.clear();
1632
2637
  cleanCompactToolCallIds.clear();
1633
2638
  cleanGroupToolCallIds.clear();
@@ -1659,9 +2664,15 @@ export default function prettyTui(pi: ExtensionAPI) {
1659
2664
  const explicitSummaryIds = new Set<string>();
1660
2665
 
1661
2666
  const rememberGroup = (group: ToolSummaryGroup, entryId?: string) => {
2667
+ const thoughtCount = Math.max(
2668
+ group.thoughtCount ?? 0,
2669
+ thoughtCountForTool(group.lastToolCallId),
2670
+ );
2671
+ group.thoughtCount = thoughtCount;
1662
2672
  settledSummaries.set(group.lastToolCallId, {
1663
2673
  count: group.count,
1664
2674
  failed: group.failed,
2675
+ thoughtCount,
1665
2676
  activity: "done",
1666
2677
  });
1667
2678
  if (group.toolCallIds?.length) {
@@ -1673,11 +2684,22 @@ export default function prettyTui(pi: ExtensionAPI) {
1673
2684
 
1674
2685
  const finishGroup = () => {
1675
2686
  if (lastToolCallId && count > 0) {
1676
- lastFinishedGroup = { count, failed, lastToolCallId, toolCallIds: [...toolCalls] };
2687
+ lastFinishedGroup = {
2688
+ count,
2689
+ failed,
2690
+ thoughtCount: thoughtCountForTool(lastToolCallId),
2691
+ lastToolCallId,
2692
+ toolCallIds: [...toolCalls],
2693
+ };
1677
2694
  inferredGroups.push(lastFinishedGroup);
1678
2695
  setCleanGroupMembers(lastToolCallId, [...toolCalls]);
1679
2696
  if (!explicitSummaryIds.has(lastToolCallId)) {
1680
- settledSummaries.set(lastToolCallId, { count, failed, activity: "done" });
2697
+ settledSummaries.set(lastToolCallId, {
2698
+ count,
2699
+ failed,
2700
+ thoughtCount: lastFinishedGroup.thoughtCount ?? 0,
2701
+ activity: "done",
2702
+ });
1681
2703
  }
1682
2704
  }
1683
2705
  count = 0;
@@ -1685,13 +2707,20 @@ export default function prettyTui(pi: ExtensionAPI) {
1685
2707
  lastToolCallId = undefined;
1686
2708
  toolCalls.clear();
1687
2709
  completedToolCalls.clear();
2710
+ activityTimeline.boundary();
1688
2711
  };
1689
2712
 
1690
2713
  const inferredGroupsOverlapping = (group: ToolSummaryGroup): ToolSummaryGroup[] => {
1691
2714
  if (!group.toolCallIds?.length) return [];
1692
2715
  const persistedIds = new Set(group.toolCallIds);
1693
2716
  const currentGroup = lastToolCallId && count > 0
1694
- ? [{ count, failed, lastToolCallId, toolCallIds: [...toolCalls] }]
2717
+ ? [{
2718
+ count,
2719
+ failed,
2720
+ thoughtCount: thoughtCountForTool(lastToolCallId),
2721
+ lastToolCallId,
2722
+ toolCallIds: [...toolCalls],
2723
+ }]
1695
2724
  : [];
1696
2725
  return [...inferredGroups, ...currentGroup].filter((inferred) =>
1697
2726
  inferred.toolCallIds?.some((toolCallId) => persistedIds.has(toolCallId)),
@@ -1770,6 +2799,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1770
2799
  const group = {
1771
2800
  count: data.count ?? 0,
1772
2801
  failed: data.failed ?? 0,
2802
+ thoughtCount: data.thoughtCount ?? 0,
1773
2803
  lastToolCallId: data.lastToolCallId,
1774
2804
  };
1775
2805
  rememberGroup(group);
@@ -1781,6 +2811,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1781
2811
  rememberGroup({
1782
2812
  count: data?.count ?? lastFinishedGroup.count,
1783
2813
  failed: data?.failed ?? lastFinishedGroup.failed,
2814
+ thoughtCount: data?.thoughtCount ?? lastFinishedGroup.thoughtCount ?? 0,
1784
2815
  lastToolCallId: lastFinishedGroup.lastToolCallId,
1785
2816
  }, entry.id);
1786
2817
  } else if (lastToolCallId && count > 0) {
@@ -1788,9 +2819,16 @@ export default function prettyTui(pi: ExtensionAPI) {
1788
2819
  rememberGroup({
1789
2820
  count: data?.count ?? count,
1790
2821
  failed: data?.failed ?? failed,
2822
+ thoughtCount: data?.thoughtCount ?? thoughtCountForTool(lastToolCallId),
1791
2823
  lastToolCallId,
1792
2824
  }, entry.id);
1793
2825
  }
2826
+ activityTimeline.boundary();
2827
+ continue;
2828
+ }
2829
+
2830
+ if (entry.type === "custom_message") {
2831
+ if (entry.display !== false) addPersistentActivityUpdate(entry);
1794
2832
  continue;
1795
2833
  }
1796
2834
 
@@ -1803,12 +2841,23 @@ export default function prettyTui(pi: ExtensionAPI) {
1803
2841
  continue;
1804
2842
  }
1805
2843
 
2844
+ if (message.role === "custom") {
2845
+ if (message.display !== false) addPersistentActivityUpdate(message);
2846
+ continue;
2847
+ }
2848
+
1806
2849
  if (message.role === "assistant") {
2850
+ const thinking = thinkingText(message);
2851
+ if (thinking && !assistantSystemBoundary(message)) {
2852
+ activityTimeline.addThinking(assistantMessageKey(message), thinking);
2853
+ }
1807
2854
  if (messageHasVisibleText(message)) finishGroup();
1808
2855
  for (const item of messageContentItems(message)) {
1809
- if (item.type !== "toolCall" || !supportedTools.has(item.name)) continue;
2856
+ if (item.type !== "toolCall" || !item.id) continue;
1810
2857
  toolCalls.add(item.id);
2858
+ activityTimeline.addTool(item.id, item.name ?? "tool");
1811
2859
  }
2860
+ if (assistantSystemBoundary(message)) finishGroup();
1812
2861
  continue;
1813
2862
  }
1814
2863
 
@@ -1829,6 +2878,17 @@ export default function prettyTui(pi: ExtensionAPI) {
1829
2878
 
1830
2879
  pi.on("session_start", (_event, ctx) => restoreCleanSession(ctx));
1831
2880
  pi.on("session_tree", (_event, ctx) => restoreCleanSession(ctx));
2881
+ pi.on("session_before_compact", () => {
2882
+ // Pi emits this for manual, threshold, and overflow-recovery compaction.
2883
+ // Seal the activity group before the compaction indicator is rendered so
2884
+ // completed work never remains labelled Running(... · thinking). A
2885
+ // lifecycle boundary overrides the minimum per-tool activity hold.
2886
+ clearToolActivityHolds();
2887
+ finishCleanGroup("done");
2888
+ settleLastCleanGroup();
2889
+ cleanRun.activity = "done";
2890
+ cleanRun.requestRender?.();
2891
+ });
1832
2892
  pi.on("session_compact", () => {
1833
2893
  // Compaction is a hard chronological boundary. Keep tools completed before
1834
2894
  // it in their own group so expanded hierarchy lines never cross the summary.
@@ -1837,31 +2897,55 @@ export default function prettyTui(pi: ExtensionAPI) {
1837
2897
  // Pre-compaction tools have been summarized intentionally. Their durable
1838
2898
  // fallback rows should not be replayed beside the compacted transcript.
1839
2899
  cleanContextCompacted = true;
2900
+ cleanRun.requestRender?.();
2901
+ });
2902
+ pi.on("session_compact_failed", () => {
2903
+ // The pre-compaction work is still complete even when compaction is
2904
+ // cancelled or fails. Keep it settled and let a retry start a new group.
2905
+ settleLastCleanGroup();
2906
+ cleanRun.activity = "done";
2907
+ cleanRun.requestRender?.();
1840
2908
  });
1841
2909
 
1842
2910
  const hasVisibleAssistantText = (message: any): boolean =>
1843
2911
  message?.role === "assistant" && messageHasVisibleText(message);
1844
2912
 
2913
+ const closeAtAssistantBoundary = (message: any, activity: string): boolean => {
2914
+ const key = assistantMessageKey(message);
2915
+ if (assistantBoundaryKeys.has(key)) return false;
2916
+ assistantBoundaryKeys.add(key);
2917
+ cleanRun.activity = activity;
2918
+ finishCleanGroup(activity);
2919
+ return true;
2920
+ };
2921
+
1845
2922
  const pendingToolCalls = (message: any): any[] =>
1846
2923
  messageContentItems(message).filter(
1847
- (item: any) => item.type === "toolCall" && supportedTools.has(item.name) && item.id,
2924
+ (item: any) => item.type === "toolCall" && item.id,
1848
2925
  );
1849
2926
 
2927
+ const trackThinkingActivity = (message: any): void => {
2928
+ if (message?.role !== "assistant" || assistantSystemBoundary(message)) return;
2929
+ const thinking = thinkingText(message);
2930
+ if (thinking) activityTimeline.addThinking(assistantMessageKey(message), thinking);
2931
+ };
2932
+
1850
2933
  const trackPendingToolActivity = (message: any): boolean => {
1851
2934
  const toolCalls = pendingToolCalls(message);
1852
2935
  if (toolCalls.length === 0) return false;
1853
2936
  for (const toolCall of toolCalls) {
2937
+ activityTimeline.addTool(toolCall.id, toolCall.name ?? "tool");
1854
2938
  if (!cleanRun.currentToolCallIds.includes(toolCall.id)) {
1855
2939
  cleanRun.currentToolCallIds.push(toolCall.id);
1856
2940
  }
1857
- cleanToolNames.set(toolCall.id, toolDisplayName(toolCall.name));
2941
+ cleanToolNames.set(toolCall.id, cleanToolNames.get(toolCall.id) ?? toolCall.name);
1858
2942
  }
1859
2943
  const toolCall = toolCalls[toolCalls.length - 1];
1860
2944
  if (cleanRun.lastCompletedToolCallId === toolCall.id) return false;
1861
2945
  setCleanGroupMembers(toolCall.id, cleanRun.currentToolCallIds.slice());
1862
2946
  cleanRun.active = true;
1863
2947
  cleanRun.activeToolCallId = toolCall.id;
1864
- beginToolActivity(toolCall.id, toolDisplayName(toolCall.name));
2948
+ beginToolActivity(toolCall.id, cleanToolNames.get(toolCall.id) ?? toolCall.name);
1865
2949
  return true;
1866
2950
  };
1867
2951
 
@@ -1881,29 +2965,42 @@ export default function prettyTui(pi: ExtensionAPI) {
1881
2965
  // message_start runs before Pi adds the user component to the transcript,
1882
2966
  // so settling here keeps the previous parent row above that message.
1883
2967
  pi.on("message_start", (event) => {
2968
+ if (event.message.role === "custom" && event.message.display !== false) {
2969
+ addPersistentActivityUpdate(event.message);
2970
+ return;
2971
+ }
1884
2972
  if (event.message.role !== "user") return;
1885
2973
  finishCleanGroup("done");
1886
2974
  settleLastCleanGroup();
1887
2975
  });
1888
2976
 
1889
- // A visible assistant response is the boundary between tool groups. Do
1890
- // this during streaming so a following tool call cannot inherit the prior
1891
- // summary, while message_end keeps the rule correct for non-streaming paths.
2977
+ // A visible assistant response is the boundary between tool groups. Seal
2978
+ // the prior group directly as Done; the response text already communicates
2979
+ // current activity and does not need a duplicate response status.
1892
2980
  pi.on("message_update", (event) => {
1893
2981
  if (hasVisibleAssistantText(event.message)) {
1894
- cleanRun.activity = "responding...";
1895
- finishCleanGroup("responding...");
2982
+ trackThinkingActivity(event.message);
2983
+ clearToolActivityHolds();
2984
+ closeAtAssistantBoundary(event.message, "done");
1896
2985
  return;
1897
2986
  }
2987
+ if (assistantSystemBoundary(event.message)) {
2988
+ closeAtAssistantBoundary(event.message, "done");
2989
+ settleLastCleanGroup();
2990
+ return;
2991
+ }
2992
+ trackThinkingActivity(event.message);
1898
2993
  trackPendingToolActivity(event.message);
1899
2994
  });
1900
2995
  pi.on("message_end", (event) => {
1901
- if (hasVisibleAssistantText(event.message)) {
2996
+ if (hasVisibleAssistantText(event.message) || assistantSystemBoundary(event.message)) {
2997
+ trackThinkingActivity(event.message);
2998
+ closeAtAssistantBoundary(event.message, "done");
1902
2999
  cleanRun.activity = "done";
1903
- finishCleanGroup("done");
1904
3000
  settleLastCleanGroup();
1905
3001
  return;
1906
3002
  }
3003
+ trackThinkingActivity(event.message);
1907
3004
  trackPendingToolActivity(event.message);
1908
3005
  });
1909
3006
 
@@ -1924,25 +3021,25 @@ export default function prettyTui(pi: ExtensionAPI) {
1924
3021
  cleanRun.activeToolCallIds.clear();
1925
3022
  cleanRun.activeToolCallId = undefined;
1926
3023
  cleanRun.activeToolName = undefined;
1927
- cleanRun.activity = "thinking...";
3024
+ cleanRun.activity = "thinking";
1928
3025
  });
1929
3026
  pi.on("tool_execution_start", (event) => {
1930
- if (!supportedTools.has(event.toolName)) return;
3027
+ activityTimeline.addTool(event.toolCallId, event.toolName);
1931
3028
  if (!cleanRun.currentToolCallIds.includes(event.toolCallId)) {
1932
3029
  cleanRun.currentToolCallIds.push(event.toolCallId);
1933
3030
  }
1934
3031
  cleanRun.activeToolCallIds.add(event.toolCallId);
1935
- cleanToolNames.set(event.toolCallId, toolDisplayName(event.toolName));
3032
+ const displayName = cleanToolNames.get(event.toolCallId) ?? event.toolName;
3033
+ cleanToolNames.set(event.toolCallId, displayName);
1936
3034
  setCleanGroupMembers(event.toolCallId, cleanRun.currentToolCallIds.slice());
1937
3035
  cleanRun.active = true;
1938
3036
  cleanRun.activeToolCallId = event.toolCallId;
1939
- beginToolActivity(event.toolCallId, toolDisplayName(event.toolName), true);
3037
+ beginToolActivity(event.toolCallId, displayName, true);
1940
3038
  });
1941
3039
  pi.on("tool_execution_end", (event) => {
1942
- if (!supportedTools.has(event.toolName)) return;
1943
3040
  const activityName = cleanRun.activeToolCallId === event.toolCallId
1944
- ? cleanRun.activeToolName ?? toolDisplayName(event.toolName)
1945
- : toolDisplayName(event.toolName);
3041
+ ? cleanRun.activeToolName ?? cleanToolNames.get(event.toolCallId) ?? event.toolName
3042
+ : cleanToolNames.get(event.toolCallId) ?? event.toolName;
1946
3043
  // Keep the tool activity visible until at least one second after start;
1947
3044
  // this only delays the status transition, never the tool result itself.
1948
3045
  holdToolActivity(event.toolCallId, activityName);
@@ -1957,7 +3054,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1957
3054
  cleanRun.activeToolName = cleanRun.activeToolCallId
1958
3055
  ? cleanToolNames.get(cleanRun.activeToolCallId)
1959
3056
  : undefined;
1960
- cleanRun.activity = cleanRun.activeToolName ?? "thinking...";
3057
+ cleanRun.activity = cleanRun.activeToolName ?? "thinking";
1961
3058
  }
1962
3059
  const visibleSummaryToolCallId = cleanRun.activeToolCallId ?? cleanRun.lastCompletedToolCallId;
1963
3060
  setCleanGroupMembers(visibleSummaryToolCallId, cleanRun.currentToolCallIds.slice());
@@ -1969,7 +3066,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1969
3066
  cleanRun.activeToolCallIds.clear();
1970
3067
  cleanRun.activeToolCallId = undefined;
1971
3068
  cleanRun.activeToolName = undefined;
1972
- cleanRun.activity = "thinking...";
3069
+ cleanRun.activity = "thinking";
1973
3070
  });
1974
3071
  pi.on("agent_settled", () => {
1975
3072
  if (cleanRun.settled) return;
@@ -1982,6 +3079,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1982
3079
  settledSummaries.set(group.lastToolCallId, {
1983
3080
  count: group.count,
1984
3081
  failed: group.failed,
3082
+ thoughtCount: group.thoughtCount ?? 0,
1985
3083
  activity: activityValueForHold(group.lastToolCallId, "done"),
1986
3084
  });
1987
3085
  }
@@ -1997,14 +3095,17 @@ export default function prettyTui(pi: ExtensionAPI) {
1997
3095
 
1998
3096
  const count = groups.reduce((total, group) => total + group.count, 0);
1999
3097
  const failed = groups.reduce((total, group) => total + group.failed, 0);
3098
+ const thoughtCount = groups.reduce((total, group) => total + (group.thoughtCount ?? 0), 0);
2000
3099
  const lastToolCallId = groups[groups.length - 1]?.lastToolCallId;
2001
3100
  pi.appendEntry<ToolSummaryData>("pretty-tui-tool-summary", {
2002
3101
  count,
2003
3102
  failed,
3103
+ thoughtCount,
2004
3104
  lastToolCallId,
2005
- groups: groups.map(({ count, failed, lastToolCallId, toolCallIds }) => ({
3105
+ groups: groups.map(({ count, failed, thoughtCount, lastToolCallId, toolCallIds }) => ({
2006
3106
  count,
2007
3107
  failed,
3108
+ thoughtCount,
2008
3109
  lastToolCallId,
2009
3110
  toolCallIds,
2010
3111
  })),
@@ -2012,15 +3113,16 @@ export default function prettyTui(pi: ExtensionAPI) {
2012
3113
  });
2013
3114
 
2014
3115
  const read = createReadTool(cwd);
3116
+ const readLabel = read.label || read.name;
2015
3117
  pi.registerTool({
2016
3118
  ...read,
2017
3119
  renderShell: "self",
2018
3120
  renderCall(args: any, theme: any, context: any) {
2019
- if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Read", context.toolCallId);
3121
+ if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, readLabel, context.toolCallId);
2020
3122
  const range = args.offset || args.limit
2021
3123
  ? ` · lines ${args.offset ?? 1}${args.limit ? `–${(args.offset ?? 1) + args.limit - 1}` : "+"}`
2022
3124
  : "";
2023
- return call(theme, "Read", `${args.path}${range}`, context.state);
3125
+ return call(theme, readLabel, `${args.path}${range}`, context.state);
2024
3126
  },
2025
3127
  renderResult(toolResult: any, options: any, theme: any, context: any) {
2026
3128
  const output = textContent(toolResult);
@@ -2042,11 +3144,12 @@ export default function prettyTui(pi: ExtensionAPI) {
2042
3144
  } as any);
2043
3145
 
2044
3146
  const bash = createBashTool(cwd);
3147
+ const bashLabel = bash.label || bash.name;
2045
3148
  pi.registerTool({
2046
3149
  ...bash,
2047
3150
  renderShell: "self",
2048
3151
  renderCall(args: any, theme: any, context: any) {
2049
- if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Bash", context.toolCallId);
3152
+ if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, bashLabel, context.toolCallId);
2050
3153
  const command = typeof args.command === "string" ? args.command : "";
2051
3154
  if (useCompactToolView(context.toolCallId, context.expanded)) {
2052
3155
  const commandLines = command.split(/\r\n|\r|\n/);
@@ -2055,9 +3158,9 @@ export default function prettyTui(pi: ExtensionAPI) {
2055
3158
  const detail = omitted > 0
2056
3159
  ? `${firstLine} … (${omitted} more ${omitted === 1 ? "line" : "lines"})`
2057
3160
  : firstLine;
2058
- return call(theme, "Bash", detail, context.state);
3161
+ return call(theme, bashLabel, detail, context.state);
2059
3162
  }
2060
- return call(theme, "Bash", command, context.state);
3163
+ return call(theme, bashLabel, command, context.state);
2061
3164
  },
2062
3165
  renderResult(toolResult: any, options: any, theme: any, context: any) {
2063
3166
  const output = textContent(toolResult);
@@ -2092,13 +3195,14 @@ export default function prettyTui(pi: ExtensionAPI) {
2092
3195
  } as any);
2093
3196
 
2094
3197
  const edit = createEditTool(cwd);
3198
+ const editLabel = edit.label || edit.name;
2095
3199
  pi.registerTool({
2096
3200
  ...edit,
2097
3201
  renderShell: "self",
2098
3202
  renderCall(args: any, theme: any, context: any) {
2099
- if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Edit", context.toolCallId);
3203
+ if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, editLabel, context.toolCallId);
2100
3204
  const count = Array.isArray(args.edits) ? ` · ${args.edits.length} changes` : "";
2101
- return call(theme, "Edit", `${args.path}${count}`, context.state);
3205
+ return call(theme, editLabel, `${args.path}${count}`, context.state);
2102
3206
  },
2103
3207
  renderResult(toolResult: any, options: any, theme: any, context: any) {
2104
3208
  const output = textContent(toolResult);
@@ -2133,20 +3237,21 @@ export default function prettyTui(pi: ExtensionAPI) {
2133
3237
  } as any);
2134
3238
 
2135
3239
  const write = createWriteTool(cwd);
3240
+ const writeLabel = write.label || write.name;
2136
3241
  pi.registerTool({
2137
3242
  ...write,
2138
3243
  renderShell: "self",
2139
3244
  renderCall(args: any, theme: any, context: any) {
2140
- if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Write", context.toolCallId);
3245
+ if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, writeLabel, context.toolCallId);
2141
3246
  const content = typeof args.content === "string" ? args.content : "";
2142
3247
  const path = String(args.path ?? "");
2143
3248
  if (useCompactToolView(context.toolCallId, context.expanded)) {
2144
3249
  const lines = content.replace(/\t/g, " ").split("\n");
2145
3250
  while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
2146
3251
  const total = lines.length;
2147
- return call(theme, "Write", `${path} · ${total} ${total === 1 ? "line" : "lines"}`, context.state);
3252
+ return call(theme, writeLabel, `${path} · ${total} ${total === 1 ? "line" : "lines"}`, context.state);
2148
3253
  }
2149
- return writeCall(theme, path, content, context.expanded, context.state);
3254
+ return writeCall(theme, writeLabel, path, content, context.expanded, context.state);
2150
3255
  },
2151
3256
  renderResult(toolResult: any, options: any, theme: any, context: any) {
2152
3257
  const output = textContent(toolResult);
@@ -2164,14 +3269,15 @@ export default function prettyTui(pi: ExtensionAPI) {
2164
3269
  } as any);
2165
3270
 
2166
3271
  const grep = createGrepTool(cwd);
3272
+ const grepLabel = grep.label || grep.name;
2167
3273
  pi.registerTool({
2168
3274
  ...grep,
2169
3275
  renderShell: "self",
2170
3276
  renderCall(args: any, theme: any, context: any) {
2171
- if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Grep", context.toolCallId);
3277
+ if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, grepLabel, context.toolCallId);
2172
3278
  const where = args.path ? ` · ${args.path}` : "";
2173
3279
  const glob = args.glob ? ` · ${args.glob}` : "";
2174
- return call(theme, "Grep", `${args.pattern}${where}${glob}`, context.state);
3280
+ return call(theme, grepLabel, `${args.pattern}${where}${glob}`, context.state);
2175
3281
  },
2176
3282
  renderResult(toolResult: any, options: any, theme: any, context: any) {
2177
3283
  const output = textContent(toolResult);
@@ -2184,12 +3290,13 @@ export default function prettyTui(pi: ExtensionAPI) {
2184
3290
  } as any);
2185
3291
 
2186
3292
  const find = createFindTool(cwd);
3293
+ const findLabel = find.label || find.name;
2187
3294
  pi.registerTool({
2188
3295
  ...find,
2189
3296
  renderShell: "self",
2190
3297
  renderCall(args: any, theme: any, context: any) {
2191
- if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Find", context.toolCallId);
2192
- return call(theme, "Find", `${args.pattern}${args.path ? ` · ${args.path}` : ""}`, context.state);
3298
+ if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, findLabel, context.toolCallId);
3299
+ return call(theme, findLabel, `${args.pattern}${args.path ? ` · ${args.path}` : ""}`, context.state);
2193
3300
  },
2194
3301
  renderResult(toolResult: any, options: any, theme: any, context: any) {
2195
3302
  const output = textContent(toolResult);
@@ -2202,12 +3309,13 @@ export default function prettyTui(pi: ExtensionAPI) {
2202
3309
  } as any);
2203
3310
 
2204
3311
  const ls = createLsTool(cwd);
3312
+ const lsLabel = ls.label || ls.name;
2205
3313
  pi.registerTool({
2206
3314
  ...ls,
2207
3315
  renderShell: "self",
2208
3316
  renderCall(args: any, theme: any, context: any) {
2209
- if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "List", context.toolCallId);
2210
- return call(theme, "List", args.path ?? ".", context.state);
3317
+ if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, lsLabel, context.toolCallId);
3318
+ return call(theme, lsLabel, args.path ?? ".", context.state);
2211
3319
  },
2212
3320
  renderResult(toolResult: any, options: any, theme: any, context: any) {
2213
3321
  const output = textContent(toolResult);