erosolar-cli 2.1.170 → 2.1.172

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 (172) hide show
  1. package/README.md +1 -1
  2. package/agents/erosolar-code.rules.json +2 -2
  3. package/agents/general.rules.json +3 -21
  4. package/dist/StringUtils.d.ts +8 -0
  5. package/dist/StringUtils.d.ts.map +1 -0
  6. package/dist/StringUtils.js +11 -0
  7. package/dist/StringUtils.js.map +1 -0
  8. package/dist/capabilities/statusCapability.js +2 -2
  9. package/dist/capabilities/statusCapability.js.map +1 -1
  10. package/dist/contracts/agent-schemas.json +5 -5
  11. package/dist/core/agent.d.ts +24 -83
  12. package/dist/core/agent.d.ts.map +1 -1
  13. package/dist/core/agent.js +248 -499
  14. package/dist/core/agent.js.map +1 -1
  15. package/dist/core/aiFlowSupervisor.d.ts +44 -0
  16. package/dist/core/aiFlowSupervisor.d.ts.map +1 -0
  17. package/dist/core/aiFlowSupervisor.js +299 -0
  18. package/dist/core/aiFlowSupervisor.js.map +1 -0
  19. package/dist/core/cliTestHarness.d.ts +200 -0
  20. package/dist/core/cliTestHarness.d.ts.map +1 -0
  21. package/dist/core/cliTestHarness.js +549 -0
  22. package/dist/core/cliTestHarness.js.map +1 -0
  23. package/dist/core/preferences.d.ts +0 -1
  24. package/dist/core/preferences.d.ts.map +1 -1
  25. package/dist/core/preferences.js +1 -8
  26. package/dist/core/preferences.js.map +1 -1
  27. package/dist/core/schemaValidator.js +3 -3
  28. package/dist/core/schemaValidator.js.map +1 -1
  29. package/dist/core/testUtils.d.ts +121 -0
  30. package/dist/core/testUtils.d.ts.map +1 -0
  31. package/dist/core/testUtils.js +235 -0
  32. package/dist/core/testUtils.js.map +1 -0
  33. package/dist/core/toolPreconditions.d.ts +11 -0
  34. package/dist/core/toolPreconditions.d.ts.map +1 -1
  35. package/dist/core/toolPreconditions.js +164 -33
  36. package/dist/core/toolPreconditions.js.map +1 -1
  37. package/dist/core/toolRuntime.d.ts.map +1 -1
  38. package/dist/core/toolRuntime.js +114 -9
  39. package/dist/core/toolRuntime.js.map +1 -1
  40. package/dist/core/toolValidation.d.ts +116 -0
  41. package/dist/core/toolValidation.d.ts.map +1 -0
  42. package/dist/core/toolValidation.js +282 -0
  43. package/dist/core/toolValidation.js.map +1 -0
  44. package/dist/core/updateChecker.d.ts +1 -61
  45. package/dist/core/updateChecker.d.ts.map +1 -1
  46. package/dist/core/updateChecker.js +3 -147
  47. package/dist/core/updateChecker.js.map +1 -1
  48. package/dist/headless/headlessApp.d.ts.map +1 -1
  49. package/dist/headless/headlessApp.js +39 -0
  50. package/dist/headless/headlessApp.js.map +1 -1
  51. package/dist/plugins/tools/nodeDefaults.d.ts.map +1 -1
  52. package/dist/plugins/tools/nodeDefaults.js +2 -0
  53. package/dist/plugins/tools/nodeDefaults.js.map +1 -1
  54. package/dist/providers/openaiResponsesProvider.d.ts.map +1 -1
  55. package/dist/providers/openaiResponsesProvider.js +74 -79
  56. package/dist/providers/openaiResponsesProvider.js.map +1 -1
  57. package/dist/runtime/agentController.d.ts.map +1 -1
  58. package/dist/runtime/agentController.js +3 -6
  59. package/dist/runtime/agentController.js.map +1 -1
  60. package/dist/runtime/agentSession.d.ts +2 -0
  61. package/dist/runtime/agentSession.d.ts.map +1 -1
  62. package/dist/runtime/agentSession.js +2 -2
  63. package/dist/runtime/agentSession.js.map +1 -1
  64. package/dist/shell/interactiveShell.d.ts +18 -11
  65. package/dist/shell/interactiveShell.d.ts.map +1 -1
  66. package/dist/shell/interactiveShell.js +291 -273
  67. package/dist/shell/interactiveShell.js.map +1 -1
  68. package/dist/shell/shellApp.d.ts.map +1 -1
  69. package/dist/shell/shellApp.js +1 -7
  70. package/dist/shell/shellApp.js.map +1 -1
  71. package/dist/shell/systemPrompt.d.ts.map +1 -1
  72. package/dist/shell/systemPrompt.js +15 -4
  73. package/dist/shell/systemPrompt.js.map +1 -1
  74. package/dist/subagents/taskRunner.js +1 -2
  75. package/dist/subagents/taskRunner.js.map +1 -1
  76. package/dist/tools/bashTools.d.ts.map +1 -1
  77. package/dist/tools/bashTools.js +8 -101
  78. package/dist/tools/bashTools.js.map +1 -1
  79. package/dist/tools/diffUtils.d.ts +2 -8
  80. package/dist/tools/diffUtils.d.ts.map +1 -1
  81. package/dist/tools/diffUtils.js +13 -72
  82. package/dist/tools/diffUtils.js.map +1 -1
  83. package/dist/tools/grepTools.d.ts.map +1 -1
  84. package/dist/tools/grepTools.js +2 -10
  85. package/dist/tools/grepTools.js.map +1 -1
  86. package/dist/tools/planningTools.d.ts +10 -0
  87. package/dist/tools/planningTools.d.ts.map +1 -1
  88. package/dist/tools/planningTools.js +16 -0
  89. package/dist/tools/planningTools.js.map +1 -1
  90. package/dist/tools/searchTools.d.ts.map +1 -1
  91. package/dist/tools/searchTools.js +2 -4
  92. package/dist/tools/searchTools.js.map +1 -1
  93. package/dist/ui/PromptController.d.ts +4 -1
  94. package/dist/ui/PromptController.d.ts.map +1 -1
  95. package/dist/ui/PromptController.js +7 -1
  96. package/dist/ui/PromptController.js.map +1 -1
  97. package/dist/ui/ShellUIAdapter.d.ts +28 -292
  98. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  99. package/dist/ui/ShellUIAdapter.js +121 -1513
  100. package/dist/ui/ShellUIAdapter.js.map +1 -1
  101. package/dist/ui/UnifiedUIRenderer.d.ts +30 -133
  102. package/dist/ui/UnifiedUIRenderer.d.ts.map +1 -1
  103. package/dist/ui/UnifiedUIRenderer.js +370 -939
  104. package/dist/ui/UnifiedUIRenderer.js.map +1 -1
  105. package/dist/ui/animatedStatus.d.ts +6 -128
  106. package/dist/ui/animatedStatus.d.ts.map +1 -1
  107. package/dist/ui/animatedStatus.js +50 -383
  108. package/dist/ui/animatedStatus.js.map +1 -1
  109. package/dist/ui/display.d.ts +26 -182
  110. package/dist/ui/display.d.ts.map +1 -1
  111. package/dist/ui/display.js +97 -678
  112. package/dist/ui/display.js.map +1 -1
  113. package/dist/ui/layout.d.ts +1 -0
  114. package/dist/ui/layout.d.ts.map +1 -1
  115. package/dist/ui/layout.js +12 -0
  116. package/dist/ui/layout.js.map +1 -1
  117. package/dist/ui/orchestration/UIUpdateCoordinator.d.ts +7 -61
  118. package/dist/ui/orchestration/UIUpdateCoordinator.d.ts.map +1 -1
  119. package/dist/ui/orchestration/UIUpdateCoordinator.js +20 -232
  120. package/dist/ui/orchestration/UIUpdateCoordinator.js.map +1 -1
  121. package/dist/ui/planOverlay.d.ts +28 -0
  122. package/dist/ui/planOverlay.d.ts.map +1 -0
  123. package/dist/ui/planOverlay.js +156 -0
  124. package/dist/ui/planOverlay.js.map +1 -0
  125. package/dist/ui/shortcutsHelp.d.ts.map +1 -1
  126. package/dist/ui/shortcutsHelp.js +1 -0
  127. package/dist/ui/shortcutsHelp.js.map +1 -1
  128. package/dist/ui/streamingFormatter.d.ts +30 -0
  129. package/dist/ui/streamingFormatter.d.ts.map +1 -0
  130. package/dist/ui/streamingFormatter.js +91 -0
  131. package/dist/ui/streamingFormatter.js.map +1 -0
  132. package/dist/ui/unified/index.d.ts +1 -30
  133. package/dist/ui/unified/index.d.ts.map +1 -1
  134. package/dist/ui/unified/index.js +2 -45
  135. package/dist/ui/unified/index.js.map +1 -1
  136. package/dist/utils/errorUtils.d.ts +16 -0
  137. package/dist/utils/errorUtils.d.ts.map +1 -0
  138. package/dist/utils/errorUtils.js +66 -0
  139. package/dist/utils/errorUtils.js.map +1 -0
  140. package/package.json +2 -1
  141. package/dist/core/reliabilityPrompt.d.ts +0 -9
  142. package/dist/core/reliabilityPrompt.d.ts.map +0 -1
  143. package/dist/core/reliabilityPrompt.js +0 -31
  144. package/dist/core/reliabilityPrompt.js.map +0 -1
  145. package/dist/ui/UnifiedUIController.d.ts +0 -81
  146. package/dist/ui/UnifiedUIController.d.ts.map +0 -1
  147. package/dist/ui/UnifiedUIController.js +0 -212
  148. package/dist/ui/UnifiedUIController.js.map +0 -1
  149. package/dist/ui/animation/AnimationScheduler.d.ts +0 -192
  150. package/dist/ui/animation/AnimationScheduler.d.ts.map +0 -1
  151. package/dist/ui/animation/AnimationScheduler.js +0 -432
  152. package/dist/ui/animation/AnimationScheduler.js.map +0 -1
  153. package/dist/ui/inPlaceUpdater.d.ts +0 -181
  154. package/dist/ui/inPlaceUpdater.d.ts.map +0 -1
  155. package/dist/ui/inPlaceUpdater.js +0 -515
  156. package/dist/ui/inPlaceUpdater.js.map +0 -1
  157. package/dist/ui/interrupts/InterruptManager.d.ts +0 -142
  158. package/dist/ui/interrupts/InterruptManager.d.ts.map +0 -1
  159. package/dist/ui/interrupts/InterruptManager.js +0 -439
  160. package/dist/ui/interrupts/InterruptManager.js.map +0 -1
  161. package/dist/ui/telemetry/ResponseTracker.d.ts +0 -22
  162. package/dist/ui/telemetry/ResponseTracker.d.ts.map +0 -1
  163. package/dist/ui/telemetry/ResponseTracker.js +0 -60
  164. package/dist/ui/telemetry/ResponseTracker.js.map +0 -1
  165. package/dist/ui/telemetry/UITelemetry.d.ts +0 -181
  166. package/dist/ui/telemetry/UITelemetry.d.ts.map +0 -1
  167. package/dist/ui/telemetry/UITelemetry.js +0 -446
  168. package/dist/ui/telemetry/UITelemetry.js.map +0 -1
  169. package/dist/ui/unified/layout.d.ts +0 -12
  170. package/dist/ui/unified/layout.d.ts.map +0 -1
  171. package/dist/ui/unified/layout.js +0 -96
  172. package/dist/ui/unified/layout.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';
@@ -27,7 +29,7 @@ import { SkillRepository } from '../skills/skillRepository.js';
27
29
  import { createSkillTools } from '../tools/skillTools.js';
28
30
  import { FileChangeTracker } from './fileChangeTracker.js';
29
31
  import { formatShortcutsHelp } from '../ui/shortcutsHelp.js';
30
- import { setPlanApprovalCallback } from '../tools/planningTools.js';
32
+ import { setPlanApprovalCallback, setPlanUpdateCallback } from '../tools/planningTools.js';
31
33
  import { MetricsTracker } from '../core/metricsTracker.js';
32
34
  import { detectFailure, clearActionHistory, findRecoveryStrategy, } from '../core/failureRecovery.js';
33
35
  import { addToolPattern } from '../core/learningPersistence.js';
@@ -40,6 +42,7 @@ import { startOffsecRun, resumeOffsecRun, recordOffsecOutcome, getOffsecNextActi
40
42
  import { generateTestFlows, detectBugs, detectUIUpdates, saveTestFlows, saveBugReports, saveUIUpdates, getTestFlowStatus, } from '../core/intelligentTestFlows.js';
41
43
  import { PromptController } from '../ui/PromptController.js';
42
44
  import { enterStreamingMode, exitStreamingMode, isStreamingMode } from '../ui/globalWriteLock.js';
45
+ import { PlanOverlay } from '../ui/planOverlay.js';
43
46
  import { setGlobalAIEnhancer } from '../tools/localExplore.js';
44
47
  import { createProvider } from '../providers/providerFactory.js';
45
48
  import { getParallelAgentManager } from '../subagents/parallelAgentManager.js';
@@ -129,11 +132,14 @@ export class InteractiveShell {
129
132
  activeContextWindowTokens = null;
130
133
  latestTokenUsage = { used: null, limit: null };
131
134
  planApprovalBridgeRegistered = false;
135
+ planUpdateBridgeRegistered = false;
136
+ planOverlay = new PlanOverlay();
132
137
  contextCompactionInFlight = false;
133
138
  contextCompactionLog = [];
134
139
  lastContextWarningLevel = null;
135
140
  sessionPreferences;
136
141
  autosaveEnabled;
142
+ autoContinueEnabled;
137
143
  verificationEnabled = false;
138
144
  criticalApprovalMode = 'auto';
139
145
  editGuardMode = 'display-edits';
@@ -179,6 +185,7 @@ export class InteractiveShell {
179
185
  streamingOutputSuppressed = false;
180
186
  aiRuntimeStart = null;
181
187
  aiRuntimeTotalMs = 0;
188
+ streamingFormatter = null;
182
189
  streamingThoughtBuffer = '';
183
190
  statusLineState = null;
184
191
  statusMessageOverride = null;
@@ -203,6 +210,7 @@ export class InteractiveShell {
203
210
  this.sessionPreferences = loadSessionPreferences();
204
211
  this.thinkingMode = this.sessionPreferences.thinkingMode;
205
212
  this.autosaveEnabled = this.sessionPreferences.autosave;
213
+ this.autoContinueEnabled = this.sessionPreferences.autoContinue;
206
214
  this.criticalApprovalMode = this.sessionPreferences.criticalApprovalMode;
207
215
  const featureFlags = loadFeatureFlags();
208
216
  this.verificationEnabled = featureFlags.verification === true;
@@ -261,6 +269,11 @@ export class InteractiveShell {
261
269
  description: 'Switch between auto and approval mode for high-impact actions',
262
270
  category: 'configuration',
263
271
  });
272
+ this.slashCommands.push({
273
+ command: '/plan',
274
+ description: 'Show, hide, or clear the current plan panel (/plan show|hide|clear)',
275
+ category: 'workflow',
276
+ });
264
277
  this.slashCommands.push({
265
278
  command: '/offsec',
266
279
  description: 'AlphaZero offensive security run (start/status/next)',
@@ -279,10 +292,6 @@ export class InteractiveShell {
279
292
  this.uiAdapter.setToolStatusCallback((status) => {
280
293
  this.updateStatusMessage(status ?? null);
281
294
  });
282
- // Set up activity callback to update activity line with streaming bash output
283
- this.uiAdapter.setActivityCallback((activity) => {
284
- this.renderer?.setActivity(activity);
285
- });
286
295
  this.skillRepository = new SkillRepository({
287
296
  workingDir: this.workingDir,
288
297
  env: process.env,
@@ -296,18 +305,20 @@ export class InteractiveShell {
296
305
  onQueue: (text) => this.handleQueuedInput(text),
297
306
  onCtrlC: ({ hadBuffer }) => this.handleCtrlCPress(hadBuffer),
298
307
  onInterrupt: () => this.handleInterrupt(),
299
- onExit: () => this.handleExit(),
308
+ onExit: () => this.shutdown(),
309
+ onExpandToolResult: () => display.expandLastToolResult?.(),
300
310
  onChange: ({ text, cursor }) => this.handleInputChange(text, cursor, 'renderer'),
301
311
  onEditModeChange: (mode) => this.handleEditModeChange(mode),
302
312
  onToggleVerify: () => this.toggleVerificationMode(),
313
+ onToggleAutoContinue: () => this.toggleAutoContinueMode(),
303
314
  onToggleThinking: () => this.cycleThinkingMode(),
304
315
  onClearContext: () => this.handleClearContext(),
305
316
  onToggleCriticalApproval: () => this.toggleCriticalApprovalMode('shortcut'),
306
- onExpandToolResult: () => this.expandLastToolResult(),
307
317
  });
308
318
  // Share renderer with Display so all output flows through the unified queue
309
319
  this.renderer = this.terminalInput.getRenderer();
310
320
  display.setRenderer(this.renderer);
321
+ this.uiAdapter.attachRenderer(this.renderer);
311
322
  display.setInlinePanelHandler((content) => {
312
323
  if (!this.shouldCaptureInlinePanel()) {
313
324
  return false;
@@ -327,6 +338,7 @@ export class InteractiveShell {
327
338
  // The control bar will be refreshed after the welcome banner
328
339
  // Allow planning tools (e.g., ProposePlan) to open the interactive approval UI just like Codex CLI
329
340
  this.registerPlanApprovalBridge();
341
+ this.registerPlanUpdateBridge();
330
342
  // Set up command autocomplete with all slash commands
331
343
  this.setupCommandAutocomplete();
332
344
  this.rebuildAgent();
@@ -447,21 +459,18 @@ export class InteractiveShell {
447
459
  const top = `╭${'─'.repeat(labelLeft)}${label}${'─'.repeat(labelRight)}╮`;
448
460
  const bottom = `╰${'─'.repeat(contentWidth)}╯`;
449
461
  const userName = process.env['USER'] || 'there';
450
- const logo = '⟣╍◎╍⟢';
462
+ const logo = clamp('⟣╍◎╍⟢');
451
463
  const versionLabel = version ? clamp(`${brand} · v${version}`) : brand;
452
464
  const modelLine = clamp(model);
453
465
  const workspaceLine = clamp(workspace);
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');
466
+ const welcomeLine = clamp(`Welcome back ${userName}!`);
458
467
  const lines = [
459
468
  frame(top),
460
469
  frame(`│${padCenter(welcomeLine, contentWidth)}│`),
470
+ frame(`│${padCenter(logo, contentWidth)}│`),
461
471
  frame(`│${padCenter(modelLine, contentWidth)}│`),
462
472
  frame(`│${padCenter(versionLabel, contentWidth)}│`),
463
473
  frame(`│${padCenter(workspaceLine, contentWidth)}│`),
464
- frame(`│${padCenter(hintsLine, contentWidth)}│`),
465
474
  frame(bottom),
466
475
  ].join('\n');
467
476
  if (this.renderer) {
@@ -472,6 +481,8 @@ export class InteractiveShell {
472
481
  else {
473
482
  display.stream(`\n${lines}\n`);
474
483
  }
484
+ // Check for updates asynchronously (non-blocking)
485
+ void this.checkAndShowUpdates();
475
486
  // Keep UI pinned; no scrollback banners
476
487
  this.requestPromptRefresh(true);
477
488
  }
@@ -509,32 +520,22 @@ export class InteractiveShell {
509
520
  }
510
521
  async checkAndShowUpdates() {
511
522
  try {
512
- const { checkForUpdates, getUpdateDecision, formatUpdateBanner, performBackgroundUpdate, readAutoUpdateState, shouldShowUpdateNotification, } = await import('../core/updateChecker.js');
523
+ const { checkForUpdates, formatUpdateNotification, maybeAutoUpdate } = await import('../core/updateChecker.js');
513
524
  const currentVersion = this.version || this.getPackageInfo().version || '1.7.458';
514
525
  const updateInfo = await checkForUpdates(currentVersion);
515
526
  if (!updateInfo || !updateInfo.updateAvailable) {
516
527
  return;
517
528
  }
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
529
+ const updateResult = await maybeAutoUpdate(currentVersion, {
530
+ updateInfo,
531
+ logger: (message) => this.streamEventBlock(message),
532
+ });
533
+ if (updateResult?.updated) {
528
534
  return;
529
535
  }
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
536
+ const note = this.describeUpdateSkipReason(updateResult);
537
+ const notification = formatUpdateNotification(updateInfo, note);
538
+ this.streamEventBlock(notification);
538
539
  }
539
540
  catch {
540
541
  // Silently fail - don't interrupt user experience
@@ -618,8 +619,6 @@ export class InteractiveShell {
618
619
  this.pushUiEvent('raw', block);
619
620
  }
620
621
  async start(initialPrompt) {
621
- // Check for updates BEFORE terminal input starts (to avoid keypress conflicts)
622
- await this.checkAndShowUpdates();
623
622
  // Initialize the renderer before emitting the banner so we don't render the prompt twice
624
623
  this.terminalInput.start();
625
624
  this.resetRendererStreamingMode();
@@ -628,9 +627,8 @@ export class InteractiveShell {
628
627
  this.refreshControlBar();
629
628
  // Now sync renderer and control bar state
630
629
  this.syncRendererInput();
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();
630
+ // Ensure the prompt/control bar is rendered after the welcome banner
631
+ this.ensureReadlineReady();
634
632
  if (initialPrompt) {
635
633
  await this.enqueuePromptForProcessing(initialPrompt, 'programmatic');
636
634
  return;
@@ -638,7 +636,7 @@ export class InteractiveShell {
638
636
  this.showLaunchCommandPalette();
639
637
  // Ensure the terminal input is visible
640
638
  this.syncRendererInput();
641
- this.renderer?.render();
639
+ this.ensureReadlineReady();
642
640
  }
643
641
  showLaunchCommandPalette() {
644
642
  // Disabled: Quick commands palette takes up too much space
@@ -706,10 +704,6 @@ export class InteractiveShell {
706
704
  */
707
705
  async runPromptJob(job) {
708
706
  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);
713
707
  await this.processInputBlock(job.text);
714
708
  job.resolve();
715
709
  }
@@ -805,6 +799,7 @@ export class InteractiveShell {
805
799
  '/output-style',
806
800
  // Mode toggles
807
801
  '/thinking',
802
+ '/autocontinue',
808
803
  // Discovery and plugins
809
804
  '/local', '/discover',
810
805
  '/plugins',
@@ -894,22 +889,25 @@ export class InteractiveShell {
894
889
  : '🛡️ High-impact actions now require your approval. (Ctrl+Shift+A to toggle; use /approvals ask to persist)';
895
890
  display.showSystemMessage(message);
896
891
  }
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
+ toggleAutoContinueMode() {
893
+ this.setAutoContinueMode(!this.autoContinueEnabled, 'shortcut');
894
+ }
895
+ setAutoContinueMode(enabled, source) {
896
+ const changed = this.autoContinueEnabled !== enabled;
897
+ this.autoContinueEnabled = enabled;
898
+ saveSessionPreferences({ autoContinue: this.autoContinueEnabled });
899
+ if (this.agent) {
900
+ this.agent.setAutoContinue(this.autoContinueEnabled);
901
+ }
902
+ this.refreshControlBar();
903
+ if (!changed && source === 'shortcut') {
905
904
  return;
906
905
  }
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`);
906
+ const modeLabel = this.autoContinueEnabled ? 'enabled' : 'disabled';
907
+ const behavior = this.autoContinueEnabled
908
+ ? 'The model will be auto-prompted to continue when it expresses intent but does not use tools.'
909
+ : 'The model will not be auto-prompted to continue.';
910
+ display.showInfo(`Auto-continue ${modeLabel}. ${behavior} Toggle with Ctrl+Shift+C.`);
913
911
  }
914
912
  /**
915
913
  * Cycle through thinking modes (Tab shortcut).
@@ -1063,13 +1061,6 @@ export class InteractiveShell {
1063
1061
  }
1064
1062
  this.ctrlCHandledThisPress = false;
1065
1063
  }
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
- }
1073
1064
  /**
1074
1065
  * Gracefully tear down the shell and exit
1075
1066
  */
@@ -1079,7 +1070,7 @@ export class InteractiveShell {
1079
1070
  }
1080
1071
  this.shuttingDown = true;
1081
1072
  // Stop any active spinner to prevent process hang
1082
- display.stopThinking(false);
1073
+ display.stopThinking();
1083
1074
  this.stopStreamingHeartbeat('quit', { quiet: true });
1084
1075
  this.endAiRuntime();
1085
1076
  this.uiUpdates.dispose();
@@ -1091,6 +1082,7 @@ export class InteractiveShell {
1091
1082
  this.pendingCleanup = null;
1092
1083
  // Unregister plan approval bridge
1093
1084
  setPlanApprovalCallback(null);
1085
+ setPlanUpdateCallback(null);
1094
1086
  // Dispose terminal input handler
1095
1087
  this.terminalInput.dispose();
1096
1088
  // Dispose unified UI adapter
@@ -1111,6 +1103,38 @@ export class InteractiveShell {
1111
1103
  });
1112
1104
  this.planApprovalBridgeRegistered = true;
1113
1105
  }
1106
+ registerPlanUpdateBridge() {
1107
+ if (this.planUpdateBridgeRegistered) {
1108
+ return;
1109
+ }
1110
+ setPlanUpdateCallback((payload) => {
1111
+ if (!payload || !payload.steps.length) {
1112
+ this.planOverlay.clear();
1113
+ this.applyPlanOverlay();
1114
+ return;
1115
+ }
1116
+ this.applyPlanUpdate(payload.steps, payload.explanation);
1117
+ });
1118
+ this.planUpdateBridgeRegistered = true;
1119
+ }
1120
+ applyPlanUpdate(steps, explanation) {
1121
+ this.planOverlay.setPlan(steps, explanation);
1122
+ this.applyPlanOverlay();
1123
+ }
1124
+ applyPlanOverlay() {
1125
+ const lines = this.planOverlay.render();
1126
+ if (!this.renderer) {
1127
+ if (this.planOverlay.hasPlan()) {
1128
+ display.showSystemMessage(this.planOverlay.asText());
1129
+ }
1130
+ return;
1131
+ }
1132
+ if (!lines.length) {
1133
+ this.renderer.clearPersistentPanel();
1134
+ return;
1135
+ }
1136
+ this.renderer.setPersistentPanel(lines);
1137
+ }
1114
1138
  /**
1115
1139
  * Update status bar message
1116
1140
  */
@@ -1639,7 +1663,9 @@ export class InteractiveShell {
1639
1663
  refreshControlBar() {
1640
1664
  this.terminalInput.setModeToggles({
1641
1665
  verificationEnabled: this.verificationEnabled,
1666
+ autoContinueEnabled: this.autoContinueEnabled,
1642
1667
  verificationHotkey: 'ctrl+shift+v',
1668
+ autoContinueHotkey: 'ctrl+shift+c',
1643
1669
  thinkingModeLabel: (this.thinkingMode || 'off').toString(),
1644
1670
  thinkingHotkey: 'tab',
1645
1671
  criticalApprovalMode: this.criticalApprovalMode,
@@ -1730,14 +1756,15 @@ export class InteractiveShell {
1730
1756
  */
1731
1757
  refreshStatusLine(forceRender = false) {
1732
1758
  const elapsedSeconds = this.getAiRuntimeSeconds();
1759
+ const thinkingMs = display.isSpinnerActive() ? display.getThinkingElapsedMs() : null;
1733
1760
  const tokensUsed = this.latestTokenUsage.used;
1734
1761
  const tokenLimit = this.latestTokenUsage.limit ?? this.activeContextWindowTokens;
1735
1762
  this.terminalInput.setMetaStatus({
1736
1763
  elapsedSeconds,
1737
1764
  tokensUsed,
1738
1765
  tokenLimit,
1739
- thinkingMs: null,
1740
- thinkingHasContent: false,
1766
+ thinkingMs,
1767
+ thinkingHasContent: display.isSpinnerActive(),
1741
1768
  });
1742
1769
  // Keep model/provider visible in the controls bar
1743
1770
  this.terminalInput.setModelContext({
@@ -1913,16 +1940,12 @@ export class InteractiveShell {
1913
1940
  enterStreamingMode();
1914
1941
  // Set up scroll region for streaming content
1915
1942
  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);
1919
1943
  this.streamingHeartbeatStart = Date.now();
1920
1944
  this.streamingContentSeen = false;
1921
1945
  this.streamingStatusText = null;
1922
1946
  this.streamingStatusLastUpdate = null;
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;
1947
+ // Suppress raw streaming lines; show only a single summary/status + final response
1948
+ this.streamingOutputSuppressed = true;
1926
1949
  const initialLabel = this.isMeaningfulStreamingSnippet(label)
1927
1950
  ? this.truncateStreamingLabel(label)
1928
1951
  : this.getStreamingFallbackLabel();
@@ -1966,8 +1989,6 @@ export class InteractiveShell {
1966
1989
  this.streamingStatusLastUpdate = null;
1967
1990
  this.streamingStatusText = null;
1968
1991
  this.streamingOutputSuppressed = false;
1969
- // Clear activity status when streaming ends
1970
- this.renderer?.setActivity(null);
1971
1992
  // Emit a streaming note for stop/quit so the status stays inside the stream
1972
1993
  if (reason === 'stop' || reason === 'quit') {
1973
1994
  const note = reason === 'quit' ? 'Session closed.' : 'Stream stopped.';
@@ -1984,15 +2005,11 @@ export class InteractiveShell {
1984
2005
  // Buffer for accumulating partial <thinking> tags during streaming
1985
2006
  thinkingTagBuffer = '';
1986
2007
  insideThinkingBlock = false;
1987
- streamingTokenCount = 0;
1988
2008
  handleStreamChunk(chunk, type = 'content') {
1989
2009
  if (!chunk) {
1990
2010
  return;
1991
2011
  }
1992
2012
  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);
1996
2013
  // Keep pinned status updated for all streaming chunks
1997
2014
  this.updateStreamingStatusFromChunk(chunk);
1998
2015
  // Handle <thinking> tags as separate events in the queue
@@ -2001,32 +2018,37 @@ export class InteractiveShell {
2001
2018
  if (!processed.contentChunk && !processed.thinkingChunk) {
2002
2019
  return;
2003
2020
  }
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.
2021
+ // Emit thinking block as separate 'thought' event
2022
+ if (processed.thinkingChunk) {
2023
+ this.pushUiEvent('thought', `${theme.ui.muted('⏺')} ${theme.ui.muted('<thinking>')}${processed.thinkingChunk}${theme.ui.muted('</thinking>')}`);
2024
+ }
2007
2025
  // Process regular content (skip if no content after extracting thinking)
2008
2026
  const contentChunk = processed.contentChunk;
2009
2027
  if (!contentChunk) {
2010
2028
  return;
2011
2029
  }
2012
2030
  // Suppress raw streaming output in scrollback; keep only status + final message.
2013
- // Reasoning tokens should also be suppressed and shown only in the indicator.
2014
- if (this.streamingOutputSuppressed) {
2031
+ // Reasoning chunks bypass this so the thought process stays visible.
2032
+ if (this.streamingOutputSuppressed && !isReasoning) {
2015
2033
  this.captureStreamingThought(contentChunk);
2016
2034
  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
- }
2023
2035
  return;
2024
2036
  }
2025
- // Buffer all streaming content - rendered as complete block via onAssistantMessage
2026
- // This prevents fragmented display and ensures thinking tags are properly processed
2037
+ if (!this.streamingFormatter) {
2038
+ this.streamingFormatter = new StreamingResponseFormatter(output.columns ?? undefined);
2039
+ this.pushUiEvent('streaming', this.streamingFormatter.header());
2040
+ }
2041
+ this.streamingFormatter.setMode(isReasoning ? 'reasoning' : 'content');
2042
+ const formatted = isReasoning
2043
+ ? this.streamingFormatter.formatReasoningChunk(contentChunk)
2044
+ : this.streamingFormatter.formatChunk(contentChunk);
2027
2045
  this.captureStreamingThought(contentChunk);
2028
- // Activity: show what the model is doing (stable, informative)
2029
- this.renderer?.setActivity(type === 'reasoning' ? 'Reasoning' : 'Writing');
2046
+ if (formatted) {
2047
+ if (formatted.trim().length > 0) {
2048
+ this.streamingContentSeen = true;
2049
+ }
2050
+ this.pushUiEvent('streaming', formatted);
2051
+ }
2030
2052
  }
2031
2053
  /**
2032
2054
  * Process streaming content to extract <thinking> blocks as separate events.
@@ -2074,12 +2096,22 @@ export class InteractiveShell {
2074
2096
  }
2075
2097
  return { thinkingChunk: thinkingContent, contentChunk: remainingContent };
2076
2098
  }
2077
- finishStreamingFormatter(_note, options) {
2078
- // Flush any remaining buffered thoughts
2099
+ finishStreamingFormatter(note, options) {
2100
+ if (!this.streamingFormatter) {
2101
+ return;
2102
+ }
2103
+ const closing = this.streamingFormatter.finish({
2104
+ note,
2105
+ mode: options?.mode ?? 'complete',
2106
+ });
2107
+ if (closing) {
2108
+ this.pushUiEvent('streaming', closing);
2109
+ }
2079
2110
  if (this.streamingThoughtBuffer.trim()) {
2080
2111
  this.ui.controller.recordAssistantThought(this.streamingThoughtBuffer.trim());
2081
2112
  }
2082
2113
  this.streamingThoughtBuffer = '';
2114
+ this.streamingFormatter = null;
2083
2115
  if (options?.refreshPrompt ?? true) {
2084
2116
  this.requestPromptRefresh(true);
2085
2117
  }
@@ -2371,6 +2403,9 @@ export class InteractiveShell {
2371
2403
  case '/approvals':
2372
2404
  this.handleApprovalsCommand(input);
2373
2405
  break;
2406
+ case '/plan':
2407
+ this.handlePlanCommand(input);
2408
+ break;
2374
2409
  case '/learn':
2375
2410
  this.showLearningStatus(input);
2376
2411
  break;
@@ -2418,6 +2453,9 @@ export class InteractiveShell {
2418
2453
  case '/thinking':
2419
2454
  this.handleThinkingCommand(input);
2420
2455
  break;
2456
+ case '/autocontinue':
2457
+ this.handleAutoContinueCommand(input);
2458
+ break;
2421
2459
  case '/shortcuts':
2422
2460
  case '/keys':
2423
2461
  this.handleShortcutsCommand();
@@ -2521,9 +2559,6 @@ export class InteractiveShell {
2521
2559
  case '/permissions':
2522
2560
  this.handlePermissionsCommand();
2523
2561
  break;
2524
- case '/update':
2525
- await this.handleUpdateCommand(input);
2526
- break;
2527
2562
  case '/init':
2528
2563
  this.handleInitCommand(input);
2529
2564
  break;
@@ -2581,6 +2616,7 @@ export class InteractiveShell {
2581
2616
  theme.bold(' Mode Toggles'),
2582
2617
  ` ${theme.info('Shift+Tab')} ${theme.ui.muted('Toggle edit mode (auto/ask)')}`,
2583
2618
  ` ${theme.info('Option+V')} ${theme.ui.muted('Toggle verification')}`,
2619
+ ` ${theme.info('Option+C')} ${theme.ui.muted('Toggle auto-continue')}`,
2584
2620
  ` ${theme.info('Ctrl+Shift+A')} ${theme.ui.muted('Toggle approvals for high-impact actions')}`,
2585
2621
  ` ${theme.info('Option+T')} ${theme.ui.muted('Cycle thinking mode')}`,
2586
2622
  ` ${theme.info('Option+E')} ${theme.ui.muted('Toggle edit permission mode')}`,
@@ -4294,6 +4330,20 @@ export class InteractiveShell {
4294
4330
  clearAutosaveSnapshot(this.profile);
4295
4331
  display.showInfo('Cleared autosave history.');
4296
4332
  }
4333
+ handleAutoContinueCommand(input) {
4334
+ const tokens = input.split(/\s+/).slice(1);
4335
+ const value = tokens[0]?.toLowerCase();
4336
+ if (!value) {
4337
+ // Show current status
4338
+ display.showInfo(`Auto-continue is ${this.autoContinueEnabled ? 'enabled' : 'disabled'}. Use /autocontinue on|off or Ctrl+Shift+C to toggle.`);
4339
+ return;
4340
+ }
4341
+ if (value !== 'on' && value !== 'off') {
4342
+ display.showWarning('Usage: /autocontinue on|off');
4343
+ return;
4344
+ }
4345
+ this.setAutoContinueMode(value === 'on', 'command');
4346
+ }
4297
4347
  // ==================== Erosolar-CLI Style Commands ====================
4298
4348
  async handleRewindCommand(_input) {
4299
4349
  const lines = [];
@@ -4525,6 +4575,7 @@ export class InteractiveShell {
4525
4575
  lines.push(`${theme.primary('Provider/Model')}: ${theme.info(`${this.providerLabel(this.sessionState.provider)} · ${this.sessionState.model}`)}`);
4526
4576
  lines.push(`${theme.primary('Workspace')}: ${theme.ui.muted(this.abbreviatePath(this.workingDir))}`);
4527
4577
  lines.push(`${theme.primary('Autosave')}: ${this.autosaveEnabled ? theme.success('on') : theme.ui.muted('off')}`);
4578
+ lines.push(`${theme.primary('Auto-continue')}: ${this.autoContinueEnabled ? theme.success('on') : theme.ui.muted('off')}`);
4528
4579
  lines.push(`${theme.primary('Verification')}: ${this.verificationEnabled ? theme.success('on') : theme.ui.muted('off')}`);
4529
4580
  lines.push(`${theme.primary('Approvals')}: ${theme.ui.muted(this.describeEditGuardMode())}`);
4530
4581
  lines.push(`${theme.primary('Critical approvals')}: ${this.criticalApprovalMode === 'approval' ? theme.warning('ask') : theme.ui.muted('auto')}`);
@@ -4660,6 +4711,52 @@ export class InteractiveShell {
4660
4711
  }
4661
4712
  display.showWarning('Usage: /approvals [auto|ask|status]');
4662
4713
  }
4714
+ handlePlanCommand(input) {
4715
+ const action = input.split(/\s+/)[1]?.toLowerCase() || 'show';
4716
+ const hasPlan = this.planOverlay.hasPlan();
4717
+ const syncPanel = () => {
4718
+ this.applyPlanOverlay();
4719
+ this.syncRendererInput();
4720
+ };
4721
+ switch (action) {
4722
+ case 'show':
4723
+ case 'on':
4724
+ if (!hasPlan) {
4725
+ display.showInfo('No active plan. The panel will populate when a planning tool updates the plan.');
4726
+ return;
4727
+ }
4728
+ this.planOverlay.show();
4729
+ syncPanel();
4730
+ display.showInfo('Plan panel pinned. Use /plan hide to minimize.');
4731
+ return;
4732
+ case 'hide':
4733
+ case 'off':
4734
+ if (!hasPlan) {
4735
+ display.showInfo('No active plan to hide.');
4736
+ return;
4737
+ }
4738
+ this.planOverlay.hide();
4739
+ syncPanel();
4740
+ display.showInfo('Plan panel hidden. Use /plan show to restore.');
4741
+ return;
4742
+ case 'clear':
4743
+ if (!hasPlan) {
4744
+ display.showInfo('No active plan to clear.');
4745
+ return;
4746
+ }
4747
+ this.planOverlay.clear();
4748
+ syncPanel();
4749
+ display.showInfo('Plan cleared.');
4750
+ return;
4751
+ case 'status':
4752
+ case 'view':
4753
+ case 'text':
4754
+ display.showSystemMessage(this.planOverlay.asText());
4755
+ return;
4756
+ default:
4757
+ display.showWarning('Usage: /plan [show|hide|clear|status]');
4758
+ }
4759
+ }
4663
4760
  handlePermissionsCommand() {
4664
4761
  const lines = [];
4665
4762
  lines.push(theme.bold('Tool Permissions'));
@@ -4680,67 +4777,6 @@ export class InteractiveShell {
4680
4777
  lines.push(theme.ui.muted('Use /tools to manage which tool suites are enabled.'));
4681
4778
  display.showSystemMessage(lines.join('\n'));
4682
4779
  }
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
- }
4744
4780
  handleInitCommand(input) {
4745
4781
  const tokens = input.split(/\s+/).slice(1);
4746
4782
  const confirm = tokens[0]?.toLowerCase() === 'confirm';
@@ -5765,7 +5801,6 @@ export class InteractiveShell {
5765
5801
  }
5766
5802
  this.isProcessing = true;
5767
5803
  this.uiUpdates.setMode('processing');
5768
- this.streamingTokenCount = 0; // Reset token counter for new request
5769
5804
  this.terminalInput.setStreaming(true);
5770
5805
  // Keep the persistent input/control bar active as we transition into streaming.
5771
5806
  this.syncRendererInput();
@@ -5780,7 +5815,6 @@ export class InteractiveShell {
5780
5815
  this.currentTaskType = classifyTaskType(request);
5781
5816
  this.currentToolCalls = [];
5782
5817
  this.clearToolUsageMeta();
5783
- this.renderer?.setActivity('Starting...');
5784
5818
  this.uiAdapter.startProcessing('Working on your request');
5785
5819
  this.setProcessingStatus();
5786
5820
  this.beginAiRuntime();
@@ -5836,6 +5870,8 @@ export class InteractiveShell {
5836
5870
  clearActionHistory();
5837
5871
  this.lastFailure = null;
5838
5872
  }
5873
+ // Run post-hoc AI flow sanity check to catch "looks right but wrong" responses
5874
+ this.analyzeAiFlowForRun(request, responseText, requestStartTime);
5839
5875
  }
5840
5876
  catch (error) {
5841
5877
  const handled = this.handleProviderError(error, () => this.processRequest(request));
@@ -5855,7 +5891,7 @@ export class InteractiveShell {
5855
5891
  this.responseRendered = true;
5856
5892
  }
5857
5893
  this.finishStreamingFormatter(undefined, { refreshPrompt: false, mode: 'complete' });
5858
- display.stopThinking(false);
5894
+ display.stopThinking();
5859
5895
  this.uiUpdates.setMode('processing');
5860
5896
  this.stopStreamingHeartbeat('complete', { quiet: true });
5861
5897
  this.endAiRuntime();
@@ -5906,7 +5942,6 @@ export class InteractiveShell {
5906
5942
  this.clearToolUsageMeta();
5907
5943
  this.isProcessing = true;
5908
5944
  this.uiUpdates.setMode('processing');
5909
- this.streamingTokenCount = 0; // Reset token counter for new request
5910
5945
  this.terminalInput.setStreaming(true);
5911
5946
  if (this.suppressNextNetworkReset) {
5912
5947
  this.suppressNextNetworkReset = false;
@@ -6072,7 +6107,7 @@ What's the next action?`;
6072
6107
  await new Promise(resolve => setTimeout(resolve, 500));
6073
6108
  }
6074
6109
  catch (error) {
6075
- display.stopThinking(false);
6110
+ display.stopThinking();
6076
6111
  // Handle context overflow specially - the agent should auto-recover
6077
6112
  // but if it propagates here, we continue the loop
6078
6113
  if (this.isContextOverflowError(error)) {
@@ -6314,6 +6349,43 @@ What's the next action?`;
6314
6349
  const parts = candidates.filter((part) => typeof part === 'string' && part.trim().length > 0);
6315
6350
  return parts.join('\n').trim();
6316
6351
  }
6352
+ /**
6353
+ * Post-run sanity check to catch hallucinated "looks right" answers.
6354
+ * Compares the prompt/response against per-run tool usage and surfaces risks.
6355
+ */
6356
+ analyzeAiFlowForRun(prompt, responseText, startedAt) {
6357
+ const toolHistory = this.runtimeSession.toolRuntime.getToolHistory?.();
6358
+ if (!toolHistory) {
6359
+ return;
6360
+ }
6361
+ const diffSnapshots = this.runtimeSession.toolRuntime.getDiffSnapshots?.() ?? [];
6362
+ const recentDiffs = diffSnapshots
6363
+ .filter((snapshot) => snapshot.timestamp >= startedAt)
6364
+ .map(({ command, output }) => ({ command, output }));
6365
+ const assessment = assessAiFlow({
6366
+ prompt,
6367
+ response: responseText,
6368
+ startedAt,
6369
+ toolHistory,
6370
+ diffSnapshots: recentDiffs,
6371
+ });
6372
+ this.reportAiFlowAssessment(assessment);
6373
+ }
6374
+ reportAiFlowAssessment(assessment) {
6375
+ if (!assessment || assessment.risks.length === 0) {
6376
+ return;
6377
+ }
6378
+ display.showSystemMessage('⚠️ AI flow check: potential unverified completion detected.');
6379
+ for (const risk of assessment.risks) {
6380
+ const detail = risk.details ? ` — ${risk.details}` : '';
6381
+ display.showWarning(`${risk.summary}${detail}`);
6382
+ }
6383
+ if (assessment.recommendations.length > 0) {
6384
+ const uniqueRecommendations = Array.from(new Set(assessment.recommendations));
6385
+ const advice = uniqueRecommendations.slice(0, 3).map((rec, idx) => `${idx + 1}. ${rec}`).join('\n');
6386
+ display.showSystemMessage(`Follow-ups to de-risk this run:\n${advice}`);
6387
+ }
6388
+ }
6317
6389
  runAutoQualityChecks(trigger, assistantResponse, verificationContext) {
6318
6390
  if (!this.verificationEnabled) {
6319
6391
  return;
@@ -6694,41 +6766,21 @@ Return ONLY JSON array:
6694
6766
  maxTokens: this.sessionState.maxTokens,
6695
6767
  systemPrompt: this.buildSystemPrompt(),
6696
6768
  reasoningEffort: this.sessionState.reasoningEffort,
6769
+ autoContinue: this.autoContinueEnabled,
6697
6770
  };
6698
6771
  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
- },
6711
6772
  onStreamChunk: (chunk, type) => {
6712
6773
  this.handleStreamChunk(chunk, type ?? 'content');
6713
6774
  },
6775
+ onStreamFallback: (info) => this.handleStreamingFallback(info),
6714
6776
  onAssistantMessage: (content, metadata) => {
6715
6777
  const enriched = this.buildDisplayMetadata(metadata);
6716
6778
  const streamedVisible = metadata.wasStreamed && this.streamingContentSeen && !this.streamingOutputSuppressed;
6717
6779
  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
- }
6726
6780
  // Update spinner based on message type
6727
6781
  if (metadata.isFinal) {
6728
6782
  const parsed = this.splitThinkingResponse(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;
6783
+ const finalContent = parsed?.response?.trim() || content;
6732
6784
  const thoughtContent = parsed?.thinking?.trim() || null;
6733
6785
  // Show the response if it wasn't already rendered during streaming
6734
6786
  if (!streamedVisible) {
@@ -6831,6 +6883,12 @@ Return ONLY JSON array:
6831
6883
  this.updateStatusMessage('Retrying with reduced context...');
6832
6884
  this.syncRendererInput();
6833
6885
  },
6886
+ onAutoContinue: (attempt, maxAttempts, _message) => {
6887
+ // Show auto-continue progress in UI
6888
+ display.showSystemMessage(`🔄 Auto-continue (${attempt}/${maxAttempts}): Model expressed intent, prompting to act...`);
6889
+ this.updateStatusMessage('Auto-continuing...');
6890
+ this.syncRendererInput();
6891
+ },
6834
6892
  onCancelled: () => {
6835
6893
  // Update UI to show operation was cancelled
6836
6894
  display.showWarning('Operation cancelled.');
@@ -6839,42 +6897,10 @@ Return ONLY JSON array:
6839
6897
  this.updateStatusMessage(null);
6840
6898
  this.terminalInput.setStreaming(false);
6841
6899
  },
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
- },
6868
6900
  onVerificationNeeded: (response, context) => {
6869
6901
  this.lastAssistantResponse = response;
6870
6902
  void this.runAutoQualityChecks('verification', response, context);
6871
6903
  },
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
- },
6878
6904
  });
6879
6905
  // Register global AI enhancer for explore tool - uses active model by default
6880
6906
  this.registerExploreAIEnhancer();
@@ -6969,30 +6995,20 @@ Return ONLY JSON array:
6969
6995
  return lines.join('\n').trim();
6970
6996
  }
6971
6997
  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');
6980
6998
  switch (this.thinkingMode) {
6981
6999
  case 'extended':
6982
7000
  return [
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>.',
7001
+ 'Extended thinking mode is enabled. Format every reply as:',
7002
+ '<thinking>',
7003
+ 'Detailed multi-step reasoning (reference tool runs/files when relevant, keep secrets redacted, no code blocks unless citing filenames).',
7004
+ '</thinking>',
7005
+ '<response>',
7006
+ 'Final answer with actionable next steps and any code/commands requested.',
7007
+ '</response>',
6988
7008
  ].join('\n');
6989
7009
  case 'balanced':
6990
7010
  default:
6991
- return [
6992
- baseRequirement,
6993
- '',
6994
- 'Balanced mode: Keep thinking concise (2-4 sentences) but always present.',
6995
- ].join('\n');
7011
+ return 'Balanced thinking mode: include a short <thinking>...</thinking> block before <response> when the reasoning is non-trivial; skip it for simple answers.';
6996
7012
  }
6997
7013
  }
6998
7014
  buildDisplayMetadata(metadata) {
@@ -7083,14 +7099,13 @@ Return ONLY JSON array:
7083
7099
  });
7084
7100
  cleanupOverlayActive = true;
7085
7101
  const triggerReason = trigger?.reason ?? 'Context optimization';
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
- }
7102
+ const limitLabel = resolvedWindowTokens
7103
+ ? `of ${resolvedWindowTokens.toLocaleString('en-US')} tokens`
7104
+ : 'of estimated context';
7105
+ display.showSystemMessage([
7106
+ `Context usage: ${resolvedTotalTokens.toLocaleString('en-US')} ${limitLabel}`,
7107
+ `(${percentUsed}% full). Auto-compacting${triggerReason ? `: ${triggerReason}` : '...'}`,
7108
+ ].join(' '));
7094
7109
  const result = await contextManager.intelligentCompact(history);
7095
7110
  let afterStats = contextManager.getStats(result.compacted);
7096
7111
  let appliedHistory = result.compacted;
@@ -7117,18 +7132,10 @@ Return ONLY JSON array:
7117
7132
  }
7118
7133
  }
7119
7134
  if (!changed) {
7120
- // Hide compacting status before showing info message
7121
- if (this.renderer) {
7122
- this.renderer.hideCompactingStatus();
7123
- }
7124
7135
  display.showInfo('Context compaction completed but no changes were applied.');
7125
7136
  return;
7126
7137
  }
7127
7138
  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
- }
7132
7139
  display.showInfo('Auto-compaction completed but did not meaningfully reduce context size. Keeping existing history.');
7133
7140
  return;
7134
7141
  }
@@ -7144,18 +7151,15 @@ Return ONLY JSON array:
7144
7151
  this.updateContextUsage(newPercentUsed, CONTEXT_AUTOCOMPACT_PERCENT);
7145
7152
  this.lastContextWarningLevel = this.getContextWarningLevel(newPercentUsed);
7146
7153
  this.refreshStatusLine(true);
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
- }
7154
+ const primarySignal = result.analysis.signals[0]?.reason ?? triggerReason;
7155
+ const savingsLabel = bestTokenSavings > 0
7156
+ ? `(saved ~${bestTokenSavings.toLocaleString('en-US')} tokens, ~${bestPercentSavings.toFixed(1)}%)`
7157
+ : '(no token savings)';
7158
+ display.showSystemMessage([
7159
+ `Context compacted: ${beforeStats.percentage}% ${afterStats.percentage}%`,
7160
+ savingsLabel,
7161
+ primarySignal ? `Reason: ${primarySignal}.` : '',
7162
+ ].filter(Boolean).join(' '));
7159
7163
  this.recordContextCompaction({
7160
7164
  timestamp: Date.now(),
7161
7165
  source: trigger?.source ?? 'auto',
@@ -7169,20 +7173,12 @@ Return ONLY JSON array:
7169
7173
  });
7170
7174
  }
7171
7175
  catch (error) {
7172
- // Hide compacting status animation on error
7173
- if (this.renderer) {
7174
- this.renderer.hideCompactingStatus();
7175
- }
7176
7176
  display.showError('Context compaction failed.', error);
7177
7177
  }
7178
7178
  finally {
7179
7179
  if (cleanupOverlayActive) {
7180
7180
  this.statusTracker.clearOverride(cleanupStatusId);
7181
7181
  }
7182
- // Ensure compacting status is cleared
7183
- if (this.renderer) {
7184
- this.renderer.hideCompactingStatus();
7185
- }
7186
7182
  this.cleanupInProgress = false;
7187
7183
  this.contextCompactionInFlight = false;
7188
7184
  }
@@ -7484,6 +7480,28 @@ Return ONLY JSON array:
7484
7480
  const message = error instanceof Error ? error.message : String(error);
7485
7481
  display.showError(message);
7486
7482
  }
7483
+ handleStreamingFallback(info) {
7484
+ const promptBlock = detectPromptBlockError(info.error ?? info.message);
7485
+ if (promptBlock) {
7486
+ this.handlePromptBlock(promptBlock);
7487
+ this.finishStreamingFormatter('Prompt blocked mid-stream', { mode: 'update' });
7488
+ display.showSystemMessage('Retrying without streaming, but the provider will likely block again until you rephrase.');
7489
+ this.startStreamingHeartbeat('Re-running without streaming');
7490
+ this.requestPromptRefresh(true);
7491
+ return;
7492
+ }
7493
+ const detailText = info.message?.trim() ?? '';
7494
+ const detail = detailText ? ` ${detailText}` : '';
7495
+ const reason = info.reason ? ` (${info.reason.replace(/-/g, ' ')})` : '';
7496
+ const partialNote = info.partialResponse
7497
+ ? ' Partial stream captured before failure; continuing from it without restarting.'
7498
+ : '';
7499
+ const baseMessage = 'Streaming interrupted, retrying without streaming';
7500
+ display.showWarning(`${baseMessage}${reason}.${detail}${partialNote}`.trim());
7501
+ this.finishStreamingFormatter('Stream interrupted - retrying without streaming', { mode: 'update' });
7502
+ this.startStreamingHeartbeat('Fallback in progress');
7503
+ this.requestPromptRefresh(true);
7504
+ }
7487
7505
  handleProviderError(error, retryAction) {
7488
7506
  const promptBlock = detectPromptBlockError(error);
7489
7507
  if (promptBlock) {