erosolar-cli 1.7.383 → 1.7.386

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 (70) hide show
  1. package/dist/core/contextManager.d.ts +4 -0
  2. package/dist/core/contextManager.d.ts.map +1 -1
  3. package/dist/core/contextManager.js +16 -0
  4. package/dist/core/contextManager.js.map +1 -1
  5. package/dist/core/secretStore.d.ts +1 -0
  6. package/dist/core/secretStore.d.ts.map +1 -1
  7. package/dist/core/secretStore.js +3 -0
  8. package/dist/core/secretStore.js.map +1 -1
  9. package/dist/core/toolPreconditions.d.ts.map +1 -1
  10. package/dist/core/toolPreconditions.js +9 -0
  11. package/dist/core/toolPreconditions.js.map +1 -1
  12. package/dist/core/toolRuntime.d.ts +6 -0
  13. package/dist/core/toolRuntime.d.ts.map +1 -1
  14. package/dist/core/toolRuntime.js +7 -0
  15. package/dist/core/toolRuntime.js.map +1 -1
  16. package/dist/runtime/agentHost.d.ts +3 -1
  17. package/dist/runtime/agentHost.d.ts.map +1 -1
  18. package/dist/runtime/agentHost.js +3 -0
  19. package/dist/runtime/agentHost.js.map +1 -1
  20. package/dist/runtime/agentSession.d.ts +2 -1
  21. package/dist/runtime/agentSession.d.ts.map +1 -1
  22. package/dist/runtime/agentSession.js +1 -0
  23. package/dist/runtime/agentSession.js.map +1 -1
  24. package/dist/runtime/universal.d.ts +2 -1
  25. package/dist/runtime/universal.d.ts.map +1 -1
  26. package/dist/runtime/universal.js +1 -0
  27. package/dist/runtime/universal.js.map +1 -1
  28. package/dist/shell/interactiveShell.d.ts +34 -0
  29. package/dist/shell/interactiveShell.d.ts.map +1 -1
  30. package/dist/shell/interactiveShell.js +424 -139
  31. package/dist/shell/interactiveShell.js.map +1 -1
  32. package/dist/shell/shellApp.d.ts.map +1 -1
  33. package/dist/shell/shellApp.js +5 -0
  34. package/dist/shell/shellApp.js.map +1 -1
  35. package/dist/shell/systemPrompt.d.ts.map +1 -1
  36. package/dist/shell/systemPrompt.js +4 -0
  37. package/dist/shell/systemPrompt.js.map +1 -1
  38. package/dist/shell/terminalInput.d.ts +43 -0
  39. package/dist/shell/terminalInput.d.ts.map +1 -1
  40. package/dist/shell/terminalInput.js +273 -28
  41. package/dist/shell/terminalInput.js.map +1 -1
  42. package/dist/shell/terminalInputAdapter.d.ts +9 -0
  43. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  44. package/dist/shell/terminalInputAdapter.js +8 -2
  45. package/dist/shell/terminalInputAdapter.js.map +1 -1
  46. package/dist/tools/fileTools.d.ts.map +1 -1
  47. package/dist/tools/fileTools.js +29 -5
  48. package/dist/tools/fileTools.js.map +1 -1
  49. package/dist/tools/grepTools.d.ts.map +1 -1
  50. package/dist/tools/grepTools.js +22 -4
  51. package/dist/tools/grepTools.js.map +1 -1
  52. package/dist/tools/searchTools.d.ts.map +1 -1
  53. package/dist/tools/searchTools.js +47 -13
  54. package/dist/tools/searchTools.js.map +1 -1
  55. package/dist/tools/webTools.d.ts.map +1 -1
  56. package/dist/tools/webTools.js +36 -9
  57. package/dist/tools/webTools.js.map +1 -1
  58. package/dist/ui/ShellUIAdapter.d.ts +13 -0
  59. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  60. package/dist/ui/ShellUIAdapter.js +40 -3
  61. package/dist/ui/ShellUIAdapter.js.map +1 -1
  62. package/dist/ui/display.d.ts +1 -0
  63. package/dist/ui/display.d.ts.map +1 -1
  64. package/dist/ui/display.js +61 -12
  65. package/dist/ui/display.js.map +1 -1
  66. package/dist/ui/layout.js +8 -7
  67. package/dist/ui/layout.js.map +1 -1
  68. package/dist/ui/unified/layout.js +2 -2
  69. package/dist/ui/unified/layout.js.map +1 -1
  70. package/package.json +1 -1
@@ -70,6 +70,9 @@ const PROVIDER_LABELS = Object.fromEntries(getProviders().map((provider) => [pro
70
70
  // Allow enough time for paste detection to kick in before flushing buffered lines
71
71
  const CONTEXT_USAGE_THRESHOLD = 0.9;
72
72
  const CONTEXT_AUTOCOMPACT_PERCENT = Math.round(CONTEXT_USAGE_THRESHOLD * 100);
73
+ const CONTEXT_AUTOCOMPACT_FLOOR = 0.6;
74
+ const MIN_COMPACTION_TOKEN_SAVINGS = 200;
75
+ const MIN_COMPACTION_PERCENT_SAVINGS = 0.5;
73
76
  const CONTEXT_RECENT_MESSAGE_COUNT = 12;
74
77
  const CONTEXT_CLEANUP_CHARS_PER_CHUNK = 6000;
75
78
  const CONTEXT_CLEANUP_MAX_OUTPUT_TOKENS = 800;
@@ -99,6 +102,8 @@ export class InteractiveShell {
99
102
  pendingCleanup = null;
100
103
  cleanupInProgress = false;
101
104
  slashPreviewVisible = false;
105
+ lastLoggedPrompt = null;
106
+ lastLoggedPromptAt = 0;
102
107
  skillRepository;
103
108
  skillToolHandlers = new Map();
104
109
  thinkingMode = 'balanced';
@@ -116,6 +121,9 @@ export class InteractiveShell {
116
121
  isDrainingQueue = false;
117
122
  activeContextWindowTokens = null;
118
123
  latestTokenUsage = { used: null, limit: null };
124
+ planApprovalBridgeRegistered = false;
125
+ contextCompactionInFlight = false;
126
+ lastContextWarningLevel = null;
119
127
  sessionPreferences;
120
128
  autosaveEnabled;
121
129
  autoContinueEnabled;
@@ -145,9 +153,12 @@ export class InteractiveShell {
145
153
  autoBuildInFlight = false;
146
154
  lastAutoBuildRun = null;
147
155
  // Streaming UX tracking
156
+ batchedOutputMode;
148
157
  streamingHeartbeatStart = null;
149
158
  streamingHeartbeatFrame = 0;
150
159
  streamingStatusLabel = null;
160
+ streamingUiActive = false;
161
+ pendingStreamBuffer = '';
151
162
  lastStreamingElapsedSeconds = null; // Preserve final elapsed time
152
163
  statusLineState = null;
153
164
  statusMessageOverride = null;
@@ -169,6 +180,8 @@ export class InteractiveShell {
169
180
  this.sessionRestoreConfig = config.sessionRestore ?? { mode: 'none' };
170
181
  this._enabledPlugins = config.enabledPlugins ?? [];
171
182
  this.version = config.version ?? '0.0.0';
183
+ const streamingEnv = process.env['EROSOLAR_STREAMING_UI']?.toLowerCase();
184
+ this.batchedOutputMode = streamingEnv !== 'on' && streamingEnv !== 'true' && streamingEnv !== '1';
172
185
  // Alternate screen disabled - use terminal-native mode for proper scrollback and text selection
173
186
  this.alternateScreenEnabled = false;
174
187
  this.initializeSessionHistory();
@@ -255,6 +268,11 @@ export class InteractiveShell {
255
268
  this.refreshContextGauge();
256
269
  // Start terminal input (sets up handlers)
257
270
  this.terminalInput.start();
271
+ // Allow planning tools (e.g., ProposePlan) to open the interactive approval UI just like Codex CLI
272
+ this.registerPlanApprovalBridge();
273
+ // Capture display output into the scrollback/chat log so system messages
274
+ // (e.g., /evolve) render above the pinned chat box.
275
+ this.terminalInput.registerOutputInterceptor(display);
258
276
  // Set up command autocomplete with all slash commands
259
277
  this.setupCommandAutocomplete();
260
278
  // Enter alternate screen buffer when enabled, otherwise clear the main screen for layout
@@ -266,7 +284,7 @@ export class InteractiveShell {
266
284
  }
267
285
  // Stream banner first - this sets up scroll region dynamically
268
286
  const banner = this.buildBanner();
269
- this.terminalInput.streamContent(banner + '\n\n');
287
+ this.terminalInput.streamContent(banner + '\n');
270
288
  // Render chat box after banner is streamed
271
289
  this.refreshControlBar();
272
290
  this.terminalInput.forceRender();
@@ -360,7 +378,6 @@ export class InteractiveShell {
360
378
  void maybeOfferCliUpdate(this.version);
361
379
  }
362
380
  if (initialPrompt) {
363
- this.logUserPrompt(initialPrompt);
364
381
  await this.processInputBlock(initialPrompt);
365
382
  return;
366
383
  }
@@ -388,7 +405,6 @@ export class InteractiveShell {
388
405
  }
389
406
  // DON'T clear the input here - keep it visible while streaming.
390
407
  // The input will be cleared after streaming completes in the finally block.
391
- this.logUserPrompt(approved);
392
408
  void this.processInputBlock(approved).catch((err) => {
393
409
  display.showError(err instanceof Error ? err.message : String(err), err);
394
410
  });
@@ -505,15 +521,9 @@ export class InteractiveShell {
505
521
  this.editGuardMode = mode;
506
522
  this.pendingPermissionInput = null;
507
523
  if (mode === 'plan') {
508
- // Register plan approval callback for interactive UI
509
- setPlanApprovalCallback((steps, explanation) => {
510
- this.showPlanApproval(steps, explanation);
511
- });
512
524
  display.showSystemMessage('📋 Plan mode enabled. AI will create a plan and ask for approval before implementing.');
513
525
  }
514
526
  else {
515
- // Unregister callback when not in plan mode
516
- setPlanApprovalCallback(null);
517
527
  if (mode === 'ask-permission') {
518
528
  display.showSystemMessage('🛡️ Ask-to-edit mode enabled. Confirm each request before sending.');
519
529
  }
@@ -729,6 +739,8 @@ export class InteractiveShell {
729
739
  if (this.alternateScreenEnabled) {
730
740
  this.terminalInput.exitAlternateScreen();
731
741
  }
742
+ // Unregister plan approval bridge
743
+ setPlanApprovalCallback(null);
732
744
  // Dispose terminal input handler
733
745
  this.terminalInput.dispose();
734
746
  // Dispose unified UI adapter
@@ -752,6 +764,18 @@ export class InteractiveShell {
752
764
  // Write directly to stdout after exiting alternate screen to preserve the transcript
753
765
  process.stdout.write(`\n${separator}\n${header}\n${transcript}\n${separator}\n`);
754
766
  }
767
+ /**
768
+ * Wire the planning tool suite to the interactive plan approval UI so ProposePlan behaves like Codex CLI.
769
+ */
770
+ registerPlanApprovalBridge() {
771
+ if (this.planApprovalBridgeRegistered) {
772
+ return;
773
+ }
774
+ setPlanApprovalCallback((steps, explanation) => {
775
+ this.showPlanApproval(steps, explanation);
776
+ });
777
+ this.planApprovalBridgeRegistered = true;
778
+ }
755
779
  /**
756
780
  * Update status bar message
757
781
  */
@@ -760,6 +784,21 @@ export class InteractiveShell {
760
784
  // During streaming we still want the spinner prefix; when idle force a fast refresh.
761
785
  this.refreshStatusLine(!this.isProcessing);
762
786
  }
787
+ /**
788
+ * Inline menu/panel helpers (rendered in TerminalInput, not in scrollback).
789
+ */
790
+ showInlineMenu(title, lines, tone = 'info', hint) {
791
+ const merged = [...lines];
792
+ if (hint) {
793
+ merged.push('', tone === 'error' ? theme.error(hint) : theme.warning(hint));
794
+ }
795
+ this.terminalInput.setInlinePanel({ title, lines: merged, tone });
796
+ this.terminalInput.render();
797
+ }
798
+ clearInlineMenu() {
799
+ this.terminalInput.setInlinePanel(null);
800
+ this.terminalInput.render();
801
+ }
763
802
  async handleToolSettingsInput(input) {
764
803
  const pending = this.pendingInteraction;
765
804
  if (!pending || pending.type !== 'tool-settings') {
@@ -1105,9 +1144,9 @@ export class InteractiveShell {
1105
1144
  };
1106
1145
  }
1107
1146
  }
1108
- updateContextUsage(percentage) {
1147
+ updateContextUsage(percentage, autoCompactThreshold = CONTEXT_AUTOCOMPACT_PERCENT) {
1109
1148
  this.uiAdapter.updateContextUsage(percentage);
1110
- this.terminalInput.setContextUsage(percentage, CONTEXT_AUTOCOMPACT_PERCENT);
1149
+ this.terminalInput.setContextUsage(percentage, autoCompactThreshold);
1111
1150
  }
1112
1151
  refreshControlBar() {
1113
1152
  this.terminalInput.setModeToggles({
@@ -1190,10 +1229,6 @@ export class InteractiveShell {
1190
1229
  if (state.detail) {
1191
1230
  parts.push(state.detail);
1192
1231
  }
1193
- const elapsedSeconds = Math.max(0, Math.floor((Date.now() - state.startedAt) / 1000));
1194
- if (elapsedSeconds > 0) {
1195
- parts.push(`${elapsedSeconds}s`);
1196
- }
1197
1232
  return parts.join(' • ');
1198
1233
  }
1199
1234
  handleSlashCommandPreviewChange() {
@@ -1240,10 +1275,18 @@ export class InteractiveShell {
1240
1275
  * Log user prompt to the scroll region so it's part of the conversation flow.
1241
1276
  */
1242
1277
  logUserPrompt(text) {
1243
- if (!text.trim())
1278
+ const normalized = text.trim();
1279
+ if (!normalized)
1280
+ return;
1281
+ // Skip duplicate renders of the exact same prompt if they happen back-to-back.
1282
+ const now = Date.now();
1283
+ if (this.lastLoggedPrompt === normalized && now - this.lastLoggedPromptAt < 2000) {
1244
1284
  return;
1285
+ }
1286
+ this.lastLoggedPrompt = normalized;
1287
+ this.lastLoggedPromptAt = now;
1245
1288
  // Format with user prompt prefix and write to scroll region
1246
- const formatted = `${theme.user('>')} ${text}\n`;
1289
+ const formatted = `${theme.user('>')} ${normalized}\n`;
1247
1290
  this.terminalInput.writeToScrollRegion(formatted);
1248
1291
  }
1249
1292
  requestPromptRefresh(force = false) {
@@ -1265,17 +1308,49 @@ export class InteractiveShell {
1265
1308
  this.promptRefreshTimer = null;
1266
1309
  }
1267
1310
  }
1311
+ resetStreamBuffer() {
1312
+ this.pendingStreamBuffer = '';
1313
+ }
1314
+ captureStreamChunk(chunk) {
1315
+ if (!this.batchedOutputMode || !chunk) {
1316
+ return;
1317
+ }
1318
+ this.pendingStreamBuffer += chunk;
1319
+ }
1320
+ consumeStreamBuffer(fallback) {
1321
+ if (!this.batchedOutputMode) {
1322
+ return fallback;
1323
+ }
1324
+ const buffered = this.pendingStreamBuffer;
1325
+ this.pendingStreamBuffer = '';
1326
+ return buffered || fallback;
1327
+ }
1328
+ currentRunElapsedMs() {
1329
+ if (!this.streamingHeartbeatStart) {
1330
+ return undefined;
1331
+ }
1332
+ return Date.now() - this.streamingHeartbeatStart;
1333
+ }
1268
1334
  startStreamingHeartbeat(label = 'Streaming') {
1269
1335
  this.stopStreamingHeartbeat();
1270
- // Enter global streaming mode - blocks all non-streaming UI output
1271
- enterStreamingMode();
1272
- // Set up scroll region for streaming content
1273
- this.terminalInput.enterStreamingScrollRegion();
1274
- this.uiUpdates.setMode('streaming');
1336
+ this.resetStreamBuffer();
1337
+ this.lastStreamingElapsedSeconds = null;
1275
1338
  this.streamingHeartbeatStart = Date.now();
1276
1339
  this.streamingHeartbeatFrame = 0;
1277
1340
  const initialFrame = STREAMING_SPINNER_FRAMES[this.streamingHeartbeatFrame];
1278
1341
  this.streamingStatusLabel = this.buildStreamingStatus(`${initialFrame} ${label}`, 0);
1342
+ if (this.batchedOutputMode) {
1343
+ this.streamingUiActive = false;
1344
+ this.uiUpdates.setMode('processing');
1345
+ }
1346
+ else {
1347
+ // Enter global streaming mode - blocks all non-streaming UI output
1348
+ enterStreamingMode();
1349
+ // Set up scroll region for streaming content
1350
+ this.terminalInput.enterStreamingScrollRegion();
1351
+ this.streamingUiActive = true;
1352
+ this.uiUpdates.setMode('streaming');
1353
+ }
1279
1354
  display.updateStreamingStatus(this.streamingStatusLabel);
1280
1355
  this.refreshStatusLine(true);
1281
1356
  // Periodically refresh the pinned input/status region while streaming so
@@ -1305,18 +1380,21 @@ export class InteractiveShell {
1305
1380
  });
1306
1381
  }
1307
1382
  stopStreamingHeartbeat() {
1308
- // Exit global streaming mode - allows UI to render again
1309
- exitStreamingMode();
1310
1383
  // Preserve final elapsed time before clearing heartbeat start
1311
1384
  if (this.streamingHeartbeatStart) {
1312
1385
  this.lastStreamingElapsedSeconds = Math.max(0, Math.floor((Date.now() - this.streamingHeartbeatStart) / 1000));
1313
1386
  }
1314
- // Exit scroll region mode
1315
- this.terminalInput.exitStreamingScrollRegion();
1387
+ if (this.streamingUiActive) {
1388
+ // Exit global streaming mode - allows UI to render again
1389
+ exitStreamingMode();
1390
+ // Exit scroll region mode
1391
+ this.terminalInput.exitStreamingScrollRegion();
1392
+ }
1316
1393
  this.uiUpdates.stopHeartbeat('streaming');
1317
1394
  this.streamingHeartbeatStart = null;
1318
1395
  this.streamingHeartbeatFrame = 0;
1319
1396
  this.streamingStatusLabel = null;
1397
+ this.streamingUiActive = false;
1320
1398
  // Clear streaming label specifically (keeps override and main status if set)
1321
1399
  this.terminalInput.setStreamingLabel(null);
1322
1400
  // Clear streaming status from display
@@ -1324,20 +1402,11 @@ export class InteractiveShell {
1324
1402
  // Force refresh to update the input area now that streaming has ended
1325
1403
  this.refreshStatusLine(true);
1326
1404
  }
1327
- buildStreamingStatus(label, elapsedSeconds) {
1328
- const detail = this.describeModelDetail();
1329
- const elapsedLabel = typeof elapsedSeconds === 'number' && elapsedSeconds >= 0
1330
- ? theme.ui.muted(this.formatElapsedShort(elapsedSeconds))
1331
- : null;
1405
+ buildStreamingStatus(label, _elapsedSeconds) {
1406
+ // Model + elapsed time already live in the pinned meta header; keep the streaming
1407
+ // status focused on the activity to avoid duplicate info.
1332
1408
  const prefix = theme.info('⏺');
1333
- const parts = [label];
1334
- if (detail) {
1335
- parts.push(theme.ui.muted('·'), detail);
1336
- }
1337
- if (elapsedLabel) {
1338
- parts.push(theme.ui.muted('·'), elapsedLabel);
1339
- }
1340
- return `${prefix} ${parts.join(' ')}`.trim();
1409
+ return `${prefix} ${label}`.trim();
1341
1410
  }
1342
1411
  formatElapsedShort(seconds) {
1343
1412
  if (seconds < 60) {
@@ -1493,6 +1562,10 @@ export class InteractiveShell {
1493
1562
  return false;
1494
1563
  }
1495
1564
  switch (this.pendingInteraction.type) {
1565
+ case 'model-loading':
1566
+ display.showInfo('Still fetching model options. Please wait a moment.');
1567
+ this.terminalInput.render();
1568
+ return true;
1496
1569
  case 'model-provider':
1497
1570
  await this.handleModelProviderSelection(input);
1498
1571
  return true;
@@ -3627,28 +3700,25 @@ export class InteractiveShell {
3627
3700
  return lines.join('\n');
3628
3701
  }
3629
3702
  async showModelMenu() {
3630
- display.showSystemMessage(theme.ui.muted('Fetching latest models from providers...'));
3631
- // Fetch live models from all configured providers
3632
- const providerStatuses = await quickCheckProviders();
3633
- const providerOptions = this.buildProviderOptionsWithDiscovery(providerStatuses);
3634
- if (!providerOptions.length) {
3635
- display.showWarning('No providers are available.');
3636
- return;
3703
+ // Hold input immediately so numeric selections don't get queued as prompts while we fetch
3704
+ this.pendingInteraction = { type: 'model-loading' };
3705
+ this.showInlineMenu('Model provider', [theme.ui.muted('Fetching latest models from providers...')], 'info');
3706
+ try {
3707
+ // Fetch live models from all configured providers
3708
+ const providerStatuses = await quickCheckProviders();
3709
+ const providerOptions = this.buildProviderOptionsWithDiscovery(providerStatuses);
3710
+ if (!providerOptions.length) {
3711
+ this.pendingInteraction = null;
3712
+ this.showInlineMenu('Model provider', [theme.warning('No providers are available.')], 'warning');
3713
+ return;
3714
+ }
3715
+ this.pendingInteraction = { type: 'model-provider', options: providerOptions };
3716
+ this.renderProviderMenu(providerOptions);
3717
+ }
3718
+ catch (error) {
3719
+ this.pendingInteraction = null;
3720
+ this.showInlineMenu('Model provider', [theme.error('Failed to load model list. Try again in a moment.')], 'error');
3637
3721
  }
3638
- const lines = [
3639
- theme.bold('Select a provider:'),
3640
- ...providerOptions.map((option, index) => {
3641
- const isCurrent = option.provider === this.sessionState.provider;
3642
- const countLabel = `${option.modelCount} model${option.modelCount === 1 ? '' : 's'}`;
3643
- const latestLabel = option.latestModel ? theme.success(` (latest: ${option.latestModel})`) : '';
3644
- const label = this.colorizeDropdownLine(`${index + 1}. ${option.label} — ${countLabel}${latestLabel}`, index);
3645
- const suffix = isCurrent ? ` ${theme.primary('• current')}` : '';
3646
- return `${label}${suffix}`;
3647
- }),
3648
- 'Type the number of the provider to continue, or type "cancel".',
3649
- ];
3650
- display.showSystemMessage(lines.join('\n'));
3651
- this.pendingInteraction = { type: 'model-provider', options: providerOptions };
3652
3722
  }
3653
3723
  buildProviderOptions() {
3654
3724
  const counts = new Map();
@@ -3738,6 +3808,21 @@ export class InteractiveShell {
3738
3808
  };
3739
3809
  });
3740
3810
  }
3811
+ renderProviderMenu(options, hint) {
3812
+ const lines = [
3813
+ theme.bold('Select a provider:'),
3814
+ ...options.map((option, index) => {
3815
+ const isCurrent = option.provider === this.sessionState.provider;
3816
+ const countLabel = `${option.modelCount} model${option.modelCount === 1 ? '' : 's'}`;
3817
+ const latestLabel = option.latestModel ? theme.success(` (latest: ${option.latestModel})`) : '';
3818
+ const label = this.colorizeDropdownLine(`${index + 1}. ${option.label} — ${countLabel}${latestLabel}`, index);
3819
+ const suffix = isCurrent ? ` ${theme.primary('• current')}` : '';
3820
+ return `${label}${suffix}`;
3821
+ }),
3822
+ 'Type the number of the provider to continue, or type "cancel".',
3823
+ ];
3824
+ this.showInlineMenu('Model provider', lines, 'info', hint);
3825
+ }
3741
3826
  showProviderModels(option) {
3742
3827
  // Start with static presets
3743
3828
  const staticModels = MODEL_PRESETS.filter((preset) => preset.provider === option.provider);
@@ -3777,13 +3862,17 @@ export class InteractiveShell {
3777
3862
  }
3778
3863
  }
3779
3864
  if (!allModels.length) {
3780
- display.showWarning(`No models available for ${option.label}.`);
3865
+ this.showInlineMenu('Model selection', [theme.warning(`No models available for ${option.label}.`)], 'warning');
3781
3866
  this.pendingInteraction = null;
3782
3867
  return;
3783
3868
  }
3869
+ this.renderModelSelection(allModels, option, null);
3870
+ this.pendingInteraction = { type: 'model', provider: option.provider, options: allModels };
3871
+ }
3872
+ renderModelSelection(models, option, hint) {
3784
3873
  const lines = [
3785
3874
  theme.bold(`Select a model from ${option.label}:`),
3786
- ...allModels.map((preset, index) => {
3875
+ ...models.map((preset, index) => {
3787
3876
  const isCurrent = preset.id === this.sessionState.model;
3788
3877
  const isLatest = preset.id === option.latestModel;
3789
3878
  const latestBadge = isLatest ? theme.success(' ★ LATEST') : '';
@@ -3794,11 +3883,23 @@ export class InteractiveShell {
3794
3883
  }),
3795
3884
  'Type the number of the model to select it, type "back" to change provider, or type "cancel".',
3796
3885
  ];
3797
- display.showSystemMessage(lines.join('\n'));
3798
- this.pendingInteraction = { type: 'model', provider: option.provider, options: allModels };
3886
+ this.showInlineMenu('Model selection', lines, 'info', hint ?? undefined);
3887
+ }
3888
+ buildProviderContext(provider, models) {
3889
+ return {
3890
+ provider,
3891
+ label: this.providerLabel(provider),
3892
+ modelCount: models.length,
3893
+ latestModel: models[0]?.id,
3894
+ discoveredModels: [],
3895
+ };
3799
3896
  }
3800
3897
  showSecretsMenu() {
3801
3898
  const definitions = listSecretDefinitions();
3899
+ this.pendingInteraction = { type: 'secret-select', options: definitions };
3900
+ this.renderSecretsMenu(definitions);
3901
+ }
3902
+ renderSecretsMenu(definitions, hint) {
3802
3903
  const lines = [
3803
3904
  theme.bold('Manage Secrets:'),
3804
3905
  ...definitions.map((definition, index) => {
@@ -3810,8 +3911,18 @@ export class InteractiveShell {
3810
3911
  }),
3811
3912
  'Enter the number to update a key, or type "cancel".',
3812
3913
  ];
3813
- display.showSystemMessage(lines.join('\n'));
3814
- this.pendingInteraction = { type: 'secret-select', options: definitions };
3914
+ this.showInlineMenu('Secrets', lines, 'info', hint);
3915
+ }
3916
+ renderSecretInput(secret, hint) {
3917
+ const value = getSecretValue(secret.id);
3918
+ const status = value ? maskSecret(value) : theme.warning('not set');
3919
+ const providers = secret.providers.map((id) => this.providerLabel(id)).join(', ');
3920
+ const lines = [
3921
+ `${secret.label} (${providers})`,
3922
+ `Current: ${status}`,
3923
+ 'Enter a new value or type "cancel".',
3924
+ ];
3925
+ this.showInlineMenu('Update secret', lines, 'info', hint);
3815
3926
  }
3816
3927
  showToolsMenu() {
3817
3928
  const options = getToolToggleOptions();
@@ -4191,26 +4302,23 @@ export class InteractiveShell {
4191
4302
  }
4192
4303
  const trimmed = input.trim();
4193
4304
  if (!trimmed) {
4194
- display.showWarning('Enter a number or type cancel.');
4195
- this.terminalInput.render();
4305
+ this.renderProviderMenu(pending.options, 'Enter a number or type cancel.');
4196
4306
  return;
4197
4307
  }
4198
4308
  if (trimmed.toLowerCase() === 'cancel') {
4199
4309
  this.pendingInteraction = null;
4200
- display.showInfo('Model selection cancelled.');
4201
- this.terminalInput.render();
4310
+ this.clearInlineMenu();
4311
+ this.updateStatusMessage('Model selection cancelled.');
4202
4312
  return;
4203
4313
  }
4204
4314
  const choice = Number.parseInt(trimmed, 10);
4205
4315
  if (!Number.isFinite(choice)) {
4206
- display.showWarning('Please enter a valid number.');
4207
- this.terminalInput.render();
4316
+ this.renderProviderMenu(pending.options, 'Please enter a valid number.');
4208
4317
  return;
4209
4318
  }
4210
4319
  const option = pending.options[choice - 1];
4211
4320
  if (!option) {
4212
- display.showWarning('That option is not available.');
4213
- this.terminalInput.render();
4321
+ this.renderProviderMenu(pending.options, 'That option is not available.');
4214
4322
  return;
4215
4323
  }
4216
4324
  this.showProviderModels(option);
@@ -4221,10 +4329,10 @@ export class InteractiveShell {
4221
4329
  if (!pending || pending.type !== 'model') {
4222
4330
  return;
4223
4331
  }
4332
+ const providerContext = this.buildProviderContext(pending.provider, pending.options);
4224
4333
  const trimmed = input.trim();
4225
4334
  if (!trimmed) {
4226
- display.showWarning('Enter a number, type "back", or type "cancel".');
4227
- this.terminalInput.render();
4335
+ this.renderModelSelection(pending.options, providerContext, 'Enter a number, type "back", or type "cancel".');
4228
4336
  return;
4229
4337
  }
4230
4338
  if (trimmed.toLowerCase() === 'back') {
@@ -4234,20 +4342,18 @@ export class InteractiveShell {
4234
4342
  }
4235
4343
  if (trimmed.toLowerCase() === 'cancel') {
4236
4344
  this.pendingInteraction = null;
4237
- display.showInfo('Model selection cancelled.');
4238
- this.terminalInput.render();
4345
+ this.clearInlineMenu();
4346
+ this.updateStatusMessage('Model selection cancelled.');
4239
4347
  return;
4240
4348
  }
4241
4349
  const choice = Number.parseInt(trimmed, 10);
4242
4350
  if (!Number.isFinite(choice)) {
4243
- display.showWarning('Please enter a valid number.');
4244
- this.terminalInput.render();
4351
+ this.renderModelSelection(pending.options, providerContext, 'Please enter a valid number.');
4245
4352
  return;
4246
4353
  }
4247
4354
  const preset = pending.options[choice - 1];
4248
4355
  if (!preset) {
4249
- display.showWarning('That option is not available.');
4250
- this.terminalInput.render();
4356
+ this.renderModelSelection(pending.options, providerContext, 'That option is not available.');
4251
4357
  return;
4252
4358
  }
4253
4359
  this.pendingInteraction = null;
@@ -4271,11 +4377,14 @@ export class InteractiveShell {
4271
4377
  };
4272
4378
  this.applyPresetReasoningDefaults();
4273
4379
  if (this.rebuildAgent()) {
4274
- display.showInfo(`Switched to ${preset.label}.`);
4380
+ this.showInlineMenu('Model updated', [`Switched to ${preset.label}.`], 'success');
4275
4381
  this.refreshBannerSessionInfo();
4276
4382
  this.persistSessionPreference();
4277
4383
  this.resetChatBoxAfterModelSwap();
4278
4384
  }
4385
+ else {
4386
+ this.showInlineMenu('Model updated', [`Using ${preset.label}.`], 'info');
4387
+ }
4279
4388
  }
4280
4389
  async handleSecretSelection(input) {
4281
4390
  const pending = this.pendingInteraction;
@@ -4284,30 +4393,27 @@ export class InteractiveShell {
4284
4393
  }
4285
4394
  const trimmed = input.trim();
4286
4395
  if (!trimmed) {
4287
- display.showWarning('Enter a number or type cancel.');
4288
- this.terminalInput.render();
4396
+ this.renderSecretsMenu(pending.options, 'Enter a number or type cancel.');
4289
4397
  return;
4290
4398
  }
4291
4399
  if (trimmed.toLowerCase() === 'cancel') {
4292
4400
  this.pendingInteraction = null;
4293
- display.showInfo('Secret management cancelled.');
4294
- this.terminalInput.render();
4401
+ this.clearInlineMenu();
4402
+ this.updateStatusMessage('Secret management cancelled.');
4295
4403
  return;
4296
4404
  }
4297
4405
  const choice = Number.parseInt(trimmed, 10);
4298
4406
  if (!Number.isFinite(choice)) {
4299
- display.showWarning('Please enter a valid number.');
4300
- this.terminalInput.render();
4407
+ this.renderSecretsMenu(pending.options, 'Please enter a valid number.');
4301
4408
  return;
4302
4409
  }
4303
4410
  const secret = pending.options[choice - 1];
4304
4411
  if (!secret) {
4305
- display.showWarning('That option is not available.');
4306
- this.terminalInput.render();
4412
+ this.renderSecretsMenu(pending.options, 'That option is not available.');
4307
4413
  return;
4308
4414
  }
4309
- display.showSystemMessage(`Enter a new value for ${secret.label} or type "cancel".`);
4310
4415
  this.pendingInteraction = { type: 'secret-input', secret };
4416
+ this.renderSecretInput(secret);
4311
4417
  this.terminalInput.render();
4312
4418
  }
4313
4419
  async handleSecretInput(input) {
@@ -4317,20 +4423,20 @@ export class InteractiveShell {
4317
4423
  }
4318
4424
  const trimmed = input.trim();
4319
4425
  if (!trimmed) {
4320
- display.showWarning('Enter a value or type cancel.');
4321
- this.terminalInput.render();
4426
+ this.renderSecretInput(pending.secret, 'Enter a value or type cancel.');
4322
4427
  return;
4323
4428
  }
4324
4429
  if (trimmed.toLowerCase() === 'cancel') {
4325
4430
  this.pendingInteraction = null;
4326
4431
  this.pendingSecretRetry = null;
4327
- display.showInfo('Secret unchanged.');
4432
+ this.clearInlineMenu();
4433
+ this.updateStatusMessage('Secret unchanged.');
4328
4434
  this.terminalInput.render();
4329
4435
  return;
4330
4436
  }
4331
4437
  try {
4332
4438
  setSecretValue(pending.secret.id, trimmed);
4333
- display.showInfo(`${pending.secret.label} updated.`);
4439
+ this.showInlineMenu('Secret updated', [`${pending.secret.label} saved.`], 'success');
4334
4440
  this.pendingInteraction = null;
4335
4441
  const deferred = this.pendingSecretRetry;
4336
4442
  this.pendingSecretRetry = null;
@@ -4345,7 +4451,7 @@ export class InteractiveShell {
4345
4451
  }
4346
4452
  catch (error) {
4347
4453
  const message = error instanceof Error ? error.message : String(error);
4348
- display.showError(message);
4454
+ this.showInlineMenu('Secret error', [message], 'error');
4349
4455
  this.pendingInteraction = null;
4350
4456
  this.pendingSecretRetry = null;
4351
4457
  }
@@ -4364,6 +4470,7 @@ export class InteractiveShell {
4364
4470
  if (!agent) {
4365
4471
  return;
4366
4472
  }
4473
+ this.logUserPrompt(request);
4367
4474
  this.isProcessing = true;
4368
4475
  this.uiUpdates.setMode('processing');
4369
4476
  this.terminalInput.setStreaming(true);
@@ -4383,7 +4490,8 @@ export class InteractiveShell {
4383
4490
  let responseText = '';
4384
4491
  try {
4385
4492
  // Start streaming - no header needed, the input area already provides context
4386
- this.startStreamingHeartbeat('Streaming response');
4493
+ const heartbeatLabel = this.batchedOutputMode ? 'Processing response' : 'Streaming response';
4494
+ this.startStreamingHeartbeat(heartbeatLabel);
4387
4495
  responseText = await agent.send(request, true);
4388
4496
  await this.awaitPendingCleanup();
4389
4497
  this.captureHistorySnapshot();
@@ -4499,7 +4607,8 @@ export class InteractiveShell {
4499
4607
  this.uiAdapter.startProcessing('Continuous execution mode');
4500
4608
  this.setProcessingStatus();
4501
4609
  // No streaming header - just start streaming directly
4502
- this.startStreamingHeartbeat('Streaming');
4610
+ const continuousLabel = this.batchedOutputMode ? 'Processing' : 'Streaming';
4611
+ this.startStreamingHeartbeat(continuousLabel);
4503
4612
  let iteration = 0;
4504
4613
  let lastResponse = '';
4505
4614
  let consecutiveNoProgress = 0;
@@ -4527,9 +4636,10 @@ When truly finished with ALL tasks, explicitly state "TASK_FULLY_COMPLETE".`;
4527
4636
  display.showSystemMessage(`\n📍 Iteration ${iteration}/${MAX_ITERATIONS}`);
4528
4637
  this.updateStatusMessage(`Working on iteration ${iteration}...`);
4529
4638
  try {
4530
- // Send the request and capture the response (streaming disabled)
4639
+ // Send the request and capture the response as a single block
4531
4640
  display.showThinking('Responding...');
4532
4641
  this.refreshStatusLine(true);
4642
+ this.resetStreamBuffer();
4533
4643
  const response = await agent.send(currentPrompt, true);
4534
4644
  await this.awaitPendingCleanup();
4535
4645
  this.captureHistorySnapshot();
@@ -5030,6 +5140,7 @@ What's the next action?`;
5030
5140
  // Send the error to the agent for fixing
5031
5141
  display.showThinking('Analyzing build errors');
5032
5142
  this.refreshStatusLine(true);
5143
+ this.resetStreamBuffer();
5033
5144
  const response = await this.agent.send(prompt, true);
5034
5145
  display.stopThinking();
5035
5146
  this.refreshStatusLine(true);
@@ -5059,27 +5170,33 @@ What's the next action?`;
5059
5170
  };
5060
5171
  this.agent = this.runtimeSession.createAgent(selection, {
5061
5172
  onStreamChunk: (chunk) => {
5173
+ if (this.batchedOutputMode) {
5174
+ this.captureStreamChunk(chunk);
5175
+ return;
5176
+ }
5062
5177
  // Stream output using clean streamContent() - chat box floats below
5063
5178
  this.terminalInput.streamContent(chunk);
5064
5179
  },
5065
5180
  onStreamFallback: (info) => this.handleStreamingFallback(info),
5066
5181
  onAssistantMessage: (content, metadata) => {
5067
5182
  const enriched = this.buildDisplayMetadata(metadata);
5183
+ const alreadyStreamedToUi = !this.batchedOutputMode && metadata.wasStreamed;
5184
+ const bufferedContent = this.consumeStreamBuffer(content);
5068
5185
  // Update spinner based on message type
5069
5186
  if (metadata.isFinal) {
5070
5187
  // Skip display if content was already streamed to avoid double-display
5071
- if (!metadata.wasStreamed) {
5072
- const parsed = this.splitThinkingResponse(content);
5188
+ if (!alreadyStreamedToUi) {
5189
+ const parsed = this.splitThinkingResponse(bufferedContent);
5073
5190
  if (parsed?.thinking) {
5074
5191
  const summary = this.extractThoughtSummary(parsed.thinking);
5075
5192
  if (summary) {
5076
5193
  display.updateThinking(`💭 ${summary}`);
5077
5194
  }
5078
- display.showAssistantMessage(parsed.thinking, { ...enriched, isFinal: false });
5195
+ display.showThinkingBlock(parsed.thinking, enriched.elapsedMs ?? this.currentRunElapsedMs());
5079
5196
  }
5080
- const finalContent = parsed?.response?.trim() || content;
5197
+ const finalContent = parsed?.response?.trim() || bufferedContent.trim();
5081
5198
  if (finalContent) {
5082
- display.showAssistantMessage(finalContent, enriched);
5199
+ display.showAssistantMessage(finalContent, { ...enriched, isFinal: true });
5083
5200
  }
5084
5201
  }
5085
5202
  // Status shown in mode controls bar - no separate status line needed
@@ -5101,8 +5218,13 @@ What's the next action?`;
5101
5218
  // Stop spinner and show the narrative text directly
5102
5219
  display.stopThinking();
5103
5220
  // Skip display if content was already streamed to avoid double-display
5104
- if (!metadata.wasStreamed) {
5105
- display.showNarrative(content.trim());
5221
+ if (!alreadyStreamedToUi) {
5222
+ const parsed = this.splitThinkingResponse(bufferedContent);
5223
+ const thoughtContent = parsed?.thinking ?? parsed?.response ?? bufferedContent;
5224
+ const trimmed = thoughtContent.trim();
5225
+ if (trimmed) {
5226
+ display.showThinkingBlock(trimmed, enriched.elapsedMs ?? this.currentRunElapsedMs());
5227
+ }
5106
5228
  }
5107
5229
  // The isProcessing flag already shows "⏳ Processing..." - no need for duplicate status
5108
5230
  this.requestPromptRefresh();
@@ -5304,15 +5426,17 @@ What's the next action?`;
5304
5426
  };
5305
5427
  // Always update context usage in the UI
5306
5428
  const percentUsed = Math.round(usageRatio * 100);
5307
- this.updateContextUsage(percentUsed);
5429
+ this.updateContextUsage(percentUsed, CONTEXT_AUTOCOMPACT_PERCENT);
5430
+ this.maybeShowContextWarning(percentUsed);
5308
5431
  this.refreshStatusLine(true);
5309
- if (usageRatio < CONTEXT_USAGE_THRESHOLD) {
5432
+ if (!this.agent || this.cleanupInProgress || this.contextCompactionInFlight) {
5310
5433
  return null;
5311
5434
  }
5312
- if (!this.agent || this.cleanupInProgress) {
5435
+ const trigger = this.shouldAutoCompactContext(usageRatio, windowTokens, total);
5436
+ if (!trigger) {
5313
5437
  return null;
5314
5438
  }
5315
- return this.runContextCleanup(windowTokens, total);
5439
+ return this.runContextCleanup(windowTokens, total, trigger);
5316
5440
  }
5317
5441
  totalTokens(usage) {
5318
5442
  if (!usage) {
@@ -5326,52 +5450,204 @@ What's the next action?`;
5326
5450
  const sum = input + output;
5327
5451
  return sum > 0 ? sum : null;
5328
5452
  }
5329
- async runContextCleanup(windowTokens, totalTokens) {
5330
- if (!this.agent) {
5453
+ async runContextCleanup(windowTokens, totalTokens, trigger) {
5454
+ const agent = this.agent;
5455
+ if (!agent) {
5456
+ return;
5457
+ }
5458
+ const contextManager = agent.getContextManager();
5459
+ if (!contextManager) {
5460
+ return;
5461
+ }
5462
+ const history = agent.getHistory();
5463
+ if (history.length <= 1) {
5331
5464
  return;
5332
5465
  }
5333
5466
  this.cleanupInProgress = true;
5467
+ this.contextCompactionInFlight = true;
5334
5468
  const cleanupStatusId = 'context-cleanup';
5335
5469
  let cleanupOverlayActive = false;
5336
5470
  try {
5337
- const history = this.agent.getHistory();
5338
- const { system, conversation } = this.partitionHistory(history);
5339
- if (!conversation.length) {
5340
- return;
5341
- }
5342
- const preserveCount = Math.min(conversation.length, CONTEXT_RECENT_MESSAGE_COUNT);
5343
- const preserved = conversation.slice(conversation.length - preserveCount);
5344
- const toSummarize = conversation.slice(0, conversation.length - preserveCount);
5345
- if (!toSummarize.length) {
5346
- return;
5471
+ const percentUsed = Math.round((totalTokens / windowTokens) * 100);
5472
+ const statusDetailParts = [`${percentUsed}% full`];
5473
+ if (trigger?.reason) {
5474
+ statusDetailParts.push(trigger.reason);
5347
5475
  }
5348
- cleanupOverlayActive = true;
5349
5476
  this.statusTracker.pushOverride(cleanupStatusId, 'Running context cleanup', {
5350
- detail: `Summarizing ${toSummarize.length} earlier messages`,
5477
+ detail: statusDetailParts.join(' · '),
5351
5478
  tone: 'warning',
5352
5479
  });
5353
- const percentUsed = Math.round((totalTokens / windowTokens) * 100);
5354
- // Update context usage in unified UI
5355
- this.updateContextUsage(percentUsed);
5480
+ cleanupOverlayActive = true;
5481
+ const triggerReason = trigger?.reason ?? 'Context optimization';
5356
5482
  display.showSystemMessage([
5357
5483
  `Context usage: ${totalTokens.toLocaleString('en-US')} of ${windowTokens.toLocaleString('en-US')} tokens`,
5358
- `(${percentUsed}% full). Running automatic cleanup...`,
5484
+ `(${percentUsed}% full). Auto-compacting${triggerReason ? `: ${triggerReason}` : '...'}`,
5359
5485
  ].join(' '));
5360
- const summary = await this.buildContextSummary(toSummarize);
5361
- if (!summary) {
5362
- throw new Error('Summary could not be generated.');
5486
+ const beforeStats = contextManager.getStats(history);
5487
+ const result = await contextManager.intelligentCompact(history);
5488
+ const afterStats = contextManager.getStats(result.compacted);
5489
+ const tokenSavings = Math.max(0, beforeStats.totalTokens - afterStats.totalTokens);
5490
+ const percentSavings = beforeStats.totalTokens > 0 ? (tokenSavings / beforeStats.totalTokens) * 100 : 0;
5491
+ const changed = this.historiesDiffer(history, result.compacted);
5492
+ const meaningfulSavings = changed &&
5493
+ (tokenSavings >= MIN_COMPACTION_TOKEN_SAVINGS || percentSavings >= MIN_COMPACTION_PERCENT_SAVINGS);
5494
+ if (!meaningfulSavings) {
5495
+ if (trigger?.forced || percentUsed >= 85) {
5496
+ display.showInfo('Auto-compaction completed but did not meaningfully reduce context size. Keeping existing history.');
5497
+ }
5498
+ return;
5363
5499
  }
5364
- const trimmed = this.buildTrimmedHistory(system, summary, preserved);
5365
- this.agent.loadHistory(trimmed);
5366
- display.showSystemMessage(`Context cleanup complete. Summarized ${toSummarize.length} earlier messages and preserved the latest ${preserved.length}.`);
5500
+ agent.loadHistory(result.compacted);
5501
+ this.cachedHistory = result.compacted;
5502
+ const newPercentUsed = windowTokens > 0
5503
+ ? Math.round((afterStats.totalTokens / windowTokens) * 100)
5504
+ : afterStats.percentage;
5505
+ this.latestTokenUsage = {
5506
+ used: afterStats.totalTokens,
5507
+ limit: windowTokens,
5508
+ };
5509
+ this.updateContextUsage(newPercentUsed, CONTEXT_AUTOCOMPACT_PERCENT);
5510
+ this.lastContextWarningLevel = this.getContextWarningLevel(newPercentUsed);
5511
+ this.refreshStatusLine(true);
5512
+ const primarySignal = result.analysis.signals[0]?.reason ?? triggerReason;
5513
+ display.showSystemMessage([
5514
+ `Context compacted: ${beforeStats.percentage}% → ${afterStats.percentage}%`,
5515
+ `(saved ~${tokenSavings.toLocaleString('en-US')} tokens).`,
5516
+ primarySignal ? `Reason: ${primarySignal}.` : '',
5517
+ ].filter(Boolean).join(' '));
5518
+ }
5519
+ catch (error) {
5520
+ display.showError('Context compaction failed.', error);
5367
5521
  }
5368
5522
  finally {
5369
5523
  if (cleanupOverlayActive) {
5370
5524
  this.statusTracker.clearOverride(cleanupStatusId);
5371
5525
  }
5372
5526
  this.cleanupInProgress = false;
5527
+ this.contextCompactionInFlight = false;
5373
5528
  }
5374
5529
  }
5530
+ shouldAutoCompactContext(usageRatio, windowTokens, totalTokens) {
5531
+ const featureFlags = loadFeatureFlags();
5532
+ if (featureFlags.autoCompact === false) {
5533
+ return null;
5534
+ }
5535
+ const agent = this.agent;
5536
+ if (!agent) {
5537
+ return null;
5538
+ }
5539
+ const contextManager = agent.getContextManager();
5540
+ if (!contextManager) {
5541
+ return null;
5542
+ }
5543
+ const history = agent.getHistory();
5544
+ if (history.length <= 1) {
5545
+ return null;
5546
+ }
5547
+ const percentUsed = Math.round(usageRatio * 100);
5548
+ const analysis = contextManager.shouldTriggerCompaction(history);
5549
+ const overflowRisk = this.hasContextOverflowRisk(contextManager);
5550
+ if (usageRatio >= CONTEXT_USAGE_THRESHOLD) {
5551
+ return { reason: analysis.reason ?? `Context usage at ${percentUsed}%`, forced: true };
5552
+ }
5553
+ if (usageRatio >= CONTEXT_AUTOCOMPACT_FLOOR && (analysis.shouldCompact || overflowRisk)) {
5554
+ const reason = analysis.reason
5555
+ ?? (overflowRisk ? 'Heavy tool output detected' : 'Compaction signals detected');
5556
+ return { reason, forced: false };
5557
+ }
5558
+ return null;
5559
+ }
5560
+ hasContextOverflowRisk(contextManager) {
5561
+ if (!contextManager?.detectContextOverflowRisk) {
5562
+ return false;
5563
+ }
5564
+ try {
5565
+ const toolHistory = this.runtimeSession.toolRuntime.getToolHistory?.();
5566
+ if (!toolHistory?.length) {
5567
+ return false;
5568
+ }
5569
+ const serialized = toolHistory.map((entry) => {
5570
+ const args = entry.args ?? {};
5571
+ const argsText = Object.keys(args).length ? ` ${JSON.stringify(args)}` : '';
5572
+ return `${entry.toolName}${argsText}`;
5573
+ });
5574
+ return contextManager.detectContextOverflowRisk(serialized);
5575
+ }
5576
+ catch {
5577
+ return false;
5578
+ }
5579
+ }
5580
+ getContextWarningLevel(percentage) {
5581
+ if (percentage >= 90) {
5582
+ return 'danger';
5583
+ }
5584
+ if (percentage >= 70) {
5585
+ return 'warning';
5586
+ }
5587
+ if (percentage >= 50) {
5588
+ return 'info';
5589
+ }
5590
+ return null;
5591
+ }
5592
+ maybeShowContextWarning(percentage) {
5593
+ const level = this.getContextWarningLevel(percentage);
5594
+ if (level === this.lastContextWarningLevel) {
5595
+ if (level === null) {
5596
+ this.lastContextWarningLevel = null;
5597
+ }
5598
+ return;
5599
+ }
5600
+ this.lastContextWarningLevel = level;
5601
+ switch (level) {
5602
+ case 'info':
5603
+ display.showInfo('Context usage is climbing. Auto-compaction will engage if it keeps growing.');
5604
+ break;
5605
+ case 'warning':
5606
+ display.showWarning('Context is getting full. Auto-compaction will try to preserve space.');
5607
+ break;
5608
+ case 'danger':
5609
+ display.showWarning('Context is near capacity. Attempting automatic compaction.');
5610
+ break;
5611
+ default:
5612
+ break;
5613
+ }
5614
+ }
5615
+ historiesDiffer(original, compacted) {
5616
+ if (original.length !== compacted.length) {
5617
+ return true;
5618
+ }
5619
+ for (let i = 0; i < original.length; i++) {
5620
+ const a = original[i];
5621
+ const b = compacted[i];
5622
+ if (a.role !== b.role) {
5623
+ return true;
5624
+ }
5625
+ if ((a.content ?? '') !== (b.content ?? '')) {
5626
+ return true;
5627
+ }
5628
+ const aName = 'name' in a ? a.name : undefined;
5629
+ const bName = 'name' in b ? b.name : undefined;
5630
+ if ((aName ?? '') !== (bName ?? '')) {
5631
+ return true;
5632
+ }
5633
+ const aToolCallId = 'toolCallId' in a ? a.toolCallId : undefined;
5634
+ const bToolCallId = 'toolCallId' in b ? b.toolCallId : undefined;
5635
+ if ((aToolCallId ?? '') !== (bToolCallId ?? '')) {
5636
+ return true;
5637
+ }
5638
+ const aTools = 'toolCalls' in a ? a.toolCalls ?? [] : [];
5639
+ const bTools = 'toolCalls' in b ? b.toolCalls ?? [] : [];
5640
+ if (aTools.length !== bTools.length) {
5641
+ return true;
5642
+ }
5643
+ for (let j = 0; j < aTools.length; j++) {
5644
+ if (JSON.stringify(aTools[j]) !== JSON.stringify(bTools[j])) {
5645
+ return true;
5646
+ }
5647
+ }
5648
+ }
5649
+ return false;
5650
+ }
5375
5651
  partitionHistory(history) {
5376
5652
  const system = [];
5377
5653
  const conversation = [];
@@ -5507,7 +5783,12 @@ What's the next action?`;
5507
5783
  const reason = info.reason ? ` (${info.reason.replace(/-/g, ' ')})` : '';
5508
5784
  const partialNote = info.partialResponse ? ' Received partial stream before failure.' : '';
5509
5785
  display.showWarning(`Streaming failed${reason}, retrying without streaming.${detail}${partialNote}`);
5510
- this.startStreamingHeartbeat('Fallback in progress');
5786
+ const bufferedPartial = this.consumeStreamBuffer(info.partialResponse ?? '');
5787
+ if (this.batchedOutputMode && bufferedPartial.trim()) {
5788
+ display.showThinkingBlock(bufferedPartial.trim(), this.currentRunElapsedMs());
5789
+ }
5790
+ const fallbackLabel = this.batchedOutputMode ? 'Retrying (batched)' : 'Fallback in progress';
5791
+ this.startStreamingHeartbeat(fallbackLabel);
5511
5792
  this.requestPromptRefresh(true);
5512
5793
  }
5513
5794
  handleProviderError(error, retryAction) {
@@ -5518,9 +5799,9 @@ What's the next action?`;
5518
5799
  this.handleApiKeyIssue(apiKeyIssue, retryAction);
5519
5800
  return true;
5520
5801
  }
5521
- handleApiKeyIssue(info, retryAction) {
5802
+ handleApiKeyIssue(info, retryAction, contextLabel) {
5522
5803
  const secret = info.secret ?? null;
5523
- const providerLabel = info.provider ? this.providerLabel(info.provider) : 'the selected provider';
5804
+ const providerLabel = contextLabel ?? (info.provider ? this.providerLabel(info.provider) : 'the selected provider');
5524
5805
  if (!secret) {
5525
5806
  this.pendingSecretRetry = null;
5526
5807
  const guidance = 'Run "/secrets" to configure the required API key or export it (e.g., EXPORT KEY=value) before launching the CLI.';
@@ -5542,6 +5823,10 @@ What's the next action?`;
5542
5823
  this.pendingInteraction = { type: 'secret-input', secret };
5543
5824
  this.showSecretGuidance(secret, isMissing);
5544
5825
  }
5826
+ handleToolApiKeyIssue(info, call) {
5827
+ const label = call?.name ? `${call.name} tool` : 'web tools';
5828
+ this.handleApiKeyIssue(info, undefined, label);
5829
+ }
5545
5830
  showSecretGuidance(secret, promptForInput) {
5546
5831
  const lines = [];
5547
5832
  if (promptForInput) {
@@ -5551,7 +5836,7 @@ What's the next action?`;
5551
5836
  lines.push(`Update the stored value for ${secret.label} or type "cancel".`);
5552
5837
  }
5553
5838
  lines.push(`Tip: run "/secrets" anytime to manage credentials or export ${secret.envVar}=<value> before launching the CLI.`);
5554
- display.showSystemMessage(lines.join('\n'));
5839
+ this.showInlineMenu('Secrets', lines, promptForInput ? 'warning' : 'info');
5555
5840
  }
5556
5841
  colorizeDropdownLine(text, index) {
5557
5842
  if (!DROPDOWN_COLORS.length) {