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.
- package/README.md +26 -6
- package/README.zh-CN.md +26 -6
- package/extensions/activity-timeline.ts +56 -0
- package/extensions/index.ts +468 -76
- package/extensions/nested-tools.ts +251 -0
- package/package.json +3 -3
package/extensions/index.ts
CHANGED
|
@@ -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
|
-
|
|
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("
|
|
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
|
|
820
|
-
//
|
|
821
|
-
//
|
|
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("
|
|
878
|
-
: markdownTheme.
|
|
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
|
-
|
|
900
|
-
|
|
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 -
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
|
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("
|
|
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
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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("
|
|
1272
|
-
groupTheme.fg("
|
|
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
|
-
//
|
|
1632
|
+
// Keyboard visibility toggles only explain these local controls.
|
|
1563
1633
|
this.showStatus(
|
|
1564
|
-
"Thought details expand individually in clean mode\nClick a thought
|
|
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
|
-
//
|
|
1665
|
-
//
|
|
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
|
-
|
|
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 =
|
|
1986
|
+
const isLeftPress = isUnmodifiedLeftPress(event);
|
|
1680
1987
|
if (isLeftPress) {
|
|
1681
|
-
if (
|
|
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" ?
|
|
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("
|
|
2435
|
+
theme.fg("text", theme.bold(name)) +
|
|
2133
2436
|
theme.fg("dim", "(") +
|
|
2134
|
-
theme.fg("
|
|
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
|
-
?
|
|
2400
|
-
: "
|
|
2702
|
+
? "success"
|
|
2703
|
+
: "dim";
|
|
2401
2704
|
const title = theme.fg(dotColor, "● ") +
|
|
2402
|
-
theme.fg("
|
|
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
|
-
|
|
2573
|
-
|
|
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 (
|
|
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
|
-
|
|
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.
|