erosolar-cli 2.1.168 → 2.1.170

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 (133) hide show
  1. package/README.md +1 -1
  2. package/agents/erosolar-code.rules.json +2 -2
  3. package/agents/general.rules.json +21 -3
  4. package/dist/capabilities/statusCapability.js +2 -2
  5. package/dist/capabilities/statusCapability.js.map +1 -1
  6. package/dist/contracts/agent-schemas.json +5 -5
  7. package/dist/core/agent.d.ts +83 -24
  8. package/dist/core/agent.d.ts.map +1 -1
  9. package/dist/core/agent.js +499 -248
  10. package/dist/core/agent.js.map +1 -1
  11. package/dist/core/preferences.d.ts +1 -0
  12. package/dist/core/preferences.d.ts.map +1 -1
  13. package/dist/core/preferences.js +8 -1
  14. package/dist/core/preferences.js.map +1 -1
  15. package/dist/core/reliabilityPrompt.d.ts +9 -0
  16. package/dist/core/reliabilityPrompt.d.ts.map +1 -0
  17. package/dist/core/reliabilityPrompt.js +31 -0
  18. package/dist/core/reliabilityPrompt.js.map +1 -0
  19. package/dist/core/schemaValidator.js +3 -3
  20. package/dist/core/schemaValidator.js.map +1 -1
  21. package/dist/core/toolPreconditions.d.ts +0 -11
  22. package/dist/core/toolPreconditions.d.ts.map +1 -1
  23. package/dist/core/toolPreconditions.js +33 -164
  24. package/dist/core/toolPreconditions.js.map +1 -1
  25. package/dist/core/toolRuntime.d.ts.map +1 -1
  26. package/dist/core/toolRuntime.js +9 -114
  27. package/dist/core/toolRuntime.js.map +1 -1
  28. package/dist/core/updateChecker.d.ts +61 -1
  29. package/dist/core/updateChecker.d.ts.map +1 -1
  30. package/dist/core/updateChecker.js +147 -3
  31. package/dist/core/updateChecker.js.map +1 -1
  32. package/dist/headless/headlessApp.d.ts.map +1 -1
  33. package/dist/headless/headlessApp.js +0 -39
  34. package/dist/headless/headlessApp.js.map +1 -1
  35. package/dist/plugins/tools/nodeDefaults.d.ts.map +1 -1
  36. package/dist/plugins/tools/nodeDefaults.js +0 -2
  37. package/dist/plugins/tools/nodeDefaults.js.map +1 -1
  38. package/dist/providers/openaiResponsesProvider.d.ts.map +1 -1
  39. package/dist/providers/openaiResponsesProvider.js +79 -74
  40. package/dist/providers/openaiResponsesProvider.js.map +1 -1
  41. package/dist/runtime/agentController.d.ts.map +1 -1
  42. package/dist/runtime/agentController.js +6 -3
  43. package/dist/runtime/agentController.js.map +1 -1
  44. package/dist/runtime/agentSession.d.ts +0 -2
  45. package/dist/runtime/agentSession.d.ts.map +1 -1
  46. package/dist/runtime/agentSession.js +2 -2
  47. package/dist/runtime/agentSession.js.map +1 -1
  48. package/dist/shell/interactiveShell.d.ts +11 -12
  49. package/dist/shell/interactiveShell.d.ts.map +1 -1
  50. package/dist/shell/interactiveShell.js +269 -193
  51. package/dist/shell/interactiveShell.js.map +1 -1
  52. package/dist/shell/systemPrompt.d.ts.map +1 -1
  53. package/dist/shell/systemPrompt.js +4 -15
  54. package/dist/shell/systemPrompt.js.map +1 -1
  55. package/dist/subagents/taskRunner.js +2 -1
  56. package/dist/subagents/taskRunner.js.map +1 -1
  57. package/dist/tools/bashTools.d.ts.map +1 -1
  58. package/dist/tools/bashTools.js +101 -8
  59. package/dist/tools/bashTools.js.map +1 -1
  60. package/dist/tools/diffUtils.d.ts +8 -2
  61. package/dist/tools/diffUtils.d.ts.map +1 -1
  62. package/dist/tools/diffUtils.js +72 -13
  63. package/dist/tools/diffUtils.js.map +1 -1
  64. package/dist/tools/grepTools.d.ts.map +1 -1
  65. package/dist/tools/grepTools.js +10 -2
  66. package/dist/tools/grepTools.js.map +1 -1
  67. package/dist/tools/searchTools.d.ts.map +1 -1
  68. package/dist/tools/searchTools.js +4 -2
  69. package/dist/tools/searchTools.js.map +1 -1
  70. package/dist/ui/PromptController.d.ts +2 -3
  71. package/dist/ui/PromptController.d.ts.map +1 -1
  72. package/dist/ui/PromptController.js +2 -3
  73. package/dist/ui/PromptController.js.map +1 -1
  74. package/dist/ui/ShellUIAdapter.d.ts +71 -18
  75. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  76. package/dist/ui/ShellUIAdapter.js +237 -139
  77. package/dist/ui/ShellUIAdapter.js.map +1 -1
  78. package/dist/ui/UnifiedUIController.d.ts +0 -1
  79. package/dist/ui/UnifiedUIController.d.ts.map +1 -1
  80. package/dist/ui/UnifiedUIController.js +0 -1
  81. package/dist/ui/UnifiedUIController.js.map +1 -1
  82. package/dist/ui/UnifiedUIRenderer.d.ts +122 -7
  83. package/dist/ui/UnifiedUIRenderer.d.ts.map +1 -1
  84. package/dist/ui/UnifiedUIRenderer.js +823 -130
  85. package/dist/ui/UnifiedUIRenderer.js.map +1 -1
  86. package/dist/ui/animatedStatus.d.ts +129 -0
  87. package/dist/ui/animatedStatus.d.ts.map +1 -0
  88. package/dist/ui/animatedStatus.js +384 -0
  89. package/dist/ui/animatedStatus.js.map +1 -0
  90. package/dist/ui/display.d.ts +13 -48
  91. package/dist/ui/display.d.ts.map +1 -1
  92. package/dist/ui/display.js +22 -105
  93. package/dist/ui/display.js.map +1 -1
  94. package/dist/ui/shortcutsHelp.d.ts.map +1 -1
  95. package/dist/ui/shortcutsHelp.js +0 -1
  96. package/dist/ui/shortcutsHelp.js.map +1 -1
  97. package/dist/ui/unified/index.d.ts +1 -1
  98. package/dist/ui/unified/index.d.ts.map +1 -1
  99. package/dist/ui/unified/index.js +0 -2
  100. package/dist/ui/unified/index.js.map +1 -1
  101. package/package.json +1 -2
  102. package/dist/StringUtils.d.ts +0 -8
  103. package/dist/StringUtils.d.ts.map +0 -1
  104. package/dist/StringUtils.js +0 -11
  105. package/dist/StringUtils.js.map +0 -1
  106. package/dist/core/aiFlowSupervisor.d.ts +0 -44
  107. package/dist/core/aiFlowSupervisor.d.ts.map +0 -1
  108. package/dist/core/aiFlowSupervisor.js +0 -299
  109. package/dist/core/aiFlowSupervisor.js.map +0 -1
  110. package/dist/core/cliTestHarness.d.ts +0 -200
  111. package/dist/core/cliTestHarness.d.ts.map +0 -1
  112. package/dist/core/cliTestHarness.js +0 -549
  113. package/dist/core/cliTestHarness.js.map +0 -1
  114. package/dist/core/testUtils.d.ts +0 -121
  115. package/dist/core/testUtils.d.ts.map +0 -1
  116. package/dist/core/testUtils.js +0 -235
  117. package/dist/core/testUtils.js.map +0 -1
  118. package/dist/core/toolValidation.d.ts +0 -116
  119. package/dist/core/toolValidation.d.ts.map +0 -1
  120. package/dist/core/toolValidation.js +0 -282
  121. package/dist/core/toolValidation.js.map +0 -1
  122. package/dist/ui/compactRenderer.d.ts +0 -139
  123. package/dist/ui/compactRenderer.d.ts.map +0 -1
  124. package/dist/ui/compactRenderer.js +0 -398
  125. package/dist/ui/compactRenderer.js.map +0 -1
  126. package/dist/ui/streamingFormatter.d.ts +0 -30
  127. package/dist/ui/streamingFormatter.d.ts.map +0 -1
  128. package/dist/ui/streamingFormatter.js +0 -91
  129. package/dist/ui/streamingFormatter.js.map +0 -1
  130. package/dist/utils/errorUtils.d.ts +0 -16
  131. package/dist/utils/errorUtils.d.ts.map +0 -1
  132. package/dist/utils/errorUtils.js +0 -66
  133. package/dist/utils/errorUtils.js.map +0 -1
@@ -7,7 +7,6 @@ import { join, resolve } from 'node:path';
7
7
  import { display } from '../ui/display.js';
8
8
  import { theme } from '../ui/theme.js';
9
9
  import { getTerminalColumns } from '../ui/layout.js';
10
- import { StreamingResponseFormatter } from '../ui/streamingFormatter.js';
11
10
  import { getContextWindowTokens } from '../core/contextWindow.js';
12
11
  import { ensureSecretForProvider, getSecretDefinitionForProvider, getSecretValue, listSecretDefinitions, maskSecret, setSecretValue, } from '../core/secretStore.js';
13
12
  import { saveActiveProfilePreference, saveModelPreference, loadToolSettings, saveToolSettings, clearToolSettings, clearActiveProfilePreference, loadSessionPreferences, saveSessionPreferences, loadFeatureFlags, saveFeatureFlags, toggleFeatureFlag, FEATURE_FLAG_INFO, } from '../core/preferences.js';
@@ -19,7 +18,6 @@ import { detectNetworkError } from '../core/errors/networkErrors.js';
19
18
  import { buildWorkspaceContext } from '../workspace.js';
20
19
  import { buildInteractiveSystemPrompt } from './systemPrompt.js';
21
20
  import { getTaskCompletionDetector, resetTaskCompletionDetector, } from './taskCompletionDetector.js';
22
- import { assessAiFlow } from '../core/aiFlowSupervisor.js';
23
21
  import { discoverAllModels, quickCheckProviders, getCachedDiscoveredModels, sortModelsByPriority } from '../core/modelDiscovery.js';
24
22
  import { getModels, getSlashCommands, getProviders } from '../core/agentSchemaLoader.js';
25
23
  import { loadMcpServers } from '../mcp/config.js';
@@ -136,7 +134,6 @@ export class InteractiveShell {
136
134
  lastContextWarningLevel = null;
137
135
  sessionPreferences;
138
136
  autosaveEnabled;
139
- autoContinueEnabled;
140
137
  verificationEnabled = false;
141
138
  criticalApprovalMode = 'auto';
142
139
  editGuardMode = 'display-edits';
@@ -182,7 +179,6 @@ export class InteractiveShell {
182
179
  streamingOutputSuppressed = false;
183
180
  aiRuntimeStart = null;
184
181
  aiRuntimeTotalMs = 0;
185
- streamingFormatter = null;
186
182
  streamingThoughtBuffer = '';
187
183
  statusLineState = null;
188
184
  statusMessageOverride = null;
@@ -207,7 +203,6 @@ export class InteractiveShell {
207
203
  this.sessionPreferences = loadSessionPreferences();
208
204
  this.thinkingMode = this.sessionPreferences.thinkingMode;
209
205
  this.autosaveEnabled = this.sessionPreferences.autosave;
210
- this.autoContinueEnabled = this.sessionPreferences.autoContinue;
211
206
  this.criticalApprovalMode = this.sessionPreferences.criticalApprovalMode;
212
207
  const featureFlags = loadFeatureFlags();
213
208
  this.verificationEnabled = featureFlags.verification === true;
@@ -284,6 +279,10 @@ export class InteractiveShell {
284
279
  this.uiAdapter.setToolStatusCallback((status) => {
285
280
  this.updateStatusMessage(status ?? null);
286
281
  });
282
+ // Set up activity callback to update activity line with streaming bash output
283
+ this.uiAdapter.setActivityCallback((activity) => {
284
+ this.renderer?.setActivity(activity);
285
+ });
287
286
  this.skillRepository = new SkillRepository({
288
287
  workingDir: this.workingDir,
289
288
  env: process.env,
@@ -297,13 +296,14 @@ export class InteractiveShell {
297
296
  onQueue: (text) => this.handleQueuedInput(text),
298
297
  onCtrlC: ({ hadBuffer }) => this.handleCtrlCPress(hadBuffer),
299
298
  onInterrupt: () => this.handleInterrupt(),
299
+ onExit: () => this.handleExit(),
300
300
  onChange: ({ text, cursor }) => this.handleInputChange(text, cursor, 'renderer'),
301
301
  onEditModeChange: (mode) => this.handleEditModeChange(mode),
302
302
  onToggleVerify: () => this.toggleVerificationMode(),
303
- onToggleAutoContinue: () => this.toggleAutoContinueMode(),
304
303
  onToggleThinking: () => this.cycleThinkingMode(),
305
304
  onClearContext: () => this.handleClearContext(),
306
305
  onToggleCriticalApproval: () => this.toggleCriticalApprovalMode('shortcut'),
306
+ onExpandToolResult: () => this.expandLastToolResult(),
307
307
  });
308
308
  // Share renderer with Display so all output flows through the unified queue
309
309
  this.renderer = this.terminalInput.getRenderer();
@@ -447,18 +447,21 @@ export class InteractiveShell {
447
447
  const top = `╭${'─'.repeat(labelLeft)}${label}${'─'.repeat(labelRight)}╮`;
448
448
  const bottom = `╰${'─'.repeat(contentWidth)}╯`;
449
449
  const userName = process.env['USER'] || 'there';
450
- const logo = clamp('⟣╍◎╍⟢');
450
+ const logo = '⟣╍◎╍⟢';
451
451
  const versionLabel = version ? clamp(`${brand} · v${version}`) : brand;
452
452
  const modelLine = clamp(model);
453
453
  const workspaceLine = clamp(workspace);
454
- const welcomeLine = clamp(`Welcome back ${userName}!`);
454
+ // Compact welcome line with logo on same line
455
+ const welcomeLine = clamp(`${logo} Welcome back ${userName}! ${logo}`);
456
+ // Quick reference hints
457
+ const hintsLine = clamp('/model · /secrets · /help · ? shortcuts');
455
458
  const lines = [
456
459
  frame(top),
457
460
  frame(`│${padCenter(welcomeLine, contentWidth)}│`),
458
- frame(`│${padCenter(logo, contentWidth)}│`),
459
461
  frame(`│${padCenter(modelLine, contentWidth)}│`),
460
462
  frame(`│${padCenter(versionLabel, contentWidth)}│`),
461
463
  frame(`│${padCenter(workspaceLine, contentWidth)}│`),
464
+ frame(`│${padCenter(hintsLine, contentWidth)}│`),
462
465
  frame(bottom),
463
466
  ].join('\n');
464
467
  if (this.renderer) {
@@ -469,8 +472,6 @@ export class InteractiveShell {
469
472
  else {
470
473
  display.stream(`\n${lines}\n`);
471
474
  }
472
- // Check for updates asynchronously (non-blocking)
473
- void this.checkAndShowUpdates();
474
475
  // Keep UI pinned; no scrollback banners
475
476
  this.requestPromptRefresh(true);
476
477
  }
@@ -508,22 +509,32 @@ export class InteractiveShell {
508
509
  }
509
510
  async checkAndShowUpdates() {
510
511
  try {
511
- const { checkForUpdates, formatUpdateNotification, maybeAutoUpdate } = await import('../core/updateChecker.js');
512
+ const { checkForUpdates, getUpdateDecision, formatUpdateBanner, performBackgroundUpdate, readAutoUpdateState, shouldShowUpdateNotification, } = await import('../core/updateChecker.js');
512
513
  const currentVersion = this.version || this.getPackageInfo().version || '1.7.458';
513
514
  const updateInfo = await checkForUpdates(currentVersion);
514
515
  if (!updateInfo || !updateInfo.updateAvailable) {
515
516
  return;
516
517
  }
517
- const updateResult = await maybeAutoUpdate(currentVersion, {
518
- updateInfo,
519
- logger: (message) => this.streamEventBlock(message),
520
- });
521
- if (updateResult?.updated) {
518
+ // Check if we should show notification (respects quiet period)
519
+ const state = readAutoUpdateState();
520
+ if (!shouldShowUpdateNotification(state)) {
521
+ return;
522
+ }
523
+ // Get user's preference-based decision (no interactive prompt)
524
+ const sessionPrefs = loadSessionPreferences();
525
+ const decision = getUpdateDecision(sessionPrefs.autoUpdate);
526
+ if (decision === 'skip') {
527
+ // User chose to always skip - silently ignore updates
522
528
  return;
523
529
  }
524
- const note = this.describeUpdateSkipReason(updateResult);
525
- const notification = formatUpdateNotification(updateInfo, note);
526
- this.streamEventBlock(notification);
530
+ // Show non-interactive banner notification
531
+ this.streamEventBlock(formatUpdateBanner(updateInfo, decision));
532
+ if (decision === 'auto') {
533
+ // User chose to always update - perform background update
534
+ await performBackgroundUpdate(updateInfo, (message) => this.streamEventBlock(message));
535
+ }
536
+ // If decision === 'ask', we just show the banner with instructions
537
+ // User can run /update check or /update auto to update
527
538
  }
528
539
  catch {
529
540
  // Silently fail - don't interrupt user experience
@@ -607,6 +618,8 @@ export class InteractiveShell {
607
618
  this.pushUiEvent('raw', block);
608
619
  }
609
620
  async start(initialPrompt) {
621
+ // Check for updates BEFORE terminal input starts (to avoid keypress conflicts)
622
+ await this.checkAndShowUpdates();
610
623
  // Initialize the renderer before emitting the banner so we don't render the prompt twice
611
624
  this.terminalInput.start();
612
625
  this.resetRendererStreamingMode();
@@ -615,8 +628,9 @@ export class InteractiveShell {
615
628
  this.refreshControlBar();
616
629
  // Now sync renderer and control bar state
617
630
  this.syncRendererInput();
618
- // Ensure the prompt/control bar is rendered after the welcome banner
619
- this.ensureReadlineReady();
631
+ // CRITICAL: Force prompt render after banner - flushEvents should have done this,
632
+ // but explicitly ensure the prompt area is visible
633
+ this.renderer?.render();
620
634
  if (initialPrompt) {
621
635
  await this.enqueuePromptForProcessing(initialPrompt, 'programmatic');
622
636
  return;
@@ -624,7 +638,7 @@ export class InteractiveShell {
624
638
  this.showLaunchCommandPalette();
625
639
  // Ensure the terminal input is visible
626
640
  this.syncRendererInput();
627
- this.ensureReadlineReady();
641
+ this.renderer?.render();
628
642
  }
629
643
  showLaunchCommandPalette() {
630
644
  // Disabled: Quick commands palette takes up too much space
@@ -692,6 +706,10 @@ export class InteractiveShell {
692
706
  */
693
707
  async runPromptJob(job) {
694
708
  try {
709
+ // Show thinking indicator instead of "processing queued prompt"
710
+ display.showThinking('Thinking…');
711
+ // Re-echo the prompt to make it clear what's being processed
712
+ this.renderer?.emitPrompt(job.text);
695
713
  await this.processInputBlock(job.text);
696
714
  job.resolve();
697
715
  }
@@ -787,7 +805,6 @@ export class InteractiveShell {
787
805
  '/output-style',
788
806
  // Mode toggles
789
807
  '/thinking',
790
- '/autocontinue',
791
808
  // Discovery and plugins
792
809
  '/local', '/discover',
793
810
  '/plugins',
@@ -877,25 +894,22 @@ export class InteractiveShell {
877
894
  : '🛡️ High-impact actions now require your approval. (Ctrl+Shift+A to toggle; use /approvals ask to persist)';
878
895
  display.showSystemMessage(message);
879
896
  }
880
- toggleAutoContinueMode() {
881
- this.setAutoContinueMode(!this.autoContinueEnabled, 'shortcut');
882
- }
883
- setAutoContinueMode(enabled, source) {
884
- const changed = this.autoContinueEnabled !== enabled;
885
- this.autoContinueEnabled = enabled;
886
- saveSessionPreferences({ autoContinue: this.autoContinueEnabled });
887
- if (this.agent) {
888
- this.agent.setAutoContinue(this.autoContinueEnabled);
889
- }
890
- this.refreshControlBar();
891
- if (!changed && source === 'shortcut') {
897
+ /**
898
+ * Expand the last tool result (Ctrl+O shortcut).
899
+ * Shows the full output from the most recent tool call.
900
+ */
901
+ expandLastToolResult() {
902
+ const result = this.uiAdapter.getLastToolResult();
903
+ if (!result) {
904
+ display.showInfo('No tool result to expand. Run a tool first, then press Ctrl+O.');
892
905
  return;
893
906
  }
894
- const modeLabel = this.autoContinueEnabled ? 'enabled' : 'disabled';
895
- const behavior = this.autoContinueEnabled
896
- ? 'The model will be auto-prompted to continue when it expresses intent but does not use tools.'
897
- : 'The model will not be auto-prompted to continue.';
898
- display.showInfo(`Auto-continue ${modeLabel}. ${behavior} Toggle with Ctrl+Shift+C.`);
907
+ // Format the expanded output with tool name header
908
+ const header = `${theme.info('━━━')} ${theme.tool(result.toolName)} ${theme.ui.muted('output')} ${theme.info('━━━')}`;
909
+ const content = result.output.trim() || '(empty result)';
910
+ const footer = theme.ui.muted('━'.repeat(Math.min(60, header.length)));
911
+ // Display the expanded result
912
+ display.stream(`\n${header}\n${content}\n${footer}\n\n`);
899
913
  }
900
914
  /**
901
915
  * Cycle through thinking modes (Tab shortcut).
@@ -1049,6 +1063,13 @@ export class InteractiveShell {
1049
1063
  }
1050
1064
  this.ctrlCHandledThisPress = false;
1051
1065
  }
1066
+ /**
1067
+ * Handle exit request (Ctrl+C with empty buffer in idle mode)
1068
+ */
1069
+ handleExit() {
1070
+ display.showSystemMessage('\nGoodbye!\n');
1071
+ this.shutdown();
1072
+ }
1052
1073
  /**
1053
1074
  * Gracefully tear down the shell and exit
1054
1075
  */
@@ -1618,9 +1639,7 @@ export class InteractiveShell {
1618
1639
  refreshControlBar() {
1619
1640
  this.terminalInput.setModeToggles({
1620
1641
  verificationEnabled: this.verificationEnabled,
1621
- autoContinueEnabled: this.autoContinueEnabled,
1622
1642
  verificationHotkey: 'ctrl+shift+v',
1623
- autoContinueHotkey: 'ctrl+shift+c',
1624
1643
  thinkingModeLabel: (this.thinkingMode || 'off').toString(),
1625
1644
  thinkingHotkey: 'tab',
1626
1645
  criticalApprovalMode: this.criticalApprovalMode,
@@ -1711,15 +1730,14 @@ export class InteractiveShell {
1711
1730
  */
1712
1731
  refreshStatusLine(forceRender = false) {
1713
1732
  const elapsedSeconds = this.getAiRuntimeSeconds();
1714
- const thinkingMs = display.isSpinnerActive() ? display.getThinkingElapsedMs() : null;
1715
1733
  const tokensUsed = this.latestTokenUsage.used;
1716
1734
  const tokenLimit = this.latestTokenUsage.limit ?? this.activeContextWindowTokens;
1717
1735
  this.terminalInput.setMetaStatus({
1718
1736
  elapsedSeconds,
1719
1737
  tokensUsed,
1720
1738
  tokenLimit,
1721
- thinkingMs,
1722
- thinkingHasContent: display.isSpinnerActive(),
1739
+ thinkingMs: null,
1740
+ thinkingHasContent: false,
1723
1741
  });
1724
1742
  // Keep model/provider visible in the controls bar
1725
1743
  this.terminalInput.setModelContext({
@@ -1895,12 +1913,16 @@ export class InteractiveShell {
1895
1913
  enterStreamingMode();
1896
1914
  // Set up scroll region for streaming content
1897
1915
  this.uiUpdates.setMode('streaming');
1916
+ // Show activity status with animated spinner - use provided label or default
1917
+ const activityLabel = label || 'Thinking';
1918
+ this.renderer?.setActivity(activityLabel);
1898
1919
  this.streamingHeartbeatStart = Date.now();
1899
1920
  this.streamingContentSeen = false;
1900
1921
  this.streamingStatusText = null;
1901
1922
  this.streamingStatusLastUpdate = null;
1902
- // Suppress raw streaming lines; show only a single summary/status + final response
1903
- this.streamingOutputSuppressed = true;
1923
+ this.streamingTokenCount = 0; // Reset token count for new prompt
1924
+ // Show raw streaming output in real-time (Claude Code style)
1925
+ this.streamingOutputSuppressed = false;
1904
1926
  const initialLabel = this.isMeaningfulStreamingSnippet(label)
1905
1927
  ? this.truncateStreamingLabel(label)
1906
1928
  : this.getStreamingFallbackLabel();
@@ -1944,6 +1966,8 @@ export class InteractiveShell {
1944
1966
  this.streamingStatusLastUpdate = null;
1945
1967
  this.streamingStatusText = null;
1946
1968
  this.streamingOutputSuppressed = false;
1969
+ // Clear activity status when streaming ends
1970
+ this.renderer?.setActivity(null);
1947
1971
  // Emit a streaming note for stop/quit so the status stays inside the stream
1948
1972
  if (reason === 'stop' || reason === 'quit') {
1949
1973
  const note = reason === 'quit' ? 'Session closed.' : 'Stream stopped.';
@@ -1960,11 +1984,15 @@ export class InteractiveShell {
1960
1984
  // Buffer for accumulating partial <thinking> tags during streaming
1961
1985
  thinkingTagBuffer = '';
1962
1986
  insideThinkingBlock = false;
1987
+ streamingTokenCount = 0;
1963
1988
  handleStreamChunk(chunk, type = 'content') {
1964
1989
  if (!chunk) {
1965
1990
  return;
1966
1991
  }
1967
1992
  const isReasoning = type === 'reasoning';
1993
+ // Approximate token count (roughly 4 chars per token)
1994
+ this.streamingTokenCount += Math.ceil(chunk.length / 4);
1995
+ this.renderer?.updateStreamingTokens(this.streamingTokenCount);
1968
1996
  // Keep pinned status updated for all streaming chunks
1969
1997
  this.updateStreamingStatusFromChunk(chunk);
1970
1998
  // Handle <thinking> tags as separate events in the queue
@@ -1973,37 +2001,32 @@ export class InteractiveShell {
1973
2001
  if (!processed.contentChunk && !processed.thinkingChunk) {
1974
2002
  return;
1975
2003
  }
1976
- // Emit thinking block as separate 'thought' event
1977
- if (processed.thinkingChunk) {
1978
- this.pushUiEvent('thought', `${theme.ui.muted('⏺')} ${theme.ui.muted('<thinking>')}${processed.thinkingChunk}${theme.ui.muted('</thinking>')}`);
1979
- }
2004
+ // Don't emit thinking blocks during streaming - they will be rendered
2005
+ // as a complete block by onAssistantMessage when the response is final.
2006
+ // This prevents duplicate display.
1980
2007
  // Process regular content (skip if no content after extracting thinking)
1981
2008
  const contentChunk = processed.contentChunk;
1982
2009
  if (!contentChunk) {
1983
2010
  return;
1984
2011
  }
1985
2012
  // Suppress raw streaming output in scrollback; keep only status + final message.
1986
- // Reasoning chunks bypass this so the thought process stays visible.
1987
- if (this.streamingOutputSuppressed && !isReasoning) {
2013
+ // Reasoning tokens should also be suppressed and shown only in the indicator.
2014
+ if (this.streamingOutputSuppressed) {
1988
2015
  this.captureStreamingThought(contentChunk);
1989
2016
  this.streamingContentSeen = true;
2017
+ // Update thinking indicator with a snippet of the content
2018
+ const prefix = isReasoning ? '○ ' : '';
2019
+ const snippet = contentChunk.replace(/\s+/g, ' ').trim().slice(0, 60);
2020
+ if (snippet) {
2021
+ display.updateThinking(prefix + snippet + (contentChunk.length > 60 ? '…' : ''));
2022
+ }
1990
2023
  return;
1991
2024
  }
1992
- if (!this.streamingFormatter) {
1993
- this.streamingFormatter = new StreamingResponseFormatter(output.columns ?? undefined);
1994
- this.pushUiEvent('streaming', this.streamingFormatter.header());
1995
- }
1996
- this.streamingFormatter.setMode(isReasoning ? 'reasoning' : 'content');
1997
- const formatted = isReasoning
1998
- ? this.streamingFormatter.formatReasoningChunk(contentChunk)
1999
- : this.streamingFormatter.formatChunk(contentChunk);
2025
+ // Buffer all streaming content - rendered as complete block via onAssistantMessage
2026
+ // This prevents fragmented display and ensures thinking tags are properly processed
2000
2027
  this.captureStreamingThought(contentChunk);
2001
- if (formatted) {
2002
- if (formatted.trim().length > 0) {
2003
- this.streamingContentSeen = true;
2004
- }
2005
- this.pushUiEvent('streaming', formatted);
2006
- }
2028
+ // Activity: show what the model is doing (stable, informative)
2029
+ this.renderer?.setActivity(type === 'reasoning' ? 'Reasoning' : 'Writing');
2007
2030
  }
2008
2031
  /**
2009
2032
  * Process streaming content to extract <thinking> blocks as separate events.
@@ -2051,22 +2074,12 @@ export class InteractiveShell {
2051
2074
  }
2052
2075
  return { thinkingChunk: thinkingContent, contentChunk: remainingContent };
2053
2076
  }
2054
- finishStreamingFormatter(note, options) {
2055
- if (!this.streamingFormatter) {
2056
- return;
2057
- }
2058
- const closing = this.streamingFormatter.finish({
2059
- note,
2060
- mode: options?.mode ?? 'complete',
2061
- });
2062
- if (closing) {
2063
- this.pushUiEvent('streaming', closing);
2064
- }
2077
+ finishStreamingFormatter(_note, options) {
2078
+ // Flush any remaining buffered thoughts
2065
2079
  if (this.streamingThoughtBuffer.trim()) {
2066
2080
  this.ui.controller.recordAssistantThought(this.streamingThoughtBuffer.trim());
2067
2081
  }
2068
2082
  this.streamingThoughtBuffer = '';
2069
- this.streamingFormatter = null;
2070
2083
  if (options?.refreshPrompt ?? true) {
2071
2084
  this.requestPromptRefresh(true);
2072
2085
  }
@@ -2405,9 +2418,6 @@ export class InteractiveShell {
2405
2418
  case '/thinking':
2406
2419
  this.handleThinkingCommand(input);
2407
2420
  break;
2408
- case '/autocontinue':
2409
- this.handleAutoContinueCommand(input);
2410
- break;
2411
2421
  case '/shortcuts':
2412
2422
  case '/keys':
2413
2423
  this.handleShortcutsCommand();
@@ -2511,6 +2521,9 @@ export class InteractiveShell {
2511
2521
  case '/permissions':
2512
2522
  this.handlePermissionsCommand();
2513
2523
  break;
2524
+ case '/update':
2525
+ await this.handleUpdateCommand(input);
2526
+ break;
2514
2527
  case '/init':
2515
2528
  this.handleInitCommand(input);
2516
2529
  break;
@@ -2568,7 +2581,6 @@ export class InteractiveShell {
2568
2581
  theme.bold(' Mode Toggles'),
2569
2582
  ` ${theme.info('Shift+Tab')} ${theme.ui.muted('Toggle edit mode (auto/ask)')}`,
2570
2583
  ` ${theme.info('Option+V')} ${theme.ui.muted('Toggle verification')}`,
2571
- ` ${theme.info('Option+C')} ${theme.ui.muted('Toggle auto-continue')}`,
2572
2584
  ` ${theme.info('Ctrl+Shift+A')} ${theme.ui.muted('Toggle approvals for high-impact actions')}`,
2573
2585
  ` ${theme.info('Option+T')} ${theme.ui.muted('Cycle thinking mode')}`,
2574
2586
  ` ${theme.info('Option+E')} ${theme.ui.muted('Toggle edit permission mode')}`,
@@ -4282,20 +4294,6 @@ export class InteractiveShell {
4282
4294
  clearAutosaveSnapshot(this.profile);
4283
4295
  display.showInfo('Cleared autosave history.');
4284
4296
  }
4285
- handleAutoContinueCommand(input) {
4286
- const tokens = input.split(/\s+/).slice(1);
4287
- const value = tokens[0]?.toLowerCase();
4288
- if (!value) {
4289
- // Show current status
4290
- display.showInfo(`Auto-continue is ${this.autoContinueEnabled ? 'enabled' : 'disabled'}. Use /autocontinue on|off or Ctrl+Shift+C to toggle.`);
4291
- return;
4292
- }
4293
- if (value !== 'on' && value !== 'off') {
4294
- display.showWarning('Usage: /autocontinue on|off');
4295
- return;
4296
- }
4297
- this.setAutoContinueMode(value === 'on', 'command');
4298
- }
4299
4297
  // ==================== Erosolar-CLI Style Commands ====================
4300
4298
  async handleRewindCommand(_input) {
4301
4299
  const lines = [];
@@ -4527,7 +4525,6 @@ export class InteractiveShell {
4527
4525
  lines.push(`${theme.primary('Provider/Model')}: ${theme.info(`${this.providerLabel(this.sessionState.provider)} · ${this.sessionState.model}`)}`);
4528
4526
  lines.push(`${theme.primary('Workspace')}: ${theme.ui.muted(this.abbreviatePath(this.workingDir))}`);
4529
4527
  lines.push(`${theme.primary('Autosave')}: ${this.autosaveEnabled ? theme.success('on') : theme.ui.muted('off')}`);
4530
- lines.push(`${theme.primary('Auto-continue')}: ${this.autoContinueEnabled ? theme.success('on') : theme.ui.muted('off')}`);
4531
4528
  lines.push(`${theme.primary('Verification')}: ${this.verificationEnabled ? theme.success('on') : theme.ui.muted('off')}`);
4532
4529
  lines.push(`${theme.primary('Approvals')}: ${theme.ui.muted(this.describeEditGuardMode())}`);
4533
4530
  lines.push(`${theme.primary('Critical approvals')}: ${this.criticalApprovalMode === 'approval' ? theme.warning('ask') : theme.ui.muted('auto')}`);
@@ -4683,6 +4680,67 @@ export class InteractiveShell {
4683
4680
  lines.push(theme.ui.muted('Use /tools to manage which tool suites are enabled.'));
4684
4681
  display.showSystemMessage(lines.join('\n'));
4685
4682
  }
4683
+ async handleUpdateCommand(input) {
4684
+ const tokens = input.split(/\s+/).slice(1);
4685
+ const subcommand = tokens[0]?.toLowerCase();
4686
+ const prefs = loadSessionPreferences();
4687
+ const currentPref = prefs.autoUpdate;
4688
+ const prefLabel = currentPref === true ? 'always update' : currentPref === false ? 'always skip' : 'notify only';
4689
+ // Show status or help
4690
+ if (!subcommand || subcommand === 'status') {
4691
+ const lines = [];
4692
+ lines.push(theme.bold('Update Settings'));
4693
+ lines.push('');
4694
+ lines.push(`Current preference: ${theme.info(prefLabel)}`);
4695
+ lines.push('');
4696
+ lines.push(theme.secondary('Commands:'));
4697
+ lines.push(' /update check - Check for updates and install immediately');
4698
+ lines.push(' /update auto - Always auto-update in background');
4699
+ lines.push(' /update skip - Never auto-update (silent)');
4700
+ lines.push(' /update notify - Show notification only (default)');
4701
+ display.showSystemMessage(lines.join('\n'));
4702
+ return;
4703
+ }
4704
+ if (subcommand === 'check') {
4705
+ // Force check and perform update immediately (non-interactive)
4706
+ try {
4707
+ const { checkForUpdates, performUpdate } = await import('../core/updateChecker.js');
4708
+ const currentVersion = this.version || this.getPackageInfo().version || '1.7.458';
4709
+ const updateInfo = await checkForUpdates(currentVersion);
4710
+ if (!updateInfo) {
4711
+ display.showWarning('Unable to check for updates (network issue or timeout).');
4712
+ return;
4713
+ }
4714
+ if (!updateInfo.updateAvailable) {
4715
+ display.showSuccess(`You're on the latest version (v${updateInfo.current}).`);
4716
+ return;
4717
+ }
4718
+ // Show update info and perform update immediately (no interactive prompt)
4719
+ display.showInfo(`Update available: v${updateInfo.current} → v${updateInfo.latest}`);
4720
+ await performUpdate(updateInfo, (msg) => this.streamEventBlock(msg));
4721
+ }
4722
+ catch (error) {
4723
+ display.showError(`Update check failed: ${error instanceof Error ? error.message : String(error)}`);
4724
+ }
4725
+ return;
4726
+ }
4727
+ if (subcommand === 'auto' || subcommand === 'always') {
4728
+ saveSessionPreferences({ autoUpdate: true });
4729
+ display.showSuccess('Auto-update enabled. Updates will install automatically.');
4730
+ return;
4731
+ }
4732
+ if (subcommand === 'skip' || subcommand === 'never' || subcommand === 'off') {
4733
+ saveSessionPreferences({ autoUpdate: false });
4734
+ display.showInfo('Auto-update disabled. Updates will be skipped silently.');
4735
+ return;
4736
+ }
4737
+ if (subcommand === 'ask' || subcommand === 'prompt' || subcommand === 'reset' || subcommand === 'notify') {
4738
+ saveSessionPreferences({ autoUpdate: null });
4739
+ display.showInfo('Update preference reset. You will see a notification when updates are available.');
4740
+ return;
4741
+ }
4742
+ display.showWarning('Usage: /update [check|auto|skip|notify|status]');
4743
+ }
4686
4744
  handleInitCommand(input) {
4687
4745
  const tokens = input.split(/\s+/).slice(1);
4688
4746
  const confirm = tokens[0]?.toLowerCase() === 'confirm';
@@ -5707,6 +5765,7 @@ export class InteractiveShell {
5707
5765
  }
5708
5766
  this.isProcessing = true;
5709
5767
  this.uiUpdates.setMode('processing');
5768
+ this.streamingTokenCount = 0; // Reset token counter for new request
5710
5769
  this.terminalInput.setStreaming(true);
5711
5770
  // Keep the persistent input/control bar active as we transition into streaming.
5712
5771
  this.syncRendererInput();
@@ -5721,6 +5780,7 @@ export class InteractiveShell {
5721
5780
  this.currentTaskType = classifyTaskType(request);
5722
5781
  this.currentToolCalls = [];
5723
5782
  this.clearToolUsageMeta();
5783
+ this.renderer?.setActivity('Starting...');
5724
5784
  this.uiAdapter.startProcessing('Working on your request');
5725
5785
  this.setProcessingStatus();
5726
5786
  this.beginAiRuntime();
@@ -5776,8 +5836,6 @@ export class InteractiveShell {
5776
5836
  clearActionHistory();
5777
5837
  this.lastFailure = null;
5778
5838
  }
5779
- // Run post-hoc AI flow sanity check to catch "looks right but wrong" responses
5780
- this.analyzeAiFlowForRun(request, responseText, requestStartTime);
5781
5839
  }
5782
5840
  catch (error) {
5783
5841
  const handled = this.handleProviderError(error, () => this.processRequest(request));
@@ -5848,6 +5906,7 @@ export class InteractiveShell {
5848
5906
  this.clearToolUsageMeta();
5849
5907
  this.isProcessing = true;
5850
5908
  this.uiUpdates.setMode('processing');
5909
+ this.streamingTokenCount = 0; // Reset token counter for new request
5851
5910
  this.terminalInput.setStreaming(true);
5852
5911
  if (this.suppressNextNetworkReset) {
5853
5912
  this.suppressNextNetworkReset = false;
@@ -6255,43 +6314,6 @@ What's the next action?`;
6255
6314
  const parts = candidates.filter((part) => typeof part === 'string' && part.trim().length > 0);
6256
6315
  return parts.join('\n').trim();
6257
6316
  }
6258
- /**
6259
- * Post-run sanity check to catch hallucinated "looks right" answers.
6260
- * Compares the prompt/response against per-run tool usage and surfaces risks.
6261
- */
6262
- analyzeAiFlowForRun(prompt, responseText, startedAt) {
6263
- const toolHistory = this.runtimeSession.toolRuntime.getToolHistory?.();
6264
- if (!toolHistory) {
6265
- return;
6266
- }
6267
- const diffSnapshots = this.runtimeSession.toolRuntime.getDiffSnapshots?.() ?? [];
6268
- const recentDiffs = diffSnapshots
6269
- .filter((snapshot) => snapshot.timestamp >= startedAt)
6270
- .map(({ command, output }) => ({ command, output }));
6271
- const assessment = assessAiFlow({
6272
- prompt,
6273
- response: responseText,
6274
- startedAt,
6275
- toolHistory,
6276
- diffSnapshots: recentDiffs,
6277
- });
6278
- this.reportAiFlowAssessment(assessment);
6279
- }
6280
- reportAiFlowAssessment(assessment) {
6281
- if (!assessment || assessment.risks.length === 0) {
6282
- return;
6283
- }
6284
- display.showSystemMessage('⚠️ AI flow check: potential unverified completion detected.');
6285
- for (const risk of assessment.risks) {
6286
- const detail = risk.details ? ` — ${risk.details}` : '';
6287
- display.showWarning(`${risk.summary}${detail}`);
6288
- }
6289
- if (assessment.recommendations.length > 0) {
6290
- const uniqueRecommendations = Array.from(new Set(assessment.recommendations));
6291
- const advice = uniqueRecommendations.slice(0, 3).map((rec, idx) => `${idx + 1}. ${rec}`).join('\n');
6292
- display.showSystemMessage(`Follow-ups to de-risk this run:\n${advice}`);
6293
- }
6294
- }
6295
6317
  runAutoQualityChecks(trigger, assistantResponse, verificationContext) {
6296
6318
  if (!this.verificationEnabled) {
6297
6319
  return;
@@ -6672,21 +6694,41 @@ Return ONLY JSON array:
6672
6694
  maxTokens: this.sessionState.maxTokens,
6673
6695
  systemPrompt: this.buildSystemPrompt(),
6674
6696
  reasoningEffort: this.sessionState.reasoningEffort,
6675
- autoContinue: this.autoContinueEnabled,
6676
6697
  };
6677
6698
  this.agent = this.runtimeSession.createAgent(selection, {
6699
+ onRequestReceived: (requestPreview) => {
6700
+ const normalized = requestPreview?.trim();
6701
+ const activity = normalized ? `Working: ${normalized}` : 'Working';
6702
+ this.renderer?.setActivity(activity);
6703
+ },
6704
+ onBeforeFirstToolCall: (toolNames) => {
6705
+ const primaryTool = toolNames[0];
6706
+ if (primaryTool) {
6707
+ this.renderer?.setActivity(`Running ${primaryTool}`);
6708
+ }
6709
+ return undefined;
6710
+ },
6678
6711
  onStreamChunk: (chunk, type) => {
6679
6712
  this.handleStreamChunk(chunk, type ?? 'content');
6680
6713
  },
6681
- onStreamFallback: (info) => this.handleStreamingFallback(info),
6682
6714
  onAssistantMessage: (content, metadata) => {
6683
6715
  const enriched = this.buildDisplayMetadata(metadata);
6684
6716
  const streamedVisible = metadata.wasStreamed && this.streamingContentSeen && !this.streamingOutputSuppressed;
6685
6717
  let renderedFinal = false;
6718
+ // Update streaming token count from usage info (more accurate than chunk counting)
6719
+ if (metadata.usage) {
6720
+ const totalTokens = this.totalTokens(metadata.usage);
6721
+ if (totalTokens !== null && totalTokens > this.streamingTokenCount) {
6722
+ this.streamingTokenCount = totalTokens;
6723
+ this.renderer?.updateStreamingTokens(this.streamingTokenCount);
6724
+ }
6725
+ }
6686
6726
  // Update spinner based on message type
6687
6727
  if (metadata.isFinal) {
6688
6728
  const parsed = this.splitThinkingResponse(content);
6689
- const finalContent = parsed?.response?.trim() || content;
6729
+ // If we successfully parsed thinking, use the parsed response (may be empty)
6730
+ // Don't fall back to original content with raw <thinking> tags
6731
+ const finalContent = parsed ? parsed.response?.trim() : content;
6690
6732
  const thoughtContent = parsed?.thinking?.trim() || null;
6691
6733
  // Show the response if it wasn't already rendered during streaming
6692
6734
  if (!streamedVisible) {
@@ -6789,12 +6831,6 @@ Return ONLY JSON array:
6789
6831
  this.updateStatusMessage('Retrying with reduced context...');
6790
6832
  this.syncRendererInput();
6791
6833
  },
6792
- onAutoContinue: (attempt, maxAttempts, _message) => {
6793
- // Show auto-continue progress in UI
6794
- display.showSystemMessage(`🔄 Auto-continue (${attempt}/${maxAttempts}): Model expressed intent, prompting to act...`);
6795
- this.updateStatusMessage('Auto-continuing...');
6796
- this.syncRendererInput();
6797
- },
6798
6834
  onCancelled: () => {
6799
6835
  // Update UI to show operation was cancelled
6800
6836
  display.showWarning('Operation cancelled.');
@@ -6803,10 +6839,42 @@ Return ONLY JSON array:
6803
6839
  this.updateStatusMessage(null);
6804
6840
  this.terminalInput.setStreaming(false);
6805
6841
  },
6842
+ onToolExecution: (toolName, isStart, args) => {
6843
+ // Update activity status to show what tool is being executed
6844
+ if (isStart) {
6845
+ // Show more specific activity for long-running tools
6846
+ let activity = `Running ${toolName}`;
6847
+ if (toolName === 'execute_bash' && args?.['command']) {
6848
+ const cmd = String(args['command']).slice(0, 40);
6849
+ activity = `$ ${cmd}${String(args['command']).length > 40 ? '...' : ''}`;
6850
+ }
6851
+ else if (toolName === 'read_file' && args?.['file_path']) {
6852
+ const path = String(args['file_path']).split('/').pop() || args['file_path'];
6853
+ activity = `Reading ${path}`;
6854
+ }
6855
+ this.renderer?.setActivity(activity);
6856
+ // Estimate tokens for tool call (~50 tokens per call)
6857
+ this.streamingTokenCount += 50;
6858
+ this.renderer?.updateStreamingTokens(this.streamingTokenCount);
6859
+ }
6860
+ else {
6861
+ // Tool finished - estimate result tokens (~100 per result)
6862
+ this.streamingTokenCount += 100;
6863
+ this.renderer?.updateStreamingTokens(this.streamingTokenCount);
6864
+ // Reset to thinking state while model generates next response
6865
+ this.renderer?.setActivity('Thinking');
6866
+ }
6867
+ },
6806
6868
  onVerificationNeeded: (response, context) => {
6807
6869
  this.lastAssistantResponse = response;
6808
6870
  void this.runAutoQualityChecks('verification', response, context);
6809
6871
  },
6872
+ // Retry notification for transient errors
6873
+ onRetrying: (attempt, maxAttempts, error) => {
6874
+ const shortError = error.message.slice(0, 100);
6875
+ display.showSystemMessage(`⚡ Retry ${attempt}/${maxAttempts}: ${shortError}${error.message.length > 100 ? '...' : ''}`);
6876
+ this.renderer?.setActivity(`Retrying (${attempt}/${maxAttempts})...`);
6877
+ },
6810
6878
  });
6811
6879
  // Register global AI enhancer for explore tool - uses active model by default
6812
6880
  this.registerExploreAIEnhancer();
@@ -6901,20 +6969,30 @@ Return ONLY JSON array:
6901
6969
  return lines.join('\n').trim();
6902
6970
  }
6903
6971
  buildThinkingDirective() {
6972
+ // Base requirement: ALWAYS think before acting (applies to all modes)
6973
+ const baseRequirement = [
6974
+ 'CRITICAL: Before calling ANY tool, ALWAYS output a <thinking>...</thinking> block explaining:',
6975
+ '1. What you understand the user is asking',
6976
+ '2. Your approach/plan to solve it',
6977
+ '3. Which tools you will use and why',
6978
+ 'This thinking block MUST appear before your first tool call in every response.',
6979
+ ].join('\n');
6904
6980
  switch (this.thinkingMode) {
6905
6981
  case 'extended':
6906
6982
  return [
6907
- 'Extended thinking mode is enabled. Format every reply as:',
6908
- '<thinking>',
6909
- 'Detailed multi-step reasoning (reference tool runs/files when relevant, keep secrets redacted, no code blocks unless citing filenames).',
6910
- '</thinking>',
6911
- '<response>',
6912
- 'Final answer with actionable next steps and any code/commands requested.',
6913
- '</response>',
6983
+ baseRequirement,
6984
+ '',
6985
+ 'Extended thinking mode: Use detailed multi-step reasoning in your <thinking> block.',
6986
+ 'Reference tool runs/files when relevant. Keep secrets redacted.',
6987
+ 'After thinking, wrap your final answer in <response>...</response>.',
6914
6988
  ].join('\n');
6915
6989
  case 'balanced':
6916
6990
  default:
6917
- return 'Balanced thinking mode: include a short <thinking>...</thinking> block before <response> when the reasoning is non-trivial; skip it for simple answers.';
6991
+ return [
6992
+ baseRequirement,
6993
+ '',
6994
+ 'Balanced mode: Keep thinking concise (2-4 sentences) but always present.',
6995
+ ].join('\n');
6918
6996
  }
6919
6997
  }
6920
6998
  buildDisplayMetadata(metadata) {
@@ -7005,13 +7083,14 @@ Return ONLY JSON array:
7005
7083
  });
7006
7084
  cleanupOverlayActive = true;
7007
7085
  const triggerReason = trigger?.reason ?? 'Context optimization';
7008
- const limitLabel = resolvedWindowTokens
7009
- ? `of ${resolvedWindowTokens.toLocaleString('en-US')} tokens`
7010
- : 'of estimated context';
7011
- display.showSystemMessage([
7012
- `Context usage: ${resolvedTotalTokens.toLocaleString('en-US')} ${limitLabel}`,
7013
- `(${percentUsed}% full). Auto-compacting${triggerReason ? `: ${triggerReason}` : '...'}`,
7014
- ].join(' '));
7086
+ // Claude Code style: Show "Compacting conversation..." with animation
7087
+ // The renderer handles the animated spinner display
7088
+ if (this.renderer) {
7089
+ this.renderer.showCompactingStatus('Compacting conversation… (esc to interrupt)');
7090
+ }
7091
+ else {
7092
+ display.showSystemMessage(`✻ Compacting conversation… (esc to interrupt)`);
7093
+ }
7015
7094
  const result = await contextManager.intelligentCompact(history);
7016
7095
  let afterStats = contextManager.getStats(result.compacted);
7017
7096
  let appliedHistory = result.compacted;
@@ -7038,10 +7117,18 @@ Return ONLY JSON array:
7038
7117
  }
7039
7118
  }
7040
7119
  if (!changed) {
7120
+ // Hide compacting status before showing info message
7121
+ if (this.renderer) {
7122
+ this.renderer.hideCompactingStatus();
7123
+ }
7041
7124
  display.showInfo('Context compaction completed but no changes were applied.');
7042
7125
  return;
7043
7126
  }
7044
7127
  if (!this.hasMeaningfulCompaction(changed, bestTokenSavings, bestPercentSavings, trigger?.forced)) {
7128
+ // Hide compacting status before showing info message
7129
+ if (this.renderer) {
7130
+ this.renderer.hideCompactingStatus();
7131
+ }
7045
7132
  display.showInfo('Auto-compaction completed but did not meaningfully reduce context size. Keeping existing history.');
7046
7133
  return;
7047
7134
  }
@@ -7057,15 +7144,18 @@ Return ONLY JSON array:
7057
7144
  this.updateContextUsage(newPercentUsed, CONTEXT_AUTOCOMPACT_PERCENT);
7058
7145
  this.lastContextWarningLevel = this.getContextWarningLevel(newPercentUsed);
7059
7146
  this.refreshStatusLine(true);
7060
- const primarySignal = result.analysis.signals[0]?.reason ?? triggerReason;
7061
- const savingsLabel = bestTokenSavings > 0
7062
- ? `(saved ~${bestTokenSavings.toLocaleString('en-US')} tokens, ~${bestPercentSavings.toFixed(1)}%)`
7063
- : '(no token savings)';
7064
- display.showSystemMessage([
7065
- `Context compacted: ${beforeStats.percentage}% ${afterStats.percentage}%`,
7066
- savingsLabel,
7067
- primarySignal ? `Reason: ${primarySignal}.` : '',
7068
- ].filter(Boolean).join(' '));
7147
+ // Claude Code style: Show compaction complete with separator and ctrl+o hint
7148
+ // Stop the compacting animation first
7149
+ if (this.renderer) {
7150
+ this.renderer.hideCompactingStatus();
7151
+ }
7152
+ // Show the Claude Code style separator: ══ Conversation compacted · ctrl+o for history ═
7153
+ if (this.renderer) {
7154
+ this.renderer.addCompactBlock('', 'Conversation compacted · ctrl+o for history');
7155
+ }
7156
+ else {
7157
+ display.showSystemMessage('══ Conversation compacted · ctrl+o for history ═');
7158
+ }
7069
7159
  this.recordContextCompaction({
7070
7160
  timestamp: Date.now(),
7071
7161
  source: trigger?.source ?? 'auto',
@@ -7079,12 +7169,20 @@ Return ONLY JSON array:
7079
7169
  });
7080
7170
  }
7081
7171
  catch (error) {
7172
+ // Hide compacting status animation on error
7173
+ if (this.renderer) {
7174
+ this.renderer.hideCompactingStatus();
7175
+ }
7082
7176
  display.showError('Context compaction failed.', error);
7083
7177
  }
7084
7178
  finally {
7085
7179
  if (cleanupOverlayActive) {
7086
7180
  this.statusTracker.clearOverride(cleanupStatusId);
7087
7181
  }
7182
+ // Ensure compacting status is cleared
7183
+ if (this.renderer) {
7184
+ this.renderer.hideCompactingStatus();
7185
+ }
7088
7186
  this.cleanupInProgress = false;
7089
7187
  this.contextCompactionInFlight = false;
7090
7188
  }
@@ -7386,28 +7484,6 @@ Return ONLY JSON array:
7386
7484
  const message = error instanceof Error ? error.message : String(error);
7387
7485
  display.showError(message);
7388
7486
  }
7389
- handleStreamingFallback(info) {
7390
- const promptBlock = detectPromptBlockError(info.error ?? info.message);
7391
- if (promptBlock) {
7392
- this.handlePromptBlock(promptBlock);
7393
- this.finishStreamingFormatter('Prompt blocked mid-stream', { mode: 'update' });
7394
- display.showSystemMessage('Retrying without streaming, but the provider will likely block again until you rephrase.');
7395
- this.startStreamingHeartbeat('Re-running without streaming');
7396
- this.requestPromptRefresh(true);
7397
- return;
7398
- }
7399
- const detailText = info.message?.trim() ?? '';
7400
- const detail = detailText ? ` ${detailText}` : '';
7401
- const reason = info.reason ? ` (${info.reason.replace(/-/g, ' ')})` : '';
7402
- const partialNote = info.partialResponse
7403
- ? ' Partial stream captured before failure; continuing from it without restarting.'
7404
- : '';
7405
- const baseMessage = 'Streaming interrupted, retrying without streaming';
7406
- display.showWarning(`${baseMessage}${reason}.${detail}${partialNote}`.trim());
7407
- this.finishStreamingFormatter('Stream interrupted - retrying without streaming', { mode: 'update' });
7408
- this.startStreamingHeartbeat('Fallback in progress');
7409
- this.requestPromptRefresh(true);
7410
- }
7411
7487
  handleProviderError(error, retryAction) {
7412
7488
  const promptBlock = detectPromptBlockError(error);
7413
7489
  if (promptBlock) {