pi-pretty-tui 0.1.7 → 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
+ };
93
184
  };
94
185
 
95
- const saveRenderMode = (mode: PrettyTuiMode) => {
96
- config = { ...config, mode };
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;
265
+ };
266
+
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),
346
838
  );
347
- if (needsRefresh) {
348
- patchedUpdateContent.call(this, this[originalMessageKey], this.isStreaming);
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), "")
885
+ );
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
+ };
898
+ }
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 };
349
944
  }
350
- return originalRender.call(this, width);
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,100 @@ 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
+
1343
+ if (token?.type === "heading") {
1344
+ const level = Math.max(1, Math.min(6, Number(token.depth) || 1));
1345
+ const maxWidth = Math.max(1, width);
1346
+ const addHeadingSpacing = (lines: string[]) => {
1347
+ if (nextTokenType && nextTokenType !== "space") lines.push("");
1348
+ return lines;
1349
+ };
1350
+ const headingStyle = (text: string) => this.theme.heading(text);
1351
+ const foregroundLuminance = (styled: string): number | undefined => {
1352
+ const trueColor = /\x1b\[38;2;(\d+);(\d+);(\d+)m/.exec(styled);
1353
+ if (!trueColor) return undefined;
1354
+ const [, red, green, blue] = trueColor.map(Number);
1355
+ return red * 0.2126 + green * 0.7152 + blue * 0.0722;
1356
+ };
1357
+ const defaultLuminance = foregroundLuminance(this.applyDefaultStyle("M"));
1358
+ const headingLuminance = foregroundLuminance(headingStyle("M"));
1359
+ const darkTheme =
1360
+ defaultLuminance !== undefined
1361
+ ? defaultLuminance >= 128
1362
+ : (headingLuminance ?? 255) >= 160;
1363
+ const whiteStyle = (text: string) => `\x1b[97m${text}\x1b[39m`;
1364
+ const titleStyle =
1365
+ level === 1 || level === 3
1366
+ ? (text: string) => headingStyle(this.theme.bold(this.theme.underline(text)))
1367
+ : level === 2
1368
+ ? (text: string) =>
1369
+ (darkTheme ? whiteStyle : headingStyle)(
1370
+ this.theme.bold(this.theme.underline(text)),
1371
+ )
1372
+ : level === 4
1373
+ ? (text: string) => headingStyle(this.theme.bold(text))
1374
+ : level === 5
1375
+ ? headingStyle
1376
+ : (text: string) => this.theme.quote(this.theme.italic(text));
1377
+ const titleContext = {
1378
+ applyText: titleStyle,
1379
+ stylePrefix: this.getStylePrefix(titleStyle),
1380
+ };
1381
+ const title = this.renderInlineTokens(token.tokens || [], titleContext);
1382
+
1383
+ if (level === 1 && maxWidth >= 6) {
1384
+ const maximumInnerWidth = maxWidth - 4;
1385
+ const titleLines = wrapTextWithAnsi(title, Math.max(1, maximumInnerWidth));
1386
+ const innerWidth = Math.max(
1387
+ 1,
1388
+ ...titleLines.map((line: string) => visibleWidth(line)),
1389
+ );
1390
+ const frameWidth = Math.min(maxWidth, innerWidth + 4);
1391
+ const frameStyle = (text: string) => headingStyle(this.theme.bold(text));
1392
+ const lines = [
1393
+ frameStyle(`╔${"═".repeat(frameWidth - 2)}╗`),
1394
+ ...titleLines.map((line: string) => {
1395
+ const padding = " ".repeat(Math.max(0, innerWidth - visibleWidth(line)));
1396
+ return frameStyle("║ ") + line + frameStyle(`${padding} ║`);
1397
+ }),
1398
+ frameStyle(`╚${"═".repeat(frameWidth - 2)}╝`),
1399
+ ];
1400
+ return addHeadingSpacing(lines);
1401
+ }
1402
+
1403
+ if (level === 2) {
1404
+ // Reverse only the rendered title cells: the theme's heading color
1405
+ // becomes a compact, content-width background label with no frame.
1406
+ const lines = wrapTextWithAnsi(title, maxWidth).map((line: string) =>
1407
+ darkTheme
1408
+ ? `\x1b[48;5;94m${line}\x1b[49m`
1409
+ : `\x1b[107m\x1b[7m${line}\x1b[27m\x1b[49m`
1410
+ );
1411
+ return addHeadingSpacing(lines);
1412
+ }
1413
+
1414
+ if (level === 5 && maxWidth >= 8) {
1415
+ const prefix = "┄┄ ";
1416
+ const suffix = " ┄┄";
1417
+ const contentWidth = Math.max(
1418
+ 1,
1419
+ maxWidth - visibleWidth(prefix) - visibleWidth(suffix),
1420
+ );
1421
+ const wrapped = wrapTextWithAnsi(title, contentWidth);
1422
+ const lines = wrapped.map((line: string, index: number) => {
1423
+ const left = index === 0 ? this.theme.heading(prefix) : " ".repeat(visibleWidth(prefix));
1424
+ const right = index === wrapped.length - 1 ? this.theme.heading(suffix) : "";
1425
+ return left + line + right;
1426
+ });
1427
+ return addHeadingSpacing(lines);
1428
+ }
1429
+
1430
+ return addHeadingSpacing(wrapTextWithAnsi(title, maxWidth));
1431
+ }
1432
+
607
1433
  if (token?.type !== "code") {
608
1434
  return originalRenderToken.call(this, token, width, nextTokenType, styleContext);
609
1435
  }
@@ -676,6 +1502,15 @@ export default function prettyTui(pi: ExtensionAPI) {
676
1502
  };
677
1503
 
678
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
+ }
679
1514
  if (event.button === "left" && (event.type === "press" || event.type === "click")) {
680
1515
  const region = (this[codeBlockRegionsKey] ?? []).find(
681
1516
  (candidate: any) =>
@@ -823,10 +1658,24 @@ export default function prettyTui(pi: ExtensionAPI) {
823
1658
  context.state.compactToolStatus = status;
824
1659
  };
825
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
+
826
1675
  const callRow = (theme: any, name: string, detail: string, state: any): DisplayRow => ({
827
1676
  prefix: () => {
828
1677
  const status = (state.compactToolStatus ?? "running") as ToolStatus;
829
- const color = status === "success" ? "success" : status === "error" ? "error" : "dim";
1678
+ const color = status === "success" ? successMarkerColor(theme) : status === "error" ? "error" : "dim";
830
1679
  return theme.fg(color, "● ");
831
1680
  },
832
1681
  continuation: " ",
@@ -840,7 +1689,7 @@ export default function prettyTui(pi: ExtensionAPI) {
840
1689
  const call = (theme: any, name: string, detail: string, state: any) =>
841
1690
  block([callRow(theme, name, detail, state)]);
842
1691
 
843
- 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) => {
844
1693
  const lines = content.replace(/\t/g, " ").split("\n");
845
1694
  while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
846
1695
 
@@ -848,14 +1697,14 @@ export default function prettyTui(pi: ExtensionAPI) {
848
1697
  const shown = lines.slice(0, expanded ? total : 10);
849
1698
  const remaining = total - shown.length;
850
1699
  const rows: DisplayRow[] = [
851
- callRow(theme, "Write", `${path} · ${total} ${total === 1 ? "line" : "lines"}`, state),
1700
+ callRow(theme, name, `${path} · ${total} ${total === 1 ? "line" : "lines"}`, state),
852
1701
  ];
853
1702
 
854
1703
  for (let index = 0; index < shown.length; index++) {
855
1704
  const isLast = index === shown.length - 1 && remaining === 0;
856
1705
  rows.push({
857
1706
  prefix: theme.fg("dim", isLast ? " └ " : " │ "),
858
- continuation: theme.fg("dim", " │ "),
1707
+ continuation: isLast ? " " : theme.fg("dim", " │ "),
859
1708
  content: theme.fg("toolOutput", shown[index] || " "),
860
1709
  });
861
1710
  }
@@ -888,19 +1737,22 @@ export default function prettyTui(pi: ExtensionAPI) {
888
1737
 
889
1738
  if (expanded && output) {
890
1739
  const lines = output.split("\n");
1740
+ while (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
891
1741
  const shown = lines.slice(0, 40);
892
- 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;
893
1745
  rows.push({
894
- prefix: theme.fg("dim", " │ "),
895
- continuation: theme.fg("dim", " │ "),
896
- content: outputStyle(line || " "),
1746
+ prefix: theme.fg("dim", isLast ? " └ " : " │ "),
1747
+ continuation: isLast ? " " : theme.fg("dim", " │ "),
1748
+ content: outputStyle(shown[index] || " "),
897
1749
  });
898
1750
  }
899
- if (lines.length > shown.length) {
1751
+ if (omitted > 0) {
900
1752
  rows.push({
901
1753
  prefix: theme.fg("muted", " └ "),
902
1754
  continuation: " ",
903
- content: theme.fg("muted", `… ${lines.length - shown.length} more lines`),
1755
+ content: theme.fg("muted", `… ${omitted} more lines`),
904
1756
  });
905
1757
  }
906
1758
  }
@@ -974,7 +1826,7 @@ export default function prettyTui(pi: ExtensionAPI) {
974
1826
  const isLast = index === shown.length - 1;
975
1827
  rows.push({
976
1828
  prefix: theme.fg("dim", isLast ? " └ " : " │ "),
977
- continuation: theme.fg("dim", " │ "),
1829
+ continuation: isLast ? " " : theme.fg("dim", " │ "),
978
1830
  content: theme.fg("toolOutput", shown[index] || " "),
979
1831
  });
980
1832
  }
@@ -991,6 +1843,7 @@ export default function prettyTui(pi: ExtensionAPI) {
991
1843
  type ToolSummaryGroup = {
992
1844
  count: number;
993
1845
  failed: number;
1846
+ thoughtCount?: number;
994
1847
  lastToolCallId: string;
995
1848
  toolCallIds?: string[];
996
1849
  activity?: string;
@@ -998,12 +1851,12 @@ export default function prettyTui(pi: ExtensionAPI) {
998
1851
  type ToolSummaryData = {
999
1852
  count?: number;
1000
1853
  failed?: number;
1854
+ thoughtCount?: number;
1001
1855
  /** Identifies the last tool component for older single-group entries. */
1002
1856
  lastToolCallId?: string;
1003
1857
  /** All groups from a run, used to restore summaries after reload. */
1004
1858
  groups?: ToolSummaryGroup[];
1005
1859
  };
1006
- const supportedTools = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
1007
1860
  type ToolActivityHold = {
1008
1861
  toolCallId: string;
1009
1862
  name: string;
@@ -1012,7 +1865,12 @@ export default function prettyTui(pi: ExtensionAPI) {
1012
1865
  started: boolean;
1013
1866
  timer?: ReturnType<typeof setTimeout>;
1014
1867
  };
1015
- 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
+ }>();
1016
1874
  const toolActivityHolds = new Map<string, ToolActivityHold>();
1017
1875
  const legacySummaryLastToolCallIds = new Map<string, string>();
1018
1876
  const knownToolCallIds = new Set<string>();
@@ -1040,8 +1898,64 @@ export default function prettyTui(pi: ExtensionAPI) {
1040
1898
  settled: false,
1041
1899
  };
1042
1900
 
1043
- const toolDisplayName = (name: string): string =>
1044
- 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
+ };
1045
1959
 
1046
1960
  const clearToolActivityHolds = () => {
1047
1961
  for (const hold of toolActivityHolds.values()) {
@@ -1088,7 +2002,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1088
2002
  toolCallId,
1089
2003
  name,
1090
2004
  until: startedAt + CLEAN_TOOL_ACTIVITY_MIN_MS,
1091
- after: "thinking...",
2005
+ after: "thinking",
1092
2006
  started,
1093
2007
  });
1094
2008
  cleanRun.activeToolName = name;
@@ -1103,14 +2017,14 @@ export default function prettyTui(pi: ExtensionAPI) {
1103
2017
  toolCallId,
1104
2018
  name,
1105
2019
  until: now + CLEAN_TOOL_ACTIVITY_MIN_MS,
1106
- after: "thinking...",
2020
+ after: "thinking",
1107
2021
  started: true,
1108
2022
  };
1109
2023
  toolActivityHolds.set(toolCallId, hold);
1110
2024
  }
1111
2025
  hold.name = name;
1112
2026
  hold.started = true;
1113
- hold.after = "thinking...";
2027
+ hold.after = "thinking";
1114
2028
  scheduleToolActivityRelease(hold);
1115
2029
  };
1116
2030
 
@@ -1124,15 +2038,18 @@ export default function prettyTui(pi: ExtensionAPI) {
1124
2038
  const liveCleanToolCount = (): number =>
1125
2039
  Math.max(cleanRun.count, cleanRun.currentToolCallIds.length);
1126
2040
 
2041
+ const thoughtCountForTool = (toolCallId: string | undefined): number =>
2042
+ toolCallId ? activityTimeline.groupForTool(toolCallId)?.thoughtCount ?? 0 : 0;
2043
+
1127
2044
  const currentCleanActivity = (): string => {
1128
2045
  if (cleanRun.activeToolCallId) {
1129
- return cleanRun.activeToolName ?? cleanRun.activity ?? "thinking...";
2046
+ return cleanRun.activeToolName ?? cleanRun.activity ?? "thinking";
1130
2047
  }
1131
2048
  if (cleanRun.lastCompletedToolCallId) {
1132
2049
  const hold = toolActivityHolds.get(cleanRun.lastCompletedToolCallId);
1133
2050
  if (hold) return heldActivity(hold);
1134
2051
  }
1135
- return cleanRun.activeToolName ?? cleanRun.activity ?? (cleanRun.active ? "thinking..." : "done");
2052
+ return cleanRun.activeToolName ?? cleanRun.activity ?? (cleanRun.active ? "thinking" : "done");
1136
2053
  };
1137
2054
 
1138
2055
  pi.on("session_shutdown", clearToolActivityHolds);
@@ -1148,116 +2065,198 @@ export default function prettyTui(pi: ExtensionAPI) {
1148
2065
  const originalToolRender = toolExecutionPrototype.render;
1149
2066
  const originalToolHandleMouse = toolExecutionPrototype.handleMouse;
1150
2067
  const renderedModeKey = Symbol("pretty-tui.tool-rendered-mode");
1151
- const revealCleanGroup = (groupToolCallIds: string[], ui?: any) => {
1152
- for (const toolCallId of groupToolCallIds) cleanCompactToolCallIds.add(toolCallId);
1153
- 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);
1154
2093
  ui?.requestRender?.();
1155
2094
  };
1156
2095
  const patchedMarkExecutionStarted = function (this: any) {
1157
- if (supportedTools.has(this.toolName)) {
1158
- if (!cleanRun.currentToolCallIds.includes(this.toolCallId)) {
1159
- cleanRun.currentToolCallIds.push(this.toolCallId);
1160
- }
1161
- cleanRun.activeToolCallIds.add(this.toolCallId);
1162
- cleanToolNames.set(this.toolCallId, toolDisplayName(this.toolName));
1163
- setCleanGroupMembers(this.toolCallId, cleanRun.currentToolCallIds.slice());
1164
- if (typeof this.ui?.requestRender === "function") {
1165
- cleanRun.requestRender = () => this.ui.requestRender();
1166
- }
1167
- cleanRun.active = true;
1168
- cleanRun.activeToolCallId = this.toolCallId;
1169
- beginToolActivity(this.toolCallId, toolDisplayName(this.toolName), true);
1170
- }
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);
1171
2110
  return originalMarkExecutionStarted.call(this);
1172
2111
  };
1173
2112
  const patchedSetExpanded = function (this: any, expanded: boolean) {
1174
- const groupToolCallIds = cleanGroupToolCallIds.get(this.toolCallId);
2113
+ const group = activityTimeline.groupForTool(this.toolCallId);
1175
2114
  if (
1176
2115
  renderMode === "clean" &&
1177
2116
  !changingAllToolsExpansion &&
1178
2117
  expanded &&
1179
2118
  !this.expanded &&
1180
- !isCleanGroupRevealed(this.toolCallId) &&
1181
- groupToolCallIds?.length
2119
+ group &&
2120
+ !activityGroupRevealed(group)
1182
2121
  ) {
1183
- revealCleanGroup(groupToolCallIds, this.ui);
2122
+ revealCleanGroup(group, this.ui);
1184
2123
  return;
1185
2124
  }
1186
2125
  return originalSetExpanded.call(this, expanded);
1187
2126
  };
1188
2127
  const patchedToolRender = function (this: any, width: number): string[] {
1189
- if (supportedTools.has(this.toolName)) {
1190
- cleanToolComponents.set(this.toolCallId, this);
1191
- knownToolCallIds.add(this.toolCallId);
1192
- }
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
+ );
1193
2135
  if (this[renderedModeKey] !== renderMode) {
1194
2136
  this[renderedModeKey] = renderMode;
1195
2137
  this.updateDisplay();
1196
2138
  }
1197
2139
 
1198
- const groupOwner = cleanToolCallGroupOwners.get(this.toolCallId);
1199
- const groupToolCallIds = groupOwner ? cleanGroupToolCallIds.get(groupOwner) : undefined;
1200
- const childIndex = groupToolCallIds?.indexOf(this.toolCallId) ?? -1;
1201
- const showAsChild = renderMode === "clean" && childIndex >= 0 && isCleanGroupRevealed(this.toolCallId);
1202
- if (!showAsChild || !groupOwner || !groupToolCallIds) {
1203
- 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
+ }
1204
2220
  }
1205
-
1206
- const rawChildPrefix = childIndex === groupToolCallIds.length - 1 ? " └─ " : " ├─ ";
1207
- const rawContinuation = childIndex === groupToolCallIds.length - 1 ? " " : " │ ";
1208
- const childTheme = cleanThemeForToolCall(this.toolCallId);
1209
- const childPrefix = childTheme ? childTheme.fg("dim", rawChildPrefix) : rawChildPrefix;
1210
- const continuation = childTheme ? childTheme.fg("dim", rawContinuation) : rawContinuation;
1211
- const childWidth = Math.max(1, width - visibleWidth(rawChildPrefix));
1212
- const lines = originalToolRender.call(this, childWidth);
1213
- if (lines.length === 0) return lines;
1214
-
1215
- const decoratedContent = lines.slice(1).map((line: string, index: number) =>
1216
- (index === 0 ? childPrefix : continuation) + line
1217
- );
1218
- if (childIndex !== 0) return decoratedContent;
1219
- return [lines[0], ...renderCleanGroupSummary(groupOwner, width), ...decoratedContent];
2221
+ if (!position.first) return decoratedContent;
2222
+ return ["", ...renderActivityGroupSummary(group, width), ...decoratedContent];
1220
2223
  };
1221
2224
  const patchedToolHandleMouse = function (this: any, event: any) {
1222
- const groupOwner = cleanToolCallGroupOwners.get(this.toolCallId);
1223
- const groupToolCallIds = groupOwner ? cleanGroupToolCallIds.get(groupOwner) : undefined;
1224
- const childIndex = groupToolCallIds?.indexOf(this.toolCallId) ?? -1;
1225
- 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;
1226
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);
1227
2233
 
1228
- // Pi normally ignores tool clicks until a result exists. Clean mode can
1229
- // still reveal the active call safely because this changes presentation
1230
- // only; it does not affect or delay execution.
1231
- if (
1232
- renderMode === "clean" &&
1233
- !showAsChild &&
1234
- isLeftClick &&
1235
- this.toolCallId === groupOwner &&
1236
- groupToolCallIds?.length
1237
- ) {
1238
- revealCleanGroup(groupToolCallIds, this.ui);
2234
+ if (!revealed && position.first && isLeftClick) {
2235
+ revealCleanGroup(group, this.ui);
1239
2236
  return { handled: true };
1240
2237
  }
1241
- if (!showAsChild || !groupToolCallIds) return originalToolHandleMouse.call(this, event);
2238
+ if (!revealed) return isLeftClick ? { handled: true } : undefined;
1242
2239
 
1243
- const summaryHeight = childIndex === 0 ? renderCleanGroupSummary(groupOwner!, event.width).length : 0;
1244
- if (
1245
- childIndex === 0 &&
1246
- event.y > 0 &&
1247
- event.y <= summaryHeight &&
1248
- isLeftClick
1249
- ) {
2240
+ const summaryHeight = position.first ? renderActivityGroupSummary(group, event.width).length : 0;
2241
+ if (position.first && event.y > 0 && event.y <= summaryHeight && isLeftClick) {
1250
2242
  changingAllToolsExpansion = true;
1251
2243
  try {
1252
- for (const toolCallId of groupToolCallIds) {
1253
- const component = cleanToolComponents.get(toolCallId);
1254
- if (component?.expanded) originalSetExpanded.call(component, false);
1255
- cleanCompactToolCallIds.delete(toolCallId);
1256
- }
1257
- for (const toolCallId of groupToolCallIds) cleanToolComponents.get(toolCallId)?.updateDisplay();
2244
+ collapseCleanGroup(group, this.ui);
1258
2245
  } finally {
1259
2246
  changingAllToolsExpansion = false;
1260
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);
1261
2260
  this.ui?.requestRender?.();
1262
2261
  return { handled: true };
1263
2262
  }
@@ -1268,12 +2267,17 @@ export default function prettyTui(pi: ExtensionAPI) {
1268
2267
  return { handled: true };
1269
2268
  }
1270
2269
 
1271
- const childPrefixWidth = visibleWidth(" ├─ ");
2270
+ const { prefixWidth } = activityTreeStyle(
2271
+ group,
2272
+ member,
2273
+ event.width,
2274
+ cleanThemeForToolCall(this.toolCallId),
2275
+ );
1272
2276
  return originalToolHandleMouse.call(this, {
1273
2277
  ...event,
1274
- x: Math.max(0, event.x - childPrefixWidth),
1275
- y: event.y - summaryHeight + (childIndex === 0 ? 0 : 1),
1276
- 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),
1277
2281
  });
1278
2282
  };
1279
2283
 
@@ -1323,6 +2327,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1323
2327
  const group: ToolSummaryGroup = {
1324
2328
  count: cleanRun.count,
1325
2329
  failed: cleanRun.failed,
2330
+ thoughtCount: thoughtCountForTool(cleanRun.lastCompletedToolCallId),
1326
2331
  lastToolCallId: cleanRun.lastCompletedToolCallId,
1327
2332
  toolCallIds: cleanRun.currentToolCallIds.slice(),
1328
2333
  activity,
@@ -1331,6 +2336,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1331
2336
  settledSummaries.set(group.lastToolCallId, {
1332
2337
  count: group.count,
1333
2338
  failed: group.failed,
2339
+ thoughtCount: group.thoughtCount ?? 0,
1334
2340
  activity: activityValueForHold(group.lastToolCallId, activity),
1335
2341
  });
1336
2342
  setCleanGroupMembers(group.lastToolCallId, group.toolCallIds ?? []);
@@ -1342,6 +2348,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1342
2348
  cleanRun.lastCompletedToolCallId = undefined;
1343
2349
  cleanRun.activeToolCallId = undefined;
1344
2350
  cleanRun.activeToolName = undefined;
2351
+ activityTimeline.boundary();
1345
2352
  };
1346
2353
 
1347
2354
  const settleLastCleanGroup = () => {
@@ -1351,30 +2358,44 @@ export default function prettyTui(pi: ExtensionAPI) {
1351
2358
  settledSummaries.set(group.lastToolCallId, {
1352
2359
  count: group.count,
1353
2360
  failed: group.failed,
2361
+ thoughtCount: group.thoughtCount ?? 0,
1354
2362
  activity: activityValueForHold(group.lastToolCallId, "done"),
1355
2363
  });
1356
2364
  };
1357
2365
 
1358
- const summaryText = (count: number, _failed: number, activity = "done"): string => {
1359
- 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;
1360
2377
  const countLabel = `${count} tool ${count === 1 ? "call" : "calls"}`;
2378
+ const thoughtLabel = thoughtCount > 0
2379
+ ? ` · ${thoughtCount} ${thoughtCount === 1 ? "thought" : "thoughts"}`
2380
+ : "";
1361
2381
  const activityLabel = activityText !== "done" && activityText.trim()
1362
2382
  ? ` · ${activityText}`
1363
2383
  : "";
1364
- return `${countLabel}${activityLabel}`;
2384
+ return `${countLabel}${thoughtLabel}${activityLabel}`;
1365
2385
  };
1366
2386
 
1367
2387
  const summaryRow = (
1368
2388
  theme: any,
1369
2389
  count: number,
1370
2390
  failed: number,
2391
+ thoughtCount: number,
1371
2392
  activity: DisplayValue = "done",
1372
2393
  collapsedDone = false,
1373
2394
  ): DisplayRow => ({
1374
2395
  prefix: () => {
1375
2396
  const currentActivity = typeof activity === "function" ? activity() : activity;
1376
2397
  const color = currentActivity === "done"
1377
- ? collapsedDone ? "thinkingText" : "success"
2398
+ ? collapsedDone ? "thinkingText" : successMarkerColor(theme)
1378
2399
  : "accent";
1379
2400
  return theme.fg(color, "● ");
1380
2401
  },
@@ -1383,12 +2404,13 @@ export default function prettyTui(pi: ExtensionAPI) {
1383
2404
  const currentActivity = typeof activity === "function" ? activity() : activity;
1384
2405
  const label = currentActivity === "done" ? "Done" : "Running";
1385
2406
  const color = label === "Done"
1386
- ? collapsedDone ? "thinkingText" : "success"
2407
+ ? collapsedDone ? "thinkingText" : successMarkerColor(theme)
1387
2408
  : "accent";
1388
2409
  const detailColor = label === "Done" && collapsedDone ? "thinkingText" : "text";
1389
2410
  return theme.fg(color, theme.bold(label)) +
1390
2411
  theme.fg("dim", "(") +
1391
- theme.fg(detailColor, summaryText(count, failed, currentActivity)) + theme.fg("dim", ")");
2412
+ theme.fg(detailColor, summaryText(count, failed, thoughtCount, currentActivity)) +
2413
+ theme.fg("dim", ")");
1392
2414
  },
1393
2415
  });
1394
2416
 
@@ -1397,18 +2419,70 @@ export default function prettyTui(pi: ExtensionAPI) {
1397
2419
  if (!summaryTheme) return [];
1398
2420
  const settled = settledSummaries.get(lastToolCallId);
1399
2421
  if (settled) {
1400
- 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);
1401
2429
  }
1402
2430
  if (cleanRun.activeToolCallId === lastToolCallId) {
1403
2431
  return block([summaryRow(
1404
2432
  summaryTheme,
1405
2433
  liveCleanToolCount(),
1406
2434
  cleanRun.failed,
2435
+ thoughtCountForTool(lastToolCallId),
1407
2436
  () => cleanRun.activeToolName ?? currentCleanActivity(),
1408
2437
  )]).render(width);
1409
2438
  }
1410
2439
  if (cleanRun.lastCompletedToolCallId === lastToolCallId && cleanRun.count > 0) {
1411
- 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));
1412
2486
  }
1413
2487
  return [];
1414
2488
  };
@@ -1421,6 +2495,8 @@ export default function prettyTui(pi: ExtensionAPI) {
1421
2495
  const cleanToolCall = (theme: any, name: string, toolCallId: string): Component => {
1422
2496
  knownToolCallIds.add(toolCallId);
1423
2497
  cleanToolThemes.set(toolCallId, theme);
2498
+ const activityGroup = activityTimeline.groupForTool(toolCallId);
2499
+ if (activityGroup) activityFallbackThemes.set(activityGroup.id, theme);
1424
2500
  return {
1425
2501
  render(width: number): string[] {
1426
2502
  if (renderMode !== "clean") return [];
@@ -1431,6 +2507,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1431
2507
  theme,
1432
2508
  settledSummary.count,
1433
2509
  settledSummary.failed,
2510
+ settledSummary.thoughtCount,
1434
2511
  settledSummary.activity,
1435
2512
  true,
1436
2513
  )]).render(width);
@@ -1442,6 +2519,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1442
2519
  theme,
1443
2520
  liveCleanToolCount(),
1444
2521
  cleanRun.failed,
2522
+ thoughtCountForTool(toolCallId),
1445
2523
  () => cleanRun.activeToolName ?? name,
1446
2524
  )]).render(width);
1447
2525
  }
@@ -1453,7 +2531,13 @@ export default function prettyTui(pi: ExtensionAPI) {
1453
2531
  cleanRun.count > 0 &&
1454
2532
  !cleanRun.activeToolCallId
1455
2533
  ) {
1456
- 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);
1457
2541
  }
1458
2542
 
1459
2543
  // Do not fall back to the per-component pending state here: Pi may
@@ -1472,8 +2556,8 @@ export default function prettyTui(pi: ExtensionAPI) {
1472
2556
  return block([]);
1473
2557
  };
1474
2558
 
1475
- // In collapsed clean mode, every supported tool call is represented by its
1476
- // 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.
1477
2561
  const hideCleanTool = (
1478
2562
  toolCallId: string,
1479
2563
  expanded: boolean,
@@ -1496,6 +2580,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1496
2580
  ? [{
1497
2581
  count: data.count ?? 0,
1498
2582
  failed: data.failed ?? 0,
2583
+ thoughtCount: data.thoughtCount ?? 0,
1499
2584
  lastToolCallId: data.lastToolCallId,
1500
2585
  }]
1501
2586
  : (() => {
@@ -1504,6 +2589,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1504
2589
  ? [{
1505
2590
  count: data?.count ?? 0,
1506
2591
  failed: data?.failed ?? 0,
2592
+ thoughtCount: data?.thoughtCount ?? 0,
1507
2593
  lastToolCallId: summaryCallId,
1508
2594
  }]
1509
2595
  : [];
@@ -1513,11 +2599,10 @@ export default function prettyTui(pi: ExtensionAPI) {
1513
2599
  // absent from an uncompacted branch.
1514
2600
  const missingGroups = groups.filter((group) => !knownToolCallIds.has(group.lastToolCallId));
1515
2601
  if (missingGroups.length === 0) return [];
1516
- // Persisted entries represent settled groups. Older versions could
1517
- // accidentally store the transient responding... activity, so always
1518
- // 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.
1519
2604
  return block(missingGroups.map((group) =>
1520
- summaryRow(theme, group.count, group.failed, "done", true)
2605
+ summaryRow(theme, group.count, group.failed, group.thoughtCount ?? 0, "done", true)
1521
2606
  )).render(width);
1522
2607
  },
1523
2608
  invalidate() {},
@@ -1538,6 +2623,16 @@ export default function prettyTui(pi: ExtensionAPI) {
1538
2623
  cleanToolsExpanded = ctx.ui.getToolsExpanded();
1539
2624
  settledSummaries.clear();
1540
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();
1541
2636
  knownToolCallIds.clear();
1542
2637
  cleanCompactToolCallIds.clear();
1543
2638
  cleanGroupToolCallIds.clear();
@@ -1569,9 +2664,15 @@ export default function prettyTui(pi: ExtensionAPI) {
1569
2664
  const explicitSummaryIds = new Set<string>();
1570
2665
 
1571
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;
1572
2672
  settledSummaries.set(group.lastToolCallId, {
1573
2673
  count: group.count,
1574
2674
  failed: group.failed,
2675
+ thoughtCount,
1575
2676
  activity: "done",
1576
2677
  });
1577
2678
  if (group.toolCallIds?.length) {
@@ -1583,11 +2684,22 @@ export default function prettyTui(pi: ExtensionAPI) {
1583
2684
 
1584
2685
  const finishGroup = () => {
1585
2686
  if (lastToolCallId && count > 0) {
1586
- lastFinishedGroup = { count, failed, lastToolCallId, toolCallIds: [...toolCalls] };
2687
+ lastFinishedGroup = {
2688
+ count,
2689
+ failed,
2690
+ thoughtCount: thoughtCountForTool(lastToolCallId),
2691
+ lastToolCallId,
2692
+ toolCallIds: [...toolCalls],
2693
+ };
1587
2694
  inferredGroups.push(lastFinishedGroup);
1588
2695
  setCleanGroupMembers(lastToolCallId, [...toolCalls]);
1589
2696
  if (!explicitSummaryIds.has(lastToolCallId)) {
1590
- settledSummaries.set(lastToolCallId, { count, failed, activity: "done" });
2697
+ settledSummaries.set(lastToolCallId, {
2698
+ count,
2699
+ failed,
2700
+ thoughtCount: lastFinishedGroup.thoughtCount ?? 0,
2701
+ activity: "done",
2702
+ });
1591
2703
  }
1592
2704
  }
1593
2705
  count = 0;
@@ -1595,13 +2707,20 @@ export default function prettyTui(pi: ExtensionAPI) {
1595
2707
  lastToolCallId = undefined;
1596
2708
  toolCalls.clear();
1597
2709
  completedToolCalls.clear();
2710
+ activityTimeline.boundary();
1598
2711
  };
1599
2712
 
1600
2713
  const inferredGroupsOverlapping = (group: ToolSummaryGroup): ToolSummaryGroup[] => {
1601
2714
  if (!group.toolCallIds?.length) return [];
1602
2715
  const persistedIds = new Set(group.toolCallIds);
1603
2716
  const currentGroup = lastToolCallId && count > 0
1604
- ? [{ count, failed, lastToolCallId, toolCallIds: [...toolCalls] }]
2717
+ ? [{
2718
+ count,
2719
+ failed,
2720
+ thoughtCount: thoughtCountForTool(lastToolCallId),
2721
+ lastToolCallId,
2722
+ toolCallIds: [...toolCalls],
2723
+ }]
1605
2724
  : [];
1606
2725
  return [...inferredGroups, ...currentGroup].filter((inferred) =>
1607
2726
  inferred.toolCallIds?.some((toolCallId) => persistedIds.has(toolCallId)),
@@ -1680,6 +2799,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1680
2799
  const group = {
1681
2800
  count: data.count ?? 0,
1682
2801
  failed: data.failed ?? 0,
2802
+ thoughtCount: data.thoughtCount ?? 0,
1683
2803
  lastToolCallId: data.lastToolCallId,
1684
2804
  };
1685
2805
  rememberGroup(group);
@@ -1691,6 +2811,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1691
2811
  rememberGroup({
1692
2812
  count: data?.count ?? lastFinishedGroup.count,
1693
2813
  failed: data?.failed ?? lastFinishedGroup.failed,
2814
+ thoughtCount: data?.thoughtCount ?? lastFinishedGroup.thoughtCount ?? 0,
1694
2815
  lastToolCallId: lastFinishedGroup.lastToolCallId,
1695
2816
  }, entry.id);
1696
2817
  } else if (lastToolCallId && count > 0) {
@@ -1698,9 +2819,16 @@ export default function prettyTui(pi: ExtensionAPI) {
1698
2819
  rememberGroup({
1699
2820
  count: data?.count ?? count,
1700
2821
  failed: data?.failed ?? failed,
2822
+ thoughtCount: data?.thoughtCount ?? thoughtCountForTool(lastToolCallId),
1701
2823
  lastToolCallId,
1702
2824
  }, entry.id);
1703
2825
  }
2826
+ activityTimeline.boundary();
2827
+ continue;
2828
+ }
2829
+
2830
+ if (entry.type === "custom_message") {
2831
+ if (entry.display !== false) addPersistentActivityUpdate(entry);
1704
2832
  continue;
1705
2833
  }
1706
2834
 
@@ -1713,12 +2841,23 @@ export default function prettyTui(pi: ExtensionAPI) {
1713
2841
  continue;
1714
2842
  }
1715
2843
 
2844
+ if (message.role === "custom") {
2845
+ if (message.display !== false) addPersistentActivityUpdate(message);
2846
+ continue;
2847
+ }
2848
+
1716
2849
  if (message.role === "assistant") {
2850
+ const thinking = thinkingText(message);
2851
+ if (thinking && !assistantSystemBoundary(message)) {
2852
+ activityTimeline.addThinking(assistantMessageKey(message), thinking);
2853
+ }
1717
2854
  if (messageHasVisibleText(message)) finishGroup();
1718
2855
  for (const item of messageContentItems(message)) {
1719
- if (item.type !== "toolCall" || !supportedTools.has(item.name)) continue;
2856
+ if (item.type !== "toolCall" || !item.id) continue;
1720
2857
  toolCalls.add(item.id);
2858
+ activityTimeline.addTool(item.id, item.name ?? "tool");
1721
2859
  }
2860
+ if (assistantSystemBoundary(message)) finishGroup();
1722
2861
  continue;
1723
2862
  }
1724
2863
 
@@ -1739,6 +2878,17 @@ export default function prettyTui(pi: ExtensionAPI) {
1739
2878
 
1740
2879
  pi.on("session_start", (_event, ctx) => restoreCleanSession(ctx));
1741
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
+ });
1742
2892
  pi.on("session_compact", () => {
1743
2893
  // Compaction is a hard chronological boundary. Keep tools completed before
1744
2894
  // it in their own group so expanded hierarchy lines never cross the summary.
@@ -1747,31 +2897,55 @@ export default function prettyTui(pi: ExtensionAPI) {
1747
2897
  // Pre-compaction tools have been summarized intentionally. Their durable
1748
2898
  // fallback rows should not be replayed beside the compacted transcript.
1749
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?.();
1750
2908
  });
1751
2909
 
1752
2910
  const hasVisibleAssistantText = (message: any): boolean =>
1753
2911
  message?.role === "assistant" && messageHasVisibleText(message);
1754
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
+
1755
2922
  const pendingToolCalls = (message: any): any[] =>
1756
2923
  messageContentItems(message).filter(
1757
- (item: any) => item.type === "toolCall" && supportedTools.has(item.name) && item.id,
2924
+ (item: any) => item.type === "toolCall" && item.id,
1758
2925
  );
1759
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
+
1760
2933
  const trackPendingToolActivity = (message: any): boolean => {
1761
2934
  const toolCalls = pendingToolCalls(message);
1762
2935
  if (toolCalls.length === 0) return false;
1763
2936
  for (const toolCall of toolCalls) {
2937
+ activityTimeline.addTool(toolCall.id, toolCall.name ?? "tool");
1764
2938
  if (!cleanRun.currentToolCallIds.includes(toolCall.id)) {
1765
2939
  cleanRun.currentToolCallIds.push(toolCall.id);
1766
2940
  }
1767
- cleanToolNames.set(toolCall.id, toolDisplayName(toolCall.name));
2941
+ cleanToolNames.set(toolCall.id, cleanToolNames.get(toolCall.id) ?? toolCall.name);
1768
2942
  }
1769
2943
  const toolCall = toolCalls[toolCalls.length - 1];
1770
2944
  if (cleanRun.lastCompletedToolCallId === toolCall.id) return false;
1771
2945
  setCleanGroupMembers(toolCall.id, cleanRun.currentToolCallIds.slice());
1772
2946
  cleanRun.active = true;
1773
2947
  cleanRun.activeToolCallId = toolCall.id;
1774
- beginToolActivity(toolCall.id, toolDisplayName(toolCall.name));
2948
+ beginToolActivity(toolCall.id, cleanToolNames.get(toolCall.id) ?? toolCall.name);
1775
2949
  return true;
1776
2950
  };
1777
2951
 
@@ -1791,29 +2965,42 @@ export default function prettyTui(pi: ExtensionAPI) {
1791
2965
  // message_start runs before Pi adds the user component to the transcript,
1792
2966
  // so settling here keeps the previous parent row above that message.
1793
2967
  pi.on("message_start", (event) => {
2968
+ if (event.message.role === "custom" && event.message.display !== false) {
2969
+ addPersistentActivityUpdate(event.message);
2970
+ return;
2971
+ }
1794
2972
  if (event.message.role !== "user") return;
1795
2973
  finishCleanGroup("done");
1796
2974
  settleLastCleanGroup();
1797
2975
  });
1798
2976
 
1799
- // A visible assistant response is the boundary between tool groups. Do
1800
- // this during streaming so a following tool call cannot inherit the prior
1801
- // 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.
1802
2980
  pi.on("message_update", (event) => {
1803
2981
  if (hasVisibleAssistantText(event.message)) {
1804
- cleanRun.activity = "responding...";
1805
- finishCleanGroup("responding...");
2982
+ trackThinkingActivity(event.message);
2983
+ clearToolActivityHolds();
2984
+ closeAtAssistantBoundary(event.message, "done");
1806
2985
  return;
1807
2986
  }
2987
+ if (assistantSystemBoundary(event.message)) {
2988
+ closeAtAssistantBoundary(event.message, "done");
2989
+ settleLastCleanGroup();
2990
+ return;
2991
+ }
2992
+ trackThinkingActivity(event.message);
1808
2993
  trackPendingToolActivity(event.message);
1809
2994
  });
1810
2995
  pi.on("message_end", (event) => {
1811
- if (hasVisibleAssistantText(event.message)) {
2996
+ if (hasVisibleAssistantText(event.message) || assistantSystemBoundary(event.message)) {
2997
+ trackThinkingActivity(event.message);
2998
+ closeAtAssistantBoundary(event.message, "done");
1812
2999
  cleanRun.activity = "done";
1813
- finishCleanGroup("done");
1814
3000
  settleLastCleanGroup();
1815
3001
  return;
1816
3002
  }
3003
+ trackThinkingActivity(event.message);
1817
3004
  trackPendingToolActivity(event.message);
1818
3005
  });
1819
3006
 
@@ -1834,25 +3021,25 @@ export default function prettyTui(pi: ExtensionAPI) {
1834
3021
  cleanRun.activeToolCallIds.clear();
1835
3022
  cleanRun.activeToolCallId = undefined;
1836
3023
  cleanRun.activeToolName = undefined;
1837
- cleanRun.activity = "thinking...";
3024
+ cleanRun.activity = "thinking";
1838
3025
  });
1839
3026
  pi.on("tool_execution_start", (event) => {
1840
- if (!supportedTools.has(event.toolName)) return;
3027
+ activityTimeline.addTool(event.toolCallId, event.toolName);
1841
3028
  if (!cleanRun.currentToolCallIds.includes(event.toolCallId)) {
1842
3029
  cleanRun.currentToolCallIds.push(event.toolCallId);
1843
3030
  }
1844
3031
  cleanRun.activeToolCallIds.add(event.toolCallId);
1845
- cleanToolNames.set(event.toolCallId, toolDisplayName(event.toolName));
3032
+ const displayName = cleanToolNames.get(event.toolCallId) ?? event.toolName;
3033
+ cleanToolNames.set(event.toolCallId, displayName);
1846
3034
  setCleanGroupMembers(event.toolCallId, cleanRun.currentToolCallIds.slice());
1847
3035
  cleanRun.active = true;
1848
3036
  cleanRun.activeToolCallId = event.toolCallId;
1849
- beginToolActivity(event.toolCallId, toolDisplayName(event.toolName), true);
3037
+ beginToolActivity(event.toolCallId, displayName, true);
1850
3038
  });
1851
3039
  pi.on("tool_execution_end", (event) => {
1852
- if (!supportedTools.has(event.toolName)) return;
1853
3040
  const activityName = cleanRun.activeToolCallId === event.toolCallId
1854
- ? cleanRun.activeToolName ?? toolDisplayName(event.toolName)
1855
- : toolDisplayName(event.toolName);
3041
+ ? cleanRun.activeToolName ?? cleanToolNames.get(event.toolCallId) ?? event.toolName
3042
+ : cleanToolNames.get(event.toolCallId) ?? event.toolName;
1856
3043
  // Keep the tool activity visible until at least one second after start;
1857
3044
  // this only delays the status transition, never the tool result itself.
1858
3045
  holdToolActivity(event.toolCallId, activityName);
@@ -1867,7 +3054,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1867
3054
  cleanRun.activeToolName = cleanRun.activeToolCallId
1868
3055
  ? cleanToolNames.get(cleanRun.activeToolCallId)
1869
3056
  : undefined;
1870
- cleanRun.activity = cleanRun.activeToolName ?? "thinking...";
3057
+ cleanRun.activity = cleanRun.activeToolName ?? "thinking";
1871
3058
  }
1872
3059
  const visibleSummaryToolCallId = cleanRun.activeToolCallId ?? cleanRun.lastCompletedToolCallId;
1873
3060
  setCleanGroupMembers(visibleSummaryToolCallId, cleanRun.currentToolCallIds.slice());
@@ -1879,7 +3066,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1879
3066
  cleanRun.activeToolCallIds.clear();
1880
3067
  cleanRun.activeToolCallId = undefined;
1881
3068
  cleanRun.activeToolName = undefined;
1882
- cleanRun.activity = "thinking...";
3069
+ cleanRun.activity = "thinking";
1883
3070
  });
1884
3071
  pi.on("agent_settled", () => {
1885
3072
  if (cleanRun.settled) return;
@@ -1892,6 +3079,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1892
3079
  settledSummaries.set(group.lastToolCallId, {
1893
3080
  count: group.count,
1894
3081
  failed: group.failed,
3082
+ thoughtCount: group.thoughtCount ?? 0,
1895
3083
  activity: activityValueForHold(group.lastToolCallId, "done"),
1896
3084
  });
1897
3085
  }
@@ -1907,14 +3095,17 @@ export default function prettyTui(pi: ExtensionAPI) {
1907
3095
 
1908
3096
  const count = groups.reduce((total, group) => total + group.count, 0);
1909
3097
  const failed = groups.reduce((total, group) => total + group.failed, 0);
3098
+ const thoughtCount = groups.reduce((total, group) => total + (group.thoughtCount ?? 0), 0);
1910
3099
  const lastToolCallId = groups[groups.length - 1]?.lastToolCallId;
1911
3100
  pi.appendEntry<ToolSummaryData>("pretty-tui-tool-summary", {
1912
3101
  count,
1913
3102
  failed,
3103
+ thoughtCount,
1914
3104
  lastToolCallId,
1915
- groups: groups.map(({ count, failed, lastToolCallId, toolCallIds }) => ({
3105
+ groups: groups.map(({ count, failed, thoughtCount, lastToolCallId, toolCallIds }) => ({
1916
3106
  count,
1917
3107
  failed,
3108
+ thoughtCount,
1918
3109
  lastToolCallId,
1919
3110
  toolCallIds,
1920
3111
  })),
@@ -1922,15 +3113,16 @@ export default function prettyTui(pi: ExtensionAPI) {
1922
3113
  });
1923
3114
 
1924
3115
  const read = createReadTool(cwd);
3116
+ const readLabel = read.label || read.name;
1925
3117
  pi.registerTool({
1926
3118
  ...read,
1927
3119
  renderShell: "self",
1928
3120
  renderCall(args: any, theme: any, context: any) {
1929
- 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);
1930
3122
  const range = args.offset || args.limit
1931
3123
  ? ` · lines ${args.offset ?? 1}${args.limit ? `–${(args.offset ?? 1) + args.limit - 1}` : "+"}`
1932
3124
  : "";
1933
- return call(theme, "Read", `${args.path}${range}`, context.state);
3125
+ return call(theme, readLabel, `${args.path}${range}`, context.state);
1934
3126
  },
1935
3127
  renderResult(toolResult: any, options: any, theme: any, context: any) {
1936
3128
  const output = textContent(toolResult);
@@ -1952,11 +3144,12 @@ export default function prettyTui(pi: ExtensionAPI) {
1952
3144
  } as any);
1953
3145
 
1954
3146
  const bash = createBashTool(cwd);
3147
+ const bashLabel = bash.label || bash.name;
1955
3148
  pi.registerTool({
1956
3149
  ...bash,
1957
3150
  renderShell: "self",
1958
3151
  renderCall(args: any, theme: any, context: any) {
1959
- 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);
1960
3153
  const command = typeof args.command === "string" ? args.command : "";
1961
3154
  if (useCompactToolView(context.toolCallId, context.expanded)) {
1962
3155
  const commandLines = command.split(/\r\n|\r|\n/);
@@ -1965,9 +3158,9 @@ export default function prettyTui(pi: ExtensionAPI) {
1965
3158
  const detail = omitted > 0
1966
3159
  ? `${firstLine} … (${omitted} more ${omitted === 1 ? "line" : "lines"})`
1967
3160
  : firstLine;
1968
- return call(theme, "Bash", detail, context.state);
3161
+ return call(theme, bashLabel, detail, context.state);
1969
3162
  }
1970
- return call(theme, "Bash", command, context.state);
3163
+ return call(theme, bashLabel, command, context.state);
1971
3164
  },
1972
3165
  renderResult(toolResult: any, options: any, theme: any, context: any) {
1973
3166
  const output = textContent(toolResult);
@@ -2002,13 +3195,14 @@ export default function prettyTui(pi: ExtensionAPI) {
2002
3195
  } as any);
2003
3196
 
2004
3197
  const edit = createEditTool(cwd);
3198
+ const editLabel = edit.label || edit.name;
2005
3199
  pi.registerTool({
2006
3200
  ...edit,
2007
3201
  renderShell: "self",
2008
3202
  renderCall(args: any, theme: any, context: any) {
2009
- 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);
2010
3204
  const count = Array.isArray(args.edits) ? ` · ${args.edits.length} changes` : "";
2011
- return call(theme, "Edit", `${args.path}${count}`, context.state);
3205
+ return call(theme, editLabel, `${args.path}${count}`, context.state);
2012
3206
  },
2013
3207
  renderResult(toolResult: any, options: any, theme: any, context: any) {
2014
3208
  const output = textContent(toolResult);
@@ -2043,20 +3237,21 @@ export default function prettyTui(pi: ExtensionAPI) {
2043
3237
  } as any);
2044
3238
 
2045
3239
  const write = createWriteTool(cwd);
3240
+ const writeLabel = write.label || write.name;
2046
3241
  pi.registerTool({
2047
3242
  ...write,
2048
3243
  renderShell: "self",
2049
3244
  renderCall(args: any, theme: any, context: any) {
2050
- 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);
2051
3246
  const content = typeof args.content === "string" ? args.content : "";
2052
3247
  const path = String(args.path ?? "");
2053
3248
  if (useCompactToolView(context.toolCallId, context.expanded)) {
2054
3249
  const lines = content.replace(/\t/g, " ").split("\n");
2055
3250
  while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
2056
3251
  const total = lines.length;
2057
- 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);
2058
3253
  }
2059
- return writeCall(theme, path, content, context.expanded, context.state);
3254
+ return writeCall(theme, writeLabel, path, content, context.expanded, context.state);
2060
3255
  },
2061
3256
  renderResult(toolResult: any, options: any, theme: any, context: any) {
2062
3257
  const output = textContent(toolResult);
@@ -2074,14 +3269,15 @@ export default function prettyTui(pi: ExtensionAPI) {
2074
3269
  } as any);
2075
3270
 
2076
3271
  const grep = createGrepTool(cwd);
3272
+ const grepLabel = grep.label || grep.name;
2077
3273
  pi.registerTool({
2078
3274
  ...grep,
2079
3275
  renderShell: "self",
2080
3276
  renderCall(args: any, theme: any, context: any) {
2081
- 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);
2082
3278
  const where = args.path ? ` · ${args.path}` : "";
2083
3279
  const glob = args.glob ? ` · ${args.glob}` : "";
2084
- return call(theme, "Grep", `${args.pattern}${where}${glob}`, context.state);
3280
+ return call(theme, grepLabel, `${args.pattern}${where}${glob}`, context.state);
2085
3281
  },
2086
3282
  renderResult(toolResult: any, options: any, theme: any, context: any) {
2087
3283
  const output = textContent(toolResult);
@@ -2094,12 +3290,13 @@ export default function prettyTui(pi: ExtensionAPI) {
2094
3290
  } as any);
2095
3291
 
2096
3292
  const find = createFindTool(cwd);
3293
+ const findLabel = find.label || find.name;
2097
3294
  pi.registerTool({
2098
3295
  ...find,
2099
3296
  renderShell: "self",
2100
3297
  renderCall(args: any, theme: any, context: any) {
2101
- if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "Find", context.toolCallId);
2102
- 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);
2103
3300
  },
2104
3301
  renderResult(toolResult: any, options: any, theme: any, context: any) {
2105
3302
  const output = textContent(toolResult);
@@ -2112,12 +3309,13 @@ export default function prettyTui(pi: ExtensionAPI) {
2112
3309
  } as any);
2113
3310
 
2114
3311
  const ls = createLsTool(cwd);
3312
+ const lsLabel = ls.label || ls.name;
2115
3313
  pi.registerTool({
2116
3314
  ...ls,
2117
3315
  renderShell: "self",
2118
3316
  renderCall(args: any, theme: any, context: any) {
2119
- if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, "List", context.toolCallId);
2120
- 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);
2121
3319
  },
2122
3320
  renderResult(toolResult: any, options: any, theme: any, context: any) {
2123
3321
  const output = textContent(toolResult);