erosolar-cli 2.1.167 → 2.1.168

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 (128) hide show
  1. package/agents/erosolar-code.rules.json +2 -2
  2. package/agents/general.rules.json +3 -21
  3. package/dist/StringUtils.d.ts +8 -0
  4. package/dist/StringUtils.d.ts.map +1 -0
  5. package/dist/StringUtils.js +11 -0
  6. package/dist/StringUtils.js.map +1 -0
  7. package/dist/capabilities/statusCapability.js +2 -2
  8. package/dist/capabilities/statusCapability.js.map +1 -1
  9. package/dist/contracts/agent-schemas.json +0 -5
  10. package/dist/core/agent.d.ts +11 -72
  11. package/dist/core/agent.d.ts.map +1 -1
  12. package/dist/core/agent.js +182 -869
  13. package/dist/core/agent.js.map +1 -1
  14. package/dist/core/aiFlowSupervisor.d.ts +44 -0
  15. package/dist/core/aiFlowSupervisor.d.ts.map +1 -0
  16. package/dist/core/aiFlowSupervisor.js +299 -0
  17. package/dist/core/aiFlowSupervisor.js.map +1 -0
  18. package/dist/core/cliTestHarness.d.ts +200 -0
  19. package/dist/core/cliTestHarness.d.ts.map +1 -0
  20. package/dist/core/cliTestHarness.js +549 -0
  21. package/dist/core/cliTestHarness.js.map +1 -0
  22. package/dist/core/preferences.d.ts +0 -1
  23. package/dist/core/preferences.d.ts.map +1 -1
  24. package/dist/core/preferences.js +2 -9
  25. package/dist/core/preferences.js.map +1 -1
  26. package/dist/core/schemaValidator.js +3 -3
  27. package/dist/core/schemaValidator.js.map +1 -1
  28. package/dist/core/testUtils.d.ts +121 -0
  29. package/dist/core/testUtils.d.ts.map +1 -0
  30. package/dist/core/testUtils.js +235 -0
  31. package/dist/core/testUtils.js.map +1 -0
  32. package/dist/core/toolPreconditions.d.ts +11 -0
  33. package/dist/core/toolPreconditions.d.ts.map +1 -1
  34. package/dist/core/toolPreconditions.js +164 -33
  35. package/dist/core/toolPreconditions.js.map +1 -1
  36. package/dist/core/toolRuntime.d.ts.map +1 -1
  37. package/dist/core/toolRuntime.js +114 -9
  38. package/dist/core/toolRuntime.js.map +1 -1
  39. package/dist/core/toolValidation.d.ts +116 -0
  40. package/dist/core/toolValidation.d.ts.map +1 -0
  41. package/dist/core/toolValidation.js +282 -0
  42. package/dist/core/toolValidation.js.map +1 -0
  43. package/dist/core/updateChecker.d.ts +1 -61
  44. package/dist/core/updateChecker.d.ts.map +1 -1
  45. package/dist/core/updateChecker.js +3 -147
  46. package/dist/core/updateChecker.js.map +1 -1
  47. package/dist/headless/headlessApp.d.ts.map +1 -1
  48. package/dist/headless/headlessApp.js +39 -0
  49. package/dist/headless/headlessApp.js.map +1 -1
  50. package/dist/plugins/tools/nodeDefaults.d.ts.map +1 -1
  51. package/dist/plugins/tools/nodeDefaults.js +2 -0
  52. package/dist/plugins/tools/nodeDefaults.js.map +1 -1
  53. package/dist/providers/openaiResponsesProvider.d.ts.map +1 -1
  54. package/dist/providers/openaiResponsesProvider.js +74 -79
  55. package/dist/providers/openaiResponsesProvider.js.map +1 -1
  56. package/dist/runtime/agentController.d.ts.map +1 -1
  57. package/dist/runtime/agentController.js +0 -6
  58. package/dist/runtime/agentController.js.map +1 -1
  59. package/dist/runtime/agentSession.d.ts.map +1 -1
  60. package/dist/runtime/agentSession.js +2 -3
  61. package/dist/runtime/agentSession.js.map +1 -1
  62. package/dist/shell/interactiveShell.d.ts +8 -16
  63. package/dist/shell/interactiveShell.d.ts.map +1 -1
  64. package/dist/shell/interactiveShell.js +159 -388
  65. package/dist/shell/interactiveShell.js.map +1 -1
  66. package/dist/shell/systemPrompt.d.ts.map +1 -1
  67. package/dist/shell/systemPrompt.js +15 -4
  68. package/dist/shell/systemPrompt.js.map +1 -1
  69. package/dist/subagents/taskRunner.js +1 -2
  70. package/dist/subagents/taskRunner.js.map +1 -1
  71. package/dist/tools/bashTools.d.ts.map +1 -1
  72. package/dist/tools/bashTools.js +8 -101
  73. package/dist/tools/bashTools.js.map +1 -1
  74. package/dist/tools/diffUtils.d.ts +2 -8
  75. package/dist/tools/diffUtils.d.ts.map +1 -1
  76. package/dist/tools/diffUtils.js +13 -72
  77. package/dist/tools/diffUtils.js.map +1 -1
  78. package/dist/tools/grepTools.d.ts.map +1 -1
  79. package/dist/tools/grepTools.js +2 -10
  80. package/dist/tools/grepTools.js.map +1 -1
  81. package/dist/tools/searchTools.d.ts.map +1 -1
  82. package/dist/tools/searchTools.js +2 -4
  83. package/dist/tools/searchTools.js.map +1 -1
  84. package/dist/ui/PromptController.d.ts +0 -2
  85. package/dist/ui/PromptController.d.ts.map +1 -1
  86. package/dist/ui/PromptController.js +0 -2
  87. package/dist/ui/PromptController.js.map +1 -1
  88. package/dist/ui/ShellUIAdapter.d.ts +18 -71
  89. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  90. package/dist/ui/ShellUIAdapter.js +139 -237
  91. package/dist/ui/ShellUIAdapter.js.map +1 -1
  92. package/dist/ui/UnifiedUIController.d.ts +1 -0
  93. package/dist/ui/UnifiedUIController.d.ts.map +1 -1
  94. package/dist/ui/UnifiedUIController.js +1 -0
  95. package/dist/ui/UnifiedUIController.js.map +1 -1
  96. package/dist/ui/UnifiedUIRenderer.d.ts +5 -122
  97. package/dist/ui/UnifiedUIRenderer.d.ts.map +1 -1
  98. package/dist/ui/UnifiedUIRenderer.js +125 -830
  99. package/dist/ui/UnifiedUIRenderer.js.map +1 -1
  100. package/dist/ui/compactRenderer.d.ts +139 -0
  101. package/dist/ui/compactRenderer.d.ts.map +1 -0
  102. package/dist/ui/compactRenderer.js +398 -0
  103. package/dist/ui/compactRenderer.js.map +1 -0
  104. package/dist/ui/display.d.ts +48 -13
  105. package/dist/ui/display.d.ts.map +1 -1
  106. package/dist/ui/display.js +105 -22
  107. package/dist/ui/display.js.map +1 -1
  108. package/dist/ui/streamingFormatter.d.ts +30 -0
  109. package/dist/ui/streamingFormatter.d.ts.map +1 -0
  110. package/dist/ui/streamingFormatter.js +91 -0
  111. package/dist/ui/streamingFormatter.js.map +1 -0
  112. package/dist/ui/unified/index.d.ts +1 -1
  113. package/dist/ui/unified/index.d.ts.map +1 -1
  114. package/dist/ui/unified/index.js +2 -0
  115. package/dist/ui/unified/index.js.map +1 -1
  116. package/dist/utils/errorUtils.d.ts +16 -0
  117. package/dist/utils/errorUtils.d.ts.map +1 -0
  118. package/dist/utils/errorUtils.js +66 -0
  119. package/dist/utils/errorUtils.js.map +1 -0
  120. package/package.json +2 -1
  121. package/dist/core/reliabilityPrompt.d.ts +0 -9
  122. package/dist/core/reliabilityPrompt.d.ts.map +0 -1
  123. package/dist/core/reliabilityPrompt.js +0 -31
  124. package/dist/core/reliabilityPrompt.js.map +0 -1
  125. package/dist/ui/animatedStatus.d.ts +0 -129
  126. package/dist/ui/animatedStatus.d.ts.map +0 -1
  127. package/dist/ui/animatedStatus.js +0 -384
  128. package/dist/ui/animatedStatus.js.map +0 -1
@@ -7,6 +7,7 @@ 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';
10
11
  import { getContextWindowTokens } from '../core/contextWindow.js';
11
12
  import { ensureSecretForProvider, getSecretDefinitionForProvider, getSecretValue, listSecretDefinitions, maskSecret, setSecretValue, } from '../core/secretStore.js';
12
13
  import { saveActiveProfilePreference, saveModelPreference, loadToolSettings, saveToolSettings, clearToolSettings, clearActiveProfilePreference, loadSessionPreferences, saveSessionPreferences, loadFeatureFlags, saveFeatureFlags, toggleFeatureFlag, FEATURE_FLAG_INFO, } from '../core/preferences.js';
@@ -18,6 +19,7 @@ import { detectNetworkError } from '../core/errors/networkErrors.js';
18
19
  import { buildWorkspaceContext } from '../workspace.js';
19
20
  import { buildInteractiveSystemPrompt } from './systemPrompt.js';
20
21
  import { getTaskCompletionDetector, resetTaskCompletionDetector, } from './taskCompletionDetector.js';
22
+ import { assessAiFlow } from '../core/aiFlowSupervisor.js';
21
23
  import { discoverAllModels, quickCheckProviders, getCachedDiscoveredModels, sortModelsByPriority } from '../core/modelDiscovery.js';
22
24
  import { getModels, getSlashCommands, getProviders } from '../core/agentSchemaLoader.js';
23
25
  import { loadMcpServers } from '../mcp/config.js';
@@ -135,7 +137,6 @@ export class InteractiveShell {
135
137
  sessionPreferences;
136
138
  autosaveEnabled;
137
139
  autoContinueEnabled;
138
- autoContinueEnforced = true;
139
140
  verificationEnabled = false;
140
141
  criticalApprovalMode = 'auto';
141
142
  editGuardMode = 'display-edits';
@@ -162,10 +163,6 @@ export class InteractiveShell {
162
163
  lastUserQuery = '';
163
164
  lastAssistantResponse = null;
164
165
  responseRendered = false;
165
- autoContinueInsightState = {
166
- attempt: 0,
167
- lastMessage: null,
168
- };
169
166
  lastFailure = null;
170
167
  lastAutoTestRun = null;
171
168
  // Auto-build tracking
@@ -185,6 +182,7 @@ export class InteractiveShell {
185
182
  streamingOutputSuppressed = false;
186
183
  aiRuntimeStart = null;
187
184
  aiRuntimeTotalMs = 0;
185
+ streamingFormatter = null;
188
186
  streamingThoughtBuffer = '';
189
187
  statusLineState = null;
190
188
  statusMessageOverride = null;
@@ -210,11 +208,6 @@ export class InteractiveShell {
210
208
  this.thinkingMode = this.sessionPreferences.thinkingMode;
211
209
  this.autosaveEnabled = this.sessionPreferences.autosave;
212
210
  this.autoContinueEnabled = this.sessionPreferences.autoContinue;
213
- if (this.autoContinueEnforced && !this.autoContinueEnabled) {
214
- // Project policy: keep auto-continue locked on to avoid stalls
215
- this.autoContinueEnabled = true;
216
- saveSessionPreferences({ autoContinue: true });
217
- }
218
211
  this.criticalApprovalMode = this.sessionPreferences.criticalApprovalMode;
219
212
  const featureFlags = loadFeatureFlags();
220
213
  this.verificationEnabled = featureFlags.verification === true;
@@ -291,10 +284,6 @@ export class InteractiveShell {
291
284
  this.uiAdapter.setToolStatusCallback((status) => {
292
285
  this.updateStatusMessage(status ?? null);
293
286
  });
294
- // Set up activity callback to update activity line with streaming bash output
295
- this.uiAdapter.setActivityCallback((activity) => {
296
- this.renderer?.setActivity(activity);
297
- });
298
287
  this.skillRepository = new SkillRepository({
299
288
  workingDir: this.workingDir,
300
289
  env: process.env,
@@ -308,7 +297,6 @@ export class InteractiveShell {
308
297
  onQueue: (text) => this.handleQueuedInput(text),
309
298
  onCtrlC: ({ hadBuffer }) => this.handleCtrlCPress(hadBuffer),
310
299
  onInterrupt: () => this.handleInterrupt(),
311
- onExit: () => this.handleExit(),
312
300
  onChange: ({ text, cursor }) => this.handleInputChange(text, cursor, 'renderer'),
313
301
  onEditModeChange: (mode) => this.handleEditModeChange(mode),
314
302
  onToggleVerify: () => this.toggleVerificationMode(),
@@ -316,7 +304,6 @@ export class InteractiveShell {
316
304
  onToggleThinking: () => this.cycleThinkingMode(),
317
305
  onClearContext: () => this.handleClearContext(),
318
306
  onToggleCriticalApproval: () => this.toggleCriticalApprovalMode('shortcut'),
319
- onExpandToolResult: () => this.expandLastToolResult(),
320
307
  });
321
308
  // Share renderer with Display so all output flows through the unified queue
322
309
  this.renderer = this.terminalInput.getRenderer();
@@ -460,21 +447,18 @@ export class InteractiveShell {
460
447
  const top = `╭${'─'.repeat(labelLeft)}${label}${'─'.repeat(labelRight)}╮`;
461
448
  const bottom = `╰${'─'.repeat(contentWidth)}╯`;
462
449
  const userName = process.env['USER'] || 'there';
463
- const logo = '⟣╍◎╍⟢';
450
+ const logo = clamp('⟣╍◎╍⟢');
464
451
  const versionLabel = version ? clamp(`${brand} · v${version}`) : brand;
465
452
  const modelLine = clamp(model);
466
453
  const workspaceLine = clamp(workspace);
467
- // Compact welcome line with logo on same line
468
- const welcomeLine = clamp(`${logo} Welcome back ${userName}! ${logo}`);
469
- // Quick reference hints
470
- const hintsLine = clamp('/model · /secrets · /help · ? shortcuts');
454
+ const welcomeLine = clamp(`Welcome back ${userName}!`);
471
455
  const lines = [
472
456
  frame(top),
473
457
  frame(`│${padCenter(welcomeLine, contentWidth)}│`),
458
+ frame(`│${padCenter(logo, contentWidth)}│`),
474
459
  frame(`│${padCenter(modelLine, contentWidth)}│`),
475
460
  frame(`│${padCenter(versionLabel, contentWidth)}│`),
476
461
  frame(`│${padCenter(workspaceLine, contentWidth)}│`),
477
- frame(`│${padCenter(hintsLine, contentWidth)}│`),
478
462
  frame(bottom),
479
463
  ].join('\n');
480
464
  if (this.renderer) {
@@ -485,6 +469,8 @@ export class InteractiveShell {
485
469
  else {
486
470
  display.stream(`\n${lines}\n`);
487
471
  }
472
+ // Check for updates asynchronously (non-blocking)
473
+ void this.checkAndShowUpdates();
488
474
  // Keep UI pinned; no scrollback banners
489
475
  this.requestPromptRefresh(true);
490
476
  }
@@ -522,32 +508,22 @@ export class InteractiveShell {
522
508
  }
523
509
  async checkAndShowUpdates() {
524
510
  try {
525
- const { checkForUpdates, getUpdateDecision, formatUpdateBanner, performBackgroundUpdate, readAutoUpdateState, shouldShowUpdateNotification, } = await import('../core/updateChecker.js');
511
+ const { checkForUpdates, formatUpdateNotification, maybeAutoUpdate } = await import('../core/updateChecker.js');
526
512
  const currentVersion = this.version || this.getPackageInfo().version || '1.7.458';
527
513
  const updateInfo = await checkForUpdates(currentVersion);
528
514
  if (!updateInfo || !updateInfo.updateAvailable) {
529
515
  return;
530
516
  }
531
- // Check if we should show notification (respects quiet period)
532
- const state = readAutoUpdateState();
533
- if (!shouldShowUpdateNotification(state)) {
534
- return;
535
- }
536
- // Get user's preference-based decision (no interactive prompt)
537
- const sessionPrefs = loadSessionPreferences();
538
- const decision = getUpdateDecision(sessionPrefs.autoUpdate);
539
- if (decision === 'skip') {
540
- // User chose to always skip - silently ignore updates
517
+ const updateResult = await maybeAutoUpdate(currentVersion, {
518
+ updateInfo,
519
+ logger: (message) => this.streamEventBlock(message),
520
+ });
521
+ if (updateResult?.updated) {
541
522
  return;
542
523
  }
543
- // Show non-interactive banner notification
544
- this.streamEventBlock(formatUpdateBanner(updateInfo, decision));
545
- if (decision === 'auto') {
546
- // User chose to always update - perform background update
547
- await performBackgroundUpdate(updateInfo, (message) => this.streamEventBlock(message));
548
- }
549
- // If decision === 'ask', we just show the banner with instructions
550
- // User can run /update check or /update auto to update
524
+ const note = this.describeUpdateSkipReason(updateResult);
525
+ const notification = formatUpdateNotification(updateInfo, note);
526
+ this.streamEventBlock(notification);
551
527
  }
552
528
  catch {
553
529
  // Silently fail - don't interrupt user experience
@@ -631,8 +607,6 @@ export class InteractiveShell {
631
607
  this.pushUiEvent('raw', block);
632
608
  }
633
609
  async start(initialPrompt) {
634
- // Check for updates BEFORE terminal input starts (to avoid keypress conflicts)
635
- await this.checkAndShowUpdates();
636
610
  // Initialize the renderer before emitting the banner so we don't render the prompt twice
637
611
  this.terminalInput.start();
638
612
  this.resetRendererStreamingMode();
@@ -641,9 +615,8 @@ export class InteractiveShell {
641
615
  this.refreshControlBar();
642
616
  // Now sync renderer and control bar state
643
617
  this.syncRendererInput();
644
- // CRITICAL: Force prompt render after banner - flushEvents should have done this,
645
- // but explicitly ensure the prompt area is visible
646
- this.renderer?.render();
618
+ // Ensure the prompt/control bar is rendered after the welcome banner
619
+ this.ensureReadlineReady();
647
620
  if (initialPrompt) {
648
621
  await this.enqueuePromptForProcessing(initialPrompt, 'programmatic');
649
622
  return;
@@ -651,7 +624,7 @@ export class InteractiveShell {
651
624
  this.showLaunchCommandPalette();
652
625
  // Ensure the terminal input is visible
653
626
  this.syncRendererInput();
654
- this.renderer?.render();
627
+ this.ensureReadlineReady();
655
628
  }
656
629
  showLaunchCommandPalette() {
657
630
  // Disabled: Quick commands palette takes up too much space
@@ -719,10 +692,6 @@ export class InteractiveShell {
719
692
  */
720
693
  async runPromptJob(job) {
721
694
  try {
722
- // Show thinking indicator instead of "processing queued prompt"
723
- display.showThinking('Thinking…');
724
- // Re-echo the prompt to make it clear what's being processed
725
- this.renderer?.emitPrompt(job.text);
726
695
  await this.processInputBlock(job.text);
727
696
  job.resolve();
728
697
  }
@@ -908,49 +877,10 @@ export class InteractiveShell {
908
877
  : '🛡️ High-impact actions now require your approval. (Ctrl+Shift+A to toggle; use /approvals ask to persist)';
909
878
  display.showSystemMessage(message);
910
879
  }
911
- enforceAutoContinuePolicy() {
912
- if (!this.autoContinueEnforced) {
913
- return;
914
- }
915
- if (!this.autoContinueEnabled) {
916
- this.autoContinueEnabled = true;
917
- saveSessionPreferences({ autoContinue: true });
918
- if (this.agent) {
919
- this.agent.setAutoContinue(true);
920
- }
921
- this.refreshControlBar();
922
- }
923
- }
924
- resetAutoContinueInsights() {
925
- this.autoContinueInsightState = { attempt: 0, lastMessage: null };
926
- }
927
- showAutoContinueInsight(attempt, maxAttempts, message) {
928
- const normalized = (message || '').trim();
929
- const isDuplicate = this.autoContinueInsightState.attempt === attempt &&
930
- this.autoContinueInsightState.lastMessage === normalized;
931
- if (isDuplicate) {
932
- return;
933
- }
934
- this.autoContinueInsightState = { attempt, lastMessage: normalized };
935
- const detail = normalized || 'Continuing automatically after intent without action.';
936
- display.showSystemMessage(`Auto-continue ${attempt}/${maxAttempts}: ${detail}`);
937
- }
938
880
  toggleAutoContinueMode() {
939
- if (this.autoContinueEnforced) {
940
- this.enforceAutoContinuePolicy();
941
- display.showInfo('Auto-continue is enforced ON for erosolar-cli reliability and cannot be disabled.');
942
- return;
943
- }
944
881
  this.setAutoContinueMode(!this.autoContinueEnabled, 'shortcut');
945
882
  }
946
883
  setAutoContinueMode(enabled, source) {
947
- if (this.autoContinueEnforced && !enabled) {
948
- this.enforceAutoContinuePolicy();
949
- if (source === 'command') {
950
- display.showInfo('Auto-continue is enforced ON for erosolar-cli reliability and cannot be disabled.');
951
- }
952
- return;
953
- }
954
884
  const changed = this.autoContinueEnabled !== enabled;
955
885
  this.autoContinueEnabled = enabled;
956
886
  saveSessionPreferences({ autoContinue: this.autoContinueEnabled });
@@ -967,23 +897,6 @@ export class InteractiveShell {
967
897
  : 'The model will not be auto-prompted to continue.';
968
898
  display.showInfo(`Auto-continue ${modeLabel}. ${behavior} Toggle with Ctrl+Shift+C.`);
969
899
  }
970
- /**
971
- * Expand the last tool result (Ctrl+O shortcut).
972
- * Shows the full output from the most recent tool call.
973
- */
974
- expandLastToolResult() {
975
- const result = this.uiAdapter.getLastToolResult();
976
- if (!result) {
977
- display.showInfo('No tool result to expand. Run a tool first, then press Ctrl+O.');
978
- return;
979
- }
980
- // Format the expanded output with tool name header
981
- const header = `${theme.info('━━━')} ${theme.tool(result.toolName)} ${theme.ui.muted('output')} ${theme.info('━━━')}`;
982
- const content = result.output.trim() || '(empty result)';
983
- const footer = theme.ui.muted('━'.repeat(Math.min(60, header.length)));
984
- // Display the expanded result
985
- display.stream(`\n${header}\n${content}\n${footer}\n\n`);
986
- }
987
900
  /**
988
901
  * Cycle through thinking modes (Tab shortcut).
989
902
  */
@@ -1136,13 +1049,6 @@ export class InteractiveShell {
1136
1049
  }
1137
1050
  this.ctrlCHandledThisPress = false;
1138
1051
  }
1139
- /**
1140
- * Handle exit request (Ctrl+C with empty buffer in idle mode)
1141
- */
1142
- handleExit() {
1143
- display.showSystemMessage('\nGoodbye!\n');
1144
- this.shutdown();
1145
- }
1146
1052
  /**
1147
1053
  * Gracefully tear down the shell and exit
1148
1054
  */
@@ -1805,14 +1711,15 @@ export class InteractiveShell {
1805
1711
  */
1806
1712
  refreshStatusLine(forceRender = false) {
1807
1713
  const elapsedSeconds = this.getAiRuntimeSeconds();
1714
+ const thinkingMs = display.isSpinnerActive() ? display.getThinkingElapsedMs() : null;
1808
1715
  const tokensUsed = this.latestTokenUsage.used;
1809
1716
  const tokenLimit = this.latestTokenUsage.limit ?? this.activeContextWindowTokens;
1810
1717
  this.terminalInput.setMetaStatus({
1811
1718
  elapsedSeconds,
1812
1719
  tokensUsed,
1813
1720
  tokenLimit,
1814
- thinkingMs: null,
1815
- thinkingHasContent: false,
1721
+ thinkingMs,
1722
+ thinkingHasContent: display.isSpinnerActive(),
1816
1723
  });
1817
1724
  // Keep model/provider visible in the controls bar
1818
1725
  this.terminalInput.setModelContext({
@@ -1988,16 +1895,12 @@ export class InteractiveShell {
1988
1895
  enterStreamingMode();
1989
1896
  // Set up scroll region for streaming content
1990
1897
  this.uiUpdates.setMode('streaming');
1991
- // Show activity status with animated spinner - use provided label or default
1992
- const activityLabel = label || 'Thinking';
1993
- this.renderer?.setActivity(activityLabel);
1994
1898
  this.streamingHeartbeatStart = Date.now();
1995
1899
  this.streamingContentSeen = false;
1996
1900
  this.streamingStatusText = null;
1997
1901
  this.streamingStatusLastUpdate = null;
1998
- this.streamingTokenCount = 0; // Reset token count for new prompt
1999
- // Show raw streaming output in real-time (Claude Code style)
2000
- this.streamingOutputSuppressed = false;
1902
+ // Suppress raw streaming lines; show only a single summary/status + final response
1903
+ this.streamingOutputSuppressed = true;
2001
1904
  const initialLabel = this.isMeaningfulStreamingSnippet(label)
2002
1905
  ? this.truncateStreamingLabel(label)
2003
1906
  : this.getStreamingFallbackLabel();
@@ -2041,8 +1944,6 @@ export class InteractiveShell {
2041
1944
  this.streamingStatusLastUpdate = null;
2042
1945
  this.streamingStatusText = null;
2043
1946
  this.streamingOutputSuppressed = false;
2044
- // Clear activity status when streaming ends
2045
- this.renderer?.setActivity(null);
2046
1947
  // Emit a streaming note for stop/quit so the status stays inside the stream
2047
1948
  if (reason === 'stop' || reason === 'quit') {
2048
1949
  const note = reason === 'quit' ? 'Session closed.' : 'Stream stopped.';
@@ -2059,15 +1960,11 @@ export class InteractiveShell {
2059
1960
  // Buffer for accumulating partial <thinking> tags during streaming
2060
1961
  thinkingTagBuffer = '';
2061
1962
  insideThinkingBlock = false;
2062
- streamingTokenCount = 0;
2063
1963
  handleStreamChunk(chunk, type = 'content') {
2064
1964
  if (!chunk) {
2065
1965
  return;
2066
1966
  }
2067
1967
  const isReasoning = type === 'reasoning';
2068
- // Approximate token count (roughly 4 chars per token)
2069
- this.streamingTokenCount += Math.ceil(chunk.length / 4);
2070
- this.renderer?.updateStreamingTokens(this.streamingTokenCount);
2071
1968
  // Keep pinned status updated for all streaming chunks
2072
1969
  this.updateStreamingStatusFromChunk(chunk);
2073
1970
  // Handle <thinking> tags as separate events in the queue
@@ -2076,32 +1973,37 @@ export class InteractiveShell {
2076
1973
  if (!processed.contentChunk && !processed.thinkingChunk) {
2077
1974
  return;
2078
1975
  }
2079
- // Don't emit thinking blocks during streaming - they will be rendered
2080
- // as a complete block by onAssistantMessage when the response is final.
2081
- // This prevents duplicate display.
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
+ }
2082
1980
  // Process regular content (skip if no content after extracting thinking)
2083
1981
  const contentChunk = processed.contentChunk;
2084
1982
  if (!contentChunk) {
2085
1983
  return;
2086
1984
  }
2087
1985
  // Suppress raw streaming output in scrollback; keep only status + final message.
2088
- // Reasoning tokens should also be suppressed and shown only in the indicator.
2089
- if (this.streamingOutputSuppressed) {
1986
+ // Reasoning chunks bypass this so the thought process stays visible.
1987
+ if (this.streamingOutputSuppressed && !isReasoning) {
2090
1988
  this.captureStreamingThought(contentChunk);
2091
1989
  this.streamingContentSeen = true;
2092
- // Update thinking indicator with a snippet of the content
2093
- const prefix = isReasoning ? '○ ' : '';
2094
- const snippet = contentChunk.replace(/\s+/g, ' ').trim().slice(0, 60);
2095
- if (snippet) {
2096
- display.updateThinking(prefix + snippet + (contentChunk.length > 60 ? '…' : ''));
2097
- }
2098
1990
  return;
2099
1991
  }
2100
- // Buffer all streaming content - rendered as complete block via onAssistantMessage
2101
- // This prevents fragmented display and ensures thinking tags are properly processed
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);
2102
2000
  this.captureStreamingThought(contentChunk);
2103
- // Activity: show what the model is doing (stable, informative)
2104
- this.renderer?.setActivity(type === 'reasoning' ? 'Reasoning' : 'Writing');
2001
+ if (formatted) {
2002
+ if (formatted.trim().length > 0) {
2003
+ this.streamingContentSeen = true;
2004
+ }
2005
+ this.pushUiEvent('streaming', formatted);
2006
+ }
2105
2007
  }
2106
2008
  /**
2107
2009
  * Process streaming content to extract <thinking> blocks as separate events.
@@ -2149,12 +2051,22 @@ export class InteractiveShell {
2149
2051
  }
2150
2052
  return { thinkingChunk: thinkingContent, contentChunk: remainingContent };
2151
2053
  }
2152
- finishStreamingFormatter(_note, options) {
2153
- // Flush any remaining buffered thoughts
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
+ }
2154
2065
  if (this.streamingThoughtBuffer.trim()) {
2155
2066
  this.ui.controller.recordAssistantThought(this.streamingThoughtBuffer.trim());
2156
2067
  }
2157
2068
  this.streamingThoughtBuffer = '';
2069
+ this.streamingFormatter = null;
2158
2070
  if (options?.refreshPrompt ?? true) {
2159
2071
  this.requestPromptRefresh(true);
2160
2072
  }
@@ -2599,9 +2511,6 @@ export class InteractiveShell {
2599
2511
  case '/permissions':
2600
2512
  this.handlePermissionsCommand();
2601
2513
  break;
2602
- case '/update':
2603
- await this.handleUpdateCommand(input);
2604
- break;
2605
2514
  case '/init':
2606
2515
  this.handleInitCommand(input);
2607
2516
  break;
@@ -4378,21 +4287,13 @@ export class InteractiveShell {
4378
4287
  const value = tokens[0]?.toLowerCase();
4379
4288
  if (!value) {
4380
4289
  // Show current status
4381
- const lockedNote = this.autoContinueEnforced
4382
- ? ' (locked ON for erosolar-cli reliability)'
4383
- : ' Use /autocontinue on|off or Ctrl+Shift+C to toggle.';
4384
- display.showInfo(`Auto-continue is ${this.autoContinueEnabled ? 'enabled' : 'disabled'}.${lockedNote}`);
4290
+ display.showInfo(`Auto-continue is ${this.autoContinueEnabled ? 'enabled' : 'disabled'}. Use /autocontinue on|off or Ctrl+Shift+C to toggle.`);
4385
4291
  return;
4386
4292
  }
4387
4293
  if (value !== 'on' && value !== 'off') {
4388
4294
  display.showWarning('Usage: /autocontinue on|off');
4389
4295
  return;
4390
4296
  }
4391
- if (value === 'off' && this.autoContinueEnforced) {
4392
- this.enforceAutoContinuePolicy();
4393
- display.showInfo('Auto-continue is enforced ON for erosolar-cli reliability and cannot be disabled.');
4394
- return;
4395
- }
4396
4297
  this.setAutoContinueMode(value === 'on', 'command');
4397
4298
  }
4398
4299
  // ==================== Erosolar-CLI Style Commands ====================
@@ -4626,9 +4527,7 @@ export class InteractiveShell {
4626
4527
  lines.push(`${theme.primary('Provider/Model')}: ${theme.info(`${this.providerLabel(this.sessionState.provider)} · ${this.sessionState.model}`)}`);
4627
4528
  lines.push(`${theme.primary('Workspace')}: ${theme.ui.muted(this.abbreviatePath(this.workingDir))}`);
4628
4529
  lines.push(`${theme.primary('Autosave')}: ${this.autosaveEnabled ? theme.success('on') : theme.ui.muted('off')}`);
4629
- const autoContinueStatus = this.autoContinueEnabled ? 'on' : 'off';
4630
- const autoContinueLabel = this.autoContinueEnforced ? `${autoContinueStatus} (locked)` : autoContinueStatus;
4631
- lines.push(`${theme.primary('Auto-continue')}: ${this.autoContinueEnabled ? theme.success(autoContinueLabel) : theme.ui.muted(autoContinueLabel)}`);
4530
+ lines.push(`${theme.primary('Auto-continue')}: ${this.autoContinueEnabled ? theme.success('on') : theme.ui.muted('off')}`);
4632
4531
  lines.push(`${theme.primary('Verification')}: ${this.verificationEnabled ? theme.success('on') : theme.ui.muted('off')}`);
4633
4532
  lines.push(`${theme.primary('Approvals')}: ${theme.ui.muted(this.describeEditGuardMode())}`);
4634
4533
  lines.push(`${theme.primary('Critical approvals')}: ${this.criticalApprovalMode === 'approval' ? theme.warning('ask') : theme.ui.muted('auto')}`);
@@ -4784,67 +4683,6 @@ export class InteractiveShell {
4784
4683
  lines.push(theme.ui.muted('Use /tools to manage which tool suites are enabled.'));
4785
4684
  display.showSystemMessage(lines.join('\n'));
4786
4685
  }
4787
- async handleUpdateCommand(input) {
4788
- const tokens = input.split(/\s+/).slice(1);
4789
- const subcommand = tokens[0]?.toLowerCase();
4790
- const prefs = loadSessionPreferences();
4791
- const currentPref = prefs.autoUpdate;
4792
- const prefLabel = currentPref === true ? 'always update' : currentPref === false ? 'always skip' : 'notify only';
4793
- // Show status or help
4794
- if (!subcommand || subcommand === 'status') {
4795
- const lines = [];
4796
- lines.push(theme.bold('Update Settings'));
4797
- lines.push('');
4798
- lines.push(`Current preference: ${theme.info(prefLabel)}`);
4799
- lines.push('');
4800
- lines.push(theme.secondary('Commands:'));
4801
- lines.push(' /update check - Check for updates and install immediately');
4802
- lines.push(' /update auto - Always auto-update in background');
4803
- lines.push(' /update skip - Never auto-update (silent)');
4804
- lines.push(' /update notify - Show notification only (default)');
4805
- display.showSystemMessage(lines.join('\n'));
4806
- return;
4807
- }
4808
- if (subcommand === 'check') {
4809
- // Force check and perform update immediately (non-interactive)
4810
- try {
4811
- const { checkForUpdates, performUpdate } = await import('../core/updateChecker.js');
4812
- const currentVersion = this.version || this.getPackageInfo().version || '1.7.458';
4813
- const updateInfo = await checkForUpdates(currentVersion);
4814
- if (!updateInfo) {
4815
- display.showWarning('Unable to check for updates (network issue or timeout).');
4816
- return;
4817
- }
4818
- if (!updateInfo.updateAvailable) {
4819
- display.showSuccess(`You're on the latest version (v${updateInfo.current}).`);
4820
- return;
4821
- }
4822
- // Show update info and perform update immediately (no interactive prompt)
4823
- display.showInfo(`Update available: v${updateInfo.current} → v${updateInfo.latest}`);
4824
- await performUpdate(updateInfo, (msg) => this.streamEventBlock(msg));
4825
- }
4826
- catch (error) {
4827
- display.showError(`Update check failed: ${error instanceof Error ? error.message : String(error)}`);
4828
- }
4829
- return;
4830
- }
4831
- if (subcommand === 'auto' || subcommand === 'always') {
4832
- saveSessionPreferences({ autoUpdate: true });
4833
- display.showSuccess('Auto-update enabled. Updates will install automatically.');
4834
- return;
4835
- }
4836
- if (subcommand === 'skip' || subcommand === 'never' || subcommand === 'off') {
4837
- saveSessionPreferences({ autoUpdate: false });
4838
- display.showInfo('Auto-update disabled. Updates will be skipped silently.');
4839
- return;
4840
- }
4841
- if (subcommand === 'ask' || subcommand === 'prompt' || subcommand === 'reset' || subcommand === 'notify') {
4842
- saveSessionPreferences({ autoUpdate: null });
4843
- display.showInfo('Update preference reset. You will see a notification when updates are available.');
4844
- return;
4845
- }
4846
- display.showWarning('Usage: /update [check|auto|skip|notify|status]');
4847
- }
4848
4686
  handleInitCommand(input) {
4849
4687
  const tokens = input.split(/\s+/).slice(1);
4850
4688
  const confirm = tokens[0]?.toLowerCase() === 'confirm';
@@ -5840,9 +5678,6 @@ export class InteractiveShell {
5840
5678
  this.clearInlinePanel();
5841
5679
  this.syncRendererInput();
5842
5680
  }
5843
- // NOTE: emitPromptAcknowledgement has been removed - acknowledgement is now handled
5844
- // by the onRequestReceived callback in agent.send(), which fires IMMEDIATELY when
5845
- // the request is received, guaranteeing instant user feedback before any processing.
5846
5681
  async processRequest(request) {
5847
5682
  if (this.isProcessing) {
5848
5683
  this.enqueueFollowUpAction({ type: 'request', text: request });
@@ -5858,8 +5693,6 @@ export class InteractiveShell {
5858
5693
  if (!agent) {
5859
5694
  return;
5860
5695
  }
5861
- this.enforceAutoContinuePolicy();
5862
- this.resetAutoContinueInsights();
5863
5696
  this.runtimeSession.toolRuntime.clearDiffSnapshots?.();
5864
5697
  if (this.suppressNextNetworkReset) {
5865
5698
  this.suppressNextNetworkReset = false;
@@ -5874,7 +5707,6 @@ export class InteractiveShell {
5874
5707
  }
5875
5708
  this.isProcessing = true;
5876
5709
  this.uiUpdates.setMode('processing');
5877
- this.streamingTokenCount = 0; // Reset token counter for new request
5878
5710
  this.terminalInput.setStreaming(true);
5879
5711
  // Keep the persistent input/control bar active as we transition into streaming.
5880
5712
  this.syncRendererInput();
@@ -5889,9 +5721,6 @@ export class InteractiveShell {
5889
5721
  this.currentTaskType = classifyTaskType(request);
5890
5722
  this.currentToolCalls = [];
5891
5723
  this.clearToolUsageMeta();
5892
- // NOTE: Acknowledgement is now handled by onRequestReceived callback in agent.send()
5893
- // This fires IMMEDIATELY when the request is received, before any provider call
5894
- this.renderer?.setActivity('Starting...');
5895
5724
  this.uiAdapter.startProcessing('Working on your request');
5896
5725
  this.setProcessingStatus();
5897
5726
  this.beginAiRuntime();
@@ -5947,6 +5776,8 @@ export class InteractiveShell {
5947
5776
  clearActionHistory();
5948
5777
  this.lastFailure = null;
5949
5778
  }
5779
+ // Run post-hoc AI flow sanity check to catch "looks right but wrong" responses
5780
+ this.analyzeAiFlowForRun(request, responseText, requestStartTime);
5950
5781
  }
5951
5782
  catch (error) {
5952
5783
  const handled = this.handleProviderError(error, () => this.processRequest(request));
@@ -6013,13 +5844,10 @@ export class InteractiveShell {
6013
5844
  if (!agent) {
6014
5845
  return;
6015
5846
  }
6016
- this.enforceAutoContinuePolicy();
6017
- this.resetAutoContinueInsights();
6018
5847
  this.lastUserQuery = initialRequest;
6019
5848
  this.clearToolUsageMeta();
6020
5849
  this.isProcessing = true;
6021
5850
  this.uiUpdates.setMode('processing');
6022
- this.streamingTokenCount = 0; // Reset token counter for new request
6023
5851
  this.terminalInput.setStreaming(true);
6024
5852
  if (this.suppressNextNetworkReset) {
6025
5853
  this.suppressNextNetworkReset = false;
@@ -6037,7 +5865,6 @@ export class InteractiveShell {
6037
5865
  completionDetector.reset();
6038
5866
  // Display user prompt in scrollback (Claude Code style)
6039
5867
  this.logUserPrompt(initialRequest);
6040
- // NOTE: Acknowledgement is now handled by onRequestReceived callback in agent.send()
6041
5868
  display.showSystemMessage(`Continuous mode active. Ctrl+C to stop.`);
6042
5869
  this.uiAdapter.startProcessing('Continuous execution mode');
6043
5870
  this.setProcessingStatus();
@@ -6074,8 +5901,6 @@ When truly finished with ALL tasks, explicitly state "TASK_FULLY_COMPLETE".`;
6074
5901
  // Send the request and capture the response (streaming disabled)
6075
5902
  display.showThinking('Responding...');
6076
5903
  this.refreshStatusLine(true);
6077
- this.enforceAutoContinuePolicy();
6078
- this.resetAutoContinueInsights();
6079
5904
  const response = await agent.send(currentPrompt, true);
6080
5905
  this.finishStreamingFormatter(undefined, { refreshPrompt: false, mode: 'complete' });
6081
5906
  await this.awaitPendingCleanup();
@@ -6430,6 +6255,43 @@ What's the next action?`;
6430
6255
  const parts = candidates.filter((part) => typeof part === 'string' && part.trim().length > 0);
6431
6256
  return parts.join('\n').trim();
6432
6257
  }
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
+ }
6433
6295
  runAutoQualityChecks(trigger, assistantResponse, verificationContext) {
6434
6296
  if (!this.verificationEnabled) {
6435
6297
  return;
@@ -6782,8 +6644,6 @@ Return ONLY JSON array:
6782
6644
  // Send the error to the agent for fixing
6783
6645
  display.showThinking('Analyzing build errors');
6784
6646
  this.refreshStatusLine(true);
6785
- this.enforceAutoContinuePolicy();
6786
- this.resetAutoContinueInsights();
6787
6647
  const response = await this.agent.send(prompt, true);
6788
6648
  display.stopThinking();
6789
6649
  this.refreshStatusLine(true);
@@ -6803,7 +6663,6 @@ Return ONLY JSON array:
6803
6663
  rebuildAgent() {
6804
6664
  try {
6805
6665
  ensureSecretForProvider(this.sessionState.provider);
6806
- this.enforceAutoContinuePolicy();
6807
6666
  this.runtimeSession.updateToolContext(this.sessionState);
6808
6667
  this.ensureActiveSummarizer(this.runtimeSession.contextManager);
6809
6668
  const selection = {
@@ -6819,39 +6678,27 @@ Return ONLY JSON array:
6819
6678
  onStreamChunk: (chunk, type) => {
6820
6679
  this.handleStreamChunk(chunk, type ?? 'content');
6821
6680
  },
6681
+ onStreamFallback: (info) => this.handleStreamingFallback(info),
6822
6682
  onAssistantMessage: (content, metadata) => {
6823
6683
  const enriched = this.buildDisplayMetadata(metadata);
6824
6684
  const streamedVisible = metadata.wasStreamed && this.streamingContentSeen && !this.streamingOutputSuppressed;
6825
- const displaySuppressed = metadata.suppressDisplay === true;
6826
6685
  let renderedFinal = false;
6827
- // Update streaming token count from usage info (more accurate than chunk counting)
6828
- if (metadata.usage) {
6829
- const totalTokens = this.totalTokens(metadata.usage);
6830
- if (totalTokens !== null && totalTokens > this.streamingTokenCount) {
6831
- this.streamingTokenCount = totalTokens;
6832
- this.renderer?.updateStreamingTokens(this.streamingTokenCount);
6833
- }
6834
- }
6835
6686
  // Update spinner based on message type
6836
6687
  if (metadata.isFinal) {
6837
6688
  const parsed = this.splitThinkingResponse(content);
6838
- // If we successfully parsed thinking, use the parsed response (may be empty)
6839
- // Don't fall back to original content with raw <thinking> tags
6840
- const finalContent = parsed ? parsed.response?.trim() : content;
6689
+ const finalContent = parsed?.response?.trim() || content;
6841
6690
  const thoughtContent = parsed?.thinking?.trim() || null;
6842
6691
  // Show the response if it wasn't already rendered during streaming
6843
6692
  if (!streamedVisible) {
6844
6693
  if (thoughtContent) {
6845
- if (!displaySuppressed) {
6846
- const summary = this.extractThoughtSummary(thoughtContent);
6847
- if (summary) {
6848
- display.updateThinking(`💭 ${summary}`);
6849
- }
6850
- display.showAssistantMessage(thoughtContent, { ...enriched, isFinal: false });
6851
- renderedFinal = true;
6694
+ const summary = this.extractThoughtSummary(thoughtContent);
6695
+ if (summary) {
6696
+ display.updateThinking(`💭 ${summary}`);
6852
6697
  }
6698
+ display.showAssistantMessage(thoughtContent, { ...enriched, isFinal: false });
6699
+ renderedFinal = true;
6853
6700
  }
6854
- if (finalContent && !displaySuppressed) {
6701
+ if (finalContent) {
6855
6702
  display.showAssistantMessage(finalContent, enriched);
6856
6703
  renderedFinal = true;
6857
6704
  }
@@ -6869,7 +6716,7 @@ Return ONLY JSON array:
6869
6716
  this.updateContextUsage(percentage);
6870
6717
  }
6871
6718
  }
6872
- if (finalContent && !displaySuppressed) {
6719
+ if (finalContent) {
6873
6720
  this.lastAssistantResponse = finalContent;
6874
6721
  }
6875
6722
  // Track thought + response as a single response event for telemetry
@@ -6891,7 +6738,7 @@ Return ONLY JSON array:
6891
6738
  // Stop spinner and show the narrative text directly
6892
6739
  display.stopThinking();
6893
6740
  // Skip display if content was already streamed to avoid double-display
6894
- if (!streamedVisible && !displaySuppressed) {
6741
+ if (!streamedVisible) {
6895
6742
  const narrative = content.trim();
6896
6743
  if (narrative) {
6897
6744
  display.showNarrative(narrative);
@@ -6942,10 +6789,11 @@ Return ONLY JSON array:
6942
6789
  this.updateStatusMessage('Retrying with reduced context...');
6943
6790
  this.syncRendererInput();
6944
6791
  },
6945
- onAutoContinue: (attempt, maxAttempts, message) => {
6946
- // Surface insight about why auto-continue fired while keeping UX responsive
6947
- this.renderer?.setActivity('Continuing...');
6948
- this.showAutoContinueInsight(attempt, maxAttempts, message);
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();
6949
6797
  },
6950
6798
  onCancelled: () => {
6951
6799
  // Update UI to show operation was cancelled
@@ -6955,79 +6803,10 @@ Return ONLY JSON array:
6955
6803
  this.updateStatusMessage(null);
6956
6804
  this.terminalInput.setStreaming(false);
6957
6805
  },
6958
- onToolExecution: (toolName, isStart, args) => {
6959
- // Update activity status to show what tool is being executed
6960
- if (isStart) {
6961
- // Show more specific activity for long-running tools
6962
- let activity = `Running ${toolName}`;
6963
- if (toolName === 'execute_bash' && args?.['command']) {
6964
- const cmd = String(args['command']).slice(0, 40);
6965
- activity = `$ ${cmd}${String(args['command']).length > 40 ? '...' : ''}`;
6966
- }
6967
- else if (toolName === 'read_file' && args?.['file_path']) {
6968
- const path = String(args['file_path']).split('/').pop() || args['file_path'];
6969
- activity = `Reading ${path}`;
6970
- }
6971
- this.renderer?.setActivity(activity);
6972
- // Estimate tokens for tool call (~50 tokens per call)
6973
- this.streamingTokenCount += 50;
6974
- this.renderer?.updateStreamingTokens(this.streamingTokenCount);
6975
- }
6976
- else {
6977
- // Tool finished - estimate result tokens (~100 per result)
6978
- this.streamingTokenCount += 100;
6979
- this.renderer?.updateStreamingTokens(this.streamingTokenCount);
6980
- // Reset to thinking state while model generates next response
6981
- this.renderer?.setActivity('Thinking');
6982
- }
6983
- },
6984
- onBeforeFirstToolCall: (toolNames, hasModelNarration) => {
6985
- // ALWAYS inject acknowledgement - even if model provided narration
6986
- // This ensures user always sees immediate feedback that their request was received
6987
- // The acknowledgement appears BEFORE any tool output
6988
- const primaryTool = toolNames[0] || 'tools';
6989
- const toolDescriptions = {
6990
- 'list_files': 'Looking at the project files...',
6991
- 'Glob': 'Searching for files...',
6992
- 'read_file': 'Reading the file...',
6993
- 'Read': 'Reading the file...',
6994
- 'execute_bash': 'Running command...',
6995
- 'Bash': 'Running command...',
6996
- 'grep_search': 'Searching the code...',
6997
- 'Grep': 'Searching the code...',
6998
- 'write_file': 'Writing the file...',
6999
- 'Write': 'Writing the file...',
7000
- 'edit_file': 'Editing the file...',
7001
- 'Edit': 'Editing the file...',
7002
- 'WebFetch': 'Fetching web content...',
7003
- 'WebSearch': 'Searching the web...',
7004
- 'Task': 'Starting task...',
7005
- };
7006
- // Return acknowledgement regardless of hasModelNarration
7007
- // If model provided narration, it will appear after this acknowledgement
7008
- return toolDescriptions[primaryTool] || 'Running tools to advance your request...';
7009
- },
7010
6806
  onVerificationNeeded: (response, context) => {
7011
6807
  this.lastAssistantResponse = response;
7012
6808
  void this.runAutoQualityChecks('verification', response, context);
7013
6809
  },
7014
- // NEW: Immediate acknowledgement when request is received (BEFORE provider call)
7015
- onRequestReceived: (requestPreview) => {
7016
- // This fires IMMEDIATELY when user submits request, guaranteeing instant feedback
7017
- // The message appears before any AI processing begins
7018
- const normalized = requestPreview?.trim();
7019
- const message = normalized
7020
- ? `⚡ Acknowledged — focusing on "${normalized}" and acting now.`
7021
- : '⚡ Acknowledged — acting now.';
7022
- display.showAssistantMessage(message, { isFinal: false });
7023
- this.renderer?.setActivity('Acknowledged');
7024
- },
7025
- // NEW: Retry notification for transient errors
7026
- onRetrying: (attempt, maxAttempts, error) => {
7027
- const shortError = error.message.slice(0, 100);
7028
- display.showSystemMessage(`⚡ Retry ${attempt}/${maxAttempts}: ${shortError}${error.message.length > 100 ? '...' : ''}`);
7029
- this.renderer?.setActivity(`Retrying (${attempt}/${maxAttempts})...`);
7030
- },
7031
6810
  });
7032
6811
  // Register global AI enhancer for explore tool - uses active model by default
7033
6812
  this.registerExploreAIEnhancer();
@@ -7122,30 +6901,20 @@ Return ONLY JSON array:
7122
6901
  return lines.join('\n').trim();
7123
6902
  }
7124
6903
  buildThinkingDirective() {
7125
- // Base requirement: ALWAYS think before acting (applies to all modes)
7126
- const baseRequirement = [
7127
- 'CRITICAL: Before calling ANY tool, ALWAYS output a <thinking>...</thinking> block explaining:',
7128
- '1. What you understand the user is asking',
7129
- '2. Your approach/plan to solve it',
7130
- '3. Which tools you will use and why',
7131
- 'This thinking block MUST appear before your first tool call in every response.',
7132
- ].join('\n');
7133
6904
  switch (this.thinkingMode) {
7134
6905
  case 'extended':
7135
6906
  return [
7136
- baseRequirement,
7137
- '',
7138
- 'Extended thinking mode: Use detailed multi-step reasoning in your <thinking> block.',
7139
- 'Reference tool runs/files when relevant. Keep secrets redacted.',
7140
- 'After thinking, wrap your final answer in <response>...</response>.',
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>',
7141
6914
  ].join('\n');
7142
6915
  case 'balanced':
7143
6916
  default:
7144
- return [
7145
- baseRequirement,
7146
- '',
7147
- 'Balanced mode: Keep thinking concise (2-4 sentences) but always present.',
7148
- ].join('\n');
6917
+ return 'Balanced thinking mode: include a short <thinking>...</thinking> block before <response> when the reasoning is non-trivial; skip it for simple answers.';
7149
6918
  }
7150
6919
  }
7151
6920
  buildDisplayMetadata(metadata) {
@@ -7236,14 +7005,13 @@ Return ONLY JSON array:
7236
7005
  });
7237
7006
  cleanupOverlayActive = true;
7238
7007
  const triggerReason = trigger?.reason ?? 'Context optimization';
7239
- // Claude Code style: Show "Compacting conversation..." with animation
7240
- // The renderer handles the animated spinner display
7241
- if (this.renderer) {
7242
- this.renderer.showCompactingStatus('Compacting conversation… (esc to interrupt)');
7243
- }
7244
- else {
7245
- display.showSystemMessage(`✻ Compacting conversation… (esc to interrupt)`);
7246
- }
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(' '));
7247
7015
  const result = await contextManager.intelligentCompact(history);
7248
7016
  let afterStats = contextManager.getStats(result.compacted);
7249
7017
  let appliedHistory = result.compacted;
@@ -7270,18 +7038,10 @@ Return ONLY JSON array:
7270
7038
  }
7271
7039
  }
7272
7040
  if (!changed) {
7273
- // Hide compacting status before showing info message
7274
- if (this.renderer) {
7275
- this.renderer.hideCompactingStatus();
7276
- }
7277
7041
  display.showInfo('Context compaction completed but no changes were applied.');
7278
7042
  return;
7279
7043
  }
7280
7044
  if (!this.hasMeaningfulCompaction(changed, bestTokenSavings, bestPercentSavings, trigger?.forced)) {
7281
- // Hide compacting status before showing info message
7282
- if (this.renderer) {
7283
- this.renderer.hideCompactingStatus();
7284
- }
7285
7045
  display.showInfo('Auto-compaction completed but did not meaningfully reduce context size. Keeping existing history.');
7286
7046
  return;
7287
7047
  }
@@ -7297,18 +7057,15 @@ Return ONLY JSON array:
7297
7057
  this.updateContextUsage(newPercentUsed, CONTEXT_AUTOCOMPACT_PERCENT);
7298
7058
  this.lastContextWarningLevel = this.getContextWarningLevel(newPercentUsed);
7299
7059
  this.refreshStatusLine(true);
7300
- // Claude Code style: Show compaction complete with separator and ctrl+o hint
7301
- // Stop the compacting animation first
7302
- if (this.renderer) {
7303
- this.renderer.hideCompactingStatus();
7304
- }
7305
- // Show the Claude Code style separator: ══ Conversation compacted · ctrl+o for history ═
7306
- if (this.renderer) {
7307
- this.renderer.addCompactBlock('', 'Conversation compacted · ctrl+o for history');
7308
- }
7309
- else {
7310
- display.showSystemMessage('══ Conversation compacted · ctrl+o for history ═');
7311
- }
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(' '));
7312
7069
  this.recordContextCompaction({
7313
7070
  timestamp: Date.now(),
7314
7071
  source: trigger?.source ?? 'auto',
@@ -7322,20 +7079,12 @@ Return ONLY JSON array:
7322
7079
  });
7323
7080
  }
7324
7081
  catch (error) {
7325
- // Hide compacting status animation on error
7326
- if (this.renderer) {
7327
- this.renderer.hideCompactingStatus();
7328
- }
7329
7082
  display.showError('Context compaction failed.', error);
7330
7083
  }
7331
7084
  finally {
7332
7085
  if (cleanupOverlayActive) {
7333
7086
  this.statusTracker.clearOverride(cleanupStatusId);
7334
7087
  }
7335
- // Ensure compacting status is cleared
7336
- if (this.renderer) {
7337
- this.renderer.hideCompactingStatus();
7338
- }
7339
7088
  this.cleanupInProgress = false;
7340
7089
  this.contextCompactionInFlight = false;
7341
7090
  }
@@ -7637,6 +7386,28 @@ Return ONLY JSON array:
7637
7386
  const message = error instanceof Error ? error.message : String(error);
7638
7387
  display.showError(message);
7639
7388
  }
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
+ }
7640
7411
  handleProviderError(error, retryAction) {
7641
7412
  const promptBlock = detectPromptBlockError(error);
7642
7413
  if (promptBlock) {