pi-pretty-tui 0.2.4 → 0.2.6

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,3 +1,4 @@
1
+ import { NestedTools, NESTED_SNAPSHOT_ENTRY } from "./nested-tools.js";
1
2
  import {
2
3
  AssistantMessageComponent,
3
4
  CustomEditor,
@@ -26,6 +27,7 @@ import {
26
27
  type TuiMouseEvent,
27
28
  visibleWidth,
28
29
  wrapTextWithAnsi,
30
+ getOsc8LinkAtColumn,
29
31
  } from "@earendil-works/pi-tui";
30
32
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
31
33
  import { join } from "node:path";
@@ -95,6 +97,7 @@ export default function prettyTui(pi: ExtensionAPI) {
95
97
  let cleanToolsExpanded = false;
96
98
  let cleanContextCompacted = false;
97
99
  let fullscreenTui = false;
100
+ const mouseClickHitTests = new WeakMap<object, (event: any) => boolean>();
98
101
  let currentTui: any;
99
102
  let currentExtensionUi: any;
100
103
  let changingAllToolsExpansion = false;
@@ -192,6 +195,7 @@ export default function prettyTui(pi: ExtensionAPI) {
192
195
  };
193
196
  const responseAnswerTexts = new Map<string, string>();
194
197
  let responseRunStartedAt: number | undefined;
198
+ const RUNNING_DOT_INTERVAL_MS = 800;
195
199
  let workingElapsedTimer: ReturnType<typeof setInterval> | undefined;
196
200
  const stopWorkingElapsedTimer = () => {
197
201
  if (workingElapsedTimer) clearInterval(workingElapsedTimer);
@@ -199,7 +203,8 @@ export default function prettyTui(pi: ExtensionAPI) {
199
203
  };
200
204
  const startWorkingElapsedTimer = () => {
201
205
  if (workingElapsedTimer) return;
202
- workingElapsedTimer = setInterval(() => currentTui?.requestRender?.(), 1000);
206
+ // Share one redraw clock between the elapsed label and Running-dot pulse.
207
+ workingElapsedTimer = setInterval(() => currentTui?.requestRender?.(), RUNNING_DOT_INTERVAL_MS);
203
208
  workingElapsedTimer.unref?.();
204
209
  };
205
210
  let responseHasAssistantMessage = false;
@@ -223,6 +228,11 @@ export default function prettyTui(pi: ExtensionAPI) {
223
228
  return `${hours}h ${String(minutes).padStart(2, "0")}m`;
224
229
  };
225
230
  const activityTimeline = new ActivityTimeline();
231
+ const nestedTools = new NestedTools();
232
+ const refreshNestedTools = (id: string) => {
233
+ cleanToolComponents.get(nestedTools.root(id))?.updateDisplay?.();
234
+ currentTui?.requestRender?.();
235
+ };
226
236
  const revealedActivityGroups = new Set<string>();
227
237
  // A local collapse must still work when Ctrl+O has expanded every group.
228
238
  const collapsedActivityGroups = new Set<string>();
@@ -236,6 +246,8 @@ export default function prettyTui(pi: ExtensionAPI) {
236
246
  component: Markdown;
237
247
  }>();
238
248
  let activityUpdateSequence = 0;
249
+ // Assigned by the interactive-mode patch below; tool events carry no `this`.
250
+ let flushDeferredCacheNotices: ((toolCallId: string) => void) | undefined;
239
251
  const assistantBoundaryKeys = new Set<string>();
240
252
  const cleanCompactToolCallIds = new Set<string>();
241
253
  const cleanGroupToolCallIds = new Map<string, string[]>();
@@ -324,7 +336,7 @@ export default function prettyTui(pi: ExtensionAPI) {
324
336
  if (color === "error") return `\x1b[31m${text}\x1b[39m`;
325
337
  if (color === "success") return `\x1b[32m${text}\x1b[39m`;
326
338
  if (color === "syntaxComment") return markdownTheme.codeBlock(text);
327
- if (color === "thinkingLow") return markdownTheme.link(text);
339
+ if (color === "thinkingLow" || color === "mdLink") return markdownTheme.link(text);
328
340
  if (color === "dim" || color === "muted" || color === "thinkingText") {
329
341
  return markdownTheme.quote(text);
330
342
  }
@@ -373,7 +385,7 @@ export default function prettyTui(pi: ExtensionAPI) {
373
385
  const renderActivityCollapse = (group: ActivityGroup, width: number): string => {
374
386
  const theme = activityGroupTheme(group);
375
387
  return truncateToWidth(
376
- theme.fg("dim", collapsePrefix) + theme.fg("muted", collapseLabel),
388
+ theme.fg("dim", collapsePrefix) + theme.fg("mdLink", collapseLabel),
377
389
  Math.max(1, width),
378
390
  "",
379
391
  );
@@ -816,10 +828,16 @@ export default function prettyTui(pi: ExtensionAPI) {
816
828
  content.paddingY = 0;
817
829
  content.setBgFn?.(undefined);
818
830
  content.invalidate?.();
819
- // Pi wraps the Markdown in a full-width Box. Render the Markdown child
820
- // directly so short messages expose their natural visible width instead
821
- // of inheriting the Box's width-filling padding.
831
+ // Older Pi wraps Markdown in a Box; Pi 1.0 renders Markdown directly
832
+ // with its own background and padding. Normalize both layouts before
833
+ // measuring, preserving the native foreground and Markdown options.
822
834
  const markdown = content.children?.[0] ?? content;
835
+ markdown.paddingX = 0;
836
+ markdown.paddingY = 0;
837
+ if (markdown.defaultTextStyle) {
838
+ markdown.defaultTextStyle = { ...markdown.defaultTextStyle, bgColor: undefined };
839
+ }
840
+ markdown.invalidate?.();
823
841
 
824
842
  const outputPad = Math.max(0, Number(this.outputPad) || 0);
825
843
  const markdownTheme = this.markdownTheme;
@@ -874,8 +892,9 @@ export default function prettyTui(pi: ExtensionAPI) {
874
892
  };
875
893
  };
876
894
  const border = (text: string) => currentExtensionUi?.theme?.fg
877
- ? currentExtensionUi.theme.fg("accent", text)
878
- : markdownTheme.quoteBorder(text);
895
+ ? currentExtensionUi.theme.fg("mdLink", text)
896
+ : markdownTheme.link(text);
897
+ const userText = typeof this.text === "string" ? this.text : "";
879
898
  const frame: Component = {
880
899
  render(width: number): string[] {
881
900
  const measured = layout(width);
@@ -894,37 +913,51 @@ export default function prettyTui(pi: ExtensionAPI) {
894
913
  return [top, ...body, bottom].map((line: string) => left + line + right);
895
914
  },
896
915
  handleMouse(event: any) {
897
- if (!markdown.handleMouse) return undefined;
898
916
  const measured = layout(event.width);
899
- if (!measured.framed) {
900
- return withTranscriptMarkdown(() => markdown.handleMouse({
917
+ const height = measured.body.length + (measured.framed ? 2 : 0);
918
+ if (
919
+ event.x < measured.leftPad ||
920
+ event.x >= measured.leftPad + measured.frameWidth ||
921
+ event.y < 0 || event.y >= height
922
+ ) return undefined;
923
+
924
+ // Preserve code-block Copy controls before the enclosing bubble action.
925
+ const contentX = measured.leftPad + (measured.framed ? 2 : 0);
926
+ const contentY = measured.framed ? 1 : 0;
927
+ let result;
928
+ if (
929
+ markdown.handleMouse &&
930
+ event.x >= contentX && event.x < contentX + measured.contentWidth &&
931
+ event.y >= contentY && event.y < contentY + measured.body.length
932
+ ) {
933
+ result = withTranscriptMarkdown(() => markdown.handleMouse({
901
934
  ...event,
902
- x: event.x - measured.leftPad,
935
+ x: event.x - contentX,
936
+ y: event.y - contentY,
903
937
  width: measured.contentWidth,
938
+ height: measured.body.length,
904
939
  }));
940
+ if (result?.handled) return result;
905
941
  }
906
- const contentX = measured.leftPad + 2;
907
- if (
908
- event.y <= 0 ||
909
- event.y > measured.body.length ||
910
- event.x < contentX ||
911
- event.x >= contentX + measured.contentWidth
912
- ) {
913
- return undefined;
942
+ if (fullscreenTui && event.type === "click" && event.button === "left" && userText.trim()) {
943
+ copyTranscriptText(userText);
944
+ return { handled: true };
914
945
  }
915
- return withTranscriptMarkdown(() => markdown.handleMouse({
916
- ...event,
917
- x: event.x - contentX,
918
- y: event.y - 1,
919
- width: measured.contentWidth,
920
- height: measured.body.length,
921
- }));
946
+ return result;
922
947
  },
923
948
  invalidate() {
924
949
  markdown.invalidate?.();
925
950
  },
926
951
  };
927
952
 
953
+ const bubbleClickHitTest = (event: any) => {
954
+ if (!fullscreenTui || !userText.trim()) return false;
955
+ const measured = layout(event.width);
956
+ return event.x >= measured.leftPad && event.x < measured.leftPad + measured.frameWidth &&
957
+ event.y >= 0 && event.y < measured.body.length + (measured.framed ? 2 : 0);
958
+ };
959
+ mouseClickHitTests.set(frame, bubbleClickHitTest);
960
+ mouseClickHitTests.set(this, bubbleClickHitTest);
928
961
  this.clear();
929
962
  this.addChild(frame);
930
963
  };
@@ -1136,7 +1169,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1136
1169
  text,
1137
1170
  theme,
1138
1171
  component: new Markdown(text, 0, 0, owner.markdownTheme, {
1139
- color: (line: string) => theme.fg("thinkingText", line),
1172
+ color: (line: string) => theme.fg("muted", line),
1140
1173
  italic: true,
1141
1174
  }),
1142
1175
  };
@@ -1221,11 +1254,15 @@ export default function prettyTui(pi: ExtensionAPI) {
1221
1254
  const terminalLines = terminalMember && visibleLines.length === 0
1222
1255
  ? renderPendingActivityUpdate(terminalMember, width)
1223
1256
  : [];
1224
- const appendTail = (activityLines: string[]) => [
1225
- ...activityLines,
1226
- ...terminalLines,
1227
- ...visibleLines,
1228
- ];
1257
+ const appendTail = (activityLines: string[]) => {
1258
+ mouseClickHitTests.set(this, (event) => renderMode === "clean" &&
1259
+ event.y >= (position.first ? 1 : 0) && event.y < activityLines.length);
1260
+ return [
1261
+ ...activityLines,
1262
+ ...terminalLines,
1263
+ ...visibleLines,
1264
+ ];
1265
+ };
1229
1266
  const revealed = activityGroupRevealed(group);
1230
1267
  if (!revealed) {
1231
1268
  const activityLines = position.first
@@ -1268,8 +1305,8 @@ export default function prettyTui(pi: ExtensionAPI) {
1268
1305
  }
1269
1306
  const stateLabel = this.isStreaming ? "thinking" : "thought";
1270
1307
  const label = truncateToWidth(stateLabel, Math.max(1, childWidth - 2), "…");
1271
- const header = groupTheme.fg("thinkingLow", "● ") +
1272
- groupTheme.fg("toolTitle", groupTheme.bold(label));
1308
+ const header = groupTheme.fg("mdLink", "● ") +
1309
+ groupTheme.fg("text", groupTheme.bold(label));
1273
1310
  let contentLines: string[] = [header];
1274
1311
  if (expanded) {
1275
1312
  contentLines.push(...thinkingDetailLines(
@@ -1472,6 +1509,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1472
1509
  const toolsExpansionPatchKey = Symbol.for("pretty-tui.clean-tool-expansion");
1473
1510
  if (!interactiveModePrototype[toolsExpansionPatchKey]) {
1474
1511
  const originalSetToolsExpanded = interactiveModePrototype.setToolsExpanded;
1512
+ const originalToggleToolOutputExpansion = interactiveModePrototype.toggleToolOutputExpansion;
1475
1513
  const originalRenderSessionEntries = interactiveModePrototype.renderSessionEntries;
1476
1514
  const originalSwitchTuiMode = interactiveModePrototype.switchTuiMode;
1477
1515
  const originalShowExtensionNotify = interactiveModePrototype.showExtensionNotify;
@@ -1479,8 +1517,28 @@ export default function prettyTui(pi: ExtensionAPI) {
1479
1517
  const originalShowStatus = interactiveModePrototype.showStatus;
1480
1518
  const originalShowWarning = interactiveModePrototype.showWarning;
1481
1519
  const originalShowError = interactiveModePrototype.showError;
1520
+ const originalAddCacheMissNotice = interactiveModePrototype.addCacheMissNotice;
1521
+ const originalMaybeShowCacheMissNotice = interactiveModePrototype.maybeShowCacheMissNotice;
1522
+ const originalAddMessageToChat = interactiveModePrototype.addMessageToChat;
1523
+ const cacheMessageContexts = new WeakMap<object, any>();
1524
+ const pendingCacheNotices = new Map<string, {
1525
+ key: string; title: string; content: string; toolCallIds: string[];
1526
+ }>();
1527
+ // The interactive-mode instance that owns the chat container. Tool events
1528
+ // carry no `this`, so deferred notices need this handle to mount.
1529
+ const cacheNoticeHost: { current?: any } = {};
1530
+ const cacheNoticeOwners = new WeakMap<object, any>();
1531
+ const patchedToggleToolOutputExpansion = function (this: any) {
1532
+ if (renderMode !== "clean") {
1533
+ return originalToggleToolOutputExpansion.call(this);
1534
+ }
1535
+ this.showStatus(
1536
+ "Activity details expand individually in clean mode\nClick a group, tool, thought, or section to expand/collapse it.",
1537
+ );
1538
+ };
1482
1539
  const patchedSetToolsExpanded = function (this: any, expanded: boolean) {
1483
1540
  cleanToolsExpanded = expanded;
1541
+ nestedTools.clearExpansion();
1484
1542
  collapsedActivityGroups.clear();
1485
1543
  changingAllToolsExpansion = true;
1486
1544
  if (!expanded) {
@@ -1504,6 +1562,18 @@ export default function prettyTui(pi: ExtensionAPI) {
1504
1562
  // while Pi's live compaction UI appends it chronologically. Keep reloads
1505
1563
  // and transcript rebuilds consistent with that live presentation.
1506
1564
  const orderedEntries = orderContextEntriesForTranscript(entries);
1565
+ cacheMessageContexts.delete(this);
1566
+ // Pi re-derives notices on each rebuild (including when its display
1567
+ // setting changes). Remove old projections, not their work groups.
1568
+ for (const group of activityTimeline.groups()) {
1569
+ for (const member of [...group.members]) {
1570
+ if (member.updateKey?.startsWith("cache-miss:") || member.updateKey?.startsWith("runtime:cache-miss:")) {
1571
+ activityTimeline.discardUpdate(member.updateKey);
1572
+ activityUpdateComponents.delete(member.id);
1573
+ activityUpdateMarkdownComponents.delete(member.id);
1574
+ }
1575
+ }
1576
+ }
1507
1577
  return originalRenderSessionEntries.call(this, orderedEntries, options);
1508
1578
  };
1509
1579
 
@@ -1559,9 +1629,9 @@ export default function prettyTui(pi: ExtensionAPI) {
1559
1629
  }
1560
1630
  // Do not change Pi's persisted hideThinkingBlock setting in clean mode.
1561
1631
  // Thought details use the activity hierarchy instead: click one thought,
1562
- // or use Ctrl+O when every member should expand together.
1632
+ // Keyboard visibility toggles only explain these local controls.
1563
1633
  this.showStatus(
1564
- "Thought details expand individually in clean mode\nClick a thought, or use Ctrl+O to expand all.",
1634
+ "Thought details expand individually in clean mode\nClick a thought to expand/collapse its details.",
1565
1635
  );
1566
1636
  };
1567
1637
 
@@ -1588,9 +1658,168 @@ export default function prettyTui(pi: ExtensionAPI) {
1588
1658
  }
1589
1659
  };
1590
1660
 
1661
+ // CacheMiss itself carries no assistant/message ID. Bind it at Pi's
1662
+ // calling sites, rather than depend on private chat-container children or
1663
+ // instanceof checks (these can be wrapped or come from another module).
1664
+ const patchedAddMessageToChat = function (this: any, message: any, ...args: any[]) {
1665
+ if (message?.role === "assistant") cacheMessageContexts.set(this, message);
1666
+ else cacheMessageContexts.delete(this);
1667
+ return originalAddMessageToChat.call(this, message, ...args);
1668
+ };
1669
+ const patchedMaybeShowCacheMissNotice = function (this: any, message: any, ...args: any[]) {
1670
+ const previous = cacheMessageContexts.get(this);
1671
+ cacheMessageContexts.set(this, message);
1672
+ try {
1673
+ return originalMaybeShowCacheMissNotice.call(this, message, ...args);
1674
+ } finally {
1675
+ if (previous) cacheMessageContexts.set(this, previous);
1676
+ else cacheMessageContexts.delete(this);
1677
+ }
1678
+ };
1679
+
1680
+ // One projection component per activity update member. Cache notices are
1681
+ // also mounted immediately after the tool that anchors them, so container
1682
+ // order matches group order even when the notice arrives before the tools.
1683
+ const mountActivityUpdate = (
1684
+ mode: any,
1685
+ group: ActivityGroup,
1686
+ member: ActivityMember,
1687
+ nativeNodes: Component[] = [],
1688
+ after?: Component,
1689
+ ): void => {
1690
+ const container = mode?.chatContainer;
1691
+ const existing = activityUpdateComponents.get(member.id);
1692
+ if (existing) {
1693
+ const children: Component[] | undefined = container?.children;
1694
+ if (after && children) {
1695
+ const target = children.indexOf(existing);
1696
+ const anchorIndex = children.indexOf(after);
1697
+ if (target >= 0 && anchorIndex >= 0 && target !== anchorIndex + 1) {
1698
+ children.splice(target, 1);
1699
+ children.splice(children.indexOf(after) + 1, 0, existing);
1700
+ }
1701
+ }
1702
+ return;
1703
+ }
1704
+ // A mode switch can keep existing components rather than rebuild them.
1705
+ const component: Component = {
1706
+ render: (width: number) => renderMode === "clean"
1707
+ ? renderActivityUpdateProjection(group, member, width)
1708
+ : nativeNodes.flatMap((node) => node.render(width)),
1709
+ invalidate() {},
1710
+ handleMouse: (event: any) => renderMode === "clean"
1711
+ ? handleActivityUpdateMouse(group, member, event)
1712
+ : undefined,
1713
+ } as Component;
1714
+ activityUpdateComponents.set(member.id, component);
1715
+ const index = after && container?.children ? container.children.indexOf(after) : -1;
1716
+ if (index >= 0) {
1717
+ container.children.splice(index + 1, 0, component);
1718
+ return;
1719
+ }
1720
+ container?.addChild?.(component);
1721
+ };
1722
+
1723
+ // Live cache misses arrive before the message's tools execute, so their
1724
+ // members do not exist yet. Attach the notice once its first tool joins the
1725
+ // group; this keeps it inside the tree instead of above it.
1726
+ const flushPendingCacheNotices = (toolCallId: string): void => {
1727
+ if (pendingCacheNotices.size === 0) return;
1728
+ for (const [messageKey, pending] of [...pendingCacheNotices]) {
1729
+ if (!pending.toolCallIds.includes(toolCallId)) continue;
1730
+ pendingCacheNotices.delete(messageKey);
1731
+ const anchorMember = activityTimeline.memberForTool(toolCallId);
1732
+ if (!anchorMember) continue;
1733
+ const member = activityTimeline.addUpdateAfterMember(
1734
+ anchorMember.id, pending.key, pending.title, pending.content, "warning",
1735
+ ) ?? activityTimeline.addStandaloneUpdate(
1736
+ pending.key, pending.title, pending.content, "warning",
1737
+ );
1738
+ const group = activityTimeline.groupForMember(member.id);
1739
+ if (!group) continue;
1740
+ mountActivityUpdate(
1741
+ cacheNoticeHost.current, group, member, [],
1742
+ cleanToolComponents.get(toolCallId),
1743
+ );
1744
+ refreshActivityGroup(group);
1745
+ cacheNoticeHost.current?.ui?.requestRender?.();
1746
+ }
1747
+ };
1748
+ flushDeferredCacheNotices = flushPendingCacheNotices;
1749
+
1750
+ const patchedAddCacheMissNotice = function (this: any, miss: any) {
1751
+ if (renderMode !== "clean" || !this.chatContainer?.addChild) {
1752
+ return originalAddCacheMissNotice.call(this, miss);
1753
+ }
1754
+ // Let Pi decide thresholds, formatting and cause labels. Capture its
1755
+ // native nodes rather than duplicating cache heuristics or matching text.
1756
+ const nativeNodes: Component[] = [];
1757
+ const captureHost = Object.create(this);
1758
+ captureHost.chatContainer = Object.create(this.chatContainer);
1759
+ captureHost.chatContainer.addChild = (node: Component) => nativeNodes.push(node);
1760
+ originalAddCacheMissNotice.call(captureHost, miss);
1761
+ const text = nativeNodes.flatMap((node) => node.render(4096))
1762
+ .map((line) => stripTerminalSequences(line).trim()).filter(Boolean).join("\n");
1763
+ if (!text) return;
1764
+
1765
+ // Rebuilds have already restored the *whole* branch. Use the assistant
1766
+ // physically preceding this insertion, not currentGroup() (the last one).
1767
+ const children: any[] = this.chatContainer.children ?? [];
1768
+ let message: any = cacheMessageContexts.get(this);
1769
+ for (let index = children.length - 1; !message && index >= 0; index--) {
1770
+ const child = children[index];
1771
+ if (cacheNoticeOwners.has(child)) { message = cacheNoticeOwners.get(child); break; }
1772
+ if (child instanceof ToolExecutionComponent) continue;
1773
+ if (child instanceof AssistantMessageComponent) { message = (child as any).lastMessage; break; }
1774
+ break; // never cross a user/custom/compaction boundary
1775
+ }
1776
+ // Live Pi may have inserted a provider diagnostic after the assistant.
1777
+ if (!message && this.streamingComponent && children.includes(this.streamingComponent)) {
1778
+ message = this.streamingMessage;
1779
+ }
1780
+ let anchor: ActivityMember | undefined;
1781
+ const calls = messageContentItems(message).filter((item: any) => item.type === "toolCall");
1782
+ for (let index = calls.length - 1; index >= 0; index--) {
1783
+ anchor = activityTimeline.memberForTool(calls[index].id);
1784
+ if (anchor) break;
1785
+ }
1786
+ if (!anchor && message && !visibleAssistantText(message)) {
1787
+ anchor = activityTimeline.memberForThinking(assistantMessageKey(message));
1788
+ }
1789
+ const key = message
1790
+ ? `cache-miss:${assistantMessageKey(message)}`
1791
+ : `runtime:cache-miss:${++activityUpdateSequence}`;
1792
+ const [title, ...details] = text.split("\n");
1793
+ const content = details.join("\n");
1794
+ cacheNoticeHost.current = this;
1795
+ if (!anchor && calls.length > 0) {
1796
+ // The tools have not started yet: their group does not exist. Defer so
1797
+ // the notice lands after the tool instead of floating above the tree.
1798
+ pendingCacheNotices.set(key, {
1799
+ key, title, content, toolCallIds: calls.map((call: any) => call.id),
1800
+ });
1801
+ return;
1802
+ }
1803
+ const member = (anchor && activityTimeline.addUpdateAfterMember(
1804
+ anchor.id, key, title, content, "warning",
1805
+ )) || activityTimeline.addStandaloneUpdate(key, title, content, "warning");
1806
+ const group = activityTimeline.groupForMember(member.id);
1807
+ if (!group) return;
1808
+ mountActivityUpdate(
1809
+ this, group, member, nativeNodes,
1810
+ anchor?.toolCallId ? cleanToolComponents.get(anchor.toolCallId) : undefined,
1811
+ );
1812
+ const component = activityUpdateComponents.get(member.id);
1813
+ if (component && message) cacheNoticeOwners.set(component, message);
1814
+ refreshActivityGroup(group);
1815
+ this.ui?.requestRender?.();
1816
+ };
1817
+
1591
1818
  interactiveModePrototype[toolsExpansionPatchKey] = {
1592
1819
  originalSetToolsExpanded,
1593
1820
  patchedSetToolsExpanded,
1821
+ originalToggleToolOutputExpansion,
1822
+ patchedToggleToolOutputExpansion,
1594
1823
  originalRenderSessionEntries,
1595
1824
  patchedRenderSessionEntries,
1596
1825
  originalSwitchTuiMode,
@@ -1605,8 +1834,15 @@ export default function prettyTui(pi: ExtensionAPI) {
1605
1834
  patchedShowWarning,
1606
1835
  originalShowError,
1607
1836
  patchedShowError,
1837
+ originalAddCacheMissNotice,
1838
+ patchedAddCacheMissNotice,
1839
+ originalMaybeShowCacheMissNotice,
1840
+ patchedMaybeShowCacheMissNotice,
1841
+ originalAddMessageToChat,
1842
+ patchedAddMessageToChat,
1608
1843
  };
1609
1844
  interactiveModePrototype.setToolsExpanded = patchedSetToolsExpanded;
1845
+ interactiveModePrototype.toggleToolOutputExpansion = patchedToggleToolOutputExpansion;
1610
1846
  interactiveModePrototype.renderSessionEntries = patchedRenderSessionEntries;
1611
1847
  interactiveModePrototype.switchTuiMode = patchedSwitchTuiMode;
1612
1848
  interactiveModePrototype.showExtensionNotify = patchedShowExtensionNotify;
@@ -1614,6 +1850,15 @@ export default function prettyTui(pi: ExtensionAPI) {
1614
1850
  interactiveModePrototype.showStatus = patchedShowStatus;
1615
1851
  interactiveModePrototype.showWarning = patchedShowWarning;
1616
1852
  interactiveModePrototype.showError = patchedShowError;
1853
+ if (typeof originalAddCacheMissNotice === "function") {
1854
+ interactiveModePrototype.addCacheMissNotice = patchedAddCacheMissNotice;
1855
+ if (typeof originalMaybeShowCacheMissNotice === "function") {
1856
+ interactiveModePrototype.maybeShowCacheMissNotice = patchedMaybeShowCacheMissNotice;
1857
+ }
1858
+ if (typeof originalAddMessageToChat === "function") {
1859
+ interactiveModePrototype.addMessageToChat = patchedAddMessageToChat;
1860
+ }
1861
+ }
1617
1862
 
1618
1863
  pi.on("session_shutdown", () => {
1619
1864
  fullscreenTui = false;
@@ -1622,6 +1867,9 @@ export default function prettyTui(pi: ExtensionAPI) {
1622
1867
  currentExtensionUi = undefined;
1623
1868
  const patch = interactiveModePrototype[toolsExpansionPatchKey];
1624
1869
  if (!patch) return;
1870
+ if (patch.patchedToggleToolOutputExpansion === interactiveModePrototype.toggleToolOutputExpansion) {
1871
+ interactiveModePrototype.toggleToolOutputExpansion = patch.originalToggleToolOutputExpansion;
1872
+ }
1625
1873
  if (patch.patchedSetToolsExpanded === interactiveModePrototype.setToolsExpanded) {
1626
1874
  interactiveModePrototype.setToolsExpanded = patch.originalSetToolsExpanded;
1627
1875
  }
@@ -1646,39 +1894,98 @@ export default function prettyTui(pi: ExtensionAPI) {
1646
1894
  if (patch.patchedShowError === interactiveModePrototype.showError) {
1647
1895
  interactiveModePrototype.showError = patch.originalShowError;
1648
1896
  }
1897
+ if (patch.patchedAddCacheMissNotice === interactiveModePrototype.addCacheMissNotice) {
1898
+ interactiveModePrototype.addCacheMissNotice = patch.originalAddCacheMissNotice;
1899
+ }
1900
+ if (patch.patchedMaybeShowCacheMissNotice === interactiveModePrototype.maybeShowCacheMissNotice) {
1901
+ interactiveModePrototype.maybeShowCacheMissNotice = patch.originalMaybeShowCacheMissNotice;
1902
+ }
1903
+ if (patch.patchedAddMessageToChat === interactiveModePrototype.addMessageToChat) {
1904
+ interactiveModePrototype.addMessageToChat = patch.originalAddMessageToChat;
1905
+ }
1649
1906
  if (
1650
1907
  interactiveModePrototype.setToolsExpanded === patch.originalSetToolsExpanded &&
1908
+ interactiveModePrototype.toggleToolOutputExpansion === patch.originalToggleToolOutputExpansion &&
1651
1909
  interactiveModePrototype.renderSessionEntries === patch.originalRenderSessionEntries &&
1652
1910
  interactiveModePrototype.switchTuiMode === patch.originalSwitchTuiMode &&
1653
1911
  interactiveModePrototype.showExtensionNotify === patch.originalShowExtensionNotify &&
1654
1912
  interactiveModePrototype.toggleThinkingBlockVisibility === patch.originalToggleThinkingBlockVisibility &&
1655
1913
  interactiveModePrototype.showStatus === patch.originalShowStatus &&
1656
1914
  interactiveModePrototype.showWarning === patch.originalShowWarning &&
1657
- interactiveModePrototype.showError === patch.originalShowError
1915
+ interactiveModePrototype.showError === patch.originalShowError &&
1916
+ interactiveModePrototype.addCacheMissNotice === patch.originalAddCacheMissNotice &&
1917
+ interactiveModePrototype.maybeShowCacheMissNotice === patch.originalMaybeShowCacheMissNotice &&
1918
+ interactiveModePrototype.addMessageToChat === patch.originalAddMessageToChat
1658
1919
  ) {
1659
1920
  delete interactiveModePrototype[toolsExpansionPatchKey];
1660
1921
  }
1661
1922
  });
1662
1923
  }
1663
1924
 
1664
- // Fullscreen selection only emits a component click when press and release
1665
- // land on the exact same cell. Give clean group rows a tiny horizontal
1666
- // tolerance so an ordinary click does not turn into an accidental selection.
1925
+ // Allow same-row horizontal jitter only on our interactive regions. Keep
1926
+ // real drags, links, ordinary transcript selection, and multi-click rules native.
1667
1927
  const altScreenPrototype = TuiAltScreen.prototype as any;
1668
1928
  const cleanSummaryClickPatchKey = Symbol.for("pretty-tui.clean-summary-click");
1669
1929
  const cleanSummaryPressKey = Symbol("pretty-tui.clean-summary-press");
1930
+ const cleanControlPressKey = Symbol("pretty-tui.control-press");
1670
1931
  if (!altScreenPrototype[cleanSummaryClickPatchKey]) {
1671
1932
  const originalHandleSelectionMouseEvent = altScreenPrototype.handleSelectionMouseEvent;
1933
+ const originalHandleMouseEvent = altScreenPrototype.handleMouseEvent;
1672
1934
  const summaryAtPoint = (screen: any, x: number, y: number): boolean => {
1673
1935
  const line = stripTerminalSequences(screen.previousScreen?.[y] ?? "");
1674
1936
  const match = /●\s+(?:Done|Running)\([^)]*\)/.exec(line);
1675
- return Boolean(match && x >= match.index && x < match.index + match[0].length);
1937
+ if (match) {
1938
+ const start = visibleWidth(line.slice(0, match.index));
1939
+ if (x >= start && x < start + visibleWidth(match[0])) return true;
1940
+ }
1941
+ const collapse = line.indexOf(collapseLabel);
1942
+ const start = visibleWidth(line.slice(0, collapse));
1943
+ return renderMode === "clean" && collapse >= 0 && x >= start && x < start + visibleWidth(collapseLabel);
1944
+ };
1945
+ const interactiveAtPoint = (screen: any, x: number, y: number): boolean => {
1946
+ if (screen.hasOverlay?.()) return false;
1947
+ if (getOsc8LinkAtColumn(screen.previousScreen?.[y] ?? "", x)) return false;
1948
+ const visit = (box: any): boolean => {
1949
+ if (!box?.rect || !box?.clip || x < box.clip.x || y < box.clip.y ||
1950
+ x >= box.clip.x + box.clip.width || y >= box.clip.y + box.clip.height) return false;
1951
+ if ((box.children ?? []).some(visit)) return true;
1952
+ const hitTest = mouseClickHitTests.get(box.component);
1953
+ return Boolean(hitTest?.({
1954
+ x: x - box.rect.x, y: y - box.rect.y,
1955
+ width: box.rect.width, height: box.rect.height,
1956
+ }));
1957
+ };
1958
+ return visit(screen.currentLayout?.root) || summaryAtPoint(screen, x, y);
1959
+ };
1960
+ const withinTolerance = (press: any, event: any): boolean =>
1961
+ event.y === press.y && Math.abs(event.x - press.x) <= 2;
1962
+ const isUnmodifiedLeftPress = (event: any): boolean =>
1963
+ !event.release && (event.button & (32 | 64 | 4 | 8 | 16)) === 0 && (event.button & 3) === 0;
1964
+ // Copy controls can consume press, bypassing the selection handler entirely.
1965
+ const patchedHandleMouseEvent = function (this: any, event: any) {
1966
+ if (isUnmodifiedLeftPress(event)) {
1967
+ if (interactiveAtPoint(this, event.x, event.y)) this[cleanControlPressKey] = { x: event.x, y: event.y };
1968
+ else delete this[cleanControlPressKey];
1969
+ } else {
1970
+ const press = this[cleanControlPressKey];
1971
+ if (press && this.mousePressTarget && !this.mouseCapture) {
1972
+ if ((event.button & 32) !== 0 && withinTolerance(press, event)) return;
1973
+ if (event.release) {
1974
+ delete this[cleanControlPressKey];
1975
+ if (withinTolerance(press, event)) {
1976
+ return originalHandleMouseEvent.call(this, { ...event, x: press.x });
1977
+ }
1978
+ } else delete this[cleanControlPressKey];
1979
+ }
1980
+ if (event.release || (event.button & 64) !== 0) delete this[cleanControlPressKey];
1981
+ }
1982
+ return originalHandleMouseEvent.call(this, event);
1676
1983
  };
1677
1984
  const patchedHandleSelectionMouseEvent = function (this: any, event: any) {
1678
1985
  const isMotion = (event.button & 32) !== 0;
1679
- const isLeftPress = !event.release && !isMotion && (event.button & 3) === 0;
1986
+ const isLeftPress = isUnmodifiedLeftPress(event);
1680
1987
  if (isLeftPress) {
1681
- if (summaryAtPoint(this, event.x, event.y)) {
1988
+ if (interactiveAtPoint(this, event.x, event.y)) {
1682
1989
  this[cleanSummaryPressKey] = { x: event.x, y: event.y };
1683
1990
  } else {
1684
1991
  delete this[cleanSummaryPressKey];
@@ -1688,8 +1995,7 @@ export default function prettyTui(pi: ExtensionAPI) {
1688
1995
 
1689
1996
  const press = this[cleanSummaryPressKey];
1690
1997
  if (press) {
1691
- const withinClickTolerance =
1692
- event.y === press.y && Math.abs(event.x - press.x) <= 2;
1998
+ const withinClickTolerance = withinTolerance(press, event);
1693
1999
  if (isMotion && withinClickTolerance) {
1694
2000
  return;
1695
2001
  }
@@ -1708,17 +2014,24 @@ export default function prettyTui(pi: ExtensionAPI) {
1708
2014
  altScreenPrototype[cleanSummaryClickPatchKey] = {
1709
2015
  originalHandleSelectionMouseEvent,
1710
2016
  patchedHandleSelectionMouseEvent,
2017
+ originalHandleMouseEvent,
2018
+ patchedHandleMouseEvent,
1711
2019
  };
1712
2020
  altScreenPrototype.handleSelectionMouseEvent = patchedHandleSelectionMouseEvent;
2021
+ altScreenPrototype.handleMouseEvent = patchedHandleMouseEvent;
1713
2022
 
1714
2023
  pi.on("session_shutdown", () => {
1715
2024
  const patch = altScreenPrototype[cleanSummaryClickPatchKey];
2025
+ if (patch?.patchedHandleMouseEvent === altScreenPrototype.handleMouseEvent) {
2026
+ altScreenPrototype.handleMouseEvent = patch.originalHandleMouseEvent;
2027
+ }
1716
2028
  if (patch?.patchedHandleSelectionMouseEvent === altScreenPrototype.handleSelectionMouseEvent) {
1717
2029
  altScreenPrototype.handleSelectionMouseEvent = patch.originalHandleSelectionMouseEvent;
1718
2030
  }
1719
2031
  if (
1720
2032
  patch &&
1721
- altScreenPrototype.handleSelectionMouseEvent === patch.originalHandleSelectionMouseEvent
2033
+ altScreenPrototype.handleSelectionMouseEvent === patch.originalHandleSelectionMouseEvent &&
2034
+ altScreenPrototype.handleMouseEvent === patch.originalHandleMouseEvent
1722
2035
  ) {
1723
2036
  delete altScreenPrototype[cleanSummaryClickPatchKey];
1724
2037
  }
@@ -1747,6 +2060,10 @@ export default function prettyTui(pi: ExtensionAPI) {
1747
2060
 
1748
2061
  const patchedRender = function (this: any, width: number): string[] {
1749
2062
  const scoped = transcriptMarkdownActive();
2063
+ mouseClickHitTests.set(this, (event) => fullscreenTui &&
2064
+ this[transcriptMarkdownStateKey] === true &&
2065
+ (this[codeBlockRegionsKey] ?? []).some((region: any) =>
2066
+ event.y === region.y && event.x >= region.xStart && event.x < region.xEnd));
1750
2067
  if (this[transcriptMarkdownStateKey] !== scoped) {
1751
2068
  delete this[codeBlockCollectionKey];
1752
2069
  delete this[codeBlockRegionsKey];
@@ -2107,31 +2424,17 @@ export default function prettyTui(pi: ExtensionAPI) {
2107
2424
  context.state.compactToolStatus = status;
2108
2425
  };
2109
2426
 
2110
- const foregroundLuminance = (styled: string): number | undefined => {
2111
- const trueColor = /\x1b\[38;2;(\d+);(\d+);(\d+)m/.exec(styled);
2112
- if (!trueColor) return undefined;
2113
- const [, red, green, blue] = trueColor.map(Number);
2114
- return red * 0.2126 + green * 0.7152 + blue * 0.0722;
2115
- };
2116
-
2117
- const successMarkerColor = (theme: any): string => {
2118
- const textLuminance = foregroundLuminance(theme.fg("text", "M"));
2119
- return textLuminance !== undefined && textLuminance < 128
2120
- ? "success"
2121
- : "syntaxComment";
2122
- };
2123
-
2124
2427
  const callRow = (theme: any, name: string, detail: string, state: any): DisplayRow => ({
2125
2428
  prefix: () => {
2126
2429
  const status = (state.compactToolStatus ?? "running") as ToolStatus;
2127
- const color = status === "success" ? successMarkerColor(theme) : status === "error" ? "error" : "dim";
2430
+ const color = status === "success" ? "success" : status === "error" ? "error" : "dim";
2128
2431
  return theme.fg(color, "● ");
2129
2432
  },
2130
2433
  continuation: " ",
2131
2434
  content:
2132
- theme.fg("accent", theme.bold(name)) +
2435
+ theme.fg("text", theme.bold(name)) +
2133
2436
  theme.fg("dim", "(") +
2134
- theme.fg("text", detail) +
2437
+ theme.fg("muted", detail) +
2135
2438
  theme.fg("dim", ")"),
2136
2439
  });
2137
2440
 
@@ -2396,10 +2699,10 @@ export default function prettyTui(pi: ExtensionAPI) {
2396
2699
  const dotColor = component.result?.isError
2397
2700
  ? "error"
2398
2701
  : component.result && !component.isPartial
2399
- ? successMarkerColor(theme)
2400
- : "accent";
2702
+ ? "success"
2703
+ : "dim";
2401
2704
  const title = theme.fg(dotColor, "● ") +
2402
- theme.fg("accent", theme.bold(label)) +
2705
+ theme.fg("text", theme.bold(label)) +
2403
2706
  (args ? theme.fg("muted", `(${args})`) : "");
2404
2707
  const result = thirdPartyResultSummary(component);
2405
2708
  const resultColor = component.result?.isError ? "error" : "muted";
@@ -2568,10 +2871,27 @@ export default function prettyTui(pi: ExtensionAPI) {
2568
2871
  const group = activityTimeline.groupForMember(member.id);
2569
2872
  if (renderMode !== "clean" || !group) return originalToolRender.call(this, width);
2570
2873
  const position = activityMemberPosition(group, member);
2874
+ let renderedHeight = 0;
2875
+ let nestedClickActions: Map<number, string> | undefined;
2876
+ const renderedSummaryHeight = position.first ? renderActivityGroupSummary(group, width).length : 0;
2877
+ mouseClickHitTests.set(this, (event) => {
2878
+ if (renderMode !== "clean") return false;
2879
+ if (event.width !== width) return false;
2880
+ const summaryHeight = renderedSummaryHeight;
2881
+ if (position.first && event.y > 0 && event.y <= summaryHeight) return true;
2882
+ if (!activityGroupRevealed(group)) return false;
2883
+ const height = renderedHeight;
2884
+ if (position.last && event.y === height - 1) {
2885
+ return activityCollapseClicked(group, member, { ...event, type: "click", button: "left" }, height - 1);
2886
+ }
2887
+ const contentY = event.y - (position.first ? summaryHeight + 1 : 0);
2888
+ if (nestedClickActions) return nestedClickActions.has(contentY);
2889
+ return contentY >= 0 && event.y < height - (position.last ? 1 : 0);
2890
+ });
2571
2891
  if (!activityGroupRevealed(group)) {
2572
- return position.first
2573
- ? ["", ...renderActivityGroupSummary(group, width, true)]
2574
- : [];
2892
+ const lines = position.first ? ["", ...renderActivityGroupSummary(group, width, true)] : [];
2893
+ renderedHeight = lines.length;
2894
+ return lines;
2575
2895
  }
2576
2896
 
2577
2897
  const childTheme = cleanThemeForToolCall(this.toolCallId) ?? activityGroupTheme(group);
@@ -2585,7 +2905,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2585
2905
  // not one of our self-rendering specialized tools.
2586
2906
  const thirdParty = !SPECIALIZED_TOOL_NAMES.has(this.toolName) ||
2587
2907
  this.toolDefinition?.renderShell !== "self";
2588
- const cacheable = !this.expanded && Boolean(this.result) && !this.isPartial;
2908
+ const cacheable = nestedTools.count(this.toolCallId) === 0 && !this.expanded && Boolean(this.result) && !this.isPartial;
2589
2909
  const cached = cacheable ? this[cleanChildRenderCacheKey] : undefined;
2590
2910
  let decoratedContent: string[];
2591
2911
  if (
@@ -2602,7 +2922,11 @@ export default function prettyTui(pi: ExtensionAPI) {
2602
2922
  decoratedContent = cached.lines;
2603
2923
  } else {
2604
2924
  let contentLines: string[];
2605
- if (thirdParty) {
2925
+ if (nestedTools.count(this.toolCallId) > 0) {
2926
+ const projection = nestedTools.render(this, childWidth, childTheme);
2927
+ contentLines = projection.lines;
2928
+ nestedClickActions = projection.actions;
2929
+ } else if (thirdParty) {
2606
2930
  const summaryLines = renderThirdPartyCompact(this, childWidth, childTheme);
2607
2931
  if (this.expanded) {
2608
2932
  const detailPrefix = " │ ";
@@ -2650,10 +2974,12 @@ export default function prettyTui(pi: ExtensionAPI) {
2650
2974
  };
2651
2975
  }
2652
2976
  }
2653
- return withActivityCollapse(
2977
+ const lines = withActivityCollapse(
2654
2978
  group, member, width,
2655
2979
  position.first ? ["", ...renderActivityGroupSummary(group, width), ...decoratedContent] : decoratedContent,
2656
2980
  );
2981
+ renderedHeight = lines.length;
2982
+ return lines;
2657
2983
  };
2658
2984
  const patchedToolHandleMouse = function (this: any, event: any) {
2659
2985
  const member = activityTimeline.memberForTool(this.toolCallId);
@@ -2686,6 +3012,22 @@ export default function prettyTui(pi: ExtensionAPI) {
2686
3012
  return { handled: true };
2687
3013
  }
2688
3014
 
3015
+ if (nestedTools.count(this.toolCallId) > 0 && isLeftClick) {
3016
+ const theme = cleanThemeForToolCall(this.toolCallId) ?? activityGroupTheme(group);
3017
+ const { childWidth } = activityTreeStyle(group, member, event.width, theme);
3018
+ const projection = nestedTools.render(this, childWidth, theme);
3019
+ const contentY = event.y - (position.first ? summaryHeight + 1 : 0);
3020
+ const action = projection.actions.get(contentY);
3021
+ if (action) {
3022
+ nestedTools.toggle(action, Boolean(this.expanded));
3023
+ this.updateDisplay?.();
3024
+ this.ui?.requestRender?.();
3025
+ }
3026
+ // The parent title is informational; only disclosure rows act on clicks.
3027
+ // Nested results are UI projections, not native renderer rows.
3028
+ return { handled: true };
3029
+ }
3030
+
2689
3031
  // Self-rendering tools reserve y=0 for Pi's leading spacer. Clean mode
2690
3032
  // replaces that spacer with the compact header for non-first members.
2691
3033
  const thirdPartyHeaderY = position.first ? summaryHeight + 1 : 0;
@@ -2815,6 +3157,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2815
3157
  _failed: number,
2816
3158
  thoughtCount: number,
2817
3159
  activity = "done",
3160
+ nestedCount = 0,
2818
3161
  ): string => {
2819
3162
  const rawActivityText = typeof activity === "string" ? activity : "done";
2820
3163
  const normalizedActivityText = rawActivityText.trim();
@@ -2830,7 +3173,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2830
3173
  const activityLabel = count > 0 && activityText !== "done" && activityText.trim()
2831
3174
  ? activityText
2832
3175
  : "";
2833
- return [countLabel, thoughtLabel, activityLabel].filter(Boolean).join(" · ");
3176
+ return [countLabel, nestedCount ? `${nestedCount} nested ${nestedCount === 1 ? "call" : "calls"}` : "", thoughtLabel, activityLabel].filter(Boolean).join(" · ");
2834
3177
  };
2835
3178
 
2836
3179
  const summaryRow = (
@@ -2840,12 +3183,15 @@ export default function prettyTui(pi: ExtensionAPI) {
2840
3183
  thoughtCount: number,
2841
3184
  activity: DisplayValue = "done",
2842
3185
  collapsedDone = false,
3186
+ nestedCount = 0,
2843
3187
  ): DisplayRow => ({
2844
3188
  prefix: () => {
2845
3189
  const currentActivity = typeof activity === "function" ? activity() : activity;
3190
+ const dimPhase = responseRunStartedAt !== undefined &&
3191
+ Math.floor(Math.max(0, Date.now() - responseRunStartedAt) / RUNNING_DOT_INTERVAL_MS) % 2 === 1;
2846
3192
  const color = currentActivity === "done"
2847
3193
  ? collapsedDone ? "thinkingText" : "success"
2848
- : "accent";
3194
+ : dimPhase ? "dim" : "accent";
2849
3195
  return theme.fg(color, "● ");
2850
3196
  },
2851
3197
  continuation: " ",
@@ -2858,7 +3204,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2858
3204
  const detailColor = label === "Done" && collapsedDone ? "thinkingText" : "text";
2859
3205
  return theme.fg(color, theme.bold(label)) +
2860
3206
  theme.fg("dim", "(") +
2861
- theme.fg(detailColor, summaryText(count, failed, thoughtCount, currentActivity)) +
3207
+ theme.fg(detailColor, summaryText(count, failed, thoughtCount, currentActivity, nestedCount)) +
2862
3208
  theme.fg("dim", ")");
2863
3209
  },
2864
3210
  });
@@ -2908,6 +3254,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2908
3254
  const owner = activityGroupOwner(group);
2909
3255
  const settled = owner ? settledSummaries.get(owner) : undefined;
2910
3256
  const toolCount = group.toolCallIds.length;
3257
+ const nestedCount = group.toolCallIds.reduce((count, id) => count + nestedTools.count(id), 0);
2911
3258
  const live = () => cleanRun.activeToolName ?? currentCleanActivity();
2912
3259
  if (settled) {
2913
3260
  return fit(block([summaryRow(
@@ -2917,6 +3264,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2917
3264
  Math.max(settled.thoughtCount, group.thoughtCount),
2918
3265
  settled.activity,
2919
3266
  collapsedDone,
3267
+ nestedCount,
2920
3268
  )]).render(width));
2921
3269
  }
2922
3270
  if (owner && !cleanRun.settled) {
@@ -2928,6 +3276,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2928
3276
  group.thoughtCount,
2929
3277
  live,
2930
3278
  collapsedDone,
3279
+ nestedCount,
2931
3280
  )]).render(width));
2932
3281
  }
2933
3282
  if (!owner && !activityTimeline.isSettled(group.id)) {
@@ -2939,6 +3288,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2939
3288
  group.thoughtCount,
2940
3289
  "thinking",
2941
3290
  collapsedDone,
3291
+ nestedCount,
2942
3292
  )]).render(width));
2943
3293
  }
2944
3294
  // Finished group with no durable summary: a completed Thought-only turn, or
@@ -2950,6 +3300,7 @@ export default function prettyTui(pi: ExtensionAPI) {
2950
3300
  group.thoughtCount,
2951
3301
  "done",
2952
3302
  collapsedDone,
3303
+ nestedCount,
2953
3304
  )]).render(width));
2954
3305
  };
2955
3306
 
@@ -3042,6 +3393,8 @@ export default function prettyTui(pi: ExtensionAPI) {
3042
3393
  return {
3043
3394
  render(width: number): string[] {
3044
3395
  const data = entry.data;
3396
+ mouseClickHitTests.set(this, (event) => fullscreenTui && copyStart >= 0 &&
3397
+ event.y === 0 && event.x >= copyStart && event.x < copyStart + copyWidth);
3045
3398
  if (!data || width <= 0) return [];
3046
3399
  copyStart = -1;
3047
3400
  copyWidth = 0;
@@ -3148,6 +3501,7 @@ export default function prettyTui(pi: ExtensionAPI) {
3148
3501
  settledSummaries.clear();
3149
3502
  legacySummaryLastToolCallIds.clear();
3150
3503
  activityTimeline.clear();
3504
+ nestedTools.clear();
3151
3505
  responseAnswerTexts.clear();
3152
3506
  responseRunStartedAt = undefined;
3153
3507
  responseHasAssistantMessage = false;
@@ -3307,6 +3661,10 @@ export default function prettyTui(pi: ExtensionAPI) {
3307
3661
  cleanContextCompacted = modelContextEntries[0]?.type === "compaction";
3308
3662
  const contextEntries = orderContextEntriesForTranscript(modelContextEntries);
3309
3663
  for (const entry of contextEntries) {
3664
+ if (entry.type === "custom" && entry.customType === NESTED_SNAPSHOT_ENTRY) {
3665
+ nestedTools.restore(entry.data);
3666
+ continue;
3667
+ }
3310
3668
  if (entry.type === "compaction") {
3311
3669
  finishGroup();
3312
3670
  lastFinishedGroup = undefined;
@@ -3391,6 +3749,10 @@ export default function prettyTui(pi: ExtensionAPI) {
3391
3749
  continue;
3392
3750
  }
3393
3751
 
3752
+ if (message.role === "toolResult") {
3753
+ nestedTools.absorb(message.toolCallId, message.nestedCalls ?? message.details?.calls, !message.details?.callsTruncated);
3754
+ }
3755
+
3394
3756
  if (
3395
3757
  message.role === "toolResult" &&
3396
3758
  toolCalls.has(message.toolCallId) &&
@@ -3473,6 +3835,7 @@ export default function prettyTui(pi: ExtensionAPI) {
3473
3835
  if (toolCalls.length === 0) return false;
3474
3836
  for (const toolCall of toolCalls) {
3475
3837
  activityTimeline.addTool(toolCall.id, toolCall.name ?? "tool");
3838
+ flushDeferredCacheNotices?.(toolCall.id);
3476
3839
  if (!cleanRun.currentToolCallIds.includes(toolCall.id)) {
3477
3840
  cleanRun.currentToolCallIds.push(toolCall.id);
3478
3841
  }
@@ -3569,7 +3932,13 @@ export default function prettyTui(pi: ExtensionAPI) {
3569
3932
  if (renderMode === "clean") startWorkingElapsedTimer();
3570
3933
  });
3571
3934
  pi.on("tool_execution_start", (event) => {
3935
+ if ((event as any).parentToolCallId) {
3936
+ nestedTools.start(event);
3937
+ refreshNestedTools(event.toolCallId);
3938
+ return;
3939
+ }
3572
3940
  activityTimeline.addTool(event.toolCallId, event.toolName);
3941
+ flushDeferredCacheNotices?.(event.toolCallId);
3573
3942
  if (!cleanRun.currentToolCallIds.includes(event.toolCallId)) {
3574
3943
  cleanRun.currentToolCallIds.push(event.toolCallId);
3575
3944
  }
@@ -3581,7 +3950,25 @@ export default function prettyTui(pi: ExtensionAPI) {
3581
3950
  cleanRun.activeToolCallId = event.toolCallId;
3582
3951
  beginToolActivity(event.toolCallId, displayName, true);
3583
3952
  });
3953
+ pi.on("tool_execution_update", (event) => {
3954
+ if ((event as any).parentToolCallId) {
3955
+ nestedTools.update(event);
3956
+ refreshNestedTools(event.toolCallId);
3957
+ } else {
3958
+ nestedTools.absorb(event.toolCallId, (event.partialResult as any)?.details?.calls, !(event.partialResult as any)?.details?.callsTruncated);
3959
+ refreshNestedTools(event.toolCallId);
3960
+ }
3961
+ });
3584
3962
  pi.on("tool_execution_end", (event) => {
3963
+ if ((event as any).parentToolCallId) {
3964
+ nestedTools.update(event, true);
3965
+ refreshNestedTools(event.toolCallId);
3966
+ return;
3967
+ }
3968
+ nestedTools.absorb(event.toolCallId, (event.result as any)?.details?.calls, !(event.result as any)?.details?.callsTruncated);
3969
+ nestedTools.finish(event.toolCallId);
3970
+ const snapshot = nestedTools.snapshot(event.toolCallId);
3971
+ if (snapshot) pi.appendEntry(NESTED_SNAPSHOT_ENTRY, snapshot);
3585
3972
  const activityName = cleanRun.activeToolCallId === event.toolCallId
3586
3973
  ? cleanRun.activeToolName ?? cleanToolNames.get(event.toolCallId) ?? event.toolName
3587
3974
  : cleanToolNames.get(event.toolCallId) ?? event.toolName;
@@ -3618,6 +4005,11 @@ export default function prettyTui(pi: ExtensionAPI) {
3618
4005
  pi.on("agent_settled", (_event, ctx) => {
3619
4006
  stopWorkingElapsedTimer();
3620
4007
  if (cleanRun.settled) return;
4008
+ for (const rootId of nestedTools.finishAll()) {
4009
+ const snapshot = nestedTools.snapshot(rootId);
4010
+ if (snapshot) pi.appendEntry(NESTED_SNAPSHOT_ENTRY, snapshot);
4011
+ refreshNestedTools(rootId);
4012
+ }
3621
4013
 
3622
4014
  clearPendingToolActivities();
3623
4015
  // Finalize the last group after retries/compaction have definitely ended.