@caupulican/pi-adaptative 0.80.34 → 0.80.37
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/dist/core/extensions/loader.d.ts +18 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +130 -17
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +6 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/session-manager.d.ts +2 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +6 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +3 -6
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +45 -21
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +24 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +377 -88
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/extensions.md +24 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +5 -5
|
@@ -68,6 +68,12 @@ import { TrustSelectorComponent } from "./components/trust-selector.js";
|
|
|
68
68
|
import { UserMessageComponent } from "./components/user-message.js";
|
|
69
69
|
import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
|
|
70
70
|
import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
|
|
71
|
+
const TUI_HISTORY_RELOAD_MAX_LINES = 1000;
|
|
72
|
+
const TUI_HISTORY_RELOAD_WRAP_WIDTH = 100;
|
|
73
|
+
const TUI_HISTORY_RELOAD_CHUNK_SIZE = 20;
|
|
74
|
+
const TUI_LIVE_HISTORY_MAX_COMPONENTS = 260;
|
|
75
|
+
const TUI_LIVE_HISTORY_TRIM_TO_COMPONENTS = 220;
|
|
76
|
+
const STREAMING_UI_UPDATE_INTERVAL_MS = 80;
|
|
71
77
|
function isExpandable(obj) {
|
|
72
78
|
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
|
73
79
|
}
|
|
@@ -408,9 +414,16 @@ export class InteractiveMode {
|
|
|
408
414
|
// Status line tracking (for mutating immediately-sequential status updates)
|
|
409
415
|
lastStatusSpacer = undefined;
|
|
410
416
|
lastStatusText = undefined;
|
|
417
|
+
// Live TUI history cap. Full session history remains in SessionManager/model state.
|
|
418
|
+
liveHistoryHiddenNotice = undefined;
|
|
419
|
+
liveHistoryHiddenComponents = 0;
|
|
420
|
+
tuiHistoryLoaded = false;
|
|
421
|
+
tuiHistoryLoadInProgress = false;
|
|
411
422
|
// Streaming message tracking
|
|
412
423
|
streamingComponent = undefined;
|
|
413
424
|
streamingMessage = undefined;
|
|
425
|
+
streamingUiUpdateTimer = undefined;
|
|
426
|
+
lastStreamingUiUpdateAt = 0;
|
|
414
427
|
// Tool execution tracking and session-scoped reusable panels
|
|
415
428
|
toolPanels = new ToolPanelRegistry();
|
|
416
429
|
// Tool output expansion state
|
|
@@ -688,7 +701,7 @@ export class InteractiveMode {
|
|
|
688
701
|
hint("app.thinking.cycle", "to cycle thinking level"),
|
|
689
702
|
rawKeyHint(`${keyText("app.model.cycleForward")}/${keyText("app.model.cycleBackward")}`, "to cycle models"),
|
|
690
703
|
hint("app.model.select", "to select model"),
|
|
691
|
-
hint("app.tools.expand", "to expand tools"),
|
|
704
|
+
hint("app.tools.expand", "to load history / expand tools"),
|
|
692
705
|
hint("app.thinking.toggle", "to expand thinking"),
|
|
693
706
|
hint("app.editor.external", "for external editor"),
|
|
694
707
|
rawKeyHint("/", "for commands"),
|
|
@@ -706,9 +719,9 @@ export class InteractiveMode {
|
|
|
706
719
|
rawKeyHint(`${keyText("app.clear")}/${keyText("app.exit")}`, "clear/exit"),
|
|
707
720
|
rawKeyHint("/", "commands"),
|
|
708
721
|
rawKeyHint("!", "bash"),
|
|
709
|
-
hint("app.tools.expand", "more"),
|
|
722
|
+
hint("app.tools.expand", "history/more"),
|
|
710
723
|
].join(theme.fg("muted", " · "));
|
|
711
|
-
const compactOnboarding = theme.fg("dim", `Press ${keyText("app.tools.expand")} to show full startup help and loaded resources.`);
|
|
724
|
+
const compactOnboarding = theme.fg("dim", `Press ${keyText("app.tools.expand")} to load session history or show full startup help and loaded resources.`);
|
|
712
725
|
const onboarding = theme.fg("dim", `Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.`);
|
|
713
726
|
this.builtInHeader = new ExpandableText(() => `${logo}\n${compactInstructions}\n${compactOnboarding}\n\n${onboarding}`, () => `${logo}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
|
|
714
727
|
// Setup UI layout
|
|
@@ -1435,7 +1448,6 @@ export class InteractiveMode {
|
|
|
1435
1448
|
if (result.cancelled) {
|
|
1436
1449
|
return { cancelled: true };
|
|
1437
1450
|
}
|
|
1438
|
-
this.chatContainer.clear();
|
|
1439
1451
|
await this.renderInitialMessages();
|
|
1440
1452
|
if (result.editorText && !this.editor.getText().trim()) {
|
|
1441
1453
|
this.editor.setText(result.editorText);
|
|
@@ -1503,7 +1515,6 @@ export class InteractiveMode {
|
|
|
1503
1515
|
process.exit(1);
|
|
1504
1516
|
}
|
|
1505
1517
|
renderCurrentSessionState() {
|
|
1506
|
-
this.chatContainer.clear();
|
|
1507
1518
|
this.pendingMessagesContainer.clear();
|
|
1508
1519
|
this.compactionQueuedMessages = [];
|
|
1509
1520
|
this.streamingComponent = undefined;
|
|
@@ -1528,21 +1539,25 @@ export class InteractiveMode {
|
|
|
1528
1539
|
const toolGroup = allowGrouping ? component.toolGroup?.trim() : undefined;
|
|
1529
1540
|
if (!toolGroup) {
|
|
1530
1541
|
this.chatContainer.addChild(component);
|
|
1542
|
+
this.trimLiveTuiHistory();
|
|
1531
1543
|
return;
|
|
1532
1544
|
}
|
|
1533
1545
|
const children = this.chatContainer.children;
|
|
1534
1546
|
const lastChild = children[children.length - 1];
|
|
1535
1547
|
if (lastChild instanceof ToolGroupComponent && lastChild.toolGroup === toolGroup) {
|
|
1536
1548
|
lastChild.addTool(component);
|
|
1549
|
+
this.trimLiveTuiHistory();
|
|
1537
1550
|
return;
|
|
1538
1551
|
}
|
|
1539
1552
|
if (lastChild instanceof ToolExecutionComponent && lastChild.toolGroup?.trim() === toolGroup) {
|
|
1540
1553
|
const group = new ToolGroupComponent(toolGroup, [lastChild, component]);
|
|
1541
1554
|
group.setExpanded(this.toolOutputExpanded);
|
|
1542
1555
|
children[children.length - 1] = group;
|
|
1556
|
+
this.trimLiveTuiHistory();
|
|
1543
1557
|
return;
|
|
1544
1558
|
}
|
|
1545
1559
|
this.chatContainer.addChild(component);
|
|
1560
|
+
this.trimLiveTuiHistory();
|
|
1546
1561
|
}
|
|
1547
1562
|
detachToolExecutionComponent(component) {
|
|
1548
1563
|
const children = this.chatContainer.children;
|
|
@@ -2287,7 +2302,7 @@ export class InteractiveMode {
|
|
|
2287
2302
|
// Global debug handler on TUI (works regardless of focus)
|
|
2288
2303
|
this.ui.onDebug = () => this.handleDebugCommand();
|
|
2289
2304
|
this.defaultEditor.onAction("app.model.select", () => void this.showModelSelector());
|
|
2290
|
-
this.defaultEditor.onAction("app.tools.expand", () => this.
|
|
2305
|
+
this.defaultEditor.onAction("app.tools.expand", () => this.loadTuiHistoryOnDemand());
|
|
2291
2306
|
this.defaultEditor.onAction("app.thinking.toggle", () => void this.toggleThinkingBlockVisibility());
|
|
2292
2307
|
this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
|
|
2293
2308
|
this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
|
|
@@ -2624,31 +2639,18 @@ export class InteractiveMode {
|
|
|
2624
2639
|
this.ui.requestRender();
|
|
2625
2640
|
}
|
|
2626
2641
|
else if (event.message.role === "assistant") {
|
|
2642
|
+
this.clearPendingStreamingUiUpdate();
|
|
2643
|
+
this.lastStreamingUiUpdateAt = 0;
|
|
2627
2644
|
this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
|
|
2628
2645
|
this.streamingMessage = event.message;
|
|
2629
2646
|
this.chatContainer.addChild(this.streamingComponent);
|
|
2630
|
-
this.
|
|
2631
|
-
this.
|
|
2647
|
+
this.applyStreamingMessageUpdate(this.streamingMessage, { force: true });
|
|
2648
|
+
this.trimLiveTuiHistory();
|
|
2632
2649
|
}
|
|
2633
2650
|
break;
|
|
2634
2651
|
case "message_update":
|
|
2635
2652
|
if (this.streamingComponent && event.message.role === "assistant") {
|
|
2636
|
-
this.
|
|
2637
|
-
this.streamingComponent.updateContent(this.streamingMessage);
|
|
2638
|
-
for (const content of this.streamingMessage.content) {
|
|
2639
|
-
if (content.type === "toolCall") {
|
|
2640
|
-
if (!this.toolPanels.hasActive(content.id)) {
|
|
2641
|
-
this.attachToolExecutionComponent(content.name, content.id, content.arguments);
|
|
2642
|
-
}
|
|
2643
|
-
else {
|
|
2644
|
-
const component = this.toolPanels.getActive(content.id);
|
|
2645
|
-
if (component) {
|
|
2646
|
-
component.updateArgs(content.arguments);
|
|
2647
|
-
}
|
|
2648
|
-
}
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
this.ui.requestRender();
|
|
2653
|
+
this.applyStreamingMessageUpdate(event.message);
|
|
2652
2654
|
}
|
|
2653
2655
|
break;
|
|
2654
2656
|
case "message_end":
|
|
@@ -2665,7 +2667,7 @@ export class InteractiveMode {
|
|
|
2665
2667
|
: "Operation aborted";
|
|
2666
2668
|
this.streamingMessage.errorMessage = errorMessage;
|
|
2667
2669
|
}
|
|
2668
|
-
this.
|
|
2670
|
+
this.applyStreamingMessageUpdate(this.streamingMessage, { force: true });
|
|
2669
2671
|
if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
|
|
2670
2672
|
if (!errorMessage) {
|
|
2671
2673
|
errorMessage = this.streamingMessage.errorMessage || "Error";
|
|
@@ -2778,7 +2780,6 @@ export class InteractiveMode {
|
|
|
2778
2780
|
}
|
|
2779
2781
|
}
|
|
2780
2782
|
else if (event.result) {
|
|
2781
|
-
this.chatContainer.clear();
|
|
2782
2783
|
await this.rebuildChatFromMessages();
|
|
2783
2784
|
this.addMessageToChat(createCompactionSummaryMessage(event.result.summary, event.result.tokensBefore, new Date().toISOString()));
|
|
2784
2785
|
this.footer.invalidate();
|
|
@@ -2850,19 +2851,139 @@ export class InteractiveMode {
|
|
|
2850
2851
|
: message.content.filter((c) => c.type === "text");
|
|
2851
2852
|
return textBlocks.map((c) => c.text).join("");
|
|
2852
2853
|
}
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2854
|
+
resetLiveTuiHistoryTrim() {
|
|
2855
|
+
this.liveHistoryHiddenNotice = undefined;
|
|
2856
|
+
this.liveHistoryHiddenComponents = 0;
|
|
2857
|
+
}
|
|
2858
|
+
clearPendingStreamingUiUpdate() {
|
|
2859
|
+
if (!this.streamingUiUpdateTimer)
|
|
2860
|
+
return;
|
|
2861
|
+
clearTimeout(this.streamingUiUpdateTimer);
|
|
2862
|
+
this.streamingUiUpdateTimer = undefined;
|
|
2863
|
+
}
|
|
2864
|
+
getSessionEntryCount() {
|
|
2865
|
+
const manager = this.sessionManager;
|
|
2866
|
+
return manager.getEntryCount?.() ?? manager.getEntries().length;
|
|
2867
|
+
}
|
|
2868
|
+
showDeferredHistoryPlaceholder(options = {}) {
|
|
2869
|
+
this.chatContainer.children = [];
|
|
2870
|
+
this.resetLiveTuiHistoryTrim();
|
|
2871
|
+
this.clearRenderedToolPanelState();
|
|
2872
|
+
const entryCount = this.getSessionEntryCount();
|
|
2873
|
+
if (entryCount > 0) {
|
|
2874
|
+
this.chatContainer.addChild(new Text(theme.fg("dim", `History hidden for typing performance (${entryCount} entries). Press ${keyText("app.tools.expand")} to load session history on demand.`), 1, 0));
|
|
2875
|
+
}
|
|
2876
|
+
if (options.requestRender ?? true)
|
|
2877
|
+
this.ui.requestRender();
|
|
2878
|
+
}
|
|
2879
|
+
loadTuiHistoryOnDemand() {
|
|
2880
|
+
if (this.tuiHistoryLoadInProgress)
|
|
2881
|
+
return;
|
|
2882
|
+
if (this.tuiHistoryLoaded || this.getSessionEntryCount() === 0) {
|
|
2883
|
+
this.toggleToolOutputExpansion();
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
this.tuiHistoryLoadInProgress = true;
|
|
2887
|
+
void (async () => {
|
|
2888
|
+
try {
|
|
2889
|
+
await this.renderInitialMessages({ forceHistoryLoad: true });
|
|
2890
|
+
}
|
|
2891
|
+
catch (error) {
|
|
2892
|
+
this.showError(`Failed to load TUI history: ${error instanceof Error ? error.message : String(error)}`);
|
|
2893
|
+
}
|
|
2894
|
+
finally {
|
|
2895
|
+
this.tuiHistoryLoadInProgress = false;
|
|
2896
|
+
}
|
|
2897
|
+
})();
|
|
2898
|
+
}
|
|
2899
|
+
attachStreamingToolPanels(message) {
|
|
2900
|
+
for (const content of message.content) {
|
|
2901
|
+
if (content.type !== "toolCall")
|
|
2902
|
+
continue;
|
|
2903
|
+
if (!this.toolPanels.hasActive(content.id)) {
|
|
2904
|
+
this.attachToolExecutionComponent(content.name, content.id, content.arguments);
|
|
2905
|
+
}
|
|
2906
|
+
else {
|
|
2907
|
+
const component = this.toolPanels.getActive(content.id);
|
|
2908
|
+
if (component) {
|
|
2909
|
+
component.updateArgs(content.arguments);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
applyStreamingMessageUpdate(message, options = {}) {
|
|
2915
|
+
this.streamingMessage = message;
|
|
2916
|
+
if (!this.streamingComponent)
|
|
2917
|
+
return;
|
|
2918
|
+
const now = performance.now();
|
|
2919
|
+
const elapsed = now - this.lastStreamingUiUpdateAt;
|
|
2920
|
+
const hasToolCall = message.content.some((content) => content.type === "toolCall");
|
|
2921
|
+
const shouldUpdateNow = options.force || hasToolCall || elapsed >= STREAMING_UI_UPDATE_INTERVAL_MS;
|
|
2922
|
+
const update = () => {
|
|
2923
|
+
if (!this.streamingComponent || !this.streamingMessage)
|
|
2924
|
+
return;
|
|
2925
|
+
this.streamingComponent.updateContent(this.streamingMessage);
|
|
2926
|
+
this.attachStreamingToolPanels(this.streamingMessage);
|
|
2927
|
+
this.lastStreamingUiUpdateAt = performance.now();
|
|
2928
|
+
this.ui.requestRender();
|
|
2929
|
+
};
|
|
2930
|
+
if (shouldUpdateNow) {
|
|
2931
|
+
this.clearPendingStreamingUiUpdate();
|
|
2932
|
+
update();
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2935
|
+
if (this.streamingUiUpdateTimer)
|
|
2936
|
+
return;
|
|
2937
|
+
this.streamingUiUpdateTimer = setTimeout(() => {
|
|
2938
|
+
this.streamingUiUpdateTimer = undefined;
|
|
2939
|
+
update();
|
|
2940
|
+
}, Math.max(0, STREAMING_UI_UPDATE_INTERVAL_MS - elapsed));
|
|
2941
|
+
}
|
|
2942
|
+
trimLiveTuiHistory() {
|
|
2943
|
+
const children = this.chatContainer.children;
|
|
2944
|
+
if (children.length <= TUI_LIVE_HISTORY_MAX_COMPONENTS)
|
|
2945
|
+
return;
|
|
2946
|
+
let protectedStart = children.length;
|
|
2947
|
+
const protect = (component) => {
|
|
2948
|
+
if (!component)
|
|
2949
|
+
return;
|
|
2950
|
+
const index = children.indexOf(component);
|
|
2951
|
+
if (index !== -1 && index < protectedStart)
|
|
2952
|
+
protectedStart = index;
|
|
2953
|
+
};
|
|
2954
|
+
protect(this.streamingComponent);
|
|
2955
|
+
protect(this.lastStatusSpacer);
|
|
2956
|
+
protect(this.lastStatusText);
|
|
2957
|
+
for (const [, component] of this.toolPanels.activeEntries()) {
|
|
2958
|
+
protect(component);
|
|
2959
|
+
}
|
|
2960
|
+
const trimStart = children[0] === this.liveHistoryHiddenNotice ? 1 : 0;
|
|
2961
|
+
const targetTrimEnd = children.length - TUI_LIVE_HISTORY_TRIM_TO_COMPONENTS;
|
|
2962
|
+
const trimEnd = Math.min(targetTrimEnd, protectedStart);
|
|
2963
|
+
if (trimEnd <= trimStart)
|
|
2964
|
+
return;
|
|
2965
|
+
const removed = children.splice(trimStart, trimEnd - trimStart);
|
|
2966
|
+
this.liveHistoryHiddenComponents += removed.length;
|
|
2967
|
+
if (removed.includes(this.lastStatusSpacer))
|
|
2968
|
+
this.lastStatusSpacer = undefined;
|
|
2969
|
+
if (removed.includes(this.lastStatusText))
|
|
2970
|
+
this.lastStatusText = undefined;
|
|
2971
|
+
const noticeText = theme.fg("dim", `Older TUI history hidden to preserve FPS (${this.liveHistoryHiddenComponents} components). Full session remains available to the model.`);
|
|
2972
|
+
if (children[0] === this.liveHistoryHiddenNotice) {
|
|
2973
|
+
this.liveHistoryHiddenNotice?.setText(noticeText);
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
this.liveHistoryHiddenNotice = new Text(noticeText, 1, 0);
|
|
2977
|
+
children.unshift(this.liveHistoryHiddenNotice);
|
|
2978
|
+
}
|
|
2979
|
+
appendStatusToChat(message, options = {}) {
|
|
2860
2980
|
const children = this.chatContainer.children;
|
|
2861
2981
|
const last = children.length > 0 ? children[children.length - 1] : undefined;
|
|
2862
2982
|
const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
|
|
2863
2983
|
if (last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {
|
|
2864
2984
|
this.lastStatusText.setText(theme.fg("dim", message));
|
|
2865
|
-
|
|
2985
|
+
if (options.requestRender ?? true)
|
|
2986
|
+
this.ui.requestRender();
|
|
2866
2987
|
return;
|
|
2867
2988
|
}
|
|
2868
2989
|
const spacer = new Spacer(1);
|
|
@@ -2871,7 +2992,18 @@ export class InteractiveMode {
|
|
|
2871
2992
|
this.chatContainer.addChild(text);
|
|
2872
2993
|
this.lastStatusSpacer = spacer;
|
|
2873
2994
|
this.lastStatusText = text;
|
|
2874
|
-
this.
|
|
2995
|
+
this.trimLiveTuiHistory();
|
|
2996
|
+
if (options.requestRender ?? true)
|
|
2997
|
+
this.ui.requestRender();
|
|
2998
|
+
}
|
|
2999
|
+
/**
|
|
3000
|
+
* Show a status message in the chat.
|
|
3001
|
+
*
|
|
3002
|
+
* If multiple status messages are emitted back-to-back (without anything else being added to the chat),
|
|
3003
|
+
* we update the previous status line instead of appending new ones to avoid log spam.
|
|
3004
|
+
*/
|
|
3005
|
+
showStatus(message) {
|
|
3006
|
+
this.appendStatusToChat(message);
|
|
2875
3007
|
}
|
|
2876
3008
|
addMessageToChat(message, options) {
|
|
2877
3009
|
switch (message.role) {
|
|
@@ -2948,6 +3080,117 @@ export class InteractiveMode {
|
|
|
2948
3080
|
const _exhaustive = message;
|
|
2949
3081
|
}
|
|
2950
3082
|
}
|
|
3083
|
+
this.trimLiveTuiHistory();
|
|
3084
|
+
}
|
|
3085
|
+
getContentText(content) {
|
|
3086
|
+
if (typeof content === "string")
|
|
3087
|
+
return content;
|
|
3088
|
+
if (Array.isArray(content)) {
|
|
3089
|
+
return content
|
|
3090
|
+
.map((part) => {
|
|
3091
|
+
const maybeText = part.text;
|
|
3092
|
+
return typeof maybeText === "string" ? maybeText : "";
|
|
3093
|
+
})
|
|
3094
|
+
.join("");
|
|
3095
|
+
}
|
|
3096
|
+
return "";
|
|
3097
|
+
}
|
|
3098
|
+
getTuiHistoryMessageText(message) {
|
|
3099
|
+
switch (message.role) {
|
|
3100
|
+
case "bashExecution":
|
|
3101
|
+
return [message.command, message.output ?? ""].filter(Boolean).join("\n");
|
|
3102
|
+
case "user":
|
|
3103
|
+
return this.getUserMessageText(message);
|
|
3104
|
+
case "assistant":
|
|
3105
|
+
return this.getContentText(message.content);
|
|
3106
|
+
case "toolResult":
|
|
3107
|
+
return this.getContentText(message.content);
|
|
3108
|
+
case "custom":
|
|
3109
|
+
return this.getContentText(message.content);
|
|
3110
|
+
case "compactionSummary":
|
|
3111
|
+
case "branchSummary":
|
|
3112
|
+
return message.summary;
|
|
3113
|
+
default: {
|
|
3114
|
+
const _exhaustive = message;
|
|
3115
|
+
return JSON.stringify(_exhaustive);
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
estimateTuiHistoryLines(message) {
|
|
3120
|
+
const text = this.getTuiHistoryMessageText(message);
|
|
3121
|
+
const hardLines = text.length > 0 ? text.split(/\r\n|\r|\n/).length : 1;
|
|
3122
|
+
const wrappedLines = Math.ceil(text.length / TUI_HISTORY_RELOAD_WRAP_WIDTH);
|
|
3123
|
+
// Add one line for role/tool chrome or spacing. Tool-call-only assistant messages
|
|
3124
|
+
// have little text but still render a component.
|
|
3125
|
+
return Math.max(1, hardLines, wrappedLines) + 1;
|
|
3126
|
+
}
|
|
3127
|
+
trimTextToTuiHistoryTail(text, maxEstimatedLines) {
|
|
3128
|
+
const maxLines = Math.max(1, maxEstimatedLines);
|
|
3129
|
+
const lines = text.split(/\r\n|\r|\n/);
|
|
3130
|
+
if (lines.length > maxLines) {
|
|
3131
|
+
const omitted = lines.length - maxLines;
|
|
3132
|
+
return `[Earlier ${omitted} line${omitted === 1 ? "" : "s"} omitted from TUI reload history; full session remains available to the model.]\n${lines.slice(-maxLines).join("\n")}`;
|
|
3133
|
+
}
|
|
3134
|
+
const maxChars = Math.max(TUI_HISTORY_RELOAD_WRAP_WIDTH, maxLines * TUI_HISTORY_RELOAD_WRAP_WIDTH);
|
|
3135
|
+
if (text.length > maxChars) {
|
|
3136
|
+
const omitted = text.length - maxChars;
|
|
3137
|
+
return `[Earlier ${omitted} character${omitted === 1 ? "" : "s"} omitted from TUI reload history; full session remains available to the model.]\n${text.slice(-maxChars)}`;
|
|
3138
|
+
}
|
|
3139
|
+
return text;
|
|
3140
|
+
}
|
|
3141
|
+
trimMessageToTuiHistoryTail(message, maxEstimatedLines) {
|
|
3142
|
+
const text = this.getTuiHistoryMessageText(message);
|
|
3143
|
+
const trimmedText = this.trimTextToTuiHistoryTail(text, maxEstimatedLines);
|
|
3144
|
+
if (trimmedText === text)
|
|
3145
|
+
return message;
|
|
3146
|
+
const clone = JSON.parse(JSON.stringify(message));
|
|
3147
|
+
const mutable = clone;
|
|
3148
|
+
if (mutable.role === "bashExecution" && typeof mutable.output === "string") {
|
|
3149
|
+
mutable.output = trimmedText;
|
|
3150
|
+
}
|
|
3151
|
+
else if (mutable.role === "compactionSummary" || mutable.role === "branchSummary") {
|
|
3152
|
+
mutable.summary = trimmedText;
|
|
3153
|
+
}
|
|
3154
|
+
else if (typeof mutable.content === "string") {
|
|
3155
|
+
mutable.content = trimmedText;
|
|
3156
|
+
}
|
|
3157
|
+
else {
|
|
3158
|
+
mutable.content = [{ type: "text", text: trimmedText }];
|
|
3159
|
+
}
|
|
3160
|
+
return clone;
|
|
3161
|
+
}
|
|
3162
|
+
messagesForTuiHistoryReload(messages) {
|
|
3163
|
+
let estimatedLines = 0;
|
|
3164
|
+
let start = messages.length;
|
|
3165
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3166
|
+
const nextLines = this.estimateTuiHistoryLines(messages[i]);
|
|
3167
|
+
if (start < messages.length && estimatedLines + nextLines > TUI_HISTORY_RELOAD_MAX_LINES)
|
|
3168
|
+
break;
|
|
3169
|
+
estimatedLines += nextLines;
|
|
3170
|
+
start = i;
|
|
3171
|
+
if (estimatedLines >= TUI_HISTORY_RELOAD_MAX_LINES)
|
|
3172
|
+
break;
|
|
3173
|
+
}
|
|
3174
|
+
const selected = messages.slice(start);
|
|
3175
|
+
if (selected.length > 0 && estimatedLines > TUI_HISTORY_RELOAD_MAX_LINES) {
|
|
3176
|
+
const tailLines = selected.slice(1).reduce((sum, message) => sum + this.estimateTuiHistoryLines(message), 0);
|
|
3177
|
+
const firstAllowance = TUI_HISTORY_RELOAD_MAX_LINES - tailLines;
|
|
3178
|
+
if (firstAllowance <= 4) {
|
|
3179
|
+
selected.shift();
|
|
3180
|
+
start += 1;
|
|
3181
|
+
estimatedLines = tailLines;
|
|
3182
|
+
}
|
|
3183
|
+
else {
|
|
3184
|
+
// Reserve room for truncation marker, role chrome, and wrap variance.
|
|
3185
|
+
selected[0] = this.trimMessageToTuiHistoryTail(selected[0], firstAllowance - 4);
|
|
3186
|
+
estimatedLines = tailLines + this.estimateTuiHistoryLines(selected[0]);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
return {
|
|
3190
|
+
messages: selected,
|
|
3191
|
+
omittedMessages: start,
|
|
3192
|
+
estimatedLines,
|
|
3193
|
+
};
|
|
2951
3194
|
}
|
|
2952
3195
|
/**
|
|
2953
3196
|
* Render session context to chat. Used for initial load and rebuild after compaction.
|
|
@@ -2957,77 +3200,123 @@ export class InteractiveMode {
|
|
|
2957
3200
|
*/
|
|
2958
3201
|
renderGeneration = 0;
|
|
2959
3202
|
async renderSessionContext(sessionContext, options = {}) {
|
|
2960
|
-
//
|
|
2961
|
-
//
|
|
2962
|
-
//
|
|
3203
|
+
// Build long history offscreen, then atomically swap it into the visible
|
|
3204
|
+
// chat container. This keeps the TUI responsive without flashing blank or
|
|
3205
|
+
// partial transcript frames during resume/reload/compaction rebuilds.
|
|
2963
3206
|
const generation = ++this.renderGeneration;
|
|
2964
|
-
const CHUNK_SIZE = 20;
|
|
2965
3207
|
let processed = 0;
|
|
3208
|
+
let committed = false;
|
|
3209
|
+
const visibleChatContainer = this.chatContainer;
|
|
3210
|
+
const previousLiveHistoryHiddenNotice = this.liveHistoryHiddenNotice;
|
|
3211
|
+
const previousLiveHistoryHiddenComponents = this.liveHistoryHiddenComponents;
|
|
3212
|
+
const previousLastStatusSpacer = this.lastStatusSpacer;
|
|
3213
|
+
const previousLastStatusText = this.lastStatusText;
|
|
3214
|
+
const stagingChatContainer = new Container();
|
|
3215
|
+
this.chatContainer = stagingChatContainer;
|
|
3216
|
+
this.resetLiveTuiHistoryTrim();
|
|
2966
3217
|
this.clearRenderedToolPanelState();
|
|
2967
3218
|
const renderedPendingTools = new Map();
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
for (const message of sessionContext.messages) {
|
|
2973
|
-
if (processed > 0 && processed % CHUNK_SIZE === 0) {
|
|
2974
|
-
this.ui.requestRender();
|
|
2975
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
2976
|
-
if (generation !== this.renderGeneration)
|
|
2977
|
-
return;
|
|
3219
|
+
try {
|
|
3220
|
+
if (options.updateFooter) {
|
|
3221
|
+
this.footer.invalidate();
|
|
3222
|
+
this.updateEditorBorderColor();
|
|
2978
3223
|
}
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
3224
|
+
const tuiHistory = this.messagesForTuiHistoryReload(sessionContext.messages);
|
|
3225
|
+
if (tuiHistory.omittedMessages > 0) {
|
|
3226
|
+
this.appendStatusToChat(`Showing last ~${TUI_HISTORY_RELOAD_MAX_LINES} TUI history lines; omitted ${tuiHistory.omittedMessages} older message${tuiHistory.omittedMessages === 1 ? "" : "s"}. Full session remains available to the model.`, { requestRender: false });
|
|
3227
|
+
}
|
|
3228
|
+
for (const message of tuiHistory.messages) {
|
|
3229
|
+
if (processed > 0 && processed % TUI_HISTORY_RELOAD_CHUNK_SIZE === 0) {
|
|
3230
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
3231
|
+
if (generation !== this.renderGeneration)
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
processed++;
|
|
3235
|
+
// Assistant messages need special handling for tool calls
|
|
3236
|
+
if (message.role === "assistant") {
|
|
3237
|
+
this.addMessageToChat(message);
|
|
3238
|
+
// Render tool call components
|
|
3239
|
+
for (const content of message.content) {
|
|
3240
|
+
if (content.type === "toolCall") {
|
|
3241
|
+
const component = this.attachToolExecutionComponent(content.name, content.id, content.arguments);
|
|
3242
|
+
if (message.stopReason === "aborted" || message.stopReason === "error") {
|
|
3243
|
+
let errorMessage;
|
|
3244
|
+
if (message.stopReason === "aborted") {
|
|
3245
|
+
const retryAttempt = this.session.retryAttempt;
|
|
3246
|
+
errorMessage =
|
|
3247
|
+
retryAttempt > 0
|
|
3248
|
+
? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
|
|
3249
|
+
: "Operation aborted";
|
|
3250
|
+
}
|
|
3251
|
+
else {
|
|
3252
|
+
errorMessage = message.errorMessage || "Error";
|
|
3253
|
+
}
|
|
3254
|
+
component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
|
|
3255
|
+
this.toolPanels.finish(content.id);
|
|
2995
3256
|
}
|
|
2996
3257
|
else {
|
|
2997
|
-
|
|
3258
|
+
renderedPendingTools.set(content.id, component);
|
|
2998
3259
|
}
|
|
2999
|
-
component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
|
|
3000
|
-
this.toolPanels.finish(content.id);
|
|
3001
|
-
}
|
|
3002
|
-
else {
|
|
3003
|
-
renderedPendingTools.set(content.id, component);
|
|
3004
3260
|
}
|
|
3005
3261
|
}
|
|
3006
3262
|
}
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3263
|
+
else if (message.role === "toolResult") {
|
|
3264
|
+
// Match tool results to pending tool components
|
|
3265
|
+
const component = renderedPendingTools.get(message.toolCallId);
|
|
3266
|
+
if (component) {
|
|
3267
|
+
component.updateResult(message);
|
|
3268
|
+
renderedPendingTools.delete(message.toolCallId);
|
|
3269
|
+
this.toolPanels.finish(message.toolCallId);
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
else {
|
|
3273
|
+
// All other messages use standard rendering
|
|
3274
|
+
this.addMessageToChat(message, options);
|
|
3015
3275
|
}
|
|
3016
3276
|
}
|
|
3277
|
+
if (generation !== this.renderGeneration)
|
|
3278
|
+
return;
|
|
3279
|
+
visibleChatContainer.children = stagingChatContainer.children;
|
|
3280
|
+
committed = true;
|
|
3281
|
+
}
|
|
3282
|
+
finally {
|
|
3283
|
+
const stagedLiveHistoryHiddenNotice = this.liveHistoryHiddenNotice;
|
|
3284
|
+
const stagedLiveHistoryHiddenComponents = this.liveHistoryHiddenComponents;
|
|
3285
|
+
const stagedLastStatusSpacer = this.lastStatusSpacer;
|
|
3286
|
+
const stagedLastStatusText = this.lastStatusText;
|
|
3287
|
+
this.chatContainer = visibleChatContainer;
|
|
3288
|
+
if (committed) {
|
|
3289
|
+
this.liveHistoryHiddenNotice = stagedLiveHistoryHiddenNotice;
|
|
3290
|
+
this.liveHistoryHiddenComponents = stagedLiveHistoryHiddenComponents;
|
|
3291
|
+
this.lastStatusSpacer = stagedLastStatusSpacer;
|
|
3292
|
+
this.lastStatusText = stagedLastStatusText;
|
|
3293
|
+
}
|
|
3017
3294
|
else {
|
|
3018
|
-
|
|
3019
|
-
this.
|
|
3295
|
+
this.liveHistoryHiddenNotice = previousLiveHistoryHiddenNotice;
|
|
3296
|
+
this.liveHistoryHiddenComponents = previousLiveHistoryHiddenComponents;
|
|
3297
|
+
this.lastStatusSpacer = previousLastStatusSpacer;
|
|
3298
|
+
this.lastStatusText = previousLastStatusText;
|
|
3020
3299
|
}
|
|
3021
3300
|
}
|
|
3022
|
-
|
|
3301
|
+
if (committed)
|
|
3302
|
+
this.ui.requestRender();
|
|
3023
3303
|
}
|
|
3024
|
-
async renderInitialMessages() {
|
|
3025
|
-
|
|
3304
|
+
async renderInitialMessages(options = {}) {
|
|
3305
|
+
if (!options.forceHistoryLoad) {
|
|
3306
|
+
this.tuiHistoryLoaded = false;
|
|
3307
|
+
this.showDeferredHistoryPlaceholder({ requestRender: true });
|
|
3308
|
+
this.footer.invalidate();
|
|
3309
|
+
this.updateEditorBorderColor();
|
|
3310
|
+
return;
|
|
3311
|
+
}
|
|
3312
|
+
// Get aligned messages and entries from session context only when the user
|
|
3313
|
+
// explicitly requests TUI history. The model/session state is already loaded.
|
|
3026
3314
|
const context = this.sessionManager.buildSessionContext();
|
|
3027
3315
|
await this.renderSessionContext(context, {
|
|
3028
3316
|
updateFooter: true,
|
|
3029
3317
|
populateHistory: true,
|
|
3030
3318
|
});
|
|
3319
|
+
this.tuiHistoryLoaded = true;
|
|
3031
3320
|
// Show compaction info if session was compacted
|
|
3032
3321
|
const allEntries = this.sessionManager.getEntries();
|
|
3033
3322
|
const compactionCount = allEntries.filter((e) => e.type === "compaction").length;
|
|
@@ -3049,7 +3338,10 @@ export class InteractiveMode {
|
|
|
3049
3338
|
});
|
|
3050
3339
|
}
|
|
3051
3340
|
async rebuildChatFromMessages() {
|
|
3052
|
-
this.
|
|
3341
|
+
if (!this.tuiHistoryLoaded) {
|
|
3342
|
+
this.showDeferredHistoryPlaceholder({ requestRender: true });
|
|
3343
|
+
return;
|
|
3344
|
+
}
|
|
3053
3345
|
const context = this.sessionManager.buildSessionContext();
|
|
3054
3346
|
await this.renderSessionContext(context);
|
|
3055
3347
|
}
|
|
@@ -3331,7 +3623,6 @@ export class InteractiveMode {
|
|
|
3331
3623
|
this.hideThinkingBlock = !this.hideThinkingBlock;
|
|
3332
3624
|
this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
|
|
3333
3625
|
// Rebuild chat from session messages
|
|
3334
|
-
this.chatContainer.clear();
|
|
3335
3626
|
await this.rebuildChatFromMessages();
|
|
3336
3627
|
// If streaming, re-add the streaming component with updated visibility and re-render
|
|
3337
3628
|
if (this.streamingComponent && this.streamingMessage) {
|
|
@@ -4657,7 +4948,6 @@ export class InteractiveMode {
|
|
|
4657
4948
|
child.setHideThinkingBlock(hidden);
|
|
4658
4949
|
}
|
|
4659
4950
|
}
|
|
4660
|
-
this.chatContainer.clear();
|
|
4661
4951
|
void this.rebuildChatFromMessages();
|
|
4662
4952
|
},
|
|
4663
4953
|
onCollapseChangelogChange: (collapsed) => {
|
|
@@ -5037,7 +5327,6 @@ export class InteractiveMode {
|
|
|
5037
5327
|
return;
|
|
5038
5328
|
}
|
|
5039
5329
|
// Update UI
|
|
5040
|
-
this.chatContainer.clear();
|
|
5041
5330
|
await this.renderInitialMessages();
|
|
5042
5331
|
if (result.editorText && !this.editor.getText().trim()) {
|
|
5043
5332
|
this.editor.setText(result.editorText);
|