erosolar-cli 1.7.383 → 1.7.385

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.
@@ -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;
@@ -116,6 +119,9 @@ export class InteractiveShell {
116
119
  isDrainingQueue = false;
117
120
  activeContextWindowTokens = null;
118
121
  latestTokenUsage = { used: null, limit: null };
122
+ planApprovalBridgeRegistered = false;
123
+ contextCompactionInFlight = false;
124
+ lastContextWarningLevel = null;
119
125
  sessionPreferences;
120
126
  autosaveEnabled;
121
127
  autoContinueEnabled;
@@ -255,6 +261,11 @@ export class InteractiveShell {
255
261
  this.refreshContextGauge();
256
262
  // Start terminal input (sets up handlers)
257
263
  this.terminalInput.start();
264
+ // Allow planning tools (e.g., ProposePlan) to open the interactive approval UI just like Codex CLI
265
+ this.registerPlanApprovalBridge();
266
+ // Capture display output into the scrollback/chat log so system messages
267
+ // (e.g., /evolve) render above the pinned chat box.
268
+ this.terminalInput.registerOutputInterceptor(display);
258
269
  // Set up command autocomplete with all slash commands
259
270
  this.setupCommandAutocomplete();
260
271
  // Enter alternate screen buffer when enabled, otherwise clear the main screen for layout
@@ -505,15 +516,9 @@ export class InteractiveShell {
505
516
  this.editGuardMode = mode;
506
517
  this.pendingPermissionInput = null;
507
518
  if (mode === 'plan') {
508
- // Register plan approval callback for interactive UI
509
- setPlanApprovalCallback((steps, explanation) => {
510
- this.showPlanApproval(steps, explanation);
511
- });
512
519
  display.showSystemMessage('📋 Plan mode enabled. AI will create a plan and ask for approval before implementing.');
513
520
  }
514
521
  else {
515
- // Unregister callback when not in plan mode
516
- setPlanApprovalCallback(null);
517
522
  if (mode === 'ask-permission') {
518
523
  display.showSystemMessage('🛡️ Ask-to-edit mode enabled. Confirm each request before sending.');
519
524
  }
@@ -729,6 +734,8 @@ export class InteractiveShell {
729
734
  if (this.alternateScreenEnabled) {
730
735
  this.terminalInput.exitAlternateScreen();
731
736
  }
737
+ // Unregister plan approval bridge
738
+ setPlanApprovalCallback(null);
732
739
  // Dispose terminal input handler
733
740
  this.terminalInput.dispose();
734
741
  // Dispose unified UI adapter
@@ -752,6 +759,18 @@ export class InteractiveShell {
752
759
  // Write directly to stdout after exiting alternate screen to preserve the transcript
753
760
  process.stdout.write(`\n${separator}\n${header}\n${transcript}\n${separator}\n`);
754
761
  }
762
+ /**
763
+ * Wire the planning tool suite to the interactive plan approval UI so ProposePlan behaves like Codex CLI.
764
+ */
765
+ registerPlanApprovalBridge() {
766
+ if (this.planApprovalBridgeRegistered) {
767
+ return;
768
+ }
769
+ setPlanApprovalCallback((steps, explanation) => {
770
+ this.showPlanApproval(steps, explanation);
771
+ });
772
+ this.planApprovalBridgeRegistered = true;
773
+ }
755
774
  /**
756
775
  * Update status bar message
757
776
  */
@@ -1105,9 +1124,9 @@ export class InteractiveShell {
1105
1124
  };
1106
1125
  }
1107
1126
  }
1108
- updateContextUsage(percentage) {
1127
+ updateContextUsage(percentage, autoCompactThreshold = CONTEXT_AUTOCOMPACT_PERCENT) {
1109
1128
  this.uiAdapter.updateContextUsage(percentage);
1110
- this.terminalInput.setContextUsage(percentage, CONTEXT_AUTOCOMPACT_PERCENT);
1129
+ this.terminalInput.setContextUsage(percentage, autoCompactThreshold);
1111
1130
  }
1112
1131
  refreshControlBar() {
1113
1132
  this.terminalInput.setModeToggles({
@@ -1190,10 +1209,6 @@ export class InteractiveShell {
1190
1209
  if (state.detail) {
1191
1210
  parts.push(state.detail);
1192
1211
  }
1193
- const elapsedSeconds = Math.max(0, Math.floor((Date.now() - state.startedAt) / 1000));
1194
- if (elapsedSeconds > 0) {
1195
- parts.push(`${elapsedSeconds}s`);
1196
- }
1197
1212
  return parts.join(' • ');
1198
1213
  }
1199
1214
  handleSlashCommandPreviewChange() {
@@ -1324,20 +1339,11 @@ export class InteractiveShell {
1324
1339
  // Force refresh to update the input area now that streaming has ended
1325
1340
  this.refreshStatusLine(true);
1326
1341
  }
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;
1342
+ buildStreamingStatus(label, _elapsedSeconds) {
1343
+ // Model + elapsed time already live in the pinned meta header; keep the streaming
1344
+ // status focused on the activity to avoid duplicate info.
1332
1345
  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();
1346
+ return `${prefix} ${label}`.trim();
1341
1347
  }
1342
1348
  formatElapsedShort(seconds) {
1343
1349
  if (seconds < 60) {
@@ -5304,15 +5310,17 @@ What's the next action?`;
5304
5310
  };
5305
5311
  // Always update context usage in the UI
5306
5312
  const percentUsed = Math.round(usageRatio * 100);
5307
- this.updateContextUsage(percentUsed);
5313
+ this.updateContextUsage(percentUsed, CONTEXT_AUTOCOMPACT_PERCENT);
5314
+ this.maybeShowContextWarning(percentUsed);
5308
5315
  this.refreshStatusLine(true);
5309
- if (usageRatio < CONTEXT_USAGE_THRESHOLD) {
5316
+ if (!this.agent || this.cleanupInProgress || this.contextCompactionInFlight) {
5310
5317
  return null;
5311
5318
  }
5312
- if (!this.agent || this.cleanupInProgress) {
5319
+ const trigger = this.shouldAutoCompactContext(usageRatio, windowTokens, total);
5320
+ if (!trigger) {
5313
5321
  return null;
5314
5322
  }
5315
- return this.runContextCleanup(windowTokens, total);
5323
+ return this.runContextCleanup(windowTokens, total, trigger);
5316
5324
  }
5317
5325
  totalTokens(usage) {
5318
5326
  if (!usage) {
@@ -5326,51 +5334,203 @@ What's the next action?`;
5326
5334
  const sum = input + output;
5327
5335
  return sum > 0 ? sum : null;
5328
5336
  }
5329
- async runContextCleanup(windowTokens, totalTokens) {
5330
- if (!this.agent) {
5337
+ async runContextCleanup(windowTokens, totalTokens, trigger) {
5338
+ const agent = this.agent;
5339
+ if (!agent) {
5340
+ return;
5341
+ }
5342
+ const contextManager = agent.getContextManager();
5343
+ if (!contextManager) {
5344
+ return;
5345
+ }
5346
+ const history = agent.getHistory();
5347
+ if (history.length <= 1) {
5331
5348
  return;
5332
5349
  }
5333
5350
  this.cleanupInProgress = true;
5351
+ this.contextCompactionInFlight = true;
5334
5352
  const cleanupStatusId = 'context-cleanup';
5335
5353
  let cleanupOverlayActive = false;
5336
5354
  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;
5355
+ const percentUsed = Math.round((totalTokens / windowTokens) * 100);
5356
+ const statusDetailParts = [`${percentUsed}% full`];
5357
+ if (trigger?.reason) {
5358
+ statusDetailParts.push(trigger.reason);
5347
5359
  }
5348
- cleanupOverlayActive = true;
5349
5360
  this.statusTracker.pushOverride(cleanupStatusId, 'Running context cleanup', {
5350
- detail: `Summarizing ${toSummarize.length} earlier messages`,
5361
+ detail: statusDetailParts.join(' · '),
5351
5362
  tone: 'warning',
5352
5363
  });
5353
- const percentUsed = Math.round((totalTokens / windowTokens) * 100);
5354
- // Update context usage in unified UI
5355
- this.updateContextUsage(percentUsed);
5364
+ cleanupOverlayActive = true;
5365
+ const triggerReason = trigger?.reason ?? 'Context optimization';
5356
5366
  display.showSystemMessage([
5357
5367
  `Context usage: ${totalTokens.toLocaleString('en-US')} of ${windowTokens.toLocaleString('en-US')} tokens`,
5358
- `(${percentUsed}% full). Running automatic cleanup...`,
5368
+ `(${percentUsed}% full). Auto-compacting${triggerReason ? `: ${triggerReason}` : '...'}`,
5359
5369
  ].join(' '));
5360
- const summary = await this.buildContextSummary(toSummarize);
5361
- if (!summary) {
5362
- throw new Error('Summary could not be generated.');
5370
+ const beforeStats = contextManager.getStats(history);
5371
+ const result = await contextManager.intelligentCompact(history);
5372
+ const afterStats = contextManager.getStats(result.compacted);
5373
+ const tokenSavings = Math.max(0, beforeStats.totalTokens - afterStats.totalTokens);
5374
+ const percentSavings = beforeStats.totalTokens > 0 ? (tokenSavings / beforeStats.totalTokens) * 100 : 0;
5375
+ const changed = this.historiesDiffer(history, result.compacted);
5376
+ const meaningfulSavings = changed &&
5377
+ (tokenSavings >= MIN_COMPACTION_TOKEN_SAVINGS || percentSavings >= MIN_COMPACTION_PERCENT_SAVINGS);
5378
+ if (!meaningfulSavings) {
5379
+ if (trigger?.forced || percentUsed >= 85) {
5380
+ display.showInfo('Auto-compaction completed but did not meaningfully reduce context size. Keeping existing history.');
5381
+ }
5382
+ return;
5363
5383
  }
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}.`);
5384
+ agent.loadHistory(result.compacted);
5385
+ this.cachedHistory = result.compacted;
5386
+ const newPercentUsed = windowTokens > 0
5387
+ ? Math.round((afterStats.totalTokens / windowTokens) * 100)
5388
+ : afterStats.percentage;
5389
+ this.latestTokenUsage = {
5390
+ used: afterStats.totalTokens,
5391
+ limit: windowTokens,
5392
+ };
5393
+ this.updateContextUsage(newPercentUsed, CONTEXT_AUTOCOMPACT_PERCENT);
5394
+ this.lastContextWarningLevel = this.getContextWarningLevel(newPercentUsed);
5395
+ this.refreshStatusLine(true);
5396
+ const primarySignal = result.analysis.signals[0]?.reason ?? triggerReason;
5397
+ display.showSystemMessage([
5398
+ `Context compacted: ${beforeStats.percentage}% → ${afterStats.percentage}%`,
5399
+ `(saved ~${tokenSavings.toLocaleString('en-US')} tokens).`,
5400
+ primarySignal ? `Reason: ${primarySignal}.` : '',
5401
+ ].filter(Boolean).join(' '));
5402
+ }
5403
+ catch (error) {
5404
+ display.showError('Context compaction failed.', error);
5367
5405
  }
5368
5406
  finally {
5369
5407
  if (cleanupOverlayActive) {
5370
5408
  this.statusTracker.clearOverride(cleanupStatusId);
5371
5409
  }
5372
5410
  this.cleanupInProgress = false;
5411
+ this.contextCompactionInFlight = false;
5412
+ }
5413
+ }
5414
+ shouldAutoCompactContext(usageRatio, windowTokens, totalTokens) {
5415
+ const featureFlags = loadFeatureFlags();
5416
+ if (featureFlags.autoCompact === false) {
5417
+ return null;
5418
+ }
5419
+ const agent = this.agent;
5420
+ if (!agent) {
5421
+ return null;
5422
+ }
5423
+ const contextManager = agent.getContextManager();
5424
+ if (!contextManager) {
5425
+ return null;
5426
+ }
5427
+ const history = agent.getHistory();
5428
+ if (history.length <= 1) {
5429
+ return null;
5373
5430
  }
5431
+ const percentUsed = Math.round(usageRatio * 100);
5432
+ const analysis = contextManager.shouldTriggerCompaction(history);
5433
+ const overflowRisk = this.hasContextOverflowRisk(contextManager);
5434
+ if (usageRatio >= CONTEXT_USAGE_THRESHOLD) {
5435
+ return { reason: analysis.reason ?? `Context usage at ${percentUsed}%`, forced: true };
5436
+ }
5437
+ if (usageRatio >= CONTEXT_AUTOCOMPACT_FLOOR && (analysis.shouldCompact || overflowRisk)) {
5438
+ const reason = analysis.reason
5439
+ ?? (overflowRisk ? 'Heavy tool output detected' : 'Compaction signals detected');
5440
+ return { reason, forced: false };
5441
+ }
5442
+ return null;
5443
+ }
5444
+ hasContextOverflowRisk(contextManager) {
5445
+ if (!contextManager?.detectContextOverflowRisk) {
5446
+ return false;
5447
+ }
5448
+ try {
5449
+ const toolHistory = this.runtimeSession.toolRuntime.getToolHistory?.();
5450
+ if (!toolHistory?.length) {
5451
+ return false;
5452
+ }
5453
+ const serialized = toolHistory.map((entry) => {
5454
+ const args = entry.args ?? {};
5455
+ const argsText = Object.keys(args).length ? ` ${JSON.stringify(args)}` : '';
5456
+ return `${entry.toolName}${argsText}`;
5457
+ });
5458
+ return contextManager.detectContextOverflowRisk(serialized);
5459
+ }
5460
+ catch {
5461
+ return false;
5462
+ }
5463
+ }
5464
+ getContextWarningLevel(percentage) {
5465
+ if (percentage >= 90) {
5466
+ return 'danger';
5467
+ }
5468
+ if (percentage >= 70) {
5469
+ return 'warning';
5470
+ }
5471
+ if (percentage >= 50) {
5472
+ return 'info';
5473
+ }
5474
+ return null;
5475
+ }
5476
+ maybeShowContextWarning(percentage) {
5477
+ const level = this.getContextWarningLevel(percentage);
5478
+ if (level === this.lastContextWarningLevel) {
5479
+ if (level === null) {
5480
+ this.lastContextWarningLevel = null;
5481
+ }
5482
+ return;
5483
+ }
5484
+ this.lastContextWarningLevel = level;
5485
+ switch (level) {
5486
+ case 'info':
5487
+ display.showInfo('Context usage is climbing. Auto-compaction will engage if it keeps growing.');
5488
+ break;
5489
+ case 'warning':
5490
+ display.showWarning('Context is getting full. Auto-compaction will try to preserve space.');
5491
+ break;
5492
+ case 'danger':
5493
+ display.showWarning('Context is near capacity. Attempting automatic compaction.');
5494
+ break;
5495
+ default:
5496
+ break;
5497
+ }
5498
+ }
5499
+ historiesDiffer(original, compacted) {
5500
+ if (original.length !== compacted.length) {
5501
+ return true;
5502
+ }
5503
+ for (let i = 0; i < original.length; i++) {
5504
+ const a = original[i];
5505
+ const b = compacted[i];
5506
+ if (a.role !== b.role) {
5507
+ return true;
5508
+ }
5509
+ if ((a.content ?? '') !== (b.content ?? '')) {
5510
+ return true;
5511
+ }
5512
+ const aName = 'name' in a ? a.name : undefined;
5513
+ const bName = 'name' in b ? b.name : undefined;
5514
+ if ((aName ?? '') !== (bName ?? '')) {
5515
+ return true;
5516
+ }
5517
+ const aToolCallId = 'toolCallId' in a ? a.toolCallId : undefined;
5518
+ const bToolCallId = 'toolCallId' in b ? b.toolCallId : undefined;
5519
+ if ((aToolCallId ?? '') !== (bToolCallId ?? '')) {
5520
+ return true;
5521
+ }
5522
+ const aTools = 'toolCalls' in a ? a.toolCalls ?? [] : [];
5523
+ const bTools = 'toolCalls' in b ? b.toolCalls ?? [] : [];
5524
+ if (aTools.length !== bTools.length) {
5525
+ return true;
5526
+ }
5527
+ for (let j = 0; j < aTools.length; j++) {
5528
+ if (JSON.stringify(aTools[j]) !== JSON.stringify(bTools[j])) {
5529
+ return true;
5530
+ }
5531
+ }
5532
+ }
5533
+ return false;
5374
5534
  }
5375
5535
  partitionHistory(history) {
5376
5536
  const system = [];