pi-pretty-tui 0.2.9 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,31 @@
1
+ import { installSelectionCopy, selectableRow, selectionMetadataActive } from "./selection-ranges.js";
2
+ import { isNativeImageRow } from "./ansi-style.js";
3
+ import { cardMargin, cardBackground, frameNativeRows, nativeFrameWidth } from "./full-frame.js";
4
+ import { ThoughtPresentation } from "./thought-presentation.js";
5
+ import { isThirdPartyTool, renderToolProjection, THIRD_PARTY_TOOL, toolLabel, type ToolProjection } from "./tool-presentation.js";
1
6
  import { NestedTools, NESTED_SNAPSHOT_ENTRY } from "./nested-tools.js";
2
7
  import {
3
8
  AssistantMessageComponent,
4
9
  CustomEditor,
5
10
  CustomMessageComponent,
11
+ CompactionSummaryMessageComponent,
6
12
  InteractiveMode,
7
13
  ToolExecutionComponent,
8
14
  UserMessageComponent,
9
15
  createBashTool,
16
+ createBashToolDefinition,
10
17
  createEditTool,
18
+ createEditToolDefinition,
11
19
  createFindTool,
20
+ createFindToolDefinition,
12
21
  createGrepTool,
22
+ createGrepToolDefinition,
13
23
  createLsTool,
24
+ createLsToolDefinition,
14
25
  createReadTool,
26
+ createReadToolDefinition,
15
27
  createWriteTool,
28
+ createWriteToolDefinition,
16
29
  copyToClipboard,
17
30
  getAgentDir,
18
31
  getMarkdownTheme,
@@ -46,29 +59,12 @@ type PrettyTuiMode = "full" | "compact" | "clean";
46
59
  const CLEAN_TOOL_ACTIVITY_MIN_MS = 1000;
47
60
  const TRANSIENT_UI_STATUS_MS = 10_000;
48
61
  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
- });
62
+
69
63
  type PrettyTuiConfig = {
70
64
  enabled?: boolean;
71
65
  mode?: PrettyTuiMode;
66
+ /** Exclude plugin decoration from fullscreen selections. Opt-in. */
67
+ selectionCopy?: boolean;
72
68
  /** Legacy location used by the first mode implementation. */
73
69
  bash?: {
74
70
  mode?: "full" | "compact";
@@ -95,6 +91,10 @@ export default function prettyTui(pi: ExtensionAPI) {
95
91
  ? configuredMode
96
92
  : "clean";
97
93
  let cleanToolsExpanded = false;
94
+ // Full starts with thoughts open; an intentional Ctrl+T overrides that
95
+ // default for this runtime, independently of compact's initial setting.
96
+ let fullThinkingHidden = false;
97
+ let compactThinkingHidden = true;
98
98
  let cleanContextCompacted = false;
99
99
  let fullscreenTui = false;
100
100
  const mouseClickHitTests = new WeakMap<object, (event: any) => boolean>();
@@ -159,7 +159,7 @@ export default function prettyTui(pi: ExtensionAPI) {
159
159
  };
160
160
  const refreshLatestActivityWidget = () => {
161
161
  if (!currentExtensionUi?.setWidget) return;
162
- if (renderMode !== "clean" || !latestActivityStatus) {
162
+ if (!latestActivityStatus) {
163
163
  if (latestActivityWidgetInstalled) {
164
164
  currentExtensionUi.setWidget(latestActivityWidgetKey, undefined);
165
165
  latestActivityWidgetInstalled = false;
@@ -261,7 +261,9 @@ export default function prettyTui(pi: ExtensionAPI) {
261
261
  const activityTimeline = new ActivityTimeline();
262
262
  const nestedTools = new NestedTools();
263
263
  const refreshNestedTools = (id: string) => {
264
- cleanToolComponents.get(nestedTools.root(id))?.updateDisplay?.();
264
+ const owner = toolComponents.get(nestedTools.root(id));
265
+ owner?.updateDisplay?.();
266
+ owner?.ui?.requestRender?.();
265
267
  currentTui?.requestRender?.();
266
268
  };
267
269
  const revealedActivityGroups = new Set<string>();
@@ -283,7 +285,7 @@ export default function prettyTui(pi: ExtensionAPI) {
283
285
  const cleanCompactToolCallIds = new Set<string>();
284
286
  const cleanGroupToolCallIds = new Map<string, string[]>();
285
287
  const cleanToolCallGroupOwners = new Map<string, string>();
286
- const cleanToolComponents = new Map<string, any>();
288
+ const toolComponents = new Map<string, any>();
287
289
  const cleanToolThemes = new Map<string, any>();
288
290
  const cleanToolNames = new Map<string, string>();
289
291
  let renderCleanGroupSummary = (_lastToolCallId: string, _width: number): string[] => [];
@@ -355,12 +357,14 @@ export default function prettyTui(pi: ExtensionAPI) {
355
357
  return group.toolCallIds[group.toolCallIds.length - 1];
356
358
  };
357
359
 
360
+ let fallbackActivityTheme: any;
358
361
  const defaultActivityTheme = () => {
359
362
  if (currentExtensionUi?.theme?.fg) return currentExtensionUi.theme;
360
363
  const markdownTheme = getMarkdownTheme();
361
- return {
364
+ return fallbackActivityTheme ??= {
362
365
  fg: (color: string, text: string) => {
363
366
  if (color === "accent") return markdownTheme.listBullet(text);
367
+ if (color === "mdHeading") return markdownTheme.heading(text);
364
368
  // Session UI normally supplies Pi's active Theme above. Keep portable
365
369
  // semantic fallbacks for early startup and isolated renderer tests.
366
370
  if (color === "warning") return `\x1b[33m${text}\x1b[39m`;
@@ -443,7 +447,7 @@ export default function prettyTui(pi: ExtensionAPI) {
443
447
  const refreshActivityGroup = (group: ActivityGroup): void => {
444
448
  for (const child of group.members) {
445
449
  if (child.kind === "tool" && child.toolCallId) {
446
- cleanToolComponents.get(child.toolCallId)?.updateDisplay?.();
450
+ toolComponents.get(child.toolCallId)?.updateDisplay?.();
447
451
  } else if (child.kind === "thinking" && child.messageKey) {
448
452
  thinkingComponents.get(child.messageKey)?.invalidate?.();
449
453
  } else if (child.kind === "update") {
@@ -468,7 +472,7 @@ export default function prettyTui(pi: ExtensionAPI) {
468
472
  else collapsedActivityGroups.delete(group.id);
469
473
  for (const toolCallId of group.toolCallIds) {
470
474
  cleanCompactToolCallIds.delete(toolCallId);
471
- const component = cleanToolComponents.get(toolCallId);
475
+ const component = toolComponents.get(toolCallId);
472
476
  if (component?.expanded) component.setExpanded(false);
473
477
  }
474
478
  for (const child of group.members) expandedThinkingMembers.delete(child.id);
@@ -580,7 +584,7 @@ export default function prettyTui(pi: ExtensionAPI) {
580
584
  };
581
585
 
582
586
  const renderStandaloneError = (member: ActivityMember, width: number, component?: any): string[] => {
583
- const run = component ? errorRun(component) : { hidden: false, count: 1, compact: false };
587
+ const run = component && renderMode !== "full" ? errorRun(component) : { hidden: false, count: 1, compact: false };
584
588
  if (run.hidden) return [];
585
589
  const theme = defaultActivityTheme();
586
590
  const style = activityUpdateStyle(member, theme);
@@ -624,7 +628,7 @@ export default function prettyTui(pi: ExtensionAPI) {
624
628
  width: number,
625
629
  component?: any,
626
630
  ): string[] => {
627
- if (!activityGroupCollapsible(group)) return renderPendingActivityUpdate(member, width, component);
631
+ if (renderMode !== "clean" || !activityGroupCollapsible(group)) return renderPendingActivityUpdate(member, width, component);
628
632
  const position = activityMemberPosition(group, member);
629
633
  if (!activityGroupRevealed(group)) {
630
634
  return position.first
@@ -640,7 +644,7 @@ export default function prettyTui(pi: ExtensionAPI) {
640
644
  event: any,
641
645
  ) => {
642
646
  const isLeftClick = event.type === "click" && event.button === "left";
643
- if (!isLeftClick || !activityGroupCollapsible(group)) return undefined;
647
+ if (renderMode !== "clean" || !isLeftClick || !activityGroupCollapsible(group)) return undefined;
644
648
  const position = activityMemberPosition(group, member);
645
649
  if (!activityGroupRevealed(group)) {
646
650
  if (!position.first) return undefined;
@@ -675,7 +679,9 @@ export default function prettyTui(pi: ExtensionAPI) {
675
679
  { value: "full", label: "full", description: "Full tool details and output" },
676
680
  { value: "compact", label: "compact", description: "Concise summaries for all built-in tools" },
677
681
  { value: "clean", label: "clean", description: "Group supported tools into Running/Done status" },
678
- { value: "status", label: "status", description: "Show the current mode" },
682
+ { value: "selection-copy on", label: "selection-copy on", description: "Copy selected text without UI borders in fullscreen mode; enable and reload" },
683
+ { value: "selection-copy off", label: "selection-copy off", description: "Restore native selection copying and reload" },
684
+ { value: "status", label: "status", description: "Show the current settings" },
679
685
  ];
680
686
  const filtered = items.filter((item) => item.value.startsWith(prefix.trim().toLowerCase()));
681
687
  return filtered.length > 0 ? filtered : null;
@@ -686,18 +692,21 @@ export default function prettyTui(pi: ExtensionAPI) {
686
692
  if (!requested) {
687
693
  if (!ctx.hasUI) {
688
694
  ctx.ui.notify(
689
- `pi-pretty-tui: ${config.enabled !== false ? "enabled" : "disabled"} · mode: ${renderMode}`,
695
+ `pi-pretty-tui: ${config.enabled !== false ? "enabled" : "disabled"} · mode: ${renderMode} · selection copy: ${config.selectionCopy === true ? "on" : "off"}`,
690
696
  "info",
691
697
  );
692
698
  return;
693
699
  }
694
700
  const enabled = `Enabled — ${config.enabled !== false ? "on" : "off"}`;
695
701
  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)" : ""}`;
702
+ const compact = `Compact — concise tool summaries and nested calls${renderMode === "compact" ? " (current)" : ""}`;
697
703
  const clean = `Clean — group supported tools into Running/Done status${renderMode === "clean" ? " (current)" : ""}`;
698
- const selected = await ctx.ui.select("pi-pretty-tui settings", [enabled, full, compact, clean]);
704
+ const selectionCopy = `Selection copy — ${config.selectionCopy === true ? "on" : "off"} · copy text without UI borders (fullscreen, experimental)`;
705
+ const selected = await ctx.ui.select("pi-pretty-tui settings", [enabled, full, compact, clean, selectionCopy]);
699
706
  if (!selected) return;
700
- requested = selected === enabled
707
+ requested = selected === selectionCopy
708
+ ? config.selectionCopy === true ? "selection-copy off" : "selection-copy on"
709
+ : selected === enabled
701
710
  ? config.enabled !== false ? "disable" : "enable"
702
711
  : selected === full
703
712
  ? "full"
@@ -708,11 +717,28 @@ export default function prettyTui(pi: ExtensionAPI) {
708
717
 
709
718
  if (requested === "status") {
710
719
  ctx.ui.notify(
711
- `pi-pretty-tui: ${config.enabled !== false ? "enabled" : "disabled"} · mode: ${renderMode}`,
720
+ `pi-pretty-tui: ${config.enabled !== false ? "enabled" : "disabled"} · mode: ${renderMode} · selection copy: ${config.selectionCopy === true ? "on" : "off"}`,
712
721
  "info",
713
722
  );
714
723
  return;
715
724
  }
725
+ if (requested === "selection-copy on" || requested === "selection-copy off") {
726
+ const selectionCopy = requested.endsWith(" on");
727
+ try {
728
+ saveConfig({ ...config, selectionCopy });
729
+ } catch (error) {
730
+ const message = error instanceof Error ? error.message : String(error);
731
+ ctx.ui.notify(`Could not save selection-copy setting: ${message}`, "error");
732
+ return;
733
+ }
734
+ ctx.ui.notify(`Border-free selection copying ${selectionCopy ? "enabled" : "disabled"}; reloading…`, "info");
735
+ try { await ctx.reload(); }
736
+ catch (error) {
737
+ const message = error instanceof Error ? error.message : String(error);
738
+ ctx.ui.notify(`Setting saved, but automatic reload failed: ${message}`, "warning");
739
+ }
740
+ return;
741
+ }
716
742
  if (requested === "enable" || requested === "disable") {
717
743
  const enabled = requested === "enable";
718
744
  try {
@@ -736,7 +762,7 @@ export default function prettyTui(pi: ExtensionAPI) {
736
762
  }
737
763
  }
738
764
  if (requested !== "full" && requested !== "compact" && requested !== "clean") {
739
- ctx.ui.notify("Usage: /pretty-tui [enable|disable|full|compact|clean|status]", "error");
765
+ ctx.ui.notify("Usage: /pretty-tui [enable|disable|full|compact|clean|status|selection-copy on|selection-copy off]", "error");
740
766
  return;
741
767
  }
742
768
 
@@ -745,14 +771,9 @@ export default function prettyTui(pi: ExtensionAPI) {
745
771
  collapsedActivityGroups.clear();
746
772
  expandedThinkingMembers.clear();
747
773
  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?.();
774
+ if (cleanRun.active && !cleanRun.settled) startWorkingElapsedTimer();
775
+ refreshLatestActivityWidget();
776
+ for (const component of toolComponents.values()) component.updateDisplay?.();
756
777
  for (const component of thinkingComponents.values()) component.invalidate?.();
757
778
  try {
758
779
  saveRenderMode(renderMode);
@@ -768,6 +789,7 @@ export default function prettyTui(pi: ExtensionAPI) {
768
789
  // render patches or override built-in tools until the next enabled reload.
769
790
  if (!activeForSession) return;
770
791
  pi.on("session_shutdown", stopWorkingElapsedTimer);
792
+ if (config.selectionCopy === true) pi.on("session_shutdown", installSelectionCopy());
771
793
 
772
794
  // Give Pi's main prompt editor a complete rounded frame. Render the native
773
795
  // editor at a two-column narrower width so cursor layout, wrapping, IME, and
@@ -784,7 +806,7 @@ export default function prettyTui(pi: ExtensionAPI) {
784
806
  if (this.tui) currentTui = this.tui;
785
807
  currentPromptEditor = this;
786
808
  const indicator = this.workingStatusIndicator;
787
- if (renderMode !== "clean" || indicator?.kind !== "working" || responseRunStartedAt === undefined) {
809
+ if (indicator?.kind !== "working" || responseRunStartedAt === undefined) {
788
810
  return originalEditorTopBorder.call(this, width, hiddenLineCount);
789
811
  }
790
812
  const elapsed = formatResponseDuration(Date.now() - responseRunStartedAt);
@@ -816,9 +838,9 @@ export default function prettyTui(pi: ExtensionAPI) {
816
838
 
817
839
  return lines.map((line, index) => {
818
840
  const fitted = fitInnerWidth(line);
819
- if (index === 0) return this.borderColor("╭") + fitted + this.borderColor("╮");
820
- if (index === bottomBorderIndex) return this.borderColor("╰") + fitted + this.borderColor("╯");
821
- if (index < bottomBorderIndex) return this.borderColor("│") + fitted + this.borderColor("│");
841
+ if (index === 0) return selectableRow(this.borderColor("╭") + fitted + this.borderColor("╮"), width, []);
842
+ if (index === bottomBorderIndex) return selectableRow(this.borderColor("╰") + fitted + this.borderColor("╯"), width, []);
843
+ if (index < bottomBorderIndex) return selectableRow(this.borderColor("│") + fitted + this.borderColor("│"), width, [[1, width - 1]]);
822
844
  // Autocomplete remains outside the input frame, aligned to its content.
823
845
  return " " + fitted + " ";
824
846
  });
@@ -872,10 +894,8 @@ export default function prettyTui(pi: ExtensionAPI) {
872
894
  });
873
895
  }
874
896
 
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.
897
+ // Clean uses content-sized bubbles; compact/full share full-width panels.
898
+ // Keep Pi's Markdown, mouse handling and OSC 133 zones in every mode.
879
899
  const userMessagePrototype = UserMessageComponent.prototype as any;
880
900
  const userMessagePatchKey = Symbol.for("pretty-tui.user-message-frame");
881
901
  if (!userMessagePrototype[userMessagePatchKey]) {
@@ -912,9 +932,9 @@ export default function prettyTui(pi: ExtensionAPI) {
912
932
  .map((line: string) => line.replace(/ +$/u, ""));
913
933
  const layout = (width: number) => {
914
934
  const terminalWidth = Math.max(1, width);
915
- const rightPad = Math.min(outputPad, Math.max(0, terminalWidth - 1));
935
+ const rightPad = renderMode !== "clean" ? 0 : Math.min(outputPad, Math.max(0, terminalWidth - 1));
916
936
  const availableWidth = Math.max(1, terminalWidth - rightPad);
917
- if (availableWidth < 4) {
937
+ if (availableWidth < (renderMode !== "clean" ? 8 : 4)) {
918
938
  return {
919
939
  framed: false,
920
940
  rightPad,
@@ -925,6 +945,16 @@ export default function prettyTui(pi: ExtensionAPI) {
925
945
  };
926
946
  }
927
947
 
948
+ if (renderMode !== "clean") {
949
+ const margin = cardMargin(terminalWidth);
950
+ const contentWidth = nativeFrameWidth(terminalWidth);
951
+ return {
952
+ framed: true, rightPad: margin, leftPad: margin,
953
+ frameWidth: terminalWidth - margin * 2, contentWidth,
954
+ body: renderMarkdown(contentWidth),
955
+ };
956
+ }
957
+
928
958
  const preferredMaximum = Math.max(4, Math.floor(terminalWidth * 0.75));
929
959
  // Keep at least one blank column on the left whenever a framed bubble
930
960
  // fits, even after a narrow terminal relaxes the usual 75% maximum.
@@ -955,8 +985,8 @@ export default function prettyTui(pi: ExtensionAPI) {
955
985
  };
956
986
  };
957
987
  const border = (text: string) => currentExtensionUi?.theme?.fg
958
- ? currentExtensionUi.theme.fg("mdLink", text)
959
- : markdownTheme.link(text);
988
+ ? currentExtensionUi.theme.fg("mdHeading", text)
989
+ : markdownTheme.heading(text);
960
990
  const userText = typeof this.text === "string" ? this.text : "";
961
991
  const frame: Component = {
962
992
  render(width: number): string[] {
@@ -964,16 +994,22 @@ export default function prettyTui(pi: ExtensionAPI) {
964
994
  const left = " ".repeat(measured.leftPad);
965
995
  const right = " ".repeat(measured.rightPad);
966
996
  if (!measured.framed) {
967
- return measured.body.map((line: string) => left + line + right);
997
+ return measured.body.map((line: string) => renderMode !== "clean"
998
+ ? cardBackground(line, width, defaultActivityTheme(), "toolPendingBg", "mdHeading") : left + line + right);
999
+ }
1000
+ if (renderMode !== "clean") {
1001
+ return frameNativeRows("User", measured.body, width, defaultActivityTheme(), undefined,
1002
+ frame, "mdHeading", "", "mdHeading").lines;
968
1003
  }
969
1004
 
970
1005
  const top = border("╭" + "─".repeat(Math.max(0, measured.frameWidth - 2)) + "╮");
971
1006
  const bottom = border("╰" + "─".repeat(Math.max(0, measured.frameWidth - 2)) + "╯");
972
1007
  const body = measured.body.map((line: string) => {
973
1008
  const padding = " ".repeat(Math.max(0, measured.contentWidth - visibleWidth(line)));
974
- return border("│ ") + line + padding + border(" │");
1009
+ return selectableRow(border("│ ") + line + padding + border(" │"), measured.frameWidth, [[2, 2 + Math.max(1, Math.min(measured.contentWidth, visibleWidth(line)))]]);
975
1010
  });
976
- return [top, ...body, bottom].map((line: string) => left + line + right);
1011
+ return [selectableRow(top, measured.frameWidth, []), ...body, selectableRow(bottom, measured.frameWidth, [])]
1012
+ .map((line: string) => selectableRow(left + line + right, width, [[measured.leftPad, width - measured.rightPad]]));
977
1013
  },
978
1014
  handleMouse(event: any) {
979
1015
  const measured = layout(event.width);
@@ -1106,6 +1142,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1106
1142
  return true;
1107
1143
  };
1108
1144
  const patchedCustomRender = function (this: any, width: number): string[] {
1145
+ if (renderMode === "full") return originalCustomRender.call(this, width);
1109
1146
  if (releaseSemanticCustomMessage(this)) {
1110
1147
  return originalCustomRender.call(this, width);
1111
1148
  }
@@ -1116,23 +1153,24 @@ export default function prettyTui(pi: ExtensionAPI) {
1116
1153
  // message_start event. Claim them at the component boundary so live
1117
1154
  // rendering matches transcript restoration instead of failing open to
1118
1155
  // the purple native box until the next reload.
1119
- if (!member && renderMode === "clean") {
1156
+ if (!member) {
1120
1157
  member = addPersistentActivityUpdate(message);
1121
1158
  }
1122
1159
  const group = member ? activityTimeline.groupForMember(member.id) : undefined;
1123
- if (renderMode !== "clean" || !member || !group) {
1160
+ if (!member || !group) {
1124
1161
  return originalCustomRender.call(this, width);
1125
1162
  }
1126
1163
  activityUpdateComponents.set(member.id, this);
1127
1164
  return renderActivityUpdateProjection(group, member, width);
1128
1165
  };
1129
1166
  const patchedCustomHandleMouse = function (this: any, event: any) {
1167
+ if (renderMode === "full") return originalCustomHandleMouse?.call(this, event);
1130
1168
  if (releaseSemanticCustomMessage(this)) {
1131
1169
  return originalCustomHandleMouse?.call(this, event);
1132
1170
  }
1133
1171
  const member = activityTimeline.memberForUpdate(customMessageKey(this.message));
1134
1172
  const group = member ? activityTimeline.groupForMember(member.id) : undefined;
1135
- if (renderMode !== "clean" || !member || !group) {
1173
+ if (!member || !group) {
1136
1174
  return originalCustomHandleMouse?.call(this, event);
1137
1175
  }
1138
1176
  return handleActivityUpdateMouse(group, member, event);
@@ -1163,20 +1201,79 @@ export default function prettyTui(pi: ExtensionAPI) {
1163
1201
  });
1164
1202
  }
1165
1203
 
1204
+ // Keep Pi's compaction content and expansion state inside the shared panel.
1205
+ const compactionPrototype = CompactionSummaryMessageComponent.prototype as any;
1206
+ const compactionPatchKey = Symbol.for("pretty-tui.compaction-panel");
1207
+ if (!compactionPrototype[compactionPatchKey]) {
1208
+ const originalRender = compactionPrototype.render;
1209
+ const originalHandleMouse = compactionPrototype.handleMouse;
1210
+ const views = new WeakMap<object, { width: number; sourceOffset: number; frame: ReturnType<typeof frameNativeRows> }>();
1211
+ const render = function (this: any, width: number): string[] {
1212
+ if (renderMode === "clean") {
1213
+ views.delete(this);
1214
+ mouseClickHitTests.delete(this);
1215
+ return originalRender.call(this, width);
1216
+ }
1217
+ const innerWidth = nativeFrameWidth(width);
1218
+ const region = this.children[0];
1219
+ // Native content begins with its [compaction] label and a spacer. The
1220
+ // panel title replaces those two rows; the summary itself is untouched.
1221
+ const content = region.child.children;
1222
+ const sourceOffset = content.slice(0, 2).reduce((height: number, child: any) => height + child.render(innerWidth).length, 0);
1223
+ const source = content.slice(2).flatMap((child: any) => child.render(innerWidth));
1224
+ const theme = defaultActivityTheme();
1225
+ const frame = frameNativeRows(`compaction ${this.expanded ? "▾" : "▸"}`, source, width, theme,
1226
+ undefined, this, "customMessageLabel", "", "customMessageLabel", "customMessageBg");
1227
+ views.set(this, { width: innerWidth, sourceOffset, frame });
1228
+ mouseClickHitTests.set(this, (event) => event.y === 0 || frame.sourceRows.has(event.y));
1229
+ return frame.lines.map((line) => isNativeImageRow(line) ? line : truncateToWidth(line, Math.max(1, width), ""));
1230
+ };
1231
+ const handleMouse = function (this: any, event: any) {
1232
+ if (renderMode === "clean") return originalHandleMouse?.call(this, event);
1233
+ render.call(this, event.width);
1234
+ const view = views.get(this);
1235
+ if (!view) return undefined;
1236
+ if (event.y === 0 && event.type === "click" && event.button === "left") {
1237
+ this.setExpanded(!this.expanded);
1238
+ currentTui?.requestRender?.();
1239
+ return { handled: true };
1240
+ }
1241
+ const sourceY = view.frame.sourceRows.get(event.y);
1242
+ if (sourceY === undefined) return undefined;
1243
+ const result = this.children[0].handleMouse?.({ ...event,
1244
+ x: Math.max(0, event.x - view.frame.inset), y: sourceY + view.sourceOffset,
1245
+ width: view.width, height: view.frame.sourceRows.size + view.sourceOffset,
1246
+ });
1247
+ if (result?.handled) currentTui?.requestRender?.();
1248
+ return result;
1249
+ };
1250
+ compactionPrototype[compactionPatchKey] = { originalRender, originalHandleMouse };
1251
+ compactionPrototype.render = render;
1252
+ compactionPrototype.handleMouse = handleMouse;
1253
+ pi.on("session_shutdown", () => {
1254
+ if (compactionPrototype.render === render) compactionPrototype.render = originalRender;
1255
+ if (compactionPrototype.handleMouse === handleMouse) compactionPrototype.handleMouse = originalHandleMouse;
1256
+ if (compactionPrototype.render === originalRender && compactionPrototype.handleMouse === originalHandleMouse) {
1257
+ delete compactionPrototype[compactionPatchKey];
1258
+ }
1259
+ });
1260
+ }
1261
+
1166
1262
  // 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.
1263
+ // Assistant components keep Pi's native visible-answer Markdown renderer.
1264
+ // All modes share thought content; only clean adds activity-group framing.
1169
1265
  const assistantPrototype = AssistantMessageComponent.prototype as any;
1170
1266
  const thinkingPatchKey = Symbol.for("pretty-tui.clean-thinking");
1171
1267
  if (!assistantPrototype[thinkingPatchKey]) {
1172
1268
  const originalUpdateContent = assistantPrototype.updateContent;
1173
1269
  const originalRender = assistantPrototype.render;
1174
1270
  const originalHandleMouse = assistantPrototype.handleMouse;
1271
+ const originalSetHideThinkingBlock = assistantPrototype.setHideThinkingBlock;
1272
+ const explicitThinkingVisibility = new WeakMap<object, boolean>();
1175
1273
  const originalMessageKey = Symbol("pretty-tui.original-assistant-message");
1176
1274
  const renderedModeKey = Symbol("pretty-tui.rendered-assistant-mode");
1177
1275
  const renderedExpansionKey = Symbol("pretty-tui.rendered-assistant-expansion");
1178
1276
  const cleanThinkingRenderCacheKey = Symbol("pretty-tui.clean-thinking-render-cache");
1179
- const cleanThinkingDetailKey = Symbol("pretty-tui.clean-thinking-detail");
1180
1277
 
1181
1278
  const assistantTerminalUpdate = (message: any): ActivityMember | undefined => {
1182
1279
  if (!assistantTerminalState(message)) return undefined;
@@ -1216,32 +1313,84 @@ export default function prettyTui(pi: ExtensionAPI) {
1216
1313
  currentTui?.requestRender?.();
1217
1314
  };
1218
1315
 
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
- }),
1316
+ const thoughtPresentation = new ThoughtPresentation();
1317
+ const fullThinkingVisibility = new WeakMap<object, boolean>();
1318
+ const patchedSetHideThinkingBlock = function (this: any, hide: boolean) {
1319
+ if (renderMode !== "clean") explicitThinkingVisibility.set(this, !hide);
1320
+ if (renderMode === "full") fullThinkingVisibility.set(this, !hide);
1321
+ return originalSetHideThinkingBlock.call(this, hide);
1322
+ };
1323
+ const panelAssistantViews = new WeakMap<object, { rows: Map<number, { sourceY: number; inset: number }>; width: number; height: number }>();
1324
+ const renderPanelAssistant = function (this: any, width: number): string[] {
1325
+ const innerWidth = nativeFrameWidth(width);
1326
+ const nativeLines = originalRender.call(this, innerWidth);
1327
+ const rows = new Map<number, { sourceY: number; inset: number }>();
1328
+ const thoughtRows = new Set<number>();
1329
+ const output: string[] = [];
1330
+ const children = this.contentContainer.children;
1331
+ let sourceY = 0;
1332
+ let thoughtRun = 0;
1333
+ const previews: string[] = [];
1334
+ if (renderMode === "compact") {
1335
+ const message = this[originalMessageKey] ?? this.lastMessage;
1336
+ let run: string[] = [];
1337
+ const finishRun = () => {
1338
+ if (run.length) previews.push(run.join("\n\n").split(/\r\n|\r|\n/).find((line) => line.trim())?.trim() ?? "");
1339
+ run = [];
1238
1340
  };
1239
- owner[cleanThinkingDetailKey] = detail;
1341
+ for (const item of message?.content ?? []) {
1342
+ if (item.type === "thinking") {
1343
+ if (item.thinking?.trim()) run.push(item.thinking.trim());
1344
+ } else finishRun();
1345
+ }
1346
+ finishRun();
1240
1347
  }
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
- );
1348
+ for (let index = 0; index < children.length; index++) {
1349
+ const child = children[index];
1350
+ const childLines = child.render(innerWidth);
1351
+ const isThought = Boolean(child.onMouse && child.child);
1352
+ if (isThought || child instanceof Markdown && childLines.length) {
1353
+ let height = childLines.length;
1354
+ // Adjacent native text blocks belong to the same answer card. Keep
1355
+ // native paragraph order and source coordinates, not copied text.
1356
+ if (!isThought) {
1357
+ while (children[index + 1] instanceof Markdown) {
1358
+ height += children[++index].render(innerWidth).length;
1359
+ }
1360
+ }
1361
+ const runIndex = thoughtRun;
1362
+ const hidden = isThought && (this.thinkingVisibilityOverrides.get(thoughtRun++) ?? this.hideThinkingBlock);
1363
+ const source = renderMode === "compact" && hidden
1364
+ ? [defaultActivityTheme().fg("thinkingText", truncateToWidth(previews[runIndex] ?? "", innerWidth, "…"))]
1365
+ : nativeLines.slice(sourceY, sourceY + height);
1366
+ const role = isThought ? "mdLink" : "text";
1367
+ const baseLabel = isThought ? this.isStreaming ? "thinking" : "thought" : "Assistant";
1368
+ const label = isThought && renderMode === "compact" ? `${baseLabel} ${hidden ? "▸" : "▾"}` : baseLabel;
1369
+ const frame = frameNativeRows(label, source, width, defaultActivityTheme(), undefined, child, role, "", role);
1370
+ for (const [y, nativeY] of frame.sourceRows) {
1371
+ rows.set(output.length + y, { sourceY: sourceY + nativeY, inset: frame.inset });
1372
+ if (isThought) thoughtRows.add(output.length + y);
1373
+ }
1374
+ if (isThought && height) {
1375
+ rows.set(output.length, { sourceY, inset: frame.inset });
1376
+ thoughtRows.add(output.length);
1377
+ }
1378
+ output.push(...frame.lines);
1379
+ sourceY += height;
1380
+ } else {
1381
+ for (let y = 0; y < childLines.length; y++) rows.set(output.length + y, { sourceY: sourceY + y, inset: 0 });
1382
+ output.push(...nativeLines.slice(sourceY, sourceY + childLines.length));
1383
+ sourceY += childLines.length;
1384
+ }
1385
+ }
1386
+ if (renderMode === "compact") {
1387
+ const terminal = assistantTerminalUpdate(this[originalMessageKey] ?? this.lastMessage);
1388
+ if (terminal) output.push(...renderPendingActivityUpdate(terminal, width, this));
1389
+ }
1390
+ panelAssistantViews.set(this, { rows, width: innerWidth, height: nativeLines.length });
1391
+ mouseClickHitTests.set(this, (event) => thoughtRows.has(event.y));
1392
+ return renderMode === "compact" && width < 8
1393
+ ? output.map((line) => isNativeImageRow(line) ? line : truncateToWidth(line, Math.max(1, width), "")) : output;
1245
1394
  };
1246
1395
 
1247
1396
  const patchedUpdateContent = function (this: any, message: any, isStreaming = this.isStreaming) {
@@ -1250,8 +1399,13 @@ export default function prettyTui(pi: ExtensionAPI) {
1250
1399
  this[renderedExpansionKey] = cleanToolsExpanded;
1251
1400
  this.lastMessage = message;
1252
1401
  if (renderMode !== "clean") {
1253
- originalUpdateContent.call(this, message, isStreaming);
1402
+ this.hideThinkingBlock = renderMode === "full"
1403
+ ? !(fullThinkingVisibility.get(this) ?? !fullThinkingHidden)
1404
+ : !(explicitThinkingVisibility.get(this) ?? !compactThinkingHidden);
1405
+ originalUpdateContent.call(this, renderMode === "compact" && assistantTerminalState(message)
1406
+ ? { ...message, errorMessage: undefined, stopReason: "stop" } : message, isStreaming);
1254
1407
  this.lastMessage = message;
1408
+ if (eligibleThinking(message)) thinkingComponents.set(assistantMessageKey(message), this);
1255
1409
  return;
1256
1410
  }
1257
1411
  const groupedThinking = eligibleThinking(message);
@@ -1263,15 +1417,17 @@ export default function prettyTui(pi: ExtensionAPI) {
1263
1417
 
1264
1418
  const previousHideThinkingBlock = this.hideThinkingBlock;
1265
1419
  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
1420
+ // Shared thought presentation owns thinking completely. Removing it from
1421
+ // Pi's native content tree also removes the native MouseRegion that would
1268
1422
  // otherwise toggle a second, unrelated `Thinking...` block.
1269
1423
  const displayMessage =
1270
1424
  groupedThinking && Array.isArray(message?.content)
1271
1425
  ? { ...message, content: message.content.filter((item: any) => item.type !== "thinking") }
1272
1426
  : message;
1273
1427
  try {
1274
- originalUpdateContent.call(this, displayMessage, isStreaming);
1428
+ originalUpdateContent.call(this, assistantTerminalState(message)
1429
+ ? { ...displayMessage, errorMessage: undefined, stopReason: "stop" }
1430
+ : displayMessage, isStreaming);
1275
1431
  } finally {
1276
1432
  this.hideThinkingBlock = previousHideThinkingBlock;
1277
1433
  }
@@ -1286,14 +1442,17 @@ export default function prettyTui(pi: ExtensionAPI) {
1286
1442
  ) {
1287
1443
  patchedUpdateContent.call(this, message, this.isStreaming);
1288
1444
  }
1289
- if (renderMode !== "clean") return originalRender.call(this, width);
1445
+ if (renderMode !== "clean") return renderPanelAssistant.call(this, width);
1290
1446
  const terminalMember = assistantTerminalUpdate(message);
1291
1447
  if (!eligibleThinking(message)) {
1292
1448
  if (!terminalMember) return originalRender.call(this, width);
1293
1449
  const terminalGroup = activityTimeline.groupForMember(terminalMember.id);
1294
1450
  if (!terminalGroup) return originalRender.call(this, width);
1295
1451
  activityUpdateComponents.set(terminalMember.id, this);
1296
- return renderActivityUpdateProjection(terminalGroup, terminalMember, width, this);
1452
+ return [
1453
+ ...renderActivityUpdateProjection(terminalGroup, terminalMember, width, this),
1454
+ ...(visibleAssistantText(message) ? originalRender.call(this, width) : []),
1455
+ ];
1297
1456
  }
1298
1457
 
1299
1458
  const key = assistantMessageKey(message);
@@ -1311,10 +1470,9 @@ export default function prettyTui(pi: ExtensionAPI) {
1311
1470
  }
1312
1471
  const position = activityMemberPosition(group, member);
1313
1472
  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
1473
+ // Native errors are filtered above; append the shared alert exactly once
1474
+ // while preserving any partial visible answer outside the thought block.
1475
+ const terminalLines = terminalMember
1318
1476
  ? renderPendingActivityUpdate(terminalMember, width)
1319
1477
  : [];
1320
1478
  const appendTail = (activityLines: string[]) => {
@@ -1343,7 +1501,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1343
1501
  );
1344
1502
  // Clean mode owns thought disclosure: revealing a group shows the thought
1345
1503
  // member, while only a member click (or Ctrl+O) reveals its full text.
1346
- const expanded = cleanToolsExpanded || expandedThinkingMembers.has(member.id);
1504
+ const expanded = expandedThinkingMembers.has(member.id) || (explicitThinkingVisibility.get(this) ?? cleanToolsExpanded);
1347
1505
  const settledOwner = position.first ? activityGroupOwner(group) : undefined;
1348
1506
  // A group without tools has no durable summary, so its own boundary is
1349
1507
  // the signal that it finished.
@@ -1367,12 +1525,10 @@ export default function prettyTui(pi: ExtensionAPI) {
1367
1525
  return appendTail(cached.lines);
1368
1526
  }
1369
1527
  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));
1528
+ const header = thoughtPresentation.header(stateLabel, childWidth, groupTheme);
1373
1529
  let contentLines: string[] = [header];
1374
1530
  if (expanded) {
1375
- contentLines.push(...thinkingDetailLines(
1531
+ contentLines.push(...thoughtPresentation.details(
1376
1532
  this,
1377
1533
  member.thinking ?? thinkingText(message),
1378
1534
  groupTheme,
@@ -1405,7 +1561,15 @@ export default function prettyTui(pi: ExtensionAPI) {
1405
1561
 
1406
1562
  const handleAssistantTranscriptMouse = function (this: any, event: any) {
1407
1563
  const message = this[originalMessageKey] ?? this.lastMessage;
1408
- if (renderMode !== "clean") return originalHandleMouse.call(this, event);
1564
+ if (renderMode !== "clean") {
1565
+ patchedRender.call(this, event.width);
1566
+ const view = panelAssistantViews.get(this);
1567
+ const row = view?.rows.get(event.y);
1568
+ if (!view || !row) return undefined;
1569
+ return originalHandleMouse.call(this, { ...event,
1570
+ x: Math.max(0, event.x - row.inset), y: row.sourceY, width: view.width, height: view.height,
1571
+ });
1572
+ }
1409
1573
  if (!eligibleThinking(message)) {
1410
1574
  const terminalMember = assistantTerminalUpdate(message);
1411
1575
  const terminalGroup = terminalMember
@@ -1478,6 +1642,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1478
1642
  originalHandleMouse,
1479
1643
  patchedHandleMouse,
1480
1644
  };
1645
+ assistantPrototype.setHideThinkingBlock = patchedSetHideThinkingBlock;
1481
1646
  assistantPrototype.updateContent = patchedUpdateContent;
1482
1647
  assistantPrototype.render = patchedRender;
1483
1648
  assistantPrototype.handleMouse = patchedHandleMouse;
@@ -1485,6 +1650,9 @@ export default function prettyTui(pi: ExtensionAPI) {
1485
1650
  pi.on("session_shutdown", () => {
1486
1651
  const patch = assistantPrototype[thinkingPatchKey];
1487
1652
  if (!patch) return;
1653
+ if (assistantPrototype.setHideThinkingBlock === patchedSetHideThinkingBlock) {
1654
+ assistantPrototype.setHideThinkingBlock = originalSetHideThinkingBlock;
1655
+ }
1488
1656
  if (patch.patchedUpdateContent === assistantPrototype.updateContent) {
1489
1657
  assistantPrototype.updateContent = patch.originalUpdateContent;
1490
1658
  }
@@ -1682,10 +1850,8 @@ export default function prettyTui(pi: ExtensionAPI) {
1682
1850
  message: string,
1683
1851
  type?: "info" | "warning" | "error",
1684
1852
  ) {
1685
- if (renderMode !== "clean") {
1686
- return originalShowExtensionNotify.call(this, message, type);
1687
- }
1688
1853
  const severity: ActivitySeverity = type ?? "info";
1854
+ if (renderMode === "full") return nativeExtensionNotify.call(this, message, severity);
1689
1855
  if (!appendRuntimeActivityUpdate(this, message, severity)) {
1690
1856
  nativeExtensionNotify.call(this, message, severity);
1691
1857
  }
@@ -1693,6 +1859,13 @@ export default function prettyTui(pi: ExtensionAPI) {
1693
1859
 
1694
1860
  const patchedToggleThinkingBlockVisibility = function (this: any) {
1695
1861
  if (renderMode !== "clean") {
1862
+ if (renderMode === "full") {
1863
+ this.hideThinkingBlock = fullThinkingHidden;
1864
+ fullThinkingHidden = !fullThinkingHidden;
1865
+ } else {
1866
+ this.hideThinkingBlock = compactThinkingHidden;
1867
+ compactThinkingHidden = !compactThinkingHidden;
1868
+ }
1696
1869
  return originalToggleThinkingBlockVisibility.call(this);
1697
1870
  }
1698
1871
  // Do not change Pi's persisted hideThinkingBlock setting in clean mode.
@@ -1704,7 +1877,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1704
1877
  };
1705
1878
 
1706
1879
  const patchedShowStatus = function (this: any, message: string) {
1707
- if (renderMode !== "clean") return originalShowStatus.call(this, message);
1880
+ if (renderMode === "full") return originalShowStatus.call(this, message);
1708
1881
  const [firstLine, ...remainingLines] = String(message).split("\n");
1709
1882
  setLatestActivityStatus({
1710
1883
  title: firstLine || "Status",
@@ -1715,13 +1888,15 @@ export default function prettyTui(pi: ExtensionAPI) {
1715
1888
  };
1716
1889
 
1717
1890
  const patchedShowWarning = function (this: any, message: string) {
1718
- if (renderMode !== "clean" || !appendRuntimeActivityUpdate(this, message, "warning")) {
1891
+ if (renderMode === "full") return originalShowWarning.call(this, message);
1892
+ if (!appendRuntimeActivityUpdate(this, message, "warning")) {
1719
1893
  return originalShowWarning.call(this, message);
1720
1894
  }
1721
1895
  };
1722
1896
 
1723
1897
  const patchedShowError = function (this: any, message: string) {
1724
- if (renderMode !== "clean" || !appendRuntimeActivityUpdate(this, message, "error")) {
1898
+ if (renderMode === "full") return originalShowError.call(this, message);
1899
+ if (!appendRuntimeActivityUpdate(this, message, "error")) {
1725
1900
  return originalShowError.call(this, message);
1726
1901
  }
1727
1902
  };
@@ -1772,9 +1947,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1772
1947
  }
1773
1948
  // A mode switch can keep existing components rather than rebuild them.
1774
1949
  const component: Component = {
1775
- render: (width: number) => renderMode === "clean"
1776
- ? renderActivityUpdateProjection(group, member, width)
1777
- : nativeNodes.flatMap((node) => node.render(width)),
1950
+ render: (width: number) => renderActivityUpdateProjection(group, member, width),
1778
1951
  invalidate() {},
1779
1952
  handleMouse: (event: any) => renderMode === "clean"
1780
1953
  ? handleActivityUpdateMouse(group, member, event)
@@ -1808,7 +1981,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1808
1981
  if (!group) continue;
1809
1982
  mountActivityUpdate(
1810
1983
  cacheNoticeHost.current, group, member, [],
1811
- cleanToolComponents.get(toolCallId),
1984
+ toolComponents.get(toolCallId),
1812
1985
  );
1813
1986
  refreshActivityGroup(group);
1814
1987
  cacheNoticeHost.current?.ui?.requestRender?.();
@@ -1817,7 +1990,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1817
1990
  flushDeferredCacheNotices = flushPendingCacheNotices;
1818
1991
 
1819
1992
  const patchedAddCacheMissNotice = function (this: any, miss: any) {
1820
- if (renderMode !== "clean" || !this.chatContainer?.addChild) {
1993
+ if (!this.chatContainer?.addChild) {
1821
1994
  return originalAddCacheMissNotice.call(this, miss);
1822
1995
  }
1823
1996
  // Let Pi decide thresholds, formatting and cause labels. Capture its
@@ -1876,7 +2049,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1876
2049
  if (!group) return;
1877
2050
  mountActivityUpdate(
1878
2051
  this, group, member, nativeNodes,
1879
- anchor?.toolCallId ? cleanToolComponents.get(anchor.toolCallId) : undefined,
2052
+ anchor?.toolCallId ? toolComponents.get(anchor.toolCallId) : undefined,
1880
2053
  );
1881
2054
  const component = activityUpdateComponents.get(member.id);
1882
2055
  if (component && message) cacheNoticeOwners.set(component, message);
@@ -2128,6 +2301,21 @@ export default function prettyTui(pi: ExtensionAPI) {
2128
2301
  return originalInvalidate.call(this);
2129
2302
  };
2130
2303
 
2304
+ const copyRows = new WeakMap<object, { source: string[]; padding: number; lines: string[] }>();
2305
+ const selectableMarkdown = (owner: any, source: string[]) => {
2306
+ if (!selectionMetadataActive()) return source;
2307
+ const padding = Math.max(0, Number(owner.paddingX) || 0);
2308
+ if (!padding) return source;
2309
+ const cached = copyRows.get(owner);
2310
+ if (cached?.source === source && cached.padding === padding) return cached.lines;
2311
+ const lines = source.map(line => {
2312
+ const width = visibleWidth(line);
2313
+ return selectableRow(line, width, [[Math.min(padding, width), Math.max(Math.min(padding, width), width - padding)]]);
2314
+ });
2315
+ copyRows.set(owner, { source, padding, lines });
2316
+ return lines;
2317
+ };
2318
+
2131
2319
  const patchedRender = function (this: any, width: number): string[] {
2132
2320
  const scoped = transcriptMarkdownActive();
2133
2321
  mouseClickHitTests.set(this, (event) => fullscreenTui &&
@@ -2147,7 +2335,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2147
2335
  if (needsRender) this[codeBlockCollectionKey] = [];
2148
2336
 
2149
2337
  const lines = originalRender.call(this, width);
2150
- if (!needsRender) return lines;
2338
+ if (!needsRender) return selectableMarkdown(this, lines);
2151
2339
 
2152
2340
  const regions: Array<{ xStart: number; xEnd: number; y: number; code: string }> = [];
2153
2341
  let searchFrom = 0;
@@ -2175,7 +2363,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2175
2363
  }
2176
2364
  this[codeBlockRegionsKey] = regions;
2177
2365
  delete this[codeBlockCollectionKey];
2178
- return lines;
2366
+ return selectableMarkdown(this, lines);
2179
2367
  };
2180
2368
 
2181
2369
  const patchedRenderToken = function (
@@ -2333,14 +2521,12 @@ export default function prettyTui(pi: ExtensionAPI) {
2333
2521
  (showCopyButton ? this.theme.codeBlockBorder(` ${copyLabel} `) : "") + frameBorder("╮");
2334
2522
  const framedCodeWidth = Math.max(1, frameWidth - 4);
2335
2523
  const lines = [
2336
- styledTopRule,
2337
- ...codeLines.map((line) =>
2338
- frameBorder("│ ") +
2339
- line +
2340
- " ".repeat(Math.max(0, framedCodeWidth - visibleWidth(line))) +
2341
- frameBorder(" │")
2342
- ),
2343
- frameBorder(`╰${"─".repeat(Math.max(0, frameWidth - 2))}╯`),
2524
+ selectableRow(styledTopRule, frameWidth, []),
2525
+ ...codeLines.map((line) => selectableRow(
2526
+ frameBorder("│ ") + line +
2527
+ " ".repeat(Math.max(0, framedCodeWidth - visibleWidth(line))) + frameBorder(" │"),
2528
+ frameWidth, [[2, frameWidth - 2]])),
2529
+ selectableRow(frameBorder(`╰${"─".repeat(Math.max(0, frameWidth - 2))}╯`), frameWidth, []),
2344
2530
  ];
2345
2531
 
2346
2532
  if (showCopyButton && Array.isArray(this[codeBlockCollectionKey])) {
@@ -2520,10 +2706,11 @@ export default function prettyTui(pi: ExtensionAPI) {
2520
2706
  prefix: () => {
2521
2707
  const status = (state.compactToolStatus ?? "running") as ToolStatus;
2522
2708
  const color = status === "success" ? "success" : status === "error" ? "error" : "dim";
2709
+ if (renderMode === "compact") return name === "bash" ? theme.fg("toolTitle", "$ ") : "";
2523
2710
  return theme.fg(color, "● ");
2524
2711
  },
2525
- continuation: " ",
2526
- content:
2712
+ continuation: renderMode === "compact" && name !== "bash" ? "" : " ",
2713
+ content: renderMode === "compact" ? theme.fg("muted", detail) :
2527
2714
  theme.fg("text", theme.bold(name)) +
2528
2715
  theme.fg("dim", "(") +
2529
2716
  theme.fg("muted", detail) +
@@ -2769,65 +2956,6 @@ export default function prettyTui(pi: ExtensionAPI) {
2769
2956
  settled: false,
2770
2957
  };
2771
2958
 
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
2959
  const clearToolActivityHolds = () => {
2832
2960
  for (const hold of toolActivityHolds.values()) {
2833
2961
  if (hold.timer) clearTimeout(hold.timer);
@@ -2937,6 +3065,38 @@ export default function prettyTui(pi: ExtensionAPI) {
2937
3065
  const originalToolHandleMouse = toolExecutionPrototype.handleMouse;
2938
3066
  const renderedModeKey = Symbol("pretty-tui.tool-rendered-mode");
2939
3067
  const cleanChildRenderCacheKey = Symbol("pretty-tui.clean-child-render-cache");
3068
+ const standaloneViews = new WeakMap<object, { projection: ToolProjection; offset: number; width: number }>();
3069
+ const projectTool = (owner: any, width: number, theme: any, mode: "compact" | "full") =>
3070
+ renderToolProjection(owner, width, theme, {
3071
+ mode, nested: nestedTools, nestedExpanded: Boolean(owner.expanded), panels: renderMode !== "clean",
3072
+ renderNative: (nativeWidth) => originalToolRender.call(owner, nativeWidth),
3073
+ });
3074
+ const handleProjectionMouse = (owner: any, projection: ToolProjection, event: any) => {
3075
+ const isClick = event.type === "click" && event.button === "left";
3076
+ const action = projection.actions.get(event.y);
3077
+ if (isClick && action) {
3078
+ nestedTools.toggle(action, Boolean(owner.expanded));
3079
+ owner.updateDisplay?.();
3080
+ owner.ui?.requestRender?.();
3081
+ return { handled: true };
3082
+ }
3083
+ if (isClick && projection.toggleRows.has(event.y)) {
3084
+ originalSetExpanded.call(owner, !owner.expanded);
3085
+ owner.ui?.requestRender?.();
3086
+ return { handled: true };
3087
+ }
3088
+ const native = projection.native;
3089
+ const sourceY = projection.sourceRows?.get(event.y);
3090
+ if (native && (projection.sourceRows ? sourceY !== undefined : event.y >= native.y && event.y < native.y + native.height)) {
3091
+ return originalToolHandleMouse.call(owner, {
3092
+ ...event, x: Math.max(0, event.x - native.x),
3093
+ y: sourceY ?? event.y - native.y + native.sourceY,
3094
+ width: native.width, height: native.height + native.sourceY,
3095
+ });
3096
+ }
3097
+ // Projected rows must never be interpreted as native renderer rows.
3098
+ return isClick ? { handled: true } : undefined;
3099
+ };
2940
3100
  const revealCleanGroup = revealActivityGroup;
2941
3101
  const collapseCleanGroup = collapseActivityGroup;
2942
3102
  const patchedMarkExecutionStarted = function (this: any) {
@@ -2972,7 +3132,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2972
3132
  return originalSetExpanded.call(this, expanded);
2973
3133
  };
2974
3134
  const patchedToolRender = function (this: any, width: number): string[] {
2975
- cleanToolComponents.set(this.toolCallId, this);
3135
+ toolComponents.set(this.toolCallId, this);
2976
3136
  knownToolCallIds.add(this.toolCallId);
2977
3137
  const member = activityTimeline.addTool(this.toolCallId, this.toolName);
2978
3138
  cleanToolNames.set(
@@ -2985,7 +3145,24 @@ export default function prettyTui(pi: ExtensionAPI) {
2985
3145
  }
2986
3146
 
2987
3147
  const group = activityTimeline.groupForMember(member.id);
2988
- if (renderMode !== "clean" || !group) return originalToolRender.call(this, width);
3148
+ if (renderMode !== "clean" || !group) {
3149
+ const projection = renderMode !== "clean"
3150
+ ? projectTool(this, width, defaultActivityTheme(), renderMode)
3151
+ : undefined;
3152
+ if (!projection) {
3153
+ standaloneViews.delete(this);
3154
+ mouseClickHitTests.delete(this);
3155
+ return originalToolRender.call(this, width);
3156
+ }
3157
+ const offset = renderMode === "full" ? 2 : 1;
3158
+ standaloneViews.set(this, { projection, offset, width });
3159
+ mouseClickHitTests.set(this, (event) => event.width === width && (
3160
+ projection.actions.has(event.y - offset) || projection.toggleRows.has(event.y - offset) ||
3161
+ Boolean(projection.sourceRows?.has(event.y - offset))
3162
+ ));
3163
+ return [...Array<string>(offset).fill(""), ...projection.lines];
3164
+ }
3165
+ standaloneViews.delete(this);
2989
3166
  const position = activityMemberPosition(group, member);
2990
3167
  let renderedHeight = 0;
2991
3168
  let nestedClickActions: Map<number, string> | undefined;
@@ -3019,8 +3196,6 @@ export default function prettyTui(pi: ExtensionAPI) {
3019
3196
  // Other extensions (notably FFF in override mode) can register their own
3020
3197
  // grep/find renderers under built-in names. Their default boxed shell is
3021
3198
  // not one of our self-rendering specialized tools.
3022
- const thirdParty = !SPECIALIZED_TOOL_NAMES.has(this.toolName) ||
3023
- this.toolDefinition?.renderShell !== "self";
3024
3199
  const cacheable = nestedTools.count(this.toolCallId) === 0 && !this.expanded && Boolean(this.result) && !this.isPartial;
3025
3200
  const cached = cacheable ? this[cleanChildRenderCacheKey] : undefined;
3026
3201
  let decoratedContent: string[];
@@ -3038,43 +3213,21 @@ export default function prettyTui(pi: ExtensionAPI) {
3038
3213
  decoratedContent = cached.lines;
3039
3214
  } else {
3040
3215
  let contentLines: string[];
3041
- if (nestedTools.count(this.toolCallId) > 0) {
3042
- const projection = nestedTools.render(this, childWidth, childTheme);
3216
+ const projection = projectTool(this, childWidth, childTheme, "compact");
3217
+ if (projection) {
3043
3218
  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
- }
3219
+ if (nestedTools.count(this.toolCallId) > 0) nestedClickActions = projection.actions;
3066
3220
  } else {
3067
3221
  const lines = originalToolRender.call(this, childWidth);
3068
3222
  if (lines.length === 0) return lines;
3069
3223
  contentLines = lines[0] === "" ? lines.slice(1) : lines;
3070
3224
  }
3071
- decoratedContent = contentLines.map((line: string, index: number) =>
3072
- truncateToWidth(
3073
- (index === 0 ? childPrefix : continuation) + line,
3074
- Math.max(1, width),
3075
- "",
3076
- )
3077
- );
3225
+ decoratedContent = contentLines.map((line: string, index: number) => {
3226
+ const prefix = index === 0 ? childPrefix : continuation;
3227
+ const rendered = truncateToWidth(prefix + line, Math.max(1, width), "");
3228
+ return selectionMetadataActive()
3229
+ ? selectableRow(rendered, visibleWidth(rendered), [[visibleWidth(prefix), visibleWidth(rendered)]]) : rendered;
3230
+ });
3078
3231
  if (cacheable) {
3079
3232
  this[cleanChildRenderCacheKey] = {
3080
3233
  width,
@@ -3102,6 +3255,11 @@ export default function prettyTui(pi: ExtensionAPI) {
3102
3255
  const group = member ? activityTimeline.groupForMember(member.id) : undefined;
3103
3256
  const isLeftClick = event.type === "click" && event.button === "left";
3104
3257
  if (renderMode !== "clean" || !member || !group) {
3258
+ patchedToolRender.call(this, event.width);
3259
+ const view = standaloneViews.get(this);
3260
+ if (view) return handleProjectionMouse(this, view.projection, {
3261
+ ...event, y: event.y - view.offset, height: view.projection.lines.length,
3262
+ });
3105
3263
  return originalToolHandleMouse.call(this, event);
3106
3264
  }
3107
3265
  const position = activityMemberPosition(group, member);
@@ -3128,33 +3286,15 @@ export default function prettyTui(pi: ExtensionAPI) {
3128
3286
  return { handled: true };
3129
3287
  }
3130
3288
 
3131
- if (nestedTools.count(this.toolCallId) > 0 && isLeftClick) {
3289
+ if (nestedTools.count(this.toolCallId) > 0 || isThirdPartyTool(this)) {
3132
3290
  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 };
3291
+ const { childWidth, prefixWidth } = activityTreeStyle(group, member, event.width, theme);
3292
+ const projection = projectTool(this, childWidth, theme, "compact");
3293
+ if (projection) return handleProjectionMouse(this, projection, {
3294
+ ...event, x: Math.max(0, event.x - prefixWidth),
3295
+ y: event.y - (position.first ? summaryHeight + 1 : 0),
3296
+ width: childWidth, height: projection.lines.length,
3297
+ });
3158
3298
  }
3159
3299
 
3160
3300
  if (!this.result && isLeftClick) {
@@ -3653,7 +3793,7 @@ export default function prettyTui(pi: ExtensionAPI) {
3653
3793
  cleanCompactToolCallIds.clear();
3654
3794
  cleanGroupToolCallIds.clear();
3655
3795
  cleanToolCallGroupOwners.clear();
3656
- cleanToolComponents.clear();
3796
+ toolComponents.clear();
3657
3797
  cleanToolThemes.clear();
3658
3798
  cleanToolNames.clear();
3659
3799
  clearToolActivityHolds();
@@ -3902,7 +4042,11 @@ export default function prettyTui(pi: ExtensionAPI) {
3902
4042
  finishGroup();
3903
4043
  };
3904
4044
 
3905
- pi.on("session_start", (_event, ctx) => restoreCleanSession(ctx));
4045
+ pi.on("session_start", (_event, ctx) => {
4046
+ fullThinkingHidden = false;
4047
+ compactThinkingHidden = true;
4048
+ restoreCleanSession(ctx);
4049
+ });
3906
4050
  pi.on("session_tree", (_event, ctx) => restoreCleanSession(ctx));
3907
4051
  pi.on("session_before_compact", () => {
3908
4052
  // Pi emits this for manual, threshold, and overflow-recovery compaction.
@@ -4071,7 +4215,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4071
4215
  cleanRun.activeToolCallId = undefined;
4072
4216
  cleanRun.activeToolName = undefined;
4073
4217
  cleanRun.activity = "thinking";
4074
- if (renderMode === "clean") startWorkingElapsedTimer();
4218
+ startWorkingElapsedTimer();
4075
4219
  });
4076
4220
  pi.on("tool_execution_start", (event) => {
4077
4221
  if ((event as any).parentToolCallId) {
@@ -4222,9 +4366,43 @@ export default function prettyTui(pi: ExtensionAPI) {
4222
4366
  });
4223
4367
  });
4224
4368
 
4369
+ const nativeDefinitions: Record<string, any> = {
4370
+ read: createReadToolDefinition(cwd), bash: createBashToolDefinition(cwd),
4371
+ edit: createEditToolDefinition(cwd), write: createWriteToolDefinition(cwd),
4372
+ grep: createGrepToolDefinition(cwd), find: createFindToolDefinition(cwd), ls: createLsToolDefinition(cwd),
4373
+ };
4374
+ const registerStyledTool = (definition: any) => {
4375
+ pi.registerToolRenderer((name, next) => {
4376
+ const downstream = next();
4377
+ if (name !== definition.name) return downstream;
4378
+ // A same-named extension/SDK tool owns its own rendering and parameters.
4379
+ // In particular, do not apply built-in summaries to an unrelated schema.
4380
+ const source = pi.getAllTools().find((tool) => tool.name === name)?.sourceInfo;
4381
+ if (source && source.path !== `builtin:${name}`) {
4382
+ return downstream ? { ...downstream, [THIRD_PARTY_TOOL]: true } : downstream;
4383
+ }
4384
+ const native = nativeDefinitions[name];
4385
+ const select = (kind: "renderCall" | "renderResult", args: any[]) => {
4386
+ const context = args.at(-1);
4387
+ const useNative = renderMode === "full" || renderMode === "compact" && context.expanded;
4388
+ const namespace = useNative ? "nativeFull" : "pretty";
4389
+ const state = context.state[namespace] ??= { state: {} };
4390
+ const scoped = { ...context, state: state.state, lastComponent: state[kind] };
4391
+ const renderer = useNative ? downstream?.[kind] ?? native[kind] : definition[kind];
4392
+ const component = renderer?.(...args.slice(0, -1), scoped);
4393
+ state[kind] = component;
4394
+ return component;
4395
+ };
4396
+ return { ...downstream, renderShell: "self",
4397
+ renderCall: (...args: any[]) => select("renderCall", args),
4398
+ renderResult: (...args: any[]) => select("renderResult", args),
4399
+ };
4400
+ });
4401
+ };
4402
+
4225
4403
  const read = createReadTool(cwd);
4226
4404
  const readLabel = read.label || read.name;
4227
- pi.registerTool({
4405
+ registerStyledTool({
4228
4406
  ...read,
4229
4407
  renderShell: "self",
4230
4408
  renderCall(args: any, theme: any, context: any) {
@@ -4253,7 +4431,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4253
4431
 
4254
4432
  const bash = createBashTool(cwd);
4255
4433
  const bashLabel = bash.label || bash.name;
4256
- pi.registerTool({
4434
+ registerStyledTool({
4257
4435
  ...bash,
4258
4436
  renderShell: "self",
4259
4437
  renderCall(args: any, theme: any, context: any) {
@@ -4304,7 +4482,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4304
4482
 
4305
4483
  const edit = createEditTool(cwd);
4306
4484
  const editLabel = edit.label || edit.name;
4307
- pi.registerTool({
4485
+ registerStyledTool({
4308
4486
  ...edit,
4309
4487
  renderShell: "self",
4310
4488
  renderCall(args: any, theme: any, context: any) {
@@ -4347,7 +4525,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4347
4525
 
4348
4526
  const write = createWriteTool(cwd);
4349
4527
  const writeLabel = write.label || write.name;
4350
- pi.registerTool({
4528
+ registerStyledTool({
4351
4529
  ...write,
4352
4530
  renderShell: "self",
4353
4531
  renderCall(args: any, theme: any, context: any) {
@@ -4379,7 +4557,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4379
4557
 
4380
4558
  const grep = createGrepTool(cwd);
4381
4559
  const grepLabel = grep.label || grep.name;
4382
- pi.registerTool({
4560
+ registerStyledTool({
4383
4561
  ...grep,
4384
4562
  renderShell: "self",
4385
4563
  renderCall(args: any, theme: any, context: any) {
@@ -4400,7 +4578,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4400
4578
 
4401
4579
  const find = createFindTool(cwd);
4402
4580
  const findLabel = find.label || find.name;
4403
- pi.registerTool({
4581
+ registerStyledTool({
4404
4582
  ...find,
4405
4583
  renderShell: "self",
4406
4584
  renderCall(args: any, theme: any, context: any) {
@@ -4419,7 +4597,7 @@ export default function prettyTui(pi: ExtensionAPI) {
4419
4597
 
4420
4598
  const ls = createLsTool(cwd);
4421
4599
  const lsLabel = ls.label || ls.name;
4422
- pi.registerTool({
4600
+ registerStyledTool({
4423
4601
  ...ls,
4424
4602
  renderShell: "self",
4425
4603
  renderCall(args: any, theme: any, context: any) {