@caupulican/pi-adaptative 0.80.34 → 0.80.38

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/README.md +1 -0
  3. package/dist/core/extensions/loader.d.ts +18 -1
  4. package/dist/core/extensions/loader.d.ts.map +1 -1
  5. package/dist/core/extensions/loader.js +130 -17
  6. package/dist/core/extensions/loader.js.map +1 -1
  7. package/dist/core/extensions/types.d.ts +6 -0
  8. package/dist/core/extensions/types.d.ts.map +1 -1
  9. package/dist/core/extensions/types.js.map +1 -1
  10. package/dist/core/model-resolver.d.ts.map +1 -1
  11. package/dist/core/model-resolver.js +1 -0
  12. package/dist/core/model-resolver.js.map +1 -1
  13. package/dist/core/provider-display-names.d.ts.map +1 -1
  14. package/dist/core/provider-display-names.js +1 -0
  15. package/dist/core/provider-display-names.js.map +1 -1
  16. package/dist/core/session-manager.d.ts +2 -0
  17. package/dist/core/session-manager.d.ts.map +1 -1
  18. package/dist/core/session-manager.js +6 -1
  19. package/dist/core/session-manager.js.map +1 -1
  20. package/dist/modes/interactive/components/footer.d.ts +3 -6
  21. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  22. package/dist/modes/interactive/components/footer.js +45 -21
  23. package/dist/modes/interactive/components/footer.js.map +1 -1
  24. package/dist/modes/interactive/interactive-mode.d.ts +24 -1
  25. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  26. package/dist/modes/interactive/interactive-mode.js +377 -88
  27. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  28. package/docs/extensions.md +24 -0
  29. package/docs/providers.md +16 -0
  30. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  31. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  32. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  33. package/examples/extensions/sandbox/package-lock.json +2 -2
  34. package/examples/extensions/sandbox/package.json +1 -1
  35. package/examples/extensions/with-deps/package-lock.json +2 -2
  36. package/examples/extensions/with-deps/package.json +1 -1
  37. package/npm-shrinkwrap.json +12 -12
  38. 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.toggleToolOutputExpansion());
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.streamingComponent.updateContent(this.streamingMessage);
2631
- this.ui.requestRender();
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.streamingMessage = event.message;
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.streamingComponent.updateContent(this.streamingMessage);
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
- * Show a status message in the chat.
2855
- *
2856
- * If multiple status messages are emitted back-to-back (without anything else being added to the chat),
2857
- * we update the previous status line instead of appending new ones to avoid log spam.
2858
- */
2859
- showStatus(message) {
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
- this.ui.requestRender();
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.ui.requestRender();
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
- // Rebuilding a long session's scrollback synchronously blocks the event loop
2961
- // for seconds (reload/resume feel frozen). Yield between chunks so input and
2962
- // repaints keep flowing; a newer rebuild supersedes this one via generation.
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
- if (options.updateFooter) {
2969
- this.footer.invalidate();
2970
- this.updateEditorBorderColor();
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
- processed++;
2980
- // Assistant messages need special handling for tool calls
2981
- if (message.role === "assistant") {
2982
- this.addMessageToChat(message);
2983
- // Render tool call components
2984
- for (const content of message.content) {
2985
- if (content.type === "toolCall") {
2986
- const component = this.attachToolExecutionComponent(content.name, content.id, content.arguments);
2987
- if (message.stopReason === "aborted" || message.stopReason === "error") {
2988
- let errorMessage;
2989
- if (message.stopReason === "aborted") {
2990
- const retryAttempt = this.session.retryAttempt;
2991
- errorMessage =
2992
- retryAttempt > 0
2993
- ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
2994
- : "Operation aborted";
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
- errorMessage = message.errorMessage || "Error";
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
- else if (message.role === "toolResult") {
3009
- // Match tool results to pending tool components
3010
- const component = renderedPendingTools.get(message.toolCallId);
3011
- if (component) {
3012
- component.updateResult(message);
3013
- renderedPendingTools.delete(message.toolCallId);
3014
- this.toolPanels.finish(message.toolCallId);
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
- // All other messages use standard rendering
3019
- this.addMessageToChat(message, options);
3295
+ this.liveHistoryHiddenNotice = previousLiveHistoryHiddenNotice;
3296
+ this.liveHistoryHiddenComponents = previousLiveHistoryHiddenComponents;
3297
+ this.lastStatusSpacer = previousLastStatusSpacer;
3298
+ this.lastStatusText = previousLastStatusText;
3020
3299
  }
3021
3300
  }
3022
- this.ui.requestRender();
3301
+ if (committed)
3302
+ this.ui.requestRender();
3023
3303
  }
3024
- async renderInitialMessages() {
3025
- // Get aligned messages and entries from session context
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.chatContainer.clear();
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);