erosolar-cli 1.7.411 โ†’ 1.7.413

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 (47) hide show
  1. package/dist/core/contextManager.d.ts +4 -2
  2. package/dist/core/contextManager.d.ts.map +1 -1
  3. package/dist/core/contextManager.js +15 -28
  4. package/dist/core/contextManager.js.map +1 -1
  5. package/dist/core/toolPreconditions.d.ts +1 -0
  6. package/dist/core/toolPreconditions.d.ts.map +1 -1
  7. package/dist/core/toolPreconditions.js +13 -4
  8. package/dist/core/toolPreconditions.js.map +1 -1
  9. package/dist/runtime/agentSession.d.ts.map +1 -1
  10. package/dist/runtime/agentSession.js +5 -6
  11. package/dist/runtime/agentSession.js.map +1 -1
  12. package/dist/shell/interactiveShell.d.ts +15 -19
  13. package/dist/shell/interactiveShell.d.ts.map +1 -1
  14. package/dist/shell/interactiveShell.js +274 -226
  15. package/dist/shell/interactiveShell.js.map +1 -1
  16. package/dist/shell/terminalInput.d.ts.map +1 -1
  17. package/dist/shell/terminalInput.js +2 -2
  18. package/dist/shell/terminalInput.js.map +1 -1
  19. package/dist/subagents/parallelAgentManager.d.ts.map +1 -1
  20. package/dist/subagents/parallelAgentManager.js +1 -2
  21. package/dist/subagents/parallelAgentManager.js.map +1 -1
  22. package/dist/tools/editTools.js +2 -2
  23. package/dist/tools/editTools.js.map +1 -1
  24. package/dist/ui/ShellUIAdapter.d.ts +3 -5
  25. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  26. package/dist/ui/ShellUIAdapter.js +15 -28
  27. package/dist/ui/ShellUIAdapter.js.map +1 -1
  28. package/dist/ui/display.d.ts.map +1 -1
  29. package/dist/ui/display.js +2 -3
  30. package/dist/ui/display.js.map +1 -1
  31. package/dist/ui/shortcutsHelp.d.ts +2 -12
  32. package/dist/ui/shortcutsHelp.d.ts.map +1 -1
  33. package/dist/ui/shortcutsHelp.js +6 -57
  34. package/dist/ui/shortcutsHelp.js.map +1 -1
  35. package/dist/ui/streamingFormatter.d.ts +9 -5
  36. package/dist/ui/streamingFormatter.d.ts.map +1 -1
  37. package/dist/ui/streamingFormatter.js +52 -22
  38. package/dist/ui/streamingFormatter.js.map +1 -1
  39. package/dist/ui/toolDisplay.d.ts +3 -3
  40. package/dist/ui/toolDisplay.d.ts.map +1 -1
  41. package/dist/ui/toolDisplay.js +5 -12
  42. package/dist/ui/toolDisplay.js.map +1 -1
  43. package/dist/ui/unified/layout.d.ts +0 -37
  44. package/dist/ui/unified/layout.d.ts.map +1 -1
  45. package/dist/ui/unified/layout.js +1 -169
  46. package/dist/ui/unified/layout.js.map +1 -1
  47. package/package.json +1 -1
@@ -6,6 +6,7 @@ import { join } from 'node:path';
6
6
  import { display } from '../ui/display.js';
7
7
  import { isPlainOutputMode } from '../ui/outputMode.js';
8
8
  import { theme } from '../ui/theme.js';
9
+ import { renderDivider } from '../ui/unified/layout.js';
9
10
  import { StreamingResponseFormatter } from '../ui/streamingFormatter.js';
10
11
  import { getContextWindowTokens } from '../core/contextWindow.js';
11
12
  import { ensureSecretForProvider, getSecretDefinitionForProvider, getSecretValue, listSecretDefinitions, maskSecret, setSecretValue, } from '../core/secretStore.js';
@@ -37,7 +38,6 @@ import { analyzeTokenUsage, discoverModularTargets, getModularStatusDisplay, gen
37
38
  import { startOffsecRun, resumeOffsecRun, recordOffsecOutcome, getOffsecNextActions, simulateOffsecRollout, formatOffsecStatus, listOffsecRuns, } from '../core/offsecAlphaZero.js';
38
39
  import { generateTestFlows, detectBugs, detectUIUpdates, saveTestFlows, saveBugReports, saveUIUpdates, getTestFlowStatus, } from '../core/intelligentTestFlows.js';
39
40
  import { TerminalInputAdapter } from './terminalInputAdapter.js';
40
- import { renderSessionFrame } from '../ui/unified/layout.js';
41
41
  import { writeLock } from '../ui/writeLock.js';
42
42
  import { enterStreamingMode, exitStreamingMode } from '../ui/globalWriteLock.js';
43
43
  import { setGlobalAIEnhancer } from '../tools/localExplore.js';
@@ -72,7 +72,7 @@ const BASE_SLASH_COMMANDS = getSlashCommands().map((cmd) => ({
72
72
  // Load PROVIDER_LABELS from centralized schema
73
73
  const PROVIDER_LABELS = Object.fromEntries(getProviders().map((provider) => [provider.id, provider.label]));
74
74
  // Allow enough time for paste detection to kick in before flushing buffered lines
75
- const CONTEXT_USAGE_THRESHOLD = 0.9;
75
+ const CONTEXT_USAGE_THRESHOLD = 0.97;
76
76
  const CONTEXT_AUTOCOMPACT_PERCENT = Math.round(CONTEXT_USAGE_THRESHOLD * 100);
77
77
  const CONTEXT_AUTOCOMPACT_FLOOR = 0.6;
78
78
  const MIN_COMPACTION_TOKEN_SAVINGS = 200;
@@ -80,12 +80,11 @@ const MIN_COMPACTION_PERCENT_SAVINGS = 0.5;
80
80
  const CONTEXT_RECENT_MESSAGE_COUNT = 12;
81
81
  const CONTEXT_CLEANUP_CHARS_PER_CHUNK = 6000;
82
82
  const CONTEXT_CLEANUP_MAX_OUTPUT_TOKENS = 800;
83
- const CONTEXT_CLEANUP_SYSTEM_PROMPT = `You condense earlier IDE collaboration logs so the agent can keep working.
84
- - Merge any prior summary with the new conversation chunk.
85
- - Capture key decisions, TODOs, file edits, tool observations, and open questions.
86
- - Clearly distinguish resolved work from outstanding follow-ups.
87
- - Keep the response under roughly 200 words, prefer short bullet lists.
88
- - Never call tools or run shell commands; respond with plain Markdown text only.`;
83
+ const CONTEXT_CLEANUP_SYSTEM_PROMPT = `Summarize earlier IDE collaboration so the agent can keep working.
84
+ - Merge any prior summary with the new chunk.
85
+ - Capture decisions, file edits/paths, tool findings, and open questions.
86
+ - Separate finished work from follow-ups; keep it under ~180 words with tight bullets.
87
+ - Respond in plain Markdown only (no tool or shell calls).`;
89
88
  export class InteractiveShell {
90
89
  agent = null;
91
90
  profile;
@@ -112,7 +111,6 @@ export class InteractiveShell {
112
111
  thinkingMode = 'balanced';
113
112
  agentMenu;
114
113
  slashCommands;
115
- bannerSessionState = null;
116
114
  statusTracker;
117
115
  ui;
118
116
  uiAdapter;
@@ -126,6 +124,7 @@ export class InteractiveShell {
126
124
  latestTokenUsage = { used: null, limit: null };
127
125
  planApprovalBridgeRegistered = false;
128
126
  contextCompactionInFlight = false;
127
+ contextCompactionLog = [];
129
128
  lastContextWarningLevel = null;
130
129
  sessionPreferences;
131
130
  autosaveEnabled;
@@ -165,11 +164,14 @@ export class InteractiveShell {
165
164
  streamingHeartbeatFrame = 0;
166
165
  streamingStatusLabel = null;
167
166
  lastStreamingElapsedSeconds = null; // Preserve final elapsed time
167
+ aiRuntimeStart = null;
168
+ aiRuntimeTotalMs = 0;
168
169
  streamingFormatter = null;
169
170
  statusLineState = null;
170
171
  statusMessageOverride = null;
171
172
  promptRefreshTimer = null;
172
173
  launchPaletteShown = false;
174
+ launchBannerText = null;
173
175
  version;
174
176
  alternateScreenEnabled;
175
177
  constructor(config) {
@@ -199,11 +201,6 @@ export class InteractiveShell {
199
201
  reasoningEffort: config.initialModel.reasoningEffort,
200
202
  };
201
203
  this.applyPresetReasoningDefaults();
202
- // The welcome banner only includes model + provider on launch, so mark that as the initial state.
203
- this.bannerSessionState = {
204
- model: this.sessionState.model,
205
- provider: this.sessionState.provider,
206
- };
207
204
  this.agentMenu = config.agentSelection ?? null;
208
205
  this.slashCommands = [...BASE_SLASH_COMMANDS];
209
206
  if (this.agentMenu) {
@@ -293,15 +290,12 @@ export class InteractiveShell {
293
290
  else if (output.isTTY) {
294
291
  this.terminalInput.clearScreen();
295
292
  }
296
- // Stream banner first - this sets up scroll region dynamically
297
- const banner = this.buildBanner();
298
- this.terminalInput.streamContent(banner + '\n\n');
299
- // Render chat box after banner is streamed
293
+ // Render chat box immediately using the streaming UI lifecycle
300
294
  this.refreshControlBar();
301
295
  this.terminalInput.forceRender();
302
296
  this.rebuildAgent();
303
297
  this.setupHandlers();
304
- this.refreshBannerSessionInfo();
298
+ this.refreshSessionContext();
305
299
  // Subscribe to parallel agent manager events
306
300
  this.setupParallelAgentTracking();
307
301
  }
@@ -383,7 +377,30 @@ export class InteractiveShell {
383
377
  display.showInfo(this.sessionResumeNotice);
384
378
  this.sessionResumeNotice = null;
385
379
  }
380
+ renderWelcomeBanner(force = false) {
381
+ if (isPlainOutputMode() || !output.isTTY) {
382
+ return;
383
+ }
384
+ const header = `${theme.fields.model(this.sessionState.model)} ${theme.ui.muted('@')} ${theme.fields.agent(this.providerLabel(this.sessionState.provider))}`;
385
+ const profileLine = `${theme.fields.profile(this.profileLabel)} ${theme.ui.muted(`(${this.profile})`)}`;
386
+ const workspaceLine = theme.fields.workspace(this.workingDir);
387
+ const versionLine = this.version ? theme.ui.muted(`v${this.version} ยท support@ero.solar`) : null;
388
+ const hintLine = theme.ui.muted('/help for commands ยท /model to switch ยท /secrets for keys');
389
+ const width = output.columns ?? 80;
390
+ const banner = [header, profileLine, workspaceLine, versionLine, hintLine, renderDivider(width)]
391
+ .filter(Boolean)
392
+ .join('\n');
393
+ if (!force && this.launchBannerText) {
394
+ return;
395
+ }
396
+ if (this.launchBannerText === banner) {
397
+ return;
398
+ }
399
+ display.stream(`${banner}\n`);
400
+ this.launchBannerText = banner;
401
+ }
386
402
  async start(initialPrompt) {
403
+ this.renderWelcomeBanner();
387
404
  if (initialPrompt) {
388
405
  await this.processInputBlock(initialPrompt);
389
406
  return;
@@ -568,18 +585,20 @@ export class InteractiveShell {
568
585
  ' Toggle with Ctrl+Shift+C.');
569
586
  }
570
587
  /**
571
- * Cycle through thinking modes (Ctrl+Shift+T keyboard shortcut).
588
+ * Cycle through thinking modes (Tab shortcut).
572
589
  */
573
590
  cycleThinkingMode() {
574
591
  const nextMode = this.thinkingMode === 'balanced' ? 'extended' : 'balanced';
575
592
  this.thinkingMode = nextMode;
576
593
  saveSessionPreferences({ thinkingMode: this.thinkingMode });
577
594
  this.refreshControlBar();
578
- const descriptions = {
579
- balanced: 'Default reasoning depth',
580
- extended: 'Deep reasoning, thorough analysis',
581
- };
582
- display.showInfo(`Thinking mode: ${theme.info(nextMode)} - ${descriptions[nextMode]}. (Alt+T to cycle)`);
595
+ const headline = nextMode === 'extended'
596
+ ? `${theme.info('Thinking on')} (Tab to toggle)`
597
+ : `${theme.info('Thinking off')} (Tab to toggle)`;
598
+ const detail = nextMode === 'extended'
599
+ ? 'Longer reasoning enabled; expect extra usage for deeper answers.'
600
+ : 'Balanced (default) reasoning restored.';
601
+ display.showSystemMessage([headline, theme.ui.muted(detail)].join('\n'));
583
602
  }
584
603
  /**
585
604
  * Handle context clear/compact request (Alt+X keyboard shortcut).
@@ -591,26 +610,35 @@ export class InteractiveShell {
591
610
  }
592
611
  // Trigger context compaction
593
612
  display.showInfo('Compacting context... This will summarize the conversation and free up space.');
594
- void this.performContextCompaction();
613
+ void this.performContextCompaction('Manual shortcut compaction');
595
614
  }
596
615
  /**
597
616
  * Perform context compaction by summarizing conversation history.
598
617
  */
599
- async performContextCompaction() {
618
+ async performContextCompaction(reason = 'Manual context compaction') {
600
619
  try {
601
- // For now, just clear the history and show a message
602
- // A full implementation would summarize the conversation
603
- const oldLength = this.cachedHistory.length;
604
- if (oldLength === 0) {
605
- display.showInfo('Context is already empty.');
620
+ const agent = this.agent;
621
+ if (!agent) {
622
+ display.showWarning('No active agent to compact context.');
606
623
  return;
607
624
  }
608
- // Keep the last few messages for continuity
609
- const keepCount = Math.min(4, oldLength);
610
- this.cachedHistory = this.cachedHistory.slice(-keepCount);
611
- display.showSuccess(`Context compacted: ${oldLength} messages reduced to ${keepCount}. ` +
612
- `Context usage reset. Continue your conversation.`);
613
- this.refreshControlBar();
625
+ const contextManager = agent.getContextManager();
626
+ if (!contextManager) {
627
+ display.showWarning('Context manager unavailable. Try sending a message first.');
628
+ return;
629
+ }
630
+ const history = agent.getHistory();
631
+ if (history.length <= 1) {
632
+ display.showInfo('Context is already minimal.');
633
+ return;
634
+ }
635
+ const stats = contextManager.getStats(history);
636
+ const windowTokens = this.activeContextWindowTokens ?? this.latestTokenUsage.limit ?? null;
637
+ await this.runContextCleanup(windowTokens, stats.totalTokens, {
638
+ reason,
639
+ forced: true,
640
+ source: 'manual',
641
+ });
614
642
  }
615
643
  catch (error) {
616
644
  display.showError('Failed to compact context', error);
@@ -722,6 +750,7 @@ export class InteractiveShell {
722
750
  // Stop any active spinner to prevent process hang
723
751
  display.stopThinking(false);
724
752
  this.stopStreamingHeartbeat();
753
+ this.endAiRuntime();
725
754
  this.uiUpdates.dispose();
726
755
  this.clearPromptRefreshTimer();
727
756
  this.teardownStatusTracking();
@@ -1147,8 +1176,8 @@ export class InteractiveShell {
1147
1176
  autoContinueEnabled: this.autoContinueEnabled,
1148
1177
  verificationHotkey: 'ctrl+shift+v',
1149
1178
  autoContinueHotkey: 'ctrl+shift+c',
1150
- thinkingModeLabel: this.thinkingMode,
1151
- thinkingHotkey: 'ctrl+shift+t',
1179
+ thinkingModeLabel: this.thinkingMode === 'extended' ? 'on' : 'off',
1180
+ thinkingHotkey: 'tab',
1152
1181
  });
1153
1182
  this.refreshStatusLine();
1154
1183
  this.terminalInput.render();
@@ -1167,6 +1196,25 @@ export class InteractiveShell {
1167
1196
  process.stdout.write(content);
1168
1197
  }, 'interactiveShell.stdout');
1169
1198
  }
1199
+ beginAiRuntime() {
1200
+ if (this.aiRuntimeStart === null) {
1201
+ this.aiRuntimeStart = Date.now();
1202
+ }
1203
+ }
1204
+ endAiRuntime() {
1205
+ if (this.aiRuntimeStart !== null) {
1206
+ this.aiRuntimeTotalMs += Date.now() - this.aiRuntimeStart;
1207
+ this.aiRuntimeStart = null;
1208
+ }
1209
+ }
1210
+ getAiRuntimeSeconds() {
1211
+ const runningMs = this.aiRuntimeStart ? Date.now() - this.aiRuntimeStart : 0;
1212
+ const totalMs = this.aiRuntimeTotalMs + runningMs;
1213
+ if (totalMs <= 0 && this.aiRuntimeStart === null) {
1214
+ return null;
1215
+ }
1216
+ return Math.max(0, Math.floor(totalMs / 1000));
1217
+ }
1170
1218
  /**
1171
1219
  * Refresh the status line in the persistent input area.
1172
1220
  * Uses combined status display: streaming label + override + main status.
@@ -1181,20 +1229,7 @@ export class InteractiveShell {
1181
1229
  const statusText = this.formatStatusLine(this.statusLineState);
1182
1230
  this.terminalInput.setStatusMessage(statusText);
1183
1231
  // Surface meta header (elapsed + context usage) above the divider
1184
- // Use streaming elapsed time if available, otherwise fall back to status line state
1185
- let elapsedSeconds = null;
1186
- if (this.streamingHeartbeatStart) {
1187
- // Actively streaming - compute live elapsed
1188
- elapsedSeconds = Math.max(0, Math.floor((Date.now() - this.streamingHeartbeatStart) / 1000));
1189
- }
1190
- else if (this.lastStreamingElapsedSeconds !== null) {
1191
- // Just finished streaming - use preserved final time
1192
- elapsedSeconds = this.lastStreamingElapsedSeconds;
1193
- }
1194
- else if (this.statusLineState) {
1195
- // Fallback to status line state elapsed
1196
- elapsedSeconds = Math.max(0, Math.floor((Date.now() - this.statusLineState.startedAt) / 1000));
1197
- }
1232
+ const elapsedSeconds = this.getAiRuntimeSeconds();
1198
1233
  const thinkingMs = display.isSpinnerActive() ? display.getThinkingElapsedMs() : null;
1199
1234
  const tokensUsed = this.latestTokenUsage.used;
1200
1235
  const tokenLimit = this.latestTokenUsage.limit ?? this.activeContextWindowTokens;
@@ -1373,6 +1408,8 @@ export class InteractiveShell {
1373
1408
  this.terminalInput.streamContent(closing);
1374
1409
  }
1375
1410
  this.streamingFormatter = null;
1411
+ // Force the prompt to re-render after streaming so it stays below the last response
1412
+ this.requestPromptRefresh(true);
1376
1413
  }
1377
1414
  buildStreamingStatus(label, _elapsedSeconds) {
1378
1415
  // Model + elapsed time already live in the pinned meta header; keep the streaming
@@ -1686,7 +1723,7 @@ export class InteractiveShell {
1686
1723
  this.handleCostCommand();
1687
1724
  break;
1688
1725
  case '/usage':
1689
- this.handleUsageCommand();
1726
+ this.handleUsageCommand(input);
1690
1727
  break;
1691
1728
  case '/clear':
1692
1729
  this.handleClearCommand();
@@ -2022,7 +2059,8 @@ export class InteractiveShell {
2022
2059
  handleThinkingCommand(input) {
2023
2060
  const value = input.slice('/thinking'.length).trim().toLowerCase();
2024
2061
  if (!value) {
2025
- display.showInfo(`Thinking mode is currently ${theme.info(this.thinkingMode)}. Usage: /thinking [balanced|extended]`);
2062
+ display.showInfo(`Thinking mode is currently ${theme.info(this.thinkingMode)}. Usage: /thinking [balanced|extended].` +
2063
+ ' Press Tab any time to toggle.');
2026
2064
  return;
2027
2065
  }
2028
2066
  if (value !== 'balanced' && value !== 'extended') {
@@ -2038,11 +2076,15 @@ export class InteractiveShell {
2038
2076
  if (this.rebuildAgent()) {
2039
2077
  this.resetChatBoxAfterModelSwap();
2040
2078
  }
2079
+ this.refreshControlBar();
2041
2080
  const descriptions = {
2042
- balanced: 'Shows short thoughts only when helpful.',
2043
- extended: 'Always emits a <thinking> block before the final response.',
2081
+ balanced: 'Balanced (default) reasoning with short thoughts only when helpful.',
2082
+ extended: 'Longer reasoning enabled; expect extra usage for deeper answers.',
2044
2083
  };
2045
- display.showInfo(`Thinking mode set to ${theme.info(value)} โ€“ ${descriptions[this.thinkingMode]}`);
2084
+ const headline = this.thinkingMode === 'extended'
2085
+ ? `${theme.info('Thinking on')} (Tab to toggle)`
2086
+ : `${theme.info('Thinking off')} (Tab to toggle)`;
2087
+ display.showSystemMessage(`${headline}\n${theme.ui.muted(descriptions[this.thinkingMode])}`);
2046
2088
  }
2047
2089
  handleShortcutsCommand() {
2048
2090
  // Display keyboard shortcuts help (Claude Code style)
@@ -3391,7 +3433,7 @@ export class InteractiveShell {
3391
3433
  lines.push(' /rewind code Rewind code only (keep conversation)');
3392
3434
  lines.push(' /rewind conv Rewind conversation only (keep code)');
3393
3435
  lines.push('');
3394
- lines.push(theme.ui.muted('Tip: Press Esc+Esc for quick access to rewind menu'));
3436
+ lines.push(theme.ui.muted('Press Esc+Esc for quick access to the rewind menu'));
3395
3437
  display.showSystemMessage(lines.join('\n'));
3396
3438
  }
3397
3439
  handleMemoryCommand(input) {
@@ -3410,7 +3452,7 @@ export class InteractiveShell {
3410
3452
  lines.push(' - Use # prefix to quickly add notes to project memory');
3411
3453
  lines.push(' - Import other files with @./relative/path syntax');
3412
3454
  lines.push('');
3413
- lines.push(theme.ui.muted('Tip: Create EROSOLAR.md with project coding standards for better results'));
3455
+ lines.push(theme.ui.muted('Create EROSOLAR.md with project coding standards for better results'));
3414
3456
  display.showSystemMessage(lines.join('\n'));
3415
3457
  }
3416
3458
  handleVimCommand() {
@@ -3468,7 +3510,13 @@ export class InteractiveShell {
3468
3510
  lines.push(theme.ui.muted('Token usage is tracked automatically during the session.'));
3469
3511
  display.showSystemMessage(lines.join('\n'));
3470
3512
  }
3471
- handleUsageCommand() {
3513
+ handleUsageCommand(input) {
3514
+ const tokens = input ? input.trim().split(/\s+/) : [];
3515
+ const subcommand = tokens[1]?.toLowerCase();
3516
+ if (subcommand === 'history' || subcommand === 'log') {
3517
+ this.showCompactionHistory();
3518
+ return;
3519
+ }
3472
3520
  const percentage = this.latestTokenUsage.limit && this.latestTokenUsage.used
3473
3521
  ? Math.round((this.latestTokenUsage.used / this.latestTokenUsage.limit) * 100)
3474
3522
  : 0;
@@ -3489,6 +3537,37 @@ export class InteractiveShell {
3489
3537
  else {
3490
3538
  lines.push(theme.success('Plenty of context available.'));
3491
3539
  }
3540
+ if (this.contextCompactionLog.length > 0) {
3541
+ lines.push('');
3542
+ lines.push(theme.secondary('Tip: /usage history shows recent context compactions.'));
3543
+ }
3544
+ display.showSystemMessage(lines.join('\n'));
3545
+ }
3546
+ showCompactionHistory() {
3547
+ if (!this.contextCompactionLog.length) {
3548
+ display.showInfo('No context compactions recorded yet.');
3549
+ return;
3550
+ }
3551
+ const entries = [...this.contextCompactionLog].slice(-10).reverse();
3552
+ const lines = [];
3553
+ lines.push(theme.bold('Context Compaction History'));
3554
+ lines.push('');
3555
+ for (const entry of entries) {
3556
+ const time = new Date(entry.timestamp).toLocaleTimeString();
3557
+ const reason = entry.reason || (entry.source === 'auto' ? 'Auto-compaction' : 'Manual compaction');
3558
+ const before = entry.beforeTokens.toLocaleString('en-US');
3559
+ const after = entry.afterTokens.toLocaleString('en-US');
3560
+ const saved = Math.max(0, entry.beforeTokens - entry.afterTokens);
3561
+ const savedLabel = saved > 0 ? `${saved.toLocaleString('en-US')} saved` : 'no savings';
3562
+ const pctBefore = entry.percentBefore ?? null;
3563
+ const pctAfter = entry.percentAfter ?? null;
3564
+ const pctLabel = pctBefore !== null && pctAfter !== null
3565
+ ? `(${pctBefore}% โ†’ ${pctAfter}%)`
3566
+ : '';
3567
+ const summaryNote = entry.summarized ? 'summary applied' : 'prune only';
3568
+ lines.push(` ${time} ยท ${reason} [${entry.source} ยท ${entry.model}]`);
3569
+ lines.push(` ${before} โ†’ ${after} tokens ${pctLabel} ยท ${savedLabel} ยท ${summaryNote}`);
3570
+ }
3492
3571
  display.showSystemMessage(lines.join('\n'));
3493
3572
  }
3494
3573
  handleClearCommand() {
@@ -3625,13 +3704,7 @@ export class InteractiveShell {
3625
3704
  return;
3626
3705
  }
3627
3706
  display.showInfo('Compacting conversation context...');
3628
- // Check if compaction is needed based on tracked usage
3629
- const { used, limit } = this.latestTokenUsage;
3630
- if (used && limit && used / limit < 0.5) {
3631
- display.showInfo('Context usage is low. No compaction needed.');
3632
- return;
3633
- }
3634
- await this.processRequest('Please summarize our conversation so far in a concise way, preserving key decisions, code changes, and next steps. This will help compact the context.');
3707
+ await this.performContextCompaction('Manual /compact request');
3635
3708
  }
3636
3709
  // ==================== End Claude Code Style Commands ====================
3637
3710
  updateActiveSession(summary, remember = false) {
@@ -4464,7 +4537,7 @@ export class InteractiveShell {
4464
4537
  this.applyPresetReasoningDefaults();
4465
4538
  if (this.rebuildAgent()) {
4466
4539
  display.showInfo(`Switched to ${preset.label}.`);
4467
- this.refreshBannerSessionInfo();
4540
+ this.refreshSessionContext();
4468
4541
  this.persistSessionPreference();
4469
4542
  this.resetChatBoxAfterModelSwap();
4470
4543
  }
@@ -4573,6 +4646,7 @@ export class InteractiveShell {
4573
4646
  this.currentToolCalls = [];
4574
4647
  this.uiAdapter.startProcessing('Working on your request');
4575
4648
  this.setProcessingStatus();
4649
+ this.beginAiRuntime();
4576
4650
  let responseText = '';
4577
4651
  try {
4578
4652
  // Start streaming - no header needed, the input area already provides context
@@ -4638,6 +4712,7 @@ export class InteractiveShell {
4638
4712
  display.stopThinking(false);
4639
4713
  this.uiUpdates.setMode('processing');
4640
4714
  this.stopStreamingHeartbeat();
4715
+ this.endAiRuntime();
4641
4716
  this.isProcessing = false;
4642
4717
  this.terminalInput.setStreaming(false);
4643
4718
  this.uiAdapter.endProcessing('Ready for prompts');
@@ -4693,6 +4768,7 @@ export class InteractiveShell {
4693
4768
  display.showSystemMessage(`Continuous mode active. Ctrl+C to stop.`);
4694
4769
  this.uiAdapter.startProcessing('Continuous execution mode');
4695
4770
  this.setProcessingStatus();
4771
+ this.beginAiRuntime();
4696
4772
  // No streaming header - just start streaming directly
4697
4773
  this.startStreamingHeartbeat('Streaming');
4698
4774
  let iteration = 0;
@@ -4868,6 +4944,7 @@ What's the next action?`;
4868
4944
  resetTaskCompletionDetector();
4869
4945
  this.uiUpdates.setMode('processing');
4870
4946
  this.stopStreamingHeartbeat();
4947
+ this.endAiRuntime();
4871
4948
  this.isProcessing = false;
4872
4949
  this.terminalInput.setStreaming(false);
4873
4950
  this.uiAdapter.endProcessing('Ready for prompts');
@@ -5424,6 +5501,7 @@ Return ONLY JSON array:
5424
5501
  try {
5425
5502
  ensureSecretForProvider(this.sessionState.provider);
5426
5503
  this.runtimeSession.updateToolContext(this.sessionState);
5504
+ this.ensureActiveSummarizer(this.runtimeSession.contextManager);
5427
5505
  const selection = {
5428
5506
  provider: this.sessionState.provider,
5429
5507
  model: this.sessionState.model,
@@ -5574,6 +5652,7 @@ Return ONLY JSON array:
5574
5652
  * Ensures the input area is visible and ready for input, just like on startup.
5575
5653
  */
5576
5654
  resetChatBoxAfterModelSwap() {
5655
+ this.renderWelcomeBanner(true);
5577
5656
  this.updateStatusMessage(null);
5578
5657
  this.terminalInput.setStreaming(false);
5579
5658
  this.terminalInput.render();
@@ -5689,7 +5768,7 @@ Return ONLY JSON array:
5689
5768
  if (!trigger) {
5690
5769
  return null;
5691
5770
  }
5692
- return this.runContextCleanup(windowTokens, total, trigger);
5771
+ return this.runContextCleanup(windowTokens, total, { ...trigger, source: 'auto' });
5693
5772
  }
5694
5773
  totalTokens(usage) {
5695
5774
  if (!usage) {
@@ -5716,58 +5795,104 @@ Return ONLY JSON array:
5716
5795
  if (history.length <= 1) {
5717
5796
  return;
5718
5797
  }
5798
+ if (!this.areToolTurnsComplete(history)) {
5799
+ display.showInfo('Context compaction postponed until all tool calls have returned results.');
5800
+ return;
5801
+ }
5802
+ this.ensureActiveSummarizer(contextManager);
5719
5803
  this.cleanupInProgress = true;
5720
5804
  this.contextCompactionInFlight = true;
5721
5805
  const cleanupStatusId = 'context-cleanup';
5722
5806
  let cleanupOverlayActive = false;
5723
5807
  try {
5724
- const percentUsed = Math.round((totalTokens / windowTokens) * 100);
5725
- const statusDetailParts = [`${percentUsed}% full`];
5726
- if (trigger?.reason) {
5727
- statusDetailParts.push(trigger.reason);
5728
- }
5808
+ const beforeStats = contextManager.getStats(history);
5809
+ const resolvedWindowTokens = windowTokens ?? this.activeContextWindowTokens ?? this.latestTokenUsage.limit ?? null;
5810
+ const resolvedTotalTokens = totalTokens ?? beforeStats.totalTokens;
5811
+ const percentUsed = resolvedWindowTokens && resolvedWindowTokens > 0
5812
+ ? Math.min(100, Math.round((resolvedTotalTokens / resolvedWindowTokens) * 100))
5813
+ : beforeStats.percentage;
5814
+ const statusDetailParts = [
5815
+ `${percentUsed}% full`,
5816
+ trigger?.reason,
5817
+ ].filter(Boolean);
5729
5818
  this.statusTracker.pushOverride(cleanupStatusId, 'Running context cleanup', {
5730
5819
  detail: statusDetailParts.join(' ยท '),
5731
5820
  tone: 'warning',
5732
5821
  });
5733
5822
  cleanupOverlayActive = true;
5734
5823
  const triggerReason = trigger?.reason ?? 'Context optimization';
5824
+ const limitLabel = resolvedWindowTokens
5825
+ ? `of ${resolvedWindowTokens.toLocaleString('en-US')} tokens`
5826
+ : 'of estimated context';
5735
5827
  display.showSystemMessage([
5736
- `Context usage: ${totalTokens.toLocaleString('en-US')} of ${windowTokens.toLocaleString('en-US')} tokens`,
5828
+ `Context usage: ${resolvedTotalTokens.toLocaleString('en-US')} ${limitLabel}`,
5737
5829
  `(${percentUsed}% full). Auto-compacting${triggerReason ? `: ${triggerReason}` : '...'}`,
5738
5830
  ].join(' '));
5739
- const beforeStats = contextManager.getStats(history);
5740
5831
  const result = await contextManager.intelligentCompact(history);
5741
- const afterStats = contextManager.getStats(result.compacted);
5742
- const tokenSavings = Math.max(0, beforeStats.totalTokens - afterStats.totalTokens);
5743
- const percentSavings = beforeStats.totalTokens > 0 ? (tokenSavings / beforeStats.totalTokens) * 100 : 0;
5744
- const changed = this.historiesDiffer(history, result.compacted);
5745
- const meaningfulSavings = changed &&
5746
- (tokenSavings >= MIN_COMPACTION_TOKEN_SAVINGS || percentSavings >= MIN_COMPACTION_PERCENT_SAVINGS);
5747
- if (!meaningfulSavings) {
5748
- if (trigger?.forced || percentUsed >= 85) {
5749
- display.showInfo('Auto-compaction completed but did not meaningfully reduce context size. Keeping existing history.');
5832
+ let afterStats = contextManager.getStats(result.compacted);
5833
+ let appliedHistory = result.compacted;
5834
+ let appliedSummarized = result.summarized;
5835
+ const initialTokenSavings = Math.max(0, beforeStats.totalTokens - afterStats.totalTokens);
5836
+ let bestTokenSavings = initialTokenSavings;
5837
+ let bestPercentSavings = beforeStats.totalTokens > 0 ? (initialTokenSavings / beforeStats.totalTokens) * 100 : 0;
5838
+ let changed = this.historiesDiffer(history, appliedHistory);
5839
+ if (!this.hasMeaningfulCompaction(changed, bestTokenSavings, bestPercentSavings, trigger?.forced)) {
5840
+ const fallback = await contextManager.pruneMessagesWithSummary(history, { force: true });
5841
+ const fallbackStats = contextManager.getStats(fallback.pruned);
5842
+ const fallbackTokenSavings = Math.max(0, beforeStats.totalTokens - fallbackStats.totalTokens);
5843
+ const fallbackPercentSavings = beforeStats.totalTokens > 0
5844
+ ? (fallbackTokenSavings / beforeStats.totalTokens) * 100
5845
+ : 0;
5846
+ if (this.historiesDiffer(history, fallback.pruned) &&
5847
+ (fallbackTokenSavings > bestTokenSavings || !changed)) {
5848
+ appliedHistory = fallback.pruned;
5849
+ afterStats = fallbackStats;
5850
+ appliedSummarized = fallback.summarized;
5851
+ bestTokenSavings = fallbackTokenSavings;
5852
+ bestPercentSavings = fallbackPercentSavings;
5853
+ changed = true;
5750
5854
  }
5855
+ }
5856
+ if (!changed) {
5857
+ display.showInfo('Context compaction completed but no changes were applied.');
5751
5858
  return;
5752
5859
  }
5753
- agent.loadHistory(result.compacted);
5754
- this.cachedHistory = result.compacted;
5755
- const newPercentUsed = windowTokens > 0
5756
- ? Math.round((afterStats.totalTokens / windowTokens) * 100)
5860
+ if (!this.hasMeaningfulCompaction(changed, bestTokenSavings, bestPercentSavings, trigger?.forced)) {
5861
+ display.showInfo('Auto-compaction completed but did not meaningfully reduce context size. Keeping existing history.');
5862
+ return;
5863
+ }
5864
+ agent.loadHistory(appliedHistory);
5865
+ this.cachedHistory = appliedHistory;
5866
+ const newPercentUsed = resolvedWindowTokens && resolvedWindowTokens > 0
5867
+ ? Math.min(100, Math.round((afterStats.totalTokens / resolvedWindowTokens) * 100))
5757
5868
  : afterStats.percentage;
5758
5869
  this.latestTokenUsage = {
5759
5870
  used: afterStats.totalTokens,
5760
- limit: windowTokens,
5871
+ limit: resolvedWindowTokens,
5761
5872
  };
5762
5873
  this.updateContextUsage(newPercentUsed, CONTEXT_AUTOCOMPACT_PERCENT);
5763
5874
  this.lastContextWarningLevel = this.getContextWarningLevel(newPercentUsed);
5764
5875
  this.refreshStatusLine(true);
5765
5876
  const primarySignal = result.analysis.signals[0]?.reason ?? triggerReason;
5877
+ const savingsLabel = bestTokenSavings > 0
5878
+ ? `(saved ~${bestTokenSavings.toLocaleString('en-US')} tokens, ~${bestPercentSavings.toFixed(1)}%)`
5879
+ : '(no token savings)';
5766
5880
  display.showSystemMessage([
5767
5881
  `Context compacted: ${beforeStats.percentage}% โ†’ ${afterStats.percentage}%`,
5768
- `(saved ~${tokenSavings.toLocaleString('en-US')} tokens).`,
5882
+ savingsLabel,
5769
5883
  primarySignal ? `Reason: ${primarySignal}.` : '',
5770
5884
  ].filter(Boolean).join(' '));
5885
+ this.recordContextCompaction({
5886
+ timestamp: Date.now(),
5887
+ source: trigger?.source ?? 'auto',
5888
+ reason: triggerReason,
5889
+ beforeTokens: beforeStats.totalTokens,
5890
+ afterTokens: afterStats.totalTokens,
5891
+ summarized: appliedSummarized,
5892
+ percentBefore: resolvedWindowTokens ? percentUsed : beforeStats.percentage,
5893
+ percentAfter: newPercentUsed,
5894
+ model: this.sessionState.model,
5895
+ });
5771
5896
  }
5772
5897
  catch (error) {
5773
5898
  display.showError('Context compaction failed.', error);
@@ -5780,7 +5905,35 @@ Return ONLY JSON array:
5780
5905
  this.contextCompactionInFlight = false;
5781
5906
  }
5782
5907
  }
5783
- shouldAutoCompactContext(usageRatio, windowTokens, totalTokens) {
5908
+ recordContextCompaction(event) {
5909
+ this.contextCompactionLog.push(event);
5910
+ if (this.contextCompactionLog.length > 20) {
5911
+ this.contextCompactionLog.shift();
5912
+ }
5913
+ }
5914
+ ensureActiveSummarizer(contextManager) {
5915
+ if (!contextManager?.updateConfig) {
5916
+ return;
5917
+ }
5918
+ const summarizer = async (messages) => {
5919
+ const summary = await this.buildContextSummary(messages);
5920
+ return summary ?? '[No content to summarize]';
5921
+ };
5922
+ contextManager.updateConfig({
5923
+ summarizationCallback: summarizer,
5924
+ useLLMSummarization: true,
5925
+ });
5926
+ }
5927
+ hasMeaningfulCompaction(changed, tokenSavings, percentSavings, forced) {
5928
+ if (!changed) {
5929
+ return false;
5930
+ }
5931
+ if (tokenSavings >= MIN_COMPACTION_TOKEN_SAVINGS || percentSavings >= MIN_COMPACTION_PERCENT_SAVINGS) {
5932
+ return true;
5933
+ }
5934
+ return Boolean(forced && tokenSavings > 0);
5935
+ }
5936
+ shouldAutoCompactContext(usageRatio, _windowTokens, _totalTokens) {
5784
5937
  const featureFlags = loadFeatureFlags();
5785
5938
  if (featureFlags.autoCompact === false) {
5786
5939
  return null;
@@ -5901,6 +6054,25 @@ Return ONLY JSON array:
5901
6054
  }
5902
6055
  return false;
5903
6056
  }
6057
+ areToolTurnsComplete(messages) {
6058
+ const pending = new Set();
6059
+ for (const message of messages) {
6060
+ if (message.role === 'assistant' && message.toolCalls && message.toolCalls.length > 0) {
6061
+ for (const call of message.toolCalls) {
6062
+ if (call.id) {
6063
+ pending.add(call.id);
6064
+ }
6065
+ }
6066
+ }
6067
+ if (message.role === 'tool') {
6068
+ const toolCallId = message.toolCallId;
6069
+ if (toolCallId && pending.has(toolCallId)) {
6070
+ pending.delete(toolCallId);
6071
+ }
6072
+ }
6073
+ }
6074
+ return pending.size === 0;
6075
+ }
5904
6076
  partitionHistory(history) {
5905
6077
  const system = [];
5906
6078
  const conversation = [];
@@ -5992,10 +6164,10 @@ Return ONLY JSON array:
5992
6164
  sections.push(`Conversation chunk:\n${chunk}`);
5993
6165
  sections.push([
5994
6166
  'Instructions:',
5995
- '- Merge the chunk into the running summary.',
5996
- '- Preserve critical TODOs, bugs, test gaps, and file references.',
5997
- '- Call out what is resolved vs. still pending.',
5998
- '- Keep the output concise (<= 200 words) using short headings or bullets.',
6167
+ '- Merge with the running summary.',
6168
+ '- Keep TODOs/bugs/test gaps and file references.',
6169
+ '- Mark what is done vs. pending.',
6170
+ '- Stay concise (<180 words) with short bullets.',
5999
6171
  ].join('\n'));
6000
6172
  return sections.join('\n\n');
6001
6173
  }
@@ -6080,7 +6252,7 @@ Return ONLY JSON array:
6080
6252
  else {
6081
6253
  lines.push(`Update the stored value for ${secret.label} or type "cancel".`);
6082
6254
  }
6083
- lines.push(`Tip: run "/secrets" anytime to manage credentials or export ${secret.envVar}=<value> before launching the CLI.`);
6255
+ lines.push(`Run "/secrets" anytime to manage credentials or export ${secret.envVar}=<value> before launching the CLI.`);
6084
6256
  display.showSystemMessage(lines.join('\n'));
6085
6257
  }
6086
6258
  colorizeDropdownLine(text, index) {
@@ -6102,136 +6274,12 @@ Return ONLY JSON array:
6102
6274
  this.sessionState.reasoningEffort = preset.reasoningEffort;
6103
6275
  }
6104
6276
  }
6105
- /**
6106
- * Build the session banner with comprehensive feature status.
6107
- */
6108
- buildBanner() {
6109
- const terminalWidth = output.columns ?? 100;
6110
- const width = Math.min(terminalWidth - 4, 110);
6111
- // Collect tool categories for display
6112
- const toolCategories = this.collectToolCategories();
6113
- // Load feature flags for banner display
6114
- const featureFlags = loadFeatureFlags();
6115
- return renderSessionFrame({
6116
- profileLabel: this.profileLabel,
6117
- profileName: this.profile,
6118
- model: this.sessionState.model,
6119
- provider: this.sessionState.provider,
6120
- workspace: this.workingDir,
6121
- version: this.version,
6122
- width,
6123
- features: {
6124
- verification: this.verificationEnabled,
6125
- autoContinue: this.autoContinueEnabled,
6126
- thinkingMode: this.thinkingMode,
6127
- plugins: this._enabledPlugins,
6128
- tools: toolCategories,
6129
- sessionId: this.activeSessionId ?? undefined,
6130
- // Include feature flags
6131
- alphaZeroDual: featureFlags.alphaZeroDual,
6132
- autoCompact: featureFlags.autoCompact,
6133
- mcpEnabled: featureFlags.mcpEnabled,
6134
- metrics: featureFlags.metrics,
6135
- },
6136
- });
6137
- }
6138
- /**
6139
- * Collect tool categories for banner display.
6140
- */
6141
- collectToolCategories() {
6142
- const categories = [];
6143
- try {
6144
- const providerTools = this.runtimeSession.toolRuntime.listProviderTools();
6145
- if (providerTools.length > 0) {
6146
- // Group by category (first word of tool name or namespace)
6147
- const groups = new Map();
6148
- for (const tool of providerTools) {
6149
- const category = this.extractToolCategory(tool.name);
6150
- groups.set(category, (groups.get(category) || 0) + 1);
6151
- }
6152
- // Convert to array sorted by count
6153
- const sorted = Array.from(groups.entries())
6154
- .sort((a, b) => b[1] - a[1])
6155
- .slice(0, 5); // Top 5 categories
6156
- for (const [name, count] of sorted) {
6157
- categories.push({ name, count, icon: this.getToolCategoryIcon(name) });
6158
- }
6159
- }
6160
- }
6161
- catch {
6162
- // Ignore errors in tool collection
6163
- }
6164
- return categories;
6165
- }
6166
- /**
6167
- * Extract category from tool name.
6168
- */
6169
- extractToolCategory(toolName) {
6170
- // Common tool prefixes
6171
- const prefixMap = {
6172
- git: 'Git',
6173
- npm: 'NPM',
6174
- bash: 'Shell',
6175
- file: 'Files',
6176
- read: 'Files',
6177
- write: 'Files',
6178
- edit: 'Files',
6179
- search: 'Search',
6180
- glob: 'Search',
6181
- grep: 'Search',
6182
- web: 'Web',
6183
- fetch: 'Web',
6184
- test: 'Testing',
6185
- build: 'Build',
6186
- deploy: 'Deploy',
6187
- cloud: 'Cloud',
6188
- browser: 'Browser',
6189
- };
6190
- const lower = toolName.toLowerCase();
6191
- for (const [prefix, category] of Object.entries(prefixMap)) {
6192
- if (lower.startsWith(prefix)) {
6193
- return category;
6194
- }
6195
- }
6196
- // Default to first word capitalized
6197
- const firstWord = toolName.split(/[_\-\s]/)[0] || 'Other';
6198
- return firstWord.charAt(0).toUpperCase() + firstWord.slice(1).toLowerCase();
6199
- }
6200
- /**
6201
- * Get icon for tool category.
6202
- */
6203
- getToolCategoryIcon(category) {
6204
- const icons = {
6205
- Git: 'โއ',
6206
- NPM: '๐Ÿ“ฆ',
6207
- Shell: 'โŒ˜',
6208
- Files: '๐Ÿ“',
6209
- Search: '๐Ÿ”',
6210
- Web: '๐ŸŒ',
6211
- Testing: '๐Ÿงช',
6212
- Build: '๐Ÿ”ง',
6213
- Deploy: '๐Ÿš€',
6214
- Cloud: 'โ˜',
6215
- Browser: '๐ŸŒ',
6216
- };
6217
- return icons[category] || 'โš™';
6218
- }
6219
- refreshBannerSessionInfo() {
6220
- const nextState = {
6221
- model: this.sessionState.model,
6222
- provider: this.sessionState.provider,
6223
- };
6224
- const previous = this.bannerSessionState;
6225
- if (previous && previous.model === nextState.model && previous.provider === nextState.provider) {
6226
- return;
6227
- }
6277
+ refreshSessionContext() {
6228
6278
  this.refreshContextGauge();
6229
- // Banner is no longer stored in display - it was streamed as content
6230
- // Model/provider changes are visible in the control bar
6231
6279
  if (!this.isProcessing) {
6232
6280
  this.setIdleStatus();
6233
6281
  }
6234
- this.bannerSessionState = nextState;
6282
+ this.refreshStatusLine(true);
6235
6283
  }
6236
6284
  providerLabel(id) {
6237
6285
  return PROVIDER_LABELS[id] ?? id;
@@ -6338,7 +6386,7 @@ Return ONLY JSON array:
6338
6386
  // Rebuild agent with new provider
6339
6387
  if (this.rebuildAgent()) {
6340
6388
  this.persistSessionPreference();
6341
- this.refreshBannerSessionInfo();
6389
+ this.refreshSessionContext();
6342
6390
  display.showInfo(`Switched from ${this.providerLabel(oldProvider)}/${oldModel} to ${match.label}/${defaultModel.id}`);
6343
6391
  this.resetChatBoxAfterModelSwap();
6344
6392
  }