pi-pretty-tui 0.2.9 → 0.3.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,18 +1,30 @@
1
+ import { isNativeImageRow } from "./ansi-style.js";
2
+ import { cardMargin, cardBackground, frameNativeRows, nativeFrameWidth } from "./full-frame.js";
3
+ import { ThoughtPresentation } from "./thought-presentation.js";
4
+ import { isThirdPartyTool, renderToolProjection, toolLabel, type ToolProjection } from "./tool-presentation.js";
1
5
  import { NestedTools, NESTED_SNAPSHOT_ENTRY } from "./nested-tools.js";
2
6
  import {
3
7
  AssistantMessageComponent,
4
8
  CustomEditor,
5
9
  CustomMessageComponent,
10
+ CompactionSummaryMessageComponent,
6
11
  InteractiveMode,
7
12
  ToolExecutionComponent,
8
13
  UserMessageComponent,
9
14
  createBashTool,
15
+ createBashToolDefinition,
10
16
  createEditTool,
17
+ createEditToolDefinition,
11
18
  createFindTool,
19
+ createFindToolDefinition,
12
20
  createGrepTool,
21
+ createGrepToolDefinition,
13
22
  createLsTool,
23
+ createLsToolDefinition,
14
24
  createReadTool,
25
+ createReadToolDefinition,
15
26
  createWriteTool,
27
+ createWriteToolDefinition,
16
28
  copyToClipboard,
17
29
  getAgentDir,
18
30
  getMarkdownTheme,
@@ -46,26 +58,7 @@ type PrettyTuiMode = "full" | "compact" | "clean";
46
58
  const CLEAN_TOOL_ACTIVITY_MIN_MS = 1000;
47
59
  const TRANSIENT_UI_STATUS_MS = 10_000;
48
60
  const RESPONSE_FOOTER_ENTRY_TYPE = "pretty-tui-response-footer";
49
- const SPECIALIZED_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
50
-
51
- const stripAnsiBackgrounds = (value: string): string =>
52
- value.replace(/\x1b\[([0-9:;]*)m/g, (sequence, raw: string) => {
53
- const fields = raw === "" ? [0] : raw.split(";").map((field) => Number(field));
54
- const kept: number[] = [];
55
- for (let index = 0; index < fields.length; index++) {
56
- const field = fields[index];
57
- if ((field >= 40 && field <= 49) || (field >= 100 && field <= 107)) continue;
58
- if (field === 48) {
59
- const mode = fields[index + 1];
60
- if (mode === 5) index += 2;
61
- else if (mode === 2) index += 4;
62
- continue;
63
- }
64
- kept.push(field);
65
- }
66
- if (raw.includes(":") && /(?:^|;)48:/u.test(raw)) return "";
67
- return kept.length > 0 ? `\x1b[${kept.join(";")}m` : "";
68
- });
61
+
69
62
  type PrettyTuiConfig = {
70
63
  enabled?: boolean;
71
64
  mode?: PrettyTuiMode;
@@ -95,6 +88,10 @@ export default function prettyTui(pi: ExtensionAPI) {
95
88
  ? configuredMode
96
89
  : "clean";
97
90
  let cleanToolsExpanded = false;
91
+ // Full starts with thoughts open; an intentional Ctrl+T overrides that
92
+ // default for this runtime, independently of compact's initial setting.
93
+ let fullThinkingHidden = false;
94
+ let compactThinkingHidden = true;
98
95
  let cleanContextCompacted = false;
99
96
  let fullscreenTui = false;
100
97
  const mouseClickHitTests = new WeakMap<object, (event: any) => boolean>();
@@ -159,7 +156,7 @@ export default function prettyTui(pi: ExtensionAPI) {
159
156
  };
160
157
  const refreshLatestActivityWidget = () => {
161
158
  if (!currentExtensionUi?.setWidget) return;
162
- if (renderMode !== "clean" || !latestActivityStatus) {
159
+ if (!latestActivityStatus) {
163
160
  if (latestActivityWidgetInstalled) {
164
161
  currentExtensionUi.setWidget(latestActivityWidgetKey, undefined);
165
162
  latestActivityWidgetInstalled = false;
@@ -261,7 +258,9 @@ export default function prettyTui(pi: ExtensionAPI) {
261
258
  const activityTimeline = new ActivityTimeline();
262
259
  const nestedTools = new NestedTools();
263
260
  const refreshNestedTools = (id: string) => {
264
- cleanToolComponents.get(nestedTools.root(id))?.updateDisplay?.();
261
+ const owner = toolComponents.get(nestedTools.root(id));
262
+ owner?.updateDisplay?.();
263
+ owner?.ui?.requestRender?.();
265
264
  currentTui?.requestRender?.();
266
265
  };
267
266
  const revealedActivityGroups = new Set<string>();
@@ -283,7 +282,7 @@ export default function prettyTui(pi: ExtensionAPI) {
283
282
  const cleanCompactToolCallIds = new Set<string>();
284
283
  const cleanGroupToolCallIds = new Map<string, string[]>();
285
284
  const cleanToolCallGroupOwners = new Map<string, string>();
286
- const cleanToolComponents = new Map<string, any>();
285
+ const toolComponents = new Map<string, any>();
287
286
  const cleanToolThemes = new Map<string, any>();
288
287
  const cleanToolNames = new Map<string, string>();
289
288
  let renderCleanGroupSummary = (_lastToolCallId: string, _width: number): string[] => [];
@@ -355,12 +354,14 @@ export default function prettyTui(pi: ExtensionAPI) {
355
354
  return group.toolCallIds[group.toolCallIds.length - 1];
356
355
  };
357
356
 
357
+ let fallbackActivityTheme: any;
358
358
  const defaultActivityTheme = () => {
359
359
  if (currentExtensionUi?.theme?.fg) return currentExtensionUi.theme;
360
360
  const markdownTheme = getMarkdownTheme();
361
- return {
361
+ return fallbackActivityTheme ??= {
362
362
  fg: (color: string, text: string) => {
363
363
  if (color === "accent") return markdownTheme.listBullet(text);
364
+ if (color === "mdHeading") return markdownTheme.heading(text);
364
365
  // Session UI normally supplies Pi's active Theme above. Keep portable
365
366
  // semantic fallbacks for early startup and isolated renderer tests.
366
367
  if (color === "warning") return `\x1b[33m${text}\x1b[39m`;
@@ -443,7 +444,7 @@ export default function prettyTui(pi: ExtensionAPI) {
443
444
  const refreshActivityGroup = (group: ActivityGroup): void => {
444
445
  for (const child of group.members) {
445
446
  if (child.kind === "tool" && child.toolCallId) {
446
- cleanToolComponents.get(child.toolCallId)?.updateDisplay?.();
447
+ toolComponents.get(child.toolCallId)?.updateDisplay?.();
447
448
  } else if (child.kind === "thinking" && child.messageKey) {
448
449
  thinkingComponents.get(child.messageKey)?.invalidate?.();
449
450
  } else if (child.kind === "update") {
@@ -468,7 +469,7 @@ export default function prettyTui(pi: ExtensionAPI) {
468
469
  else collapsedActivityGroups.delete(group.id);
469
470
  for (const toolCallId of group.toolCallIds) {
470
471
  cleanCompactToolCallIds.delete(toolCallId);
471
- const component = cleanToolComponents.get(toolCallId);
472
+ const component = toolComponents.get(toolCallId);
472
473
  if (component?.expanded) component.setExpanded(false);
473
474
  }
474
475
  for (const child of group.members) expandedThinkingMembers.delete(child.id);
@@ -580,7 +581,7 @@ export default function prettyTui(pi: ExtensionAPI) {
580
581
  };
581
582
 
582
583
  const renderStandaloneError = (member: ActivityMember, width: number, component?: any): string[] => {
583
- const run = component ? errorRun(component) : { hidden: false, count: 1, compact: false };
584
+ const run = component && renderMode !== "full" ? errorRun(component) : { hidden: false, count: 1, compact: false };
584
585
  if (run.hidden) return [];
585
586
  const theme = defaultActivityTheme();
586
587
  const style = activityUpdateStyle(member, theme);
@@ -624,7 +625,7 @@ export default function prettyTui(pi: ExtensionAPI) {
624
625
  width: number,
625
626
  component?: any,
626
627
  ): string[] => {
627
- if (!activityGroupCollapsible(group)) return renderPendingActivityUpdate(member, width, component);
628
+ if (renderMode !== "clean" || !activityGroupCollapsible(group)) return renderPendingActivityUpdate(member, width, component);
628
629
  const position = activityMemberPosition(group, member);
629
630
  if (!activityGroupRevealed(group)) {
630
631
  return position.first
@@ -640,7 +641,7 @@ export default function prettyTui(pi: ExtensionAPI) {
640
641
  event: any,
641
642
  ) => {
642
643
  const isLeftClick = event.type === "click" && event.button === "left";
643
- if (!isLeftClick || !activityGroupCollapsible(group)) return undefined;
644
+ if (renderMode !== "clean" || !isLeftClick || !activityGroupCollapsible(group)) return undefined;
644
645
  const position = activityMemberPosition(group, member);
645
646
  if (!activityGroupRevealed(group)) {
646
647
  if (!position.first) return undefined;
@@ -693,7 +694,7 @@ export default function prettyTui(pi: ExtensionAPI) {
693
694
  }
694
695
  const enabled = `Enabled — ${config.enabled !== false ? "on" : "off"}`;
695
696
  const full = `Full — full tool details and output${renderMode === "full" ? " (current)" : ""}`;
696
- const compact = `Compact — concise summaries for all built-in tools${renderMode === "compact" ? " (current)" : ""}`;
697
+ const compact = `Compact — concise tool summaries and nested calls${renderMode === "compact" ? " (current)" : ""}`;
697
698
  const clean = `Clean — group supported tools into Running/Done status${renderMode === "clean" ? " (current)" : ""}`;
698
699
  const selected = await ctx.ui.select("pi-pretty-tui settings", [enabled, full, compact, clean]);
699
700
  if (!selected) return;
@@ -745,14 +746,9 @@ export default function prettyTui(pi: ExtensionAPI) {
745
746
  collapsedActivityGroups.clear();
746
747
  expandedThinkingMembers.clear();
747
748
  renderMode = requested;
748
- if (renderMode === "clean") {
749
- if (cleanRun.active && !cleanRun.settled) startWorkingElapsedTimer();
750
- refreshLatestActivityWidget();
751
- } else {
752
- stopWorkingElapsedTimer();
753
- clearLatestActivityStatus();
754
- }
755
- for (const component of cleanToolComponents.values()) component.updateDisplay?.();
749
+ if (cleanRun.active && !cleanRun.settled) startWorkingElapsedTimer();
750
+ refreshLatestActivityWidget();
751
+ for (const component of toolComponents.values()) component.updateDisplay?.();
756
752
  for (const component of thinkingComponents.values()) component.invalidate?.();
757
753
  try {
758
754
  saveRenderMode(renderMode);
@@ -784,7 +780,7 @@ export default function prettyTui(pi: ExtensionAPI) {
784
780
  if (this.tui) currentTui = this.tui;
785
781
  currentPromptEditor = this;
786
782
  const indicator = this.workingStatusIndicator;
787
- if (renderMode !== "clean" || indicator?.kind !== "working" || responseRunStartedAt === undefined) {
783
+ if (indicator?.kind !== "working" || responseRunStartedAt === undefined) {
788
784
  return originalEditorTopBorder.call(this, width, hiddenLineCount);
789
785
  }
790
786
  const elapsed = formatResponseDuration(Date.now() - responseRunStartedAt);
@@ -872,10 +868,8 @@ export default function prettyTui(pi: ExtensionAPI) {
872
868
  });
873
869
  }
874
870
 
875
- // Render user prompts as right-aligned, content-sized chat bubbles while
876
- // preserving Pi's Markdown component, wrapping, output padding, and OSC 133
877
- // zones. Long messages stop at 75% of the terminal width; narrow terminals
878
- // trade away the left gap before sacrificing readability.
871
+ // Clean uses content-sized bubbles; compact/full share full-width panels.
872
+ // Keep Pi's Markdown, mouse handling and OSC 133 zones in every mode.
879
873
  const userMessagePrototype = UserMessageComponent.prototype as any;
880
874
  const userMessagePatchKey = Symbol.for("pretty-tui.user-message-frame");
881
875
  if (!userMessagePrototype[userMessagePatchKey]) {
@@ -912,9 +906,9 @@ export default function prettyTui(pi: ExtensionAPI) {
912
906
  .map((line: string) => line.replace(/ +$/u, ""));
913
907
  const layout = (width: number) => {
914
908
  const terminalWidth = Math.max(1, width);
915
- const rightPad = Math.min(outputPad, Math.max(0, terminalWidth - 1));
909
+ const rightPad = renderMode !== "clean" ? 0 : Math.min(outputPad, Math.max(0, terminalWidth - 1));
916
910
  const availableWidth = Math.max(1, terminalWidth - rightPad);
917
- if (availableWidth < 4) {
911
+ if (availableWidth < (renderMode !== "clean" ? 8 : 4)) {
918
912
  return {
919
913
  framed: false,
920
914
  rightPad,
@@ -925,6 +919,16 @@ export default function prettyTui(pi: ExtensionAPI) {
925
919
  };
926
920
  }
927
921
 
922
+ if (renderMode !== "clean") {
923
+ const margin = cardMargin(terminalWidth);
924
+ const contentWidth = nativeFrameWidth(terminalWidth);
925
+ return {
926
+ framed: true, rightPad: margin, leftPad: margin,
927
+ frameWidth: terminalWidth - margin * 2, contentWidth,
928
+ body: renderMarkdown(contentWidth),
929
+ };
930
+ }
931
+
928
932
  const preferredMaximum = Math.max(4, Math.floor(terminalWidth * 0.75));
929
933
  // Keep at least one blank column on the left whenever a framed bubble
930
934
  // fits, even after a narrow terminal relaxes the usual 75% maximum.
@@ -955,8 +959,8 @@ export default function prettyTui(pi: ExtensionAPI) {
955
959
  };
956
960
  };
957
961
  const border = (text: string) => currentExtensionUi?.theme?.fg
958
- ? currentExtensionUi.theme.fg("mdLink", text)
959
- : markdownTheme.link(text);
962
+ ? currentExtensionUi.theme.fg("mdHeading", text)
963
+ : markdownTheme.heading(text);
960
964
  const userText = typeof this.text === "string" ? this.text : "";
961
965
  const frame: Component = {
962
966
  render(width: number): string[] {
@@ -964,7 +968,12 @@ export default function prettyTui(pi: ExtensionAPI) {
964
968
  const left = " ".repeat(measured.leftPad);
965
969
  const right = " ".repeat(measured.rightPad);
966
970
  if (!measured.framed) {
967
- return measured.body.map((line: string) => left + line + right);
971
+ return measured.body.map((line: string) => renderMode !== "clean"
972
+ ? cardBackground(line, width, defaultActivityTheme(), "toolPendingBg", "mdHeading") : left + line + right);
973
+ }
974
+ if (renderMode !== "clean") {
975
+ return frameNativeRows("User", measured.body, width, defaultActivityTheme(), undefined,
976
+ frame, "mdHeading", "", "mdHeading").lines;
968
977
  }
969
978
 
970
979
  const top = border("╭" + "─".repeat(Math.max(0, measured.frameWidth - 2)) + "╮");
@@ -1106,6 +1115,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1106
1115
  return true;
1107
1116
  };
1108
1117
  const patchedCustomRender = function (this: any, width: number): string[] {
1118
+ if (renderMode === "full") return originalCustomRender.call(this, width);
1109
1119
  if (releaseSemanticCustomMessage(this)) {
1110
1120
  return originalCustomRender.call(this, width);
1111
1121
  }
@@ -1116,23 +1126,24 @@ export default function prettyTui(pi: ExtensionAPI) {
1116
1126
  // message_start event. Claim them at the component boundary so live
1117
1127
  // rendering matches transcript restoration instead of failing open to
1118
1128
  // the purple native box until the next reload.
1119
- if (!member && renderMode === "clean") {
1129
+ if (!member) {
1120
1130
  member = addPersistentActivityUpdate(message);
1121
1131
  }
1122
1132
  const group = member ? activityTimeline.groupForMember(member.id) : undefined;
1123
- if (renderMode !== "clean" || !member || !group) {
1133
+ if (!member || !group) {
1124
1134
  return originalCustomRender.call(this, width);
1125
1135
  }
1126
1136
  activityUpdateComponents.set(member.id, this);
1127
1137
  return renderActivityUpdateProjection(group, member, width);
1128
1138
  };
1129
1139
  const patchedCustomHandleMouse = function (this: any, event: any) {
1140
+ if (renderMode === "full") return originalCustomHandleMouse?.call(this, event);
1130
1141
  if (releaseSemanticCustomMessage(this)) {
1131
1142
  return originalCustomHandleMouse?.call(this, event);
1132
1143
  }
1133
1144
  const member = activityTimeline.memberForUpdate(customMessageKey(this.message));
1134
1145
  const group = member ? activityTimeline.groupForMember(member.id) : undefined;
1135
- if (renderMode !== "clean" || !member || !group) {
1146
+ if (!member || !group) {
1136
1147
  return originalCustomHandleMouse?.call(this, event);
1137
1148
  }
1138
1149
  return handleActivityUpdateMouse(group, member, event);
@@ -1163,20 +1174,79 @@ export default function prettyTui(pi: ExtensionAPI) {
1163
1174
  });
1164
1175
  }
1165
1176
 
1177
+ // Keep Pi's compaction content and expansion state inside the shared panel.
1178
+ const compactionPrototype = CompactionSummaryMessageComponent.prototype as any;
1179
+ const compactionPatchKey = Symbol.for("pretty-tui.compaction-panel");
1180
+ if (!compactionPrototype[compactionPatchKey]) {
1181
+ const originalRender = compactionPrototype.render;
1182
+ const originalHandleMouse = compactionPrototype.handleMouse;
1183
+ const views = new WeakMap<object, { width: number; sourceOffset: number; frame: ReturnType<typeof frameNativeRows> }>();
1184
+ const render = function (this: any, width: number): string[] {
1185
+ if (renderMode === "clean") {
1186
+ views.delete(this);
1187
+ mouseClickHitTests.delete(this);
1188
+ return originalRender.call(this, width);
1189
+ }
1190
+ const innerWidth = nativeFrameWidth(width);
1191
+ const region = this.children[0];
1192
+ // Native content begins with its [compaction] label and a spacer. The
1193
+ // panel title replaces those two rows; the summary itself is untouched.
1194
+ const content = region.child.children;
1195
+ const sourceOffset = content.slice(0, 2).reduce((height: number, child: any) => height + child.render(innerWidth).length, 0);
1196
+ const source = content.slice(2).flatMap((child: any) => child.render(innerWidth));
1197
+ const theme = defaultActivityTheme();
1198
+ const frame = frameNativeRows(`compaction ${this.expanded ? "▾" : "▸"}`, source, width, theme,
1199
+ undefined, this, "customMessageLabel", "", "customMessageLabel", "customMessageBg");
1200
+ views.set(this, { width: innerWidth, sourceOffset, frame });
1201
+ mouseClickHitTests.set(this, (event) => event.y === 0 || frame.sourceRows.has(event.y));
1202
+ return frame.lines.map((line) => isNativeImageRow(line) ? line : truncateToWidth(line, Math.max(1, width), ""));
1203
+ };
1204
+ const handleMouse = function (this: any, event: any) {
1205
+ if (renderMode === "clean") return originalHandleMouse?.call(this, event);
1206
+ render.call(this, event.width);
1207
+ const view = views.get(this);
1208
+ if (!view) return undefined;
1209
+ if (event.y === 0 && event.type === "click" && event.button === "left") {
1210
+ this.setExpanded(!this.expanded);
1211
+ currentTui?.requestRender?.();
1212
+ return { handled: true };
1213
+ }
1214
+ const sourceY = view.frame.sourceRows.get(event.y);
1215
+ if (sourceY === undefined) return undefined;
1216
+ const result = this.children[0].handleMouse?.({ ...event,
1217
+ x: Math.max(0, event.x - view.frame.inset), y: sourceY + view.sourceOffset,
1218
+ width: view.width, height: view.frame.sourceRows.size + view.sourceOffset,
1219
+ });
1220
+ if (result?.handled) currentTui?.requestRender?.();
1221
+ return result;
1222
+ };
1223
+ compactionPrototype[compactionPatchKey] = { originalRender, originalHandleMouse };
1224
+ compactionPrototype.render = render;
1225
+ compactionPrototype.handleMouse = handleMouse;
1226
+ pi.on("session_shutdown", () => {
1227
+ if (compactionPrototype.render === render) compactionPrototype.render = originalRender;
1228
+ if (compactionPrototype.handleMouse === handleMouse) compactionPrototype.handleMouse = originalHandleMouse;
1229
+ if (compactionPrototype.render === originalRender && compactionPrototype.handleMouse === originalHandleMouse) {
1230
+ delete compactionPrototype[compactionPatchKey];
1231
+ }
1232
+ });
1233
+ }
1234
+
1166
1235
  // Thinking and tools are peers in the transcript-first activity timeline.
1167
- // Assistant components keep Pi's native Markdown renderer, while clean mode
1168
- // projects each thinking run as a compact, independently expandable child.
1236
+ // Assistant components keep Pi's native visible-answer Markdown renderer.
1237
+ // All modes share thought content; only clean adds activity-group framing.
1169
1238
  const assistantPrototype = AssistantMessageComponent.prototype as any;
1170
1239
  const thinkingPatchKey = Symbol.for("pretty-tui.clean-thinking");
1171
1240
  if (!assistantPrototype[thinkingPatchKey]) {
1172
1241
  const originalUpdateContent = assistantPrototype.updateContent;
1173
1242
  const originalRender = assistantPrototype.render;
1174
1243
  const originalHandleMouse = assistantPrototype.handleMouse;
1244
+ const originalSetHideThinkingBlock = assistantPrototype.setHideThinkingBlock;
1245
+ const explicitThinkingVisibility = new WeakMap<object, boolean>();
1175
1246
  const originalMessageKey = Symbol("pretty-tui.original-assistant-message");
1176
1247
  const renderedModeKey = Symbol("pretty-tui.rendered-assistant-mode");
1177
1248
  const renderedExpansionKey = Symbol("pretty-tui.rendered-assistant-expansion");
1178
1249
  const cleanThinkingRenderCacheKey = Symbol("pretty-tui.clean-thinking-render-cache");
1179
- const cleanThinkingDetailKey = Symbol("pretty-tui.clean-thinking-detail");
1180
1250
 
1181
1251
  const assistantTerminalUpdate = (message: any): ActivityMember | undefined => {
1182
1252
  if (!assistantTerminalState(message)) return undefined;
@@ -1216,32 +1286,84 @@ export default function prettyTui(pi: ExtensionAPI) {
1216
1286
  currentTui?.requestRender?.();
1217
1287
  };
1218
1288
 
1219
- // Shared detail projection for a Thought, used both as a member of a tool
1220
- // group and as a standalone row. The cache records its theme because the
1221
- // same component can switch between the Markdown fallback and a real tool
1222
- // theme once the first tool of the group is constructed.
1223
- const thinkingDetailLines = (
1224
- owner: any,
1225
- text: string,
1226
- theme: any,
1227
- childWidth: number,
1228
- ): string[] => {
1229
- let detail = owner[cleanThinkingDetailKey];
1230
- if (!detail || detail.text !== text || detail.theme !== theme) {
1231
- detail = {
1232
- text,
1233
- theme,
1234
- component: new Markdown(text, 0, 0, owner.markdownTheme, {
1235
- color: (line: string) => theme.fg("muted", line),
1236
- italic: true,
1237
- }),
1289
+ const thoughtPresentation = new ThoughtPresentation();
1290
+ const fullThinkingVisibility = new WeakMap<object, boolean>();
1291
+ const patchedSetHideThinkingBlock = function (this: any, hide: boolean) {
1292
+ if (renderMode !== "clean") explicitThinkingVisibility.set(this, !hide);
1293
+ if (renderMode === "full") fullThinkingVisibility.set(this, !hide);
1294
+ return originalSetHideThinkingBlock.call(this, hide);
1295
+ };
1296
+ const panelAssistantViews = new WeakMap<object, { rows: Map<number, { sourceY: number; inset: number }>; width: number; height: number }>();
1297
+ const renderPanelAssistant = function (this: any, width: number): string[] {
1298
+ const innerWidth = nativeFrameWidth(width);
1299
+ const nativeLines = originalRender.call(this, innerWidth);
1300
+ const rows = new Map<number, { sourceY: number; inset: number }>();
1301
+ const thoughtRows = new Set<number>();
1302
+ const output: string[] = [];
1303
+ const children = this.contentContainer.children;
1304
+ let sourceY = 0;
1305
+ let thoughtRun = 0;
1306
+ const previews: string[] = [];
1307
+ if (renderMode === "compact") {
1308
+ const message = this[originalMessageKey] ?? this.lastMessage;
1309
+ let run: string[] = [];
1310
+ const finishRun = () => {
1311
+ if (run.length) previews.push(run.join("\n\n").split(/\r\n|\r|\n/).find((line) => line.trim())?.trim() ?? "");
1312
+ run = [];
1238
1313
  };
1239
- owner[cleanThinkingDetailKey] = detail;
1314
+ for (const item of message?.content ?? []) {
1315
+ if (item.type === "thinking") {
1316
+ if (item.thinking?.trim()) run.push(item.thinking.trim());
1317
+ } else finishRun();
1318
+ }
1319
+ finishRun();
1240
1320
  }
1241
- const lines: string[] = detail.component.render(Math.max(1, childWidth));
1242
- return lines.map((line, index) =>
1243
- theme.fg("dim", index === lines.length - 1 ? " └ " : " │ ") + line
1244
- );
1321
+ for (let index = 0; index < children.length; index++) {
1322
+ const child = children[index];
1323
+ const childLines = child.render(innerWidth);
1324
+ const isThought = Boolean(child.onMouse && child.child);
1325
+ if (isThought || child instanceof Markdown && childLines.length) {
1326
+ let height = childLines.length;
1327
+ // Adjacent native text blocks belong to the same answer card. Keep
1328
+ // native paragraph order and source coordinates, not copied text.
1329
+ if (!isThought) {
1330
+ while (children[index + 1] instanceof Markdown) {
1331
+ height += children[++index].render(innerWidth).length;
1332
+ }
1333
+ }
1334
+ const runIndex = thoughtRun;
1335
+ const hidden = isThought && (this.thinkingVisibilityOverrides.get(thoughtRun++) ?? this.hideThinkingBlock);
1336
+ const source = renderMode === "compact" && hidden
1337
+ ? [defaultActivityTheme().fg("thinkingText", truncateToWidth(previews[runIndex] ?? "", innerWidth, "…"))]
1338
+ : nativeLines.slice(sourceY, sourceY + height);
1339
+ const role = isThought ? "mdLink" : "text";
1340
+ const baseLabel = isThought ? this.isStreaming ? "thinking" : "thought" : "Assistant";
1341
+ const label = isThought && renderMode === "compact" ? `${baseLabel} ${hidden ? "▸" : "▾"}` : baseLabel;
1342
+ const frame = frameNativeRows(label, source, width, defaultActivityTheme(), undefined, child, role, "", role);
1343
+ for (const [y, nativeY] of frame.sourceRows) {
1344
+ rows.set(output.length + y, { sourceY: sourceY + nativeY, inset: frame.inset });
1345
+ if (isThought) thoughtRows.add(output.length + y);
1346
+ }
1347
+ if (isThought && height) {
1348
+ rows.set(output.length, { sourceY, inset: frame.inset });
1349
+ thoughtRows.add(output.length);
1350
+ }
1351
+ output.push(...frame.lines);
1352
+ sourceY += height;
1353
+ } else {
1354
+ for (let y = 0; y < childLines.length; y++) rows.set(output.length + y, { sourceY: sourceY + y, inset: 0 });
1355
+ output.push(...nativeLines.slice(sourceY, sourceY + childLines.length));
1356
+ sourceY += childLines.length;
1357
+ }
1358
+ }
1359
+ if (renderMode === "compact") {
1360
+ const terminal = assistantTerminalUpdate(this[originalMessageKey] ?? this.lastMessage);
1361
+ if (terminal) output.push(...renderPendingActivityUpdate(terminal, width, this));
1362
+ }
1363
+ panelAssistantViews.set(this, { rows, width: innerWidth, height: nativeLines.length });
1364
+ mouseClickHitTests.set(this, (event) => thoughtRows.has(event.y));
1365
+ return renderMode === "compact" && width < 8
1366
+ ? output.map((line) => isNativeImageRow(line) ? line : truncateToWidth(line, Math.max(1, width), "")) : output;
1245
1367
  };
1246
1368
 
1247
1369
  const patchedUpdateContent = function (this: any, message: any, isStreaming = this.isStreaming) {
@@ -1250,8 +1372,13 @@ export default function prettyTui(pi: ExtensionAPI) {
1250
1372
  this[renderedExpansionKey] = cleanToolsExpanded;
1251
1373
  this.lastMessage = message;
1252
1374
  if (renderMode !== "clean") {
1253
- originalUpdateContent.call(this, message, isStreaming);
1375
+ this.hideThinkingBlock = renderMode === "full"
1376
+ ? !(fullThinkingVisibility.get(this) ?? !fullThinkingHidden)
1377
+ : !(explicitThinkingVisibility.get(this) ?? !compactThinkingHidden);
1378
+ originalUpdateContent.call(this, renderMode === "compact" && assistantTerminalState(message)
1379
+ ? { ...message, errorMessage: undefined, stopReason: "stop" } : message, isStreaming);
1254
1380
  this.lastMessage = message;
1381
+ if (eligibleThinking(message)) thinkingComponents.set(assistantMessageKey(message), this);
1255
1382
  return;
1256
1383
  }
1257
1384
  const groupedThinking = eligibleThinking(message);
@@ -1263,15 +1390,17 @@ export default function prettyTui(pi: ExtensionAPI) {
1263
1390
 
1264
1391
  const previousHideThinkingBlock = this.hideThinkingBlock;
1265
1392
  this.hideThinkingBlock = false;
1266
- // Clean mode owns grouped thinking completely. Removing it from Pi's
1267
- // native content tree also removes the native MouseRegion that would
1393
+ // Shared thought presentation owns thinking completely. Removing it from
1394
+ // Pi's native content tree also removes the native MouseRegion that would
1268
1395
  // otherwise toggle a second, unrelated `Thinking...` block.
1269
1396
  const displayMessage =
1270
1397
  groupedThinking && Array.isArray(message?.content)
1271
1398
  ? { ...message, content: message.content.filter((item: any) => item.type !== "thinking") }
1272
1399
  : message;
1273
1400
  try {
1274
- originalUpdateContent.call(this, displayMessage, isStreaming);
1401
+ originalUpdateContent.call(this, assistantTerminalState(message)
1402
+ ? { ...displayMessage, errorMessage: undefined, stopReason: "stop" }
1403
+ : displayMessage, isStreaming);
1275
1404
  } finally {
1276
1405
  this.hideThinkingBlock = previousHideThinkingBlock;
1277
1406
  }
@@ -1286,14 +1415,17 @@ export default function prettyTui(pi: ExtensionAPI) {
1286
1415
  ) {
1287
1416
  patchedUpdateContent.call(this, message, this.isStreaming);
1288
1417
  }
1289
- if (renderMode !== "clean") return originalRender.call(this, width);
1418
+ if (renderMode !== "clean") return renderPanelAssistant.call(this, width);
1290
1419
  const terminalMember = assistantTerminalUpdate(message);
1291
1420
  if (!eligibleThinking(message)) {
1292
1421
  if (!terminalMember) return originalRender.call(this, width);
1293
1422
  const terminalGroup = activityTimeline.groupForMember(terminalMember.id);
1294
1423
  if (!terminalGroup) return originalRender.call(this, width);
1295
1424
  activityUpdateComponents.set(terminalMember.id, this);
1296
- return renderActivityUpdateProjection(terminalGroup, terminalMember, width, this);
1425
+ return [
1426
+ ...renderActivityUpdateProjection(terminalGroup, terminalMember, width, this),
1427
+ ...(visibleAssistantText(message) ? originalRender.call(this, width) : []),
1428
+ ];
1297
1429
  }
1298
1430
 
1299
1431
  const key = assistantMessageKey(message);
@@ -1311,10 +1443,9 @@ export default function prettyTui(pi: ExtensionAPI) {
1311
1443
  }
1312
1444
  const position = activityMemberPosition(group, member);
1313
1445
  const visibleLines = visibleAssistantText(message) ? originalRender.call(this, width) : [];
1314
- // Pi's native renderer prints its own error line for messages with no
1315
- // tool calls, so this component only adds the styled alert card when no
1316
- // visible text can carry it.
1317
- const terminalLines = terminalMember && visibleLines.length === 0
1446
+ // Native errors are filtered above; append the shared alert exactly once
1447
+ // while preserving any partial visible answer outside the thought block.
1448
+ const terminalLines = terminalMember
1318
1449
  ? renderPendingActivityUpdate(terminalMember, width)
1319
1450
  : [];
1320
1451
  const appendTail = (activityLines: string[]) => {
@@ -1343,7 +1474,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1343
1474
  );
1344
1475
  // Clean mode owns thought disclosure: revealing a group shows the thought
1345
1476
  // member, while only a member click (or Ctrl+O) reveals its full text.
1346
- const expanded = cleanToolsExpanded || expandedThinkingMembers.has(member.id);
1477
+ const expanded = expandedThinkingMembers.has(member.id) || (explicitThinkingVisibility.get(this) ?? cleanToolsExpanded);
1347
1478
  const settledOwner = position.first ? activityGroupOwner(group) : undefined;
1348
1479
  // A group without tools has no durable summary, so its own boundary is
1349
1480
  // the signal that it finished.
@@ -1367,12 +1498,10 @@ export default function prettyTui(pi: ExtensionAPI) {
1367
1498
  return appendTail(cached.lines);
1368
1499
  }
1369
1500
  const stateLabel = this.isStreaming ? "thinking" : "thought";
1370
- const label = truncateToWidth(stateLabel, Math.max(1, childWidth - 2), "…");
1371
- const header = groupTheme.fg("mdLink", "● ") +
1372
- groupTheme.fg("text", groupTheme.bold(label));
1501
+ const header = thoughtPresentation.header(stateLabel, childWidth, groupTheme);
1373
1502
  let contentLines: string[] = [header];
1374
1503
  if (expanded) {
1375
- contentLines.push(...thinkingDetailLines(
1504
+ contentLines.push(...thoughtPresentation.details(
1376
1505
  this,
1377
1506
  member.thinking ?? thinkingText(message),
1378
1507
  groupTheme,
@@ -1405,7 +1534,15 @@ export default function prettyTui(pi: ExtensionAPI) {
1405
1534
 
1406
1535
  const handleAssistantTranscriptMouse = function (this: any, event: any) {
1407
1536
  const message = this[originalMessageKey] ?? this.lastMessage;
1408
- if (renderMode !== "clean") return originalHandleMouse.call(this, event);
1537
+ if (renderMode !== "clean") {
1538
+ patchedRender.call(this, event.width);
1539
+ const view = panelAssistantViews.get(this);
1540
+ const row = view?.rows.get(event.y);
1541
+ if (!view || !row) return undefined;
1542
+ return originalHandleMouse.call(this, { ...event,
1543
+ x: Math.max(0, event.x - row.inset), y: row.sourceY, width: view.width, height: view.height,
1544
+ });
1545
+ }
1409
1546
  if (!eligibleThinking(message)) {
1410
1547
  const terminalMember = assistantTerminalUpdate(message);
1411
1548
  const terminalGroup = terminalMember
@@ -1478,6 +1615,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1478
1615
  originalHandleMouse,
1479
1616
  patchedHandleMouse,
1480
1617
  };
1618
+ assistantPrototype.setHideThinkingBlock = patchedSetHideThinkingBlock;
1481
1619
  assistantPrototype.updateContent = patchedUpdateContent;
1482
1620
  assistantPrototype.render = patchedRender;
1483
1621
  assistantPrototype.handleMouse = patchedHandleMouse;
@@ -1485,6 +1623,9 @@ export default function prettyTui(pi: ExtensionAPI) {
1485
1623
  pi.on("session_shutdown", () => {
1486
1624
  const patch = assistantPrototype[thinkingPatchKey];
1487
1625
  if (!patch) return;
1626
+ if (assistantPrototype.setHideThinkingBlock === patchedSetHideThinkingBlock) {
1627
+ assistantPrototype.setHideThinkingBlock = originalSetHideThinkingBlock;
1628
+ }
1488
1629
  if (patch.patchedUpdateContent === assistantPrototype.updateContent) {
1489
1630
  assistantPrototype.updateContent = patch.originalUpdateContent;
1490
1631
  }
@@ -1682,10 +1823,8 @@ export default function prettyTui(pi: ExtensionAPI) {
1682
1823
  message: string,
1683
1824
  type?: "info" | "warning" | "error",
1684
1825
  ) {
1685
- if (renderMode !== "clean") {
1686
- return originalShowExtensionNotify.call(this, message, type);
1687
- }
1688
1826
  const severity: ActivitySeverity = type ?? "info";
1827
+ if (renderMode === "full") return nativeExtensionNotify.call(this, message, severity);
1689
1828
  if (!appendRuntimeActivityUpdate(this, message, severity)) {
1690
1829
  nativeExtensionNotify.call(this, message, severity);
1691
1830
  }
@@ -1693,6 +1832,13 @@ export default function prettyTui(pi: ExtensionAPI) {
1693
1832
 
1694
1833
  const patchedToggleThinkingBlockVisibility = function (this: any) {
1695
1834
  if (renderMode !== "clean") {
1835
+ if (renderMode === "full") {
1836
+ this.hideThinkingBlock = fullThinkingHidden;
1837
+ fullThinkingHidden = !fullThinkingHidden;
1838
+ } else {
1839
+ this.hideThinkingBlock = compactThinkingHidden;
1840
+ compactThinkingHidden = !compactThinkingHidden;
1841
+ }
1696
1842
  return originalToggleThinkingBlockVisibility.call(this);
1697
1843
  }
1698
1844
  // Do not change Pi's persisted hideThinkingBlock setting in clean mode.
@@ -1704,7 +1850,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1704
1850
  };
1705
1851
 
1706
1852
  const patchedShowStatus = function (this: any, message: string) {
1707
- if (renderMode !== "clean") return originalShowStatus.call(this, message);
1853
+ if (renderMode === "full") return originalShowStatus.call(this, message);
1708
1854
  const [firstLine, ...remainingLines] = String(message).split("\n");
1709
1855
  setLatestActivityStatus({
1710
1856
  title: firstLine || "Status",
@@ -1715,13 +1861,15 @@ export default function prettyTui(pi: ExtensionAPI) {
1715
1861
  };
1716
1862
 
1717
1863
  const patchedShowWarning = function (this: any, message: string) {
1718
- if (renderMode !== "clean" || !appendRuntimeActivityUpdate(this, message, "warning")) {
1864
+ if (renderMode === "full") return originalShowWarning.call(this, message);
1865
+ if (!appendRuntimeActivityUpdate(this, message, "warning")) {
1719
1866
  return originalShowWarning.call(this, message);
1720
1867
  }
1721
1868
  };
1722
1869
 
1723
1870
  const patchedShowError = function (this: any, message: string) {
1724
- if (renderMode !== "clean" || !appendRuntimeActivityUpdate(this, message, "error")) {
1871
+ if (renderMode === "full") return originalShowError.call(this, message);
1872
+ if (!appendRuntimeActivityUpdate(this, message, "error")) {
1725
1873
  return originalShowError.call(this, message);
1726
1874
  }
1727
1875
  };
@@ -1772,9 +1920,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1772
1920
  }
1773
1921
  // A mode switch can keep existing components rather than rebuild them.
1774
1922
  const component: Component = {
1775
- render: (width: number) => renderMode === "clean"
1776
- ? renderActivityUpdateProjection(group, member, width)
1777
- : nativeNodes.flatMap((node) => node.render(width)),
1923
+ render: (width: number) => renderActivityUpdateProjection(group, member, width),
1778
1924
  invalidate() {},
1779
1925
  handleMouse: (event: any) => renderMode === "clean"
1780
1926
  ? handleActivityUpdateMouse(group, member, event)
@@ -1808,7 +1954,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1808
1954
  if (!group) continue;
1809
1955
  mountActivityUpdate(
1810
1956
  cacheNoticeHost.current, group, member, [],
1811
- cleanToolComponents.get(toolCallId),
1957
+ toolComponents.get(toolCallId),
1812
1958
  );
1813
1959
  refreshActivityGroup(group);
1814
1960
  cacheNoticeHost.current?.ui?.requestRender?.();
@@ -1817,7 +1963,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1817
1963
  flushDeferredCacheNotices = flushPendingCacheNotices;
1818
1964
 
1819
1965
  const patchedAddCacheMissNotice = function (this: any, miss: any) {
1820
- if (renderMode !== "clean" || !this.chatContainer?.addChild) {
1966
+ if (!this.chatContainer?.addChild) {
1821
1967
  return originalAddCacheMissNotice.call(this, miss);
1822
1968
  }
1823
1969
  // Let Pi decide thresholds, formatting and cause labels. Capture its
@@ -1876,7 +2022,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1876
2022
  if (!group) return;
1877
2023
  mountActivityUpdate(
1878
2024
  this, group, member, nativeNodes,
1879
- anchor?.toolCallId ? cleanToolComponents.get(anchor.toolCallId) : undefined,
2025
+ anchor?.toolCallId ? toolComponents.get(anchor.toolCallId) : undefined,
1880
2026
  );
1881
2027
  const component = activityUpdateComponents.get(member.id);
1882
2028
  if (component && message) cacheNoticeOwners.set(component, message);
@@ -2520,10 +2666,11 @@ export default function prettyTui(pi: ExtensionAPI) {
2520
2666
  prefix: () => {
2521
2667
  const status = (state.compactToolStatus ?? "running") as ToolStatus;
2522
2668
  const color = status === "success" ? "success" : status === "error" ? "error" : "dim";
2669
+ if (renderMode === "compact") return name === "bash" ? theme.fg("toolTitle", "$ ") : "";
2523
2670
  return theme.fg(color, "● ");
2524
2671
  },
2525
- continuation: " ",
2526
- content:
2672
+ continuation: renderMode === "compact" && name !== "bash" ? "" : " ",
2673
+ content: renderMode === "compact" ? theme.fg("muted", detail) :
2527
2674
  theme.fg("text", theme.bold(name)) +
2528
2675
  theme.fg("dim", "(") +
2529
2676
  theme.fg("muted", detail) +
@@ -2769,65 +2916,6 @@ export default function prettyTui(pi: ExtensionAPI) {
2769
2916
  settled: false,
2770
2917
  };
2771
2918
 
2772
- const toolLabel = (toolName: string, label?: string): string => label || toolName;
2773
-
2774
- const conciseThirdPartyArgs = (args: any): string => {
2775
- if (!args || typeof args !== "object" || Array.isArray(args)) return "";
2776
- const sensitive = /(?:token|secret|password|authorization|credential|api[_-]?key)/iu;
2777
- const preferred = ["path", "query", "url", "offset", "id"];
2778
- const entries = Object.entries(args)
2779
- .filter(([key, value]) => !sensitive.test(key) && ["string", "number", "boolean"].includes(typeof value))
2780
- .sort(([left], [right]) => {
2781
- const leftRank = preferred.indexOf(left);
2782
- const rightRank = preferred.indexOf(right);
2783
- return (leftRank < 0 ? preferred.length : leftRank) -
2784
- (rightRank < 0 ? preferred.length : rightRank);
2785
- })
2786
- .slice(0, 2)
2787
- .map(([key, value]) => {
2788
- const safeValue = String(value)
2789
- .replace(/(https?:\/\/)[^\s/@:]+:[^\s/@]+@/giu, "$1…:…@")
2790
- .replace(/([?&](?:token|secret|password|api[_-]?key)=)[^&\s]+/giu, "$1…");
2791
- const rendered = truncateToWidth(safeValue, 36, "…");
2792
- return `${key}=${rendered}`;
2793
- });
2794
- return entries.join(" · ");
2795
- };
2796
-
2797
- const thirdPartyResultSummary = (component: any): string => {
2798
- const detailsError = component.result?.details?.error;
2799
- const firstText = component.result?.content?.find(
2800
- (item: any) => item?.type === "text" && typeof item.text === "string" && item.text.length > 0,
2801
- )?.text;
2802
- const text = String(typeof detailsError === "string" ? detailsError : firstText ?? "");
2803
- const boundedText = text.slice(0, 512);
2804
- const newline = boundedText.indexOf("\n");
2805
- const boundedFirstLine = boundedText.slice(0, newline < 0 ? boundedText.length : newline);
2806
- const firstLine = stripTerminalSequences(boundedFirstLine).trim();
2807
- if (firstLine) return truncateToWidth(firstLine, 72, "…");
2808
- if (component.result) return component.result.isError ? "Failed" : "Done";
2809
- return component.executionStarted ? "Running…" : "Pending";
2810
- };
2811
-
2812
- const renderThirdPartyCompact = (component: any, width: number, theme: any): string[] => {
2813
- const label = toolLabel(component.toolName, component.toolDefinition?.label);
2814
- const args = conciseThirdPartyArgs(component.args);
2815
- const dotColor = component.result?.isError
2816
- ? "error"
2817
- : component.result && !component.isPartial
2818
- ? "success"
2819
- : "dim";
2820
- const title = theme.fg(dotColor, "● ") +
2821
- theme.fg("text", theme.bold(label)) +
2822
- (args ? theme.fg("muted", `(${args})`) : "");
2823
- const result = thirdPartyResultSummary(component);
2824
- const resultColor = component.result?.isError ? "error" : "muted";
2825
- return [
2826
- truncateToWidth(title, Math.max(1, width), "…"),
2827
- theme.fg(resultColor, truncateToWidth(` └ ${result}`, Math.max(1, width), "…")),
2828
- ];
2829
- };
2830
-
2831
2919
  const clearToolActivityHolds = () => {
2832
2920
  for (const hold of toolActivityHolds.values()) {
2833
2921
  if (hold.timer) clearTimeout(hold.timer);
@@ -2937,6 +3025,38 @@ export default function prettyTui(pi: ExtensionAPI) {
2937
3025
  const originalToolHandleMouse = toolExecutionPrototype.handleMouse;
2938
3026
  const renderedModeKey = Symbol("pretty-tui.tool-rendered-mode");
2939
3027
  const cleanChildRenderCacheKey = Symbol("pretty-tui.clean-child-render-cache");
3028
+ const standaloneViews = new WeakMap<object, { projection: ToolProjection; offset: number; width: number }>();
3029
+ const projectTool = (owner: any, width: number, theme: any, mode: "compact" | "full") =>
3030
+ renderToolProjection(owner, width, theme, {
3031
+ mode, nested: nestedTools, nestedExpanded: Boolean(owner.expanded), panels: renderMode !== "clean",
3032
+ renderNative: (nativeWidth) => originalToolRender.call(owner, nativeWidth),
3033
+ });
3034
+ const handleProjectionMouse = (owner: any, projection: ToolProjection, event: any) => {
3035
+ const isClick = event.type === "click" && event.button === "left";
3036
+ const action = projection.actions.get(event.y);
3037
+ if (isClick && action) {
3038
+ nestedTools.toggle(action, Boolean(owner.expanded));
3039
+ owner.updateDisplay?.();
3040
+ owner.ui?.requestRender?.();
3041
+ return { handled: true };
3042
+ }
3043
+ if (isClick && projection.toggleRows.has(event.y)) {
3044
+ originalSetExpanded.call(owner, !owner.expanded);
3045
+ owner.ui?.requestRender?.();
3046
+ return { handled: true };
3047
+ }
3048
+ const native = projection.native;
3049
+ const sourceY = projection.sourceRows?.get(event.y);
3050
+ if (native && (projection.sourceRows ? sourceY !== undefined : event.y >= native.y && event.y < native.y + native.height)) {
3051
+ return originalToolHandleMouse.call(owner, {
3052
+ ...event, x: Math.max(0, event.x - native.x),
3053
+ y: sourceY ?? event.y - native.y + native.sourceY,
3054
+ width: native.width, height: native.height + native.sourceY,
3055
+ });
3056
+ }
3057
+ // Projected rows must never be interpreted as native renderer rows.
3058
+ return isClick ? { handled: true } : undefined;
3059
+ };
2940
3060
  const revealCleanGroup = revealActivityGroup;
2941
3061
  const collapseCleanGroup = collapseActivityGroup;
2942
3062
  const patchedMarkExecutionStarted = function (this: any) {
@@ -2972,7 +3092,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2972
3092
  return originalSetExpanded.call(this, expanded);
2973
3093
  };
2974
3094
  const patchedToolRender = function (this: any, width: number): string[] {
2975
- cleanToolComponents.set(this.toolCallId, this);
3095
+ toolComponents.set(this.toolCallId, this);
2976
3096
  knownToolCallIds.add(this.toolCallId);
2977
3097
  const member = activityTimeline.addTool(this.toolCallId, this.toolName);
2978
3098
  cleanToolNames.set(
@@ -2985,7 +3105,24 @@ export default function prettyTui(pi: ExtensionAPI) {
2985
3105
  }
2986
3106
 
2987
3107
  const group = activityTimeline.groupForMember(member.id);
2988
- if (renderMode !== "clean" || !group) return originalToolRender.call(this, width);
3108
+ if (renderMode !== "clean" || !group) {
3109
+ const projection = renderMode !== "clean"
3110
+ ? projectTool(this, width, defaultActivityTheme(), renderMode)
3111
+ : undefined;
3112
+ if (!projection) {
3113
+ standaloneViews.delete(this);
3114
+ mouseClickHitTests.delete(this);
3115
+ return originalToolRender.call(this, width);
3116
+ }
3117
+ const offset = renderMode === "full" ? 2 : 1;
3118
+ standaloneViews.set(this, { projection, offset, width });
3119
+ mouseClickHitTests.set(this, (event) => event.width === width && (
3120
+ projection.actions.has(event.y - offset) || projection.toggleRows.has(event.y - offset) ||
3121
+ Boolean(projection.sourceRows?.has(event.y - offset))
3122
+ ));
3123
+ return [...Array<string>(offset).fill(""), ...projection.lines];
3124
+ }
3125
+ standaloneViews.delete(this);
2989
3126
  const position = activityMemberPosition(group, member);
2990
3127
  let renderedHeight = 0;
2991
3128
  let nestedClickActions: Map<number, string> | undefined;
@@ -3019,8 +3156,6 @@ export default function prettyTui(pi: ExtensionAPI) {
3019
3156
  // Other extensions (notably FFF in override mode) can register their own
3020
3157
  // grep/find renderers under built-in names. Their default boxed shell is
3021
3158
  // not one of our self-rendering specialized tools.
3022
- const thirdParty = !SPECIALIZED_TOOL_NAMES.has(this.toolName) ||
3023
- this.toolDefinition?.renderShell !== "self";
3024
3159
  const cacheable = nestedTools.count(this.toolCallId) === 0 && !this.expanded && Boolean(this.result) && !this.isPartial;
3025
3160
  const cached = cacheable ? this[cleanChildRenderCacheKey] : undefined;
3026
3161
  let decoratedContent: string[];
@@ -3038,31 +3173,10 @@ export default function prettyTui(pi: ExtensionAPI) {
3038
3173
  decoratedContent = cached.lines;
3039
3174
  } else {
3040
3175
  let contentLines: string[];
3041
- if (nestedTools.count(this.toolCallId) > 0) {
3042
- const projection = nestedTools.render(this, childWidth, childTheme);
3176
+ const projection = projectTool(this, childWidth, childTheme, "compact");
3177
+ if (projection) {
3043
3178
  contentLines = projection.lines;
3044
- nestedClickActions = projection.actions;
3045
- } else if (thirdParty) {
3046
- const summaryLines = renderThirdPartyCompact(this, childWidth, childTheme);
3047
- if (this.expanded) {
3048
- const detailPrefix = " │ ";
3049
- const nativeLines = originalToolRender.call(
3050
- this,
3051
- Math.max(1, childWidth - visibleWidth(detailPrefix)),
3052
- );
3053
- const nativeContentLines = nativeLines[0] === "" ? nativeLines.slice(1) : nativeLines;
3054
- contentLines = [
3055
- summaryLines[0],
3056
- ...nativeContentLines.map((line: string, index: number) =>
3057
- childTheme.fg(
3058
- "dim",
3059
- index === nativeContentLines.length - 1 ? " └ " : detailPrefix,
3060
- ) + stripAnsiBackgrounds(line)
3061
- ),
3062
- ];
3063
- } else {
3064
- contentLines = summaryLines;
3065
- }
3179
+ if (nestedTools.count(this.toolCallId) > 0) nestedClickActions = projection.actions;
3066
3180
  } else {
3067
3181
  const lines = originalToolRender.call(this, childWidth);
3068
3182
  if (lines.length === 0) return lines;
@@ -3102,6 +3216,11 @@ export default function prettyTui(pi: ExtensionAPI) {
3102
3216
  const group = member ? activityTimeline.groupForMember(member.id) : undefined;
3103
3217
  const isLeftClick = event.type === "click" && event.button === "left";
3104
3218
  if (renderMode !== "clean" || !member || !group) {
3219
+ patchedToolRender.call(this, event.width);
3220
+ const view = standaloneViews.get(this);
3221
+ if (view) return handleProjectionMouse(this, view.projection, {
3222
+ ...event, y: event.y - view.offset, height: view.projection.lines.length,
3223
+ });
3105
3224
  return originalToolHandleMouse.call(this, event);
3106
3225
  }
3107
3226
  const position = activityMemberPosition(group, member);
@@ -3128,33 +3247,15 @@ export default function prettyTui(pi: ExtensionAPI) {
3128
3247
  return { handled: true };
3129
3248
  }
3130
3249
 
3131
- if (nestedTools.count(this.toolCallId) > 0 && isLeftClick) {
3250
+ if (nestedTools.count(this.toolCallId) > 0 || isThirdPartyTool(this)) {
3132
3251
  const theme = cleanThemeForToolCall(this.toolCallId) ?? activityGroupTheme(group);
3133
- const { childWidth } = activityTreeStyle(group, member, event.width, theme);
3134
- const projection = nestedTools.render(this, childWidth, theme);
3135
- const contentY = event.y - (position.first ? summaryHeight + 1 : 0);
3136
- const action = projection.actions.get(contentY);
3137
- if (action) {
3138
- nestedTools.toggle(action, Boolean(this.expanded));
3139
- this.updateDisplay?.();
3140
- this.ui?.requestRender?.();
3141
- }
3142
- // The parent title is informational; only disclosure rows act on clicks.
3143
- // Nested results are UI projections, not native renderer rows.
3144
- return { handled: true };
3145
- }
3146
-
3147
- // Self-rendering tools reserve y=0 for Pi's leading spacer. Clean mode
3148
- // replaces that spacer with the compact header for non-first members.
3149
- const thirdPartyHeaderY = position.first ? summaryHeight + 1 : 0;
3150
- if (
3151
- (!SPECIALIZED_TOOL_NAMES.has(this.toolName) || this.toolDefinition?.renderShell !== "self") &&
3152
- isLeftClick &&
3153
- event.y === thirdPartyHeaderY
3154
- ) {
3155
- originalSetExpanded.call(this, !this.expanded);
3156
- this.ui?.requestRender?.();
3157
- return { handled: true };
3252
+ const { childWidth, prefixWidth } = activityTreeStyle(group, member, event.width, theme);
3253
+ const projection = projectTool(this, childWidth, theme, "compact");
3254
+ if (projection) return handleProjectionMouse(this, projection, {
3255
+ ...event, x: Math.max(0, event.x - prefixWidth),
3256
+ y: event.y - (position.first ? summaryHeight + 1 : 0),
3257
+ width: childWidth, height: projection.lines.length,
3258
+ });
3158
3259
  }
3159
3260
 
3160
3261
  if (!this.result && isLeftClick) {
@@ -3653,7 +3754,7 @@ export default function prettyTui(pi: ExtensionAPI) {
3653
3754
  cleanCompactToolCallIds.clear();
3654
3755
  cleanGroupToolCallIds.clear();
3655
3756
  cleanToolCallGroupOwners.clear();
3656
- cleanToolComponents.clear();
3757
+ toolComponents.clear();
3657
3758
  cleanToolThemes.clear();
3658
3759
  cleanToolNames.clear();
3659
3760
  clearToolActivityHolds();
@@ -3902,7 +4003,11 @@ export default function prettyTui(pi: ExtensionAPI) {
3902
4003
  finishGroup();
3903
4004
  };
3904
4005
 
3905
- pi.on("session_start", (_event, ctx) => restoreCleanSession(ctx));
4006
+ pi.on("session_start", (_event, ctx) => {
4007
+ fullThinkingHidden = false;
4008
+ compactThinkingHidden = true;
4009
+ restoreCleanSession(ctx);
4010
+ });
3906
4011
  pi.on("session_tree", (_event, ctx) => restoreCleanSession(ctx));
3907
4012
  pi.on("session_before_compact", () => {
3908
4013
  // Pi emits this for manual, threshold, and overflow-recovery compaction.
@@ -4071,7 +4176,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4071
4176
  cleanRun.activeToolCallId = undefined;
4072
4177
  cleanRun.activeToolName = undefined;
4073
4178
  cleanRun.activity = "thinking";
4074
- if (renderMode === "clean") startWorkingElapsedTimer();
4179
+ startWorkingElapsedTimer();
4075
4180
  });
4076
4181
  pi.on("tool_execution_start", (event) => {
4077
4182
  if ((event as any).parentToolCallId) {
@@ -4222,9 +4327,33 @@ export default function prettyTui(pi: ExtensionAPI) {
4222
4327
  });
4223
4328
  });
4224
4329
 
4330
+ const nativeDefinitions: Record<string, any> = {
4331
+ read: createReadToolDefinition(cwd), bash: createBashToolDefinition(cwd),
4332
+ edit: createEditToolDefinition(cwd), write: createWriteToolDefinition(cwd),
4333
+ grep: createGrepToolDefinition(cwd), find: createFindToolDefinition(cwd), ls: createLsToolDefinition(cwd),
4334
+ };
4335
+ const registerStyledTool = (definition: any) => {
4336
+ const native = nativeDefinitions[definition.name];
4337
+ const select = (kind: "renderCall" | "renderResult", args: any[]) => {
4338
+ const context = args.at(-1);
4339
+ const useNative = renderMode === "full" || renderMode === "compact" && context.expanded;
4340
+ const namespace = useNative ? "nativeFull" : "pretty";
4341
+ const state = context.state[namespace] ??= { state: {} };
4342
+ const scoped = { ...context, state: state.state, lastComponent: state[kind] };
4343
+ const renderer = useNative ? native[kind] : definition[kind];
4344
+ const component = renderer?.(...args.slice(0, -1), scoped);
4345
+ state[kind] = component;
4346
+ return component;
4347
+ };
4348
+ pi.registerTool({ ...definition,
4349
+ renderCall: (...args: any[]) => select("renderCall", args),
4350
+ renderResult: (...args: any[]) => select("renderResult", args),
4351
+ });
4352
+ };
4353
+
4225
4354
  const read = createReadTool(cwd);
4226
4355
  const readLabel = read.label || read.name;
4227
- pi.registerTool({
4356
+ registerStyledTool({
4228
4357
  ...read,
4229
4358
  renderShell: "self",
4230
4359
  renderCall(args: any, theme: any, context: any) {
@@ -4253,7 +4382,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4253
4382
 
4254
4383
  const bash = createBashTool(cwd);
4255
4384
  const bashLabel = bash.label || bash.name;
4256
- pi.registerTool({
4385
+ registerStyledTool({
4257
4386
  ...bash,
4258
4387
  renderShell: "self",
4259
4388
  renderCall(args: any, theme: any, context: any) {
@@ -4304,7 +4433,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4304
4433
 
4305
4434
  const edit = createEditTool(cwd);
4306
4435
  const editLabel = edit.label || edit.name;
4307
- pi.registerTool({
4436
+ registerStyledTool({
4308
4437
  ...edit,
4309
4438
  renderShell: "self",
4310
4439
  renderCall(args: any, theme: any, context: any) {
@@ -4347,7 +4476,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4347
4476
 
4348
4477
  const write = createWriteTool(cwd);
4349
4478
  const writeLabel = write.label || write.name;
4350
- pi.registerTool({
4479
+ registerStyledTool({
4351
4480
  ...write,
4352
4481
  renderShell: "self",
4353
4482
  renderCall(args: any, theme: any, context: any) {
@@ -4379,7 +4508,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4379
4508
 
4380
4509
  const grep = createGrepTool(cwd);
4381
4510
  const grepLabel = grep.label || grep.name;
4382
- pi.registerTool({
4511
+ registerStyledTool({
4383
4512
  ...grep,
4384
4513
  renderShell: "self",
4385
4514
  renderCall(args: any, theme: any, context: any) {
@@ -4400,7 +4529,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4400
4529
 
4401
4530
  const find = createFindTool(cwd);
4402
4531
  const findLabel = find.label || find.name;
4403
- pi.registerTool({
4532
+ registerStyledTool({
4404
4533
  ...find,
4405
4534
  renderShell: "self",
4406
4535
  renderCall(args: any, theme: any, context: any) {
@@ -4419,7 +4548,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4419
4548
 
4420
4549
  const ls = createLsTool(cwd);
4421
4550
  const lsLabel = ls.label || ls.name;
4422
- pi.registerTool({
4551
+ registerStyledTool({
4423
4552
  ...ls,
4424
4553
  renderShell: "self",
4425
4554
  renderCall(args: any, theme: any, context: any) {