@caupulican/pi-adaptative 0.80.31 → 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.
Files changed (33) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/core/agent-session.d.ts.map +1 -1
  3. package/dist/core/agent-session.js +13 -1
  4. package/dist/core/agent-session.js.map +1 -1
  5. package/dist/core/extensions/loader.d.ts +18 -1
  6. package/dist/core/extensions/loader.d.ts.map +1 -1
  7. package/dist/core/extensions/loader.js +130 -17
  8. package/dist/core/extensions/loader.js.map +1 -1
  9. package/dist/core/extensions/types.d.ts +6 -0
  10. package/dist/core/extensions/types.d.ts.map +1 -1
  11. package/dist/core/extensions/types.js.map +1 -1
  12. package/dist/core/session-manager.d.ts +2 -0
  13. package/dist/core/session-manager.d.ts.map +1 -1
  14. package/dist/core/session-manager.js +6 -1
  15. package/dist/core/session-manager.js.map +1 -1
  16. package/dist/modes/interactive/components/footer.d.ts +3 -6
  17. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  18. package/dist/modes/interactive/components/footer.js +45 -21
  19. package/dist/modes/interactive/components/footer.js.map +1 -1
  20. package/dist/modes/interactive/interactive-mode.d.ts +24 -1
  21. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  22. package/dist/modes/interactive/interactive-mode.js +412 -96
  23. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  24. package/docs/extensions.md +24 -0
  25. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  26. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  27. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  28. package/examples/extensions/sandbox/package-lock.json +2 -2
  29. package/examples/extensions/sandbox/package.json +1 -1
  30. package/examples/extensions/with-deps/package-lock.json +2 -2
  31. package/examples/extensions/with-deps/package.json +1 -1
  32. package/npm-shrinkwrap.json +12 -12
  33. 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
  }
@@ -102,7 +108,7 @@ const AUTO_LEARN_DEFAULTS = {
102
108
  maxConcurrentLearners: 1,
103
109
  applyHighConfidence: false,
104
110
  reflectionReview: true,
105
- reflectionMinToolCalls: 8,
111
+ reflectionMinToolCalls: 5,
106
112
  reflectionCooldownMinutes: 24 * 60,
107
113
  };
108
114
  const AUTONOMY_AUTO_LEARN_PRESETS = {
@@ -117,7 +123,7 @@ const AUTONOMY_AUTO_LEARN_PRESETS = {
117
123
  maxConcurrentLearners: 1,
118
124
  applyHighConfidence: false,
119
125
  reflectionReview: true,
120
- reflectionMinToolCalls: 8,
126
+ reflectionMinToolCalls: 5,
121
127
  reflectionCooldownMinutes: 24 * 60,
122
128
  },
123
129
  balanced: {
@@ -130,7 +136,7 @@ const AUTONOMY_AUTO_LEARN_PRESETS = {
130
136
  maxConcurrentLearners: 1,
131
137
  applyHighConfidence: false,
132
138
  reflectionReview: true,
133
- reflectionMinToolCalls: 8,
139
+ reflectionMinToolCalls: 5,
134
140
  reflectionCooldownMinutes: 24 * 60,
135
141
  },
136
142
  full: {
@@ -143,13 +149,14 @@ const AUTONOMY_AUTO_LEARN_PRESETS = {
143
149
  maxConcurrentLearners: 1,
144
150
  applyHighConfidence: true,
145
151
  reflectionReview: true,
146
- reflectionMinToolCalls: 8,
152
+ reflectionMinToolCalls: 5,
147
153
  reflectionCooldownMinutes: 24 * 60,
148
154
  },
149
155
  };
150
156
  const AUTONOMY_MODES = ["off", "safe", "balanced", "full"];
151
157
  const AUTO_LEARN_RESERVATION_MS = 2 * 60 * 1000;
152
158
  const AUTO_LEARN_THINKING_LEVEL = "xhigh";
159
+ const AUTO_LEARN_COMPLEX_TASK_TOOL_CALLS = 5;
153
160
  export const AUTO_LEARN_HISTORY_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
154
161
  function definedStringSet(values) {
155
162
  const set = new Set();
@@ -407,9 +414,16 @@ export class InteractiveMode {
407
414
  // Status line tracking (for mutating immediately-sequential status updates)
408
415
  lastStatusSpacer = undefined;
409
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;
410
422
  // Streaming message tracking
411
423
  streamingComponent = undefined;
412
424
  streamingMessage = undefined;
425
+ streamingUiUpdateTimer = undefined;
426
+ lastStreamingUiUpdateAt = 0;
413
427
  // Tool execution tracking and session-scoped reusable panels
414
428
  toolPanels = new ToolPanelRegistry();
415
429
  // Tool output expansion state
@@ -687,7 +701,7 @@ export class InteractiveMode {
687
701
  hint("app.thinking.cycle", "to cycle thinking level"),
688
702
  rawKeyHint(`${keyText("app.model.cycleForward")}/${keyText("app.model.cycleBackward")}`, "to cycle models"),
689
703
  hint("app.model.select", "to select model"),
690
- hint("app.tools.expand", "to expand tools"),
704
+ hint("app.tools.expand", "to load history / expand tools"),
691
705
  hint("app.thinking.toggle", "to expand thinking"),
692
706
  hint("app.editor.external", "for external editor"),
693
707
  rawKeyHint("/", "for commands"),
@@ -705,9 +719,9 @@ export class InteractiveMode {
705
719
  rawKeyHint(`${keyText("app.clear")}/${keyText("app.exit")}`, "clear/exit"),
706
720
  rawKeyHint("/", "commands"),
707
721
  rawKeyHint("!", "bash"),
708
- hint("app.tools.expand", "more"),
722
+ hint("app.tools.expand", "history/more"),
709
723
  ].join(theme.fg("muted", " · "));
710
- 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.`);
711
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.`);
712
726
  this.builtInHeader = new ExpandableText(() => `${logo}\n${compactInstructions}\n${compactOnboarding}\n\n${onboarding}`, () => `${logo}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
713
727
  // Setup UI layout
@@ -1434,7 +1448,6 @@ export class InteractiveMode {
1434
1448
  if (result.cancelled) {
1435
1449
  return { cancelled: true };
1436
1450
  }
1437
- this.chatContainer.clear();
1438
1451
  await this.renderInitialMessages();
1439
1452
  if (result.editorText && !this.editor.getText().trim()) {
1440
1453
  this.editor.setText(result.editorText);
@@ -1502,7 +1515,6 @@ export class InteractiveMode {
1502
1515
  process.exit(1);
1503
1516
  }
1504
1517
  renderCurrentSessionState() {
1505
- this.chatContainer.clear();
1506
1518
  this.pendingMessagesContainer.clear();
1507
1519
  this.compactionQueuedMessages = [];
1508
1520
  this.streamingComponent = undefined;
@@ -1527,21 +1539,25 @@ export class InteractiveMode {
1527
1539
  const toolGroup = allowGrouping ? component.toolGroup?.trim() : undefined;
1528
1540
  if (!toolGroup) {
1529
1541
  this.chatContainer.addChild(component);
1542
+ this.trimLiveTuiHistory();
1530
1543
  return;
1531
1544
  }
1532
1545
  const children = this.chatContainer.children;
1533
1546
  const lastChild = children[children.length - 1];
1534
1547
  if (lastChild instanceof ToolGroupComponent && lastChild.toolGroup === toolGroup) {
1535
1548
  lastChild.addTool(component);
1549
+ this.trimLiveTuiHistory();
1536
1550
  return;
1537
1551
  }
1538
1552
  if (lastChild instanceof ToolExecutionComponent && lastChild.toolGroup?.trim() === toolGroup) {
1539
1553
  const group = new ToolGroupComponent(toolGroup, [lastChild, component]);
1540
1554
  group.setExpanded(this.toolOutputExpanded);
1541
1555
  children[children.length - 1] = group;
1556
+ this.trimLiveTuiHistory();
1542
1557
  return;
1543
1558
  }
1544
1559
  this.chatContainer.addChild(component);
1560
+ this.trimLiveTuiHistory();
1545
1561
  }
1546
1562
  detachToolExecutionComponent(component) {
1547
1563
  const children = this.chatContainer.children;
@@ -2286,7 +2302,7 @@ export class InteractiveMode {
2286
2302
  // Global debug handler on TUI (works regardless of focus)
2287
2303
  this.ui.onDebug = () => this.handleDebugCommand();
2288
2304
  this.defaultEditor.onAction("app.model.select", () => void this.showModelSelector());
2289
- this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
2305
+ this.defaultEditor.onAction("app.tools.expand", () => this.loadTuiHistoryOnDemand());
2290
2306
  this.defaultEditor.onAction("app.thinking.toggle", () => void this.toggleThinkingBlockVisibility());
2291
2307
  this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
2292
2308
  this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
@@ -2623,31 +2639,18 @@ export class InteractiveMode {
2623
2639
  this.ui.requestRender();
2624
2640
  }
2625
2641
  else if (event.message.role === "assistant") {
2642
+ this.clearPendingStreamingUiUpdate();
2643
+ this.lastStreamingUiUpdateAt = 0;
2626
2644
  this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
2627
2645
  this.streamingMessage = event.message;
2628
2646
  this.chatContainer.addChild(this.streamingComponent);
2629
- this.streamingComponent.updateContent(this.streamingMessage);
2630
- this.ui.requestRender();
2647
+ this.applyStreamingMessageUpdate(this.streamingMessage, { force: true });
2648
+ this.trimLiveTuiHistory();
2631
2649
  }
2632
2650
  break;
2633
2651
  case "message_update":
2634
2652
  if (this.streamingComponent && event.message.role === "assistant") {
2635
- this.streamingMessage = event.message;
2636
- this.streamingComponent.updateContent(this.streamingMessage);
2637
- for (const content of this.streamingMessage.content) {
2638
- if (content.type === "toolCall") {
2639
- if (!this.toolPanels.hasActive(content.id)) {
2640
- this.attachToolExecutionComponent(content.name, content.id, content.arguments);
2641
- }
2642
- else {
2643
- const component = this.toolPanels.getActive(content.id);
2644
- if (component) {
2645
- component.updateArgs(content.arguments);
2646
- }
2647
- }
2648
- }
2649
- }
2650
- this.ui.requestRender();
2653
+ this.applyStreamingMessageUpdate(event.message);
2651
2654
  }
2652
2655
  break;
2653
2656
  case "message_end":
@@ -2664,7 +2667,7 @@ export class InteractiveMode {
2664
2667
  : "Operation aborted";
2665
2668
  this.streamingMessage.errorMessage = errorMessage;
2666
2669
  }
2667
- this.streamingComponent.updateContent(this.streamingMessage);
2670
+ this.applyStreamingMessageUpdate(this.streamingMessage, { force: true });
2668
2671
  if (this.streamingMessage.stopReason === "aborted" || this.streamingMessage.stopReason === "error") {
2669
2672
  if (!errorMessage) {
2670
2673
  errorMessage = this.streamingMessage.errorMessage || "Error";
@@ -2777,7 +2780,6 @@ export class InteractiveMode {
2777
2780
  }
2778
2781
  }
2779
2782
  else if (event.result) {
2780
- this.chatContainer.clear();
2781
2783
  await this.rebuildChatFromMessages();
2782
2784
  this.addMessageToChat(createCompactionSummaryMessage(event.result.summary, event.result.tokensBefore, new Date().toISOString()));
2783
2785
  this.footer.invalidate();
@@ -2849,19 +2851,139 @@ export class InteractiveMode {
2849
2851
  : message.content.filter((c) => c.type === "text");
2850
2852
  return textBlocks.map((c) => c.text).join("");
2851
2853
  }
2852
- /**
2853
- * Show a status message in the chat.
2854
- *
2855
- * If multiple status messages are emitted back-to-back (without anything else being added to the chat),
2856
- * we update the previous status line instead of appending new ones to avoid log spam.
2857
- */
2858
- 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 = {}) {
2859
2980
  const children = this.chatContainer.children;
2860
2981
  const last = children.length > 0 ? children[children.length - 1] : undefined;
2861
2982
  const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
2862
2983
  if (last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {
2863
2984
  this.lastStatusText.setText(theme.fg("dim", message));
2864
- this.ui.requestRender();
2985
+ if (options.requestRender ?? true)
2986
+ this.ui.requestRender();
2865
2987
  return;
2866
2988
  }
2867
2989
  const spacer = new Spacer(1);
@@ -2870,7 +2992,18 @@ export class InteractiveMode {
2870
2992
  this.chatContainer.addChild(text);
2871
2993
  this.lastStatusSpacer = spacer;
2872
2994
  this.lastStatusText = text;
2873
- 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);
2874
3007
  }
2875
3008
  addMessageToChat(message, options) {
2876
3009
  switch (message.role) {
@@ -2947,6 +3080,117 @@ export class InteractiveMode {
2947
3080
  const _exhaustive = message;
2948
3081
  }
2949
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
+ };
2950
3194
  }
2951
3195
  /**
2952
3196
  * Render session context to chat. Used for initial load and rebuild after compaction.
@@ -2956,77 +3200,123 @@ export class InteractiveMode {
2956
3200
  */
2957
3201
  renderGeneration = 0;
2958
3202
  async renderSessionContext(sessionContext, options = {}) {
2959
- // Rebuilding a long session's scrollback synchronously blocks the event loop
2960
- // for seconds (reload/resume feel frozen). Yield between chunks so input and
2961
- // 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.
2962
3206
  const generation = ++this.renderGeneration;
2963
- const CHUNK_SIZE = 20;
2964
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();
2965
3217
  this.clearRenderedToolPanelState();
2966
3218
  const renderedPendingTools = new Map();
2967
- if (options.updateFooter) {
2968
- this.footer.invalidate();
2969
- this.updateEditorBorderColor();
2970
- }
2971
- for (const message of sessionContext.messages) {
2972
- if (processed > 0 && processed % CHUNK_SIZE === 0) {
2973
- this.ui.requestRender();
2974
- await new Promise((resolve) => setImmediate(resolve));
2975
- if (generation !== this.renderGeneration)
2976
- return;
3219
+ try {
3220
+ if (options.updateFooter) {
3221
+ this.footer.invalidate();
3222
+ this.updateEditorBorderColor();
2977
3223
  }
2978
- processed++;
2979
- // Assistant messages need special handling for tool calls
2980
- if (message.role === "assistant") {
2981
- this.addMessageToChat(message);
2982
- // Render tool call components
2983
- for (const content of message.content) {
2984
- if (content.type === "toolCall") {
2985
- const component = this.attachToolExecutionComponent(content.name, content.id, content.arguments);
2986
- if (message.stopReason === "aborted" || message.stopReason === "error") {
2987
- let errorMessage;
2988
- if (message.stopReason === "aborted") {
2989
- const retryAttempt = this.session.retryAttempt;
2990
- errorMessage =
2991
- retryAttempt > 0
2992
- ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
2993
- : "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);
2994
3256
  }
2995
3257
  else {
2996
- errorMessage = message.errorMessage || "Error";
3258
+ renderedPendingTools.set(content.id, component);
2997
3259
  }
2998
- component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
2999
- this.toolPanels.finish(content.id);
3000
- }
3001
- else {
3002
- renderedPendingTools.set(content.id, component);
3003
3260
  }
3004
3261
  }
3005
3262
  }
3006
- }
3007
- else if (message.role === "toolResult") {
3008
- // Match tool results to pending tool components
3009
- const component = renderedPendingTools.get(message.toolCallId);
3010
- if (component) {
3011
- component.updateResult(message);
3012
- renderedPendingTools.delete(message.toolCallId);
3013
- 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
+ }
3014
3271
  }
3272
+ else {
3273
+ // All other messages use standard rendering
3274
+ this.addMessageToChat(message, options);
3275
+ }
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;
3015
3293
  }
3016
3294
  else {
3017
- // All other messages use standard rendering
3018
- this.addMessageToChat(message, options);
3295
+ this.liveHistoryHiddenNotice = previousLiveHistoryHiddenNotice;
3296
+ this.liveHistoryHiddenComponents = previousLiveHistoryHiddenComponents;
3297
+ this.lastStatusSpacer = previousLastStatusSpacer;
3298
+ this.lastStatusText = previousLastStatusText;
3019
3299
  }
3020
3300
  }
3021
- this.ui.requestRender();
3301
+ if (committed)
3302
+ this.ui.requestRender();
3022
3303
  }
3023
- async renderInitialMessages() {
3024
- // 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.
3025
3314
  const context = this.sessionManager.buildSessionContext();
3026
3315
  await this.renderSessionContext(context, {
3027
3316
  updateFooter: true,
3028
3317
  populateHistory: true,
3029
3318
  });
3319
+ this.tuiHistoryLoaded = true;
3030
3320
  // Show compaction info if session was compacted
3031
3321
  const allEntries = this.sessionManager.getEntries();
3032
3322
  const compactionCount = allEntries.filter((e) => e.type === "compaction").length;
@@ -3048,7 +3338,10 @@ export class InteractiveMode {
3048
3338
  });
3049
3339
  }
3050
3340
  async rebuildChatFromMessages() {
3051
- this.chatContainer.clear();
3341
+ if (!this.tuiHistoryLoaded) {
3342
+ this.showDeferredHistoryPlaceholder({ requestRender: true });
3343
+ return;
3344
+ }
3052
3345
  const context = this.sessionManager.buildSessionContext();
3053
3346
  await this.renderSessionContext(context);
3054
3347
  }
@@ -3330,7 +3623,6 @@ export class InteractiveMode {
3330
3623
  this.hideThinkingBlock = !this.hideThinkingBlock;
3331
3624
  this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
3332
3625
  // Rebuild chat from session messages
3333
- this.chatContainer.clear();
3334
3626
  await this.rebuildChatFromMessages();
3335
3627
  // If streaming, re-add the streaming component with updated visibility and re-render
3336
3628
  if (this.streamingComponent && this.streamingMessage) {
@@ -4020,16 +4312,16 @@ export class InteractiveMode {
4020
4312
  ? `\n\nLatest completed turn digest (bounded; use only as current-session evidence, not as longitudinal proof):\n<turn_digest>\n${options.turnDigest}\n</turn_digest>`
4021
4313
  : "";
4022
4314
  const objective = options.kind === "reflection"
4023
- ? "review the latest completed turn for durable memory, skill, validation, and tooling-improvement cues, then run one bounded continuous-learning pass if the learning tools are available"
4315
+ ? "review the latest completed turn for durable memory, skill, validation, tooling, and code-baked self-improvement cues, then run one bounded continuous-learning pass if the learning tools are available"
4024
4316
  : "run one bounded continuous-learning pass for this Pi tenant";
4025
- return `You are Pi Auto Learn running as a background learner.\n\nObjective: ${objective}.\nTrigger: ${reason}.\n\n${authorityBlock}\n\nRequired workflow:\n1. Query existing durable memory/rules first when tools allow it. Memory confrontation is mandatory before accepting, merging, upgrading, or rejecting learning candidates.\n2. Run the available Auto Learn tooling, preferably learning_run_auto, with applyHighConfidence=${settings.applyHighConfidence}. Process candidate validation in vectorized chunks/batches; avoid scalar per-candidate memory queries except for final selected writes.\n3. Apply the learning validation tree to each candidate chunk: (a) Why is this good for the user? (b) Is it unique, or similar to existing memory/skills/agents so it should merge or upgrade existing knowledge? (c) Will this make Pi a better agent? Candidates that cannot answer all three are noise.\n4. Treat the latest-turn digest as current-session evidence only; do not auto-commit one-off cues unless deterministic tooling and memory confrontation corroborate them.\n5. In mode=full, apply safe memory/skill/user-extension/authorized-source improvements under the standing grant above; otherwise keep them proposal-gated.\n6. Never cross hard-stop boundaries from the authority policy.\n7. If the learning tools are unavailable, report BLOCKED with the missing tool names and do not improvise.\n8. Finish with PASS, BLOCKED, or FAIL and concise evidence, including chunk counts, merge/upgrade decisions, and cleanup/purge status.${reflectionBlock}`;
4317
+ return `You are Pi Auto Learn running as a background learner.\n\nObjective: ${objective}.\nTrigger: ${reason}.\n\n${authorityBlock}\n\nRequired workflow:\n1. Query existing durable memory/rules first when tools allow it. Memory confrontation is mandatory before accepting, merging, upgrading, or rejecting learning candidates.\n2. Run the available Auto Learn tooling, preferably learning_run_auto, with applyHighConfidence=${settings.applyHighConfidence}. Process candidate validation in vectorized chunks/batches; avoid scalar per-candidate memory queries except for final selected writes.\n3. Apply the learning validation tree to each candidate chunk: (a) Why is this good for the user? (b) Is it unique, or similar to existing memory/skills/agents so it should merge or upgrade existing knowledge? (c) Will this make Pi a better agent? Candidates that cannot answer all three are noise.\n4. Hermes-style learning cycle: after a complex task (${AUTO_LEARN_COMPLEX_TASK_TOOL_CALLS}+ tool calls), user correction, repeated steering pattern, non-trivial fix/workaround/debugging path, loaded-skill defect, trigger gap, tool gap, or harness workflow defect, actively create or update durable learning artifacts. Memory stores compact facts/preferences/state; skills/prompts/agents/extensions/source store procedural behavior. When a lesson changes how Pi should act on a future class of task, memory alone is not completion.\n5. Skill update preference order: (1) patch the currently loaded or consulted skill that governed the task; (2) patch an existing class-level umbrella skill/agent/prompt; (3) add a support file under references/, templates/, or scripts/ and add a SKILL.md pointer; (4) create a new class-level umbrella skill only when no existing artifact fits. Never create one-off PR/error/codename/session skills.\n6. Behavioral self-improvement is code-baked by default: prefer the lowest durable executable layer that fixes the behavior — patch an existing skill/prompt/agent/extension/tool, tune an approved setting, or edit the authorized Pi source when source authority is available. Use Automata only for concise facts/evidence pointers that support the baked change.\n7. Do not harden transient or environment-dependent failures into durable behavior: missing binaries, fresh-install package gaps, credentials not configured, path mismatches, one-off task narratives, or negative tool-broken claims should become setup/troubleshooting fixes only when the fix itself is reusable.\n8. Treat the latest-turn digest as current-session evidence only; do not auto-commit one-off cues unless deterministic tooling and memory confrontation corroborate them.\n9. In mode=full, apply safe memory/skill/user-extension/authorized-source improvements under the standing grant above; otherwise keep them proposal-gated.\n10. Never cross hard-stop boundaries from the authority policy.\n11. If the learning tools are unavailable, report BLOCKED with the missing tool names and do not improvise.\n12. Finish with PASS, BLOCKED, or FAIL and concise evidence, including chunk counts, merge/upgrade/code-bake decisions, changed paths/settings, validation, and cleanup/purge status.${reflectionBlock}`;
4026
4318
  }
4027
4319
  reserveAutoLearnRun(params) {
4028
4320
  return this.withAutoLearnStateLock((current) => {
4029
4321
  const now = Date.now();
4030
4322
  const state = this.pruneAutoLearnHistoryFromState(current, now);
4031
4323
  const tenant = this.getAutoLearnTenantKey();
4032
- if (params.cooldownKind === "reflection") {
4324
+ if (params.cooldownKind === "reflection" && !params.bypassReflectionCooldown) {
4033
4325
  const lastReflection = state.lastReflectionByTenant?.[tenant] ?? 0;
4034
4326
  const cooldownMs = params.settings.reflectionCooldownMinutes * 60 * 1000;
4035
4327
  if (Math.max(0, lastReflection + cooldownMs - now) > 0) {
@@ -4168,6 +4460,7 @@ export class InteractiveMode {
4168
4460
  settings,
4169
4461
  force,
4170
4462
  cooldownKind: options.cooldownKind,
4463
+ bypassReflectionCooldown: options.bypassReflectionCooldown,
4171
4464
  runId,
4172
4465
  modelPattern,
4173
4466
  reason,
@@ -4332,6 +4625,10 @@ export class InteractiveMode {
4332
4625
  .map((message) => this.getAgentMessagePlainText(message))
4333
4626
  .join("\n");
4334
4627
  const correctionSignal = /\b(next time|for future|from now on|remember this|don't|do not|avoid|instead|you should|should have|you forgot|you missed|not what i asked|wrong again)\b/i.test(userText);
4628
+ const behavioralSelfImprovementSignal = /\b(harness|pi|agent|autonomy|autonomous|self[- ]?improv(?:e|ement|ing)?|steer(?:ing)?|trigger(?:s)?|skill(?:s)?|code[- ]?bak(?:e|ed)|bake(?:d)? into code|not (?:automata|memory)|reference agent|hermes)\b/i.test(userText) &&
4629
+ /\b(improve|automatic(?:ally)?|autonomous|trigger|fire|skill|steer|self[- ]?improv(?:e|ement|ing)?|code[- ]?bak(?:e|ed)|bake(?:d)?|too much|less)\b/i.test(userText);
4630
+ const complexTaskSignal = toolCalls >= AUTO_LEARN_COMPLEX_TASK_TOOL_CALLS;
4631
+ const bypassCooldown = correctionSignal || behavioralSelfImprovementSignal || complexTaskSignal;
4335
4632
  const base = { messageCount, contextPercent, cooldownRemainingMs, runningCount, toolCalls };
4336
4633
  if (!settings.enabled)
4337
4634
  return { ...base, shouldRun: false, reason: "disabled" };
@@ -4344,14 +4641,34 @@ export class InteractiveMode {
4344
4641
  reason: `max tenant learners running (${runningCount}/${settings.maxConcurrentLearners})`,
4345
4642
  };
4346
4643
  }
4347
- if (cooldownRemainingMs > 0)
4644
+ if (cooldownRemainingMs > 0 && !bypassCooldown) {
4348
4645
  return { ...base, shouldRun: false, reason: "reflection cooldown" };
4646
+ }
4647
+ if (behavioralSelfImprovementSignal) {
4648
+ return {
4649
+ ...base,
4650
+ shouldRun: true,
4651
+ reason: "reflection behavioral self-improvement signal",
4652
+ digest: this.buildAutonomyReviewDigest(messages),
4653
+ bypassCooldown: true,
4654
+ };
4655
+ }
4349
4656
  if (correctionSignal) {
4350
4657
  return {
4351
4658
  ...base,
4352
4659
  shouldRun: true,
4353
4660
  reason: "reflection correction signal",
4354
4661
  digest: this.buildAutonomyReviewDigest(messages),
4662
+ bypassCooldown: true,
4663
+ };
4664
+ }
4665
+ if (complexTaskSignal) {
4666
+ return {
4667
+ ...base,
4668
+ shouldRun: true,
4669
+ reason: `reflection complex task learning signal (${toolCalls}/${AUTO_LEARN_COMPLEX_TASK_TOOL_CALLS} tool calls)`,
4670
+ digest: this.buildAutonomyReviewDigest(messages),
4671
+ bypassCooldown: true,
4355
4672
  };
4356
4673
  }
4357
4674
  if (autonomy.mode === "full") {
@@ -4395,6 +4712,7 @@ export class InteractiveMode {
4395
4712
  cooldownKind: "reflection",
4396
4713
  promptKind: "reflection",
4397
4714
  turnDigest: decision.digest,
4715
+ bypassReflectionCooldown: decision.bypassCooldown,
4398
4716
  });
4399
4717
  if (!message.startsWith("Auto Learn started"))
4400
4718
  this.showStatus(message);
@@ -4630,7 +4948,6 @@ export class InteractiveMode {
4630
4948
  child.setHideThinkingBlock(hidden);
4631
4949
  }
4632
4950
  }
4633
- this.chatContainer.clear();
4634
4951
  void this.rebuildChatFromMessages();
4635
4952
  },
4636
4953
  onCollapseChangelogChange: (collapsed) => {
@@ -5010,7 +5327,6 @@ export class InteractiveMode {
5010
5327
  return;
5011
5328
  }
5012
5329
  // Update UI
5013
- this.chatContainer.clear();
5014
5330
  await this.renderInitialMessages();
5015
5331
  if (result.editorText && !this.editor.getText().trim()) {
5016
5332
  this.editor.setText(result.editorText);