erosolar-cli 2.1.172 → 2.1.174

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 +21 -3
  4. package/dist/capabilities/statusCapability.js +2 -2
  5. package/dist/capabilities/statusCapability.js.map +1 -1
  6. package/dist/contracts/agent-schemas.json +5 -5
  7. package/dist/core/agent.d.ts +83 -24
  8. package/dist/core/agent.d.ts.map +1 -1
  9. package/dist/core/agent.js +499 -248
  10. package/dist/core/agent.js.map +1 -1
  11. package/dist/core/preferences.d.ts +1 -0
  12. package/dist/core/preferences.d.ts.map +1 -1
  13. package/dist/core/preferences.js +8 -1
  14. package/dist/core/preferences.js.map +1 -1
  15. package/dist/core/reliabilityPrompt.d.ts +9 -0
  16. package/dist/core/reliabilityPrompt.d.ts.map +1 -0
  17. package/dist/core/reliabilityPrompt.js +31 -0
  18. package/dist/core/reliabilityPrompt.js.map +1 -0
  19. package/dist/core/schemaValidator.js +3 -3
  20. package/dist/core/schemaValidator.js.map +1 -1
  21. package/dist/core/toolPreconditions.d.ts +0 -11
  22. package/dist/core/toolPreconditions.d.ts.map +1 -1
  23. package/dist/core/toolPreconditions.js +33 -164
  24. package/dist/core/toolPreconditions.js.map +1 -1
  25. package/dist/core/toolRuntime.d.ts.map +1 -1
  26. package/dist/core/toolRuntime.js +9 -114
  27. package/dist/core/toolRuntime.js.map +1 -1
  28. package/dist/core/updateChecker.d.ts +61 -1
  29. package/dist/core/updateChecker.d.ts.map +1 -1
  30. package/dist/core/updateChecker.js +147 -3
  31. package/dist/core/updateChecker.js.map +1 -1
  32. package/dist/headless/headlessApp.d.ts.map +1 -1
  33. package/dist/headless/headlessApp.js +0 -39
  34. package/dist/headless/headlessApp.js.map +1 -1
  35. package/dist/plugins/tools/nodeDefaults.d.ts.map +1 -1
  36. package/dist/plugins/tools/nodeDefaults.js +0 -2
  37. package/dist/plugins/tools/nodeDefaults.js.map +1 -1
  38. package/dist/providers/openaiResponsesProvider.d.ts.map +1 -1
  39. package/dist/providers/openaiResponsesProvider.js +79 -74
  40. package/dist/providers/openaiResponsesProvider.js.map +1 -1
  41. package/dist/runtime/agentController.d.ts.map +1 -1
  42. package/dist/runtime/agentController.js +6 -3
  43. package/dist/runtime/agentController.js.map +1 -1
  44. package/dist/runtime/agentSession.d.ts +0 -2
  45. package/dist/runtime/agentSession.d.ts.map +1 -1
  46. package/dist/runtime/agentSession.js +2 -2
  47. package/dist/runtime/agentSession.js.map +1 -1
  48. package/dist/shell/interactiveShell.d.ts +11 -18
  49. package/dist/shell/interactiveShell.d.ts.map +1 -1
  50. package/dist/shell/interactiveShell.js +273 -291
  51. package/dist/shell/interactiveShell.js.map +1 -1
  52. package/dist/shell/shellApp.d.ts.map +1 -1
  53. package/dist/shell/shellApp.js +7 -1
  54. package/dist/shell/shellApp.js.map +1 -1
  55. package/dist/shell/systemPrompt.d.ts.map +1 -1
  56. package/dist/shell/systemPrompt.js +4 -15
  57. package/dist/shell/systemPrompt.js.map +1 -1
  58. package/dist/subagents/taskRunner.js +2 -1
  59. package/dist/subagents/taskRunner.js.map +1 -1
  60. package/dist/tools/bashTools.d.ts.map +1 -1
  61. package/dist/tools/bashTools.js +101 -8
  62. package/dist/tools/bashTools.js.map +1 -1
  63. package/dist/tools/diffUtils.d.ts +8 -2
  64. package/dist/tools/diffUtils.d.ts.map +1 -1
  65. package/dist/tools/diffUtils.js +72 -13
  66. package/dist/tools/diffUtils.js.map +1 -1
  67. package/dist/tools/grepTools.d.ts.map +1 -1
  68. package/dist/tools/grepTools.js +10 -2
  69. package/dist/tools/grepTools.js.map +1 -1
  70. package/dist/tools/planningTools.d.ts +0 -10
  71. package/dist/tools/planningTools.d.ts.map +1 -1
  72. package/dist/tools/planningTools.js +0 -16
  73. package/dist/tools/planningTools.js.map +1 -1
  74. package/dist/tools/searchTools.d.ts.map +1 -1
  75. package/dist/tools/searchTools.js +4 -2
  76. package/dist/tools/searchTools.js.map +1 -1
  77. package/dist/ui/PromptController.d.ts +1 -4
  78. package/dist/ui/PromptController.d.ts.map +1 -1
  79. package/dist/ui/PromptController.js +1 -7
  80. package/dist/ui/PromptController.js.map +1 -1
  81. package/dist/ui/ShellUIAdapter.d.ts +292 -28
  82. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  83. package/dist/ui/ShellUIAdapter.js +1513 -121
  84. package/dist/ui/ShellUIAdapter.js.map +1 -1
  85. package/dist/ui/UnifiedUIController.d.ts +81 -0
  86. package/dist/ui/UnifiedUIController.d.ts.map +1 -0
  87. package/dist/ui/UnifiedUIController.js +212 -0
  88. package/dist/ui/UnifiedUIController.js.map +1 -0
  89. package/dist/ui/UnifiedUIRenderer.d.ts +133 -30
  90. package/dist/ui/UnifiedUIRenderer.d.ts.map +1 -1
  91. package/dist/ui/UnifiedUIRenderer.js +939 -370
  92. package/dist/ui/UnifiedUIRenderer.js.map +1 -1
  93. package/dist/ui/animatedStatus.d.ts +128 -6
  94. package/dist/ui/animatedStatus.d.ts.map +1 -1
  95. package/dist/ui/animatedStatus.js +383 -50
  96. package/dist/ui/animatedStatus.js.map +1 -1
  97. package/dist/ui/animation/AnimationScheduler.d.ts +192 -0
  98. package/dist/ui/animation/AnimationScheduler.d.ts.map +1 -0
  99. package/dist/ui/animation/AnimationScheduler.js +432 -0
  100. package/dist/ui/animation/AnimationScheduler.js.map +1 -0
  101. package/dist/ui/display.d.ts +182 -26
  102. package/dist/ui/display.d.ts.map +1 -1
  103. package/dist/ui/display.js +678 -97
  104. package/dist/ui/display.js.map +1 -1
  105. package/dist/ui/inPlaceUpdater.d.ts +181 -0
  106. package/dist/ui/inPlaceUpdater.d.ts.map +1 -0
  107. package/dist/ui/inPlaceUpdater.js +515 -0
  108. package/dist/ui/inPlaceUpdater.js.map +1 -0
  109. package/dist/ui/interrupts/InterruptManager.d.ts +142 -0
  110. package/dist/ui/interrupts/InterruptManager.d.ts.map +1 -0
  111. package/dist/ui/interrupts/InterruptManager.js +439 -0
  112. package/dist/ui/interrupts/InterruptManager.js.map +1 -0
  113. package/dist/ui/layout.d.ts +0 -1
  114. package/dist/ui/layout.d.ts.map +1 -1
  115. package/dist/ui/layout.js +0 -12
  116. package/dist/ui/layout.js.map +1 -1
  117. package/dist/ui/orchestration/UIUpdateCoordinator.d.ts +61 -7
  118. package/dist/ui/orchestration/UIUpdateCoordinator.d.ts.map +1 -1
  119. package/dist/ui/orchestration/UIUpdateCoordinator.js +232 -20
  120. package/dist/ui/orchestration/UIUpdateCoordinator.js.map +1 -1
  121. package/dist/ui/shortcutsHelp.d.ts.map +1 -1
  122. package/dist/ui/shortcutsHelp.js +0 -1
  123. package/dist/ui/shortcutsHelp.js.map +1 -1
  124. package/dist/ui/telemetry/ResponseTracker.d.ts +22 -0
  125. package/dist/ui/telemetry/ResponseTracker.d.ts.map +1 -0
  126. package/dist/ui/telemetry/ResponseTracker.js +60 -0
  127. package/dist/ui/telemetry/ResponseTracker.js.map +1 -0
  128. package/dist/ui/telemetry/UITelemetry.d.ts +181 -0
  129. package/dist/ui/telemetry/UITelemetry.d.ts.map +1 -0
  130. package/dist/ui/telemetry/UITelemetry.js +446 -0
  131. package/dist/ui/telemetry/UITelemetry.js.map +1 -0
  132. package/dist/ui/unified/index.d.ts +30 -1
  133. package/dist/ui/unified/index.d.ts.map +1 -1
  134. package/dist/ui/unified/index.js +45 -2
  135. package/dist/ui/unified/index.js.map +1 -1
  136. package/dist/ui/unified/layout.d.ts +12 -0
  137. package/dist/ui/unified/layout.d.ts.map +1 -0
  138. package/dist/ui/unified/layout.js +96 -0
  139. package/dist/ui/unified/layout.js.map +1 -0
  140. package/package.json +2 -3
  141. package/dist/StringUtils.d.ts +0 -8
  142. package/dist/StringUtils.d.ts.map +0 -1
  143. package/dist/StringUtils.js +0 -11
  144. package/dist/StringUtils.js.map +0 -1
  145. package/dist/core/aiFlowSupervisor.d.ts +0 -44
  146. package/dist/core/aiFlowSupervisor.d.ts.map +0 -1
  147. package/dist/core/aiFlowSupervisor.js +0 -299
  148. package/dist/core/aiFlowSupervisor.js.map +0 -1
  149. package/dist/core/cliTestHarness.d.ts +0 -200
  150. package/dist/core/cliTestHarness.d.ts.map +0 -1
  151. package/dist/core/cliTestHarness.js +0 -549
  152. package/dist/core/cliTestHarness.js.map +0 -1
  153. package/dist/core/testUtils.d.ts +0 -121
  154. package/dist/core/testUtils.d.ts.map +0 -1
  155. package/dist/core/testUtils.js +0 -235
  156. package/dist/core/testUtils.js.map +0 -1
  157. package/dist/core/toolValidation.d.ts +0 -116
  158. package/dist/core/toolValidation.d.ts.map +0 -1
  159. package/dist/core/toolValidation.js +0 -282
  160. package/dist/core/toolValidation.js.map +0 -1
  161. package/dist/ui/planOverlay.d.ts +0 -28
  162. package/dist/ui/planOverlay.d.ts.map +0 -1
  163. package/dist/ui/planOverlay.js +0 -156
  164. package/dist/ui/planOverlay.js.map +0 -1
  165. package/dist/ui/streamingFormatter.d.ts +0 -30
  166. package/dist/ui/streamingFormatter.d.ts.map +0 -1
  167. package/dist/ui/streamingFormatter.js +0 -91
  168. package/dist/ui/streamingFormatter.js.map +0 -1
  169. package/dist/utils/errorUtils.d.ts +0 -16
  170. package/dist/utils/errorUtils.d.ts.map +0 -1
  171. package/dist/utils/errorUtils.js +0 -66
  172. package/dist/utils/errorUtils.js.map +0 -1
@@ -7,7 +7,6 @@ import { join, resolve } from 'node:path';
7
7
  import { display } from '../ui/display.js';
8
8
  import { theme } from '../ui/theme.js';
9
9
  import { getTerminalColumns } from '../ui/layout.js';
10
- import { StreamingResponseFormatter } from '../ui/streamingFormatter.js';
11
10
  import { getContextWindowTokens } from '../core/contextWindow.js';
12
11
  import { ensureSecretForProvider, getSecretDefinitionForProvider, getSecretValue, listSecretDefinitions, maskSecret, setSecretValue, } from '../core/secretStore.js';
13
12
  import { saveActiveProfilePreference, saveModelPreference, loadToolSettings, saveToolSettings, clearToolSettings, clearActiveProfilePreference, loadSessionPreferences, saveSessionPreferences, loadFeatureFlags, saveFeatureFlags, toggleFeatureFlag, FEATURE_FLAG_INFO, } from '../core/preferences.js';
@@ -19,7 +18,6 @@ import { detectNetworkError } from '../core/errors/networkErrors.js';
19
18
  import { buildWorkspaceContext } from '../workspace.js';
20
19
  import { buildInteractiveSystemPrompt } from './systemPrompt.js';
21
20
  import { getTaskCompletionDetector, resetTaskCompletionDetector, } from './taskCompletionDetector.js';
22
- import { assessAiFlow } from '../core/aiFlowSupervisor.js';
23
21
  import { discoverAllModels, quickCheckProviders, getCachedDiscoveredModels, sortModelsByPriority } from '../core/modelDiscovery.js';
24
22
  import { getModels, getSlashCommands, getProviders } from '../core/agentSchemaLoader.js';
25
23
  import { loadMcpServers } from '../mcp/config.js';
@@ -29,7 +27,7 @@ import { SkillRepository } from '../skills/skillRepository.js';
29
27
  import { createSkillTools } from '../tools/skillTools.js';
30
28
  import { FileChangeTracker } from './fileChangeTracker.js';
31
29
  import { formatShortcutsHelp } from '../ui/shortcutsHelp.js';
32
- import { setPlanApprovalCallback, setPlanUpdateCallback } from '../tools/planningTools.js';
30
+ import { setPlanApprovalCallback } from '../tools/planningTools.js';
33
31
  import { MetricsTracker } from '../core/metricsTracker.js';
34
32
  import { detectFailure, clearActionHistory, findRecoveryStrategy, } from '../core/failureRecovery.js';
35
33
  import { addToolPattern } from '../core/learningPersistence.js';
@@ -42,7 +40,6 @@ import { startOffsecRun, resumeOffsecRun, recordOffsecOutcome, getOffsecNextActi
42
40
  import { generateTestFlows, detectBugs, detectUIUpdates, saveTestFlows, saveBugReports, saveUIUpdates, getTestFlowStatus, } from '../core/intelligentTestFlows.js';
43
41
  import { PromptController } from '../ui/PromptController.js';
44
42
  import { enterStreamingMode, exitStreamingMode, isStreamingMode } from '../ui/globalWriteLock.js';
45
- import { PlanOverlay } from '../ui/planOverlay.js';
46
43
  import { setGlobalAIEnhancer } from '../tools/localExplore.js';
47
44
  import { createProvider } from '../providers/providerFactory.js';
48
45
  import { getParallelAgentManager } from '../subagents/parallelAgentManager.js';
@@ -132,14 +129,11 @@ export class InteractiveShell {
132
129
  activeContextWindowTokens = null;
133
130
  latestTokenUsage = { used: null, limit: null };
134
131
  planApprovalBridgeRegistered = false;
135
- planUpdateBridgeRegistered = false;
136
- planOverlay = new PlanOverlay();
137
132
  contextCompactionInFlight = false;
138
133
  contextCompactionLog = [];
139
134
  lastContextWarningLevel = null;
140
135
  sessionPreferences;
141
136
  autosaveEnabled;
142
- autoContinueEnabled;
143
137
  verificationEnabled = false;
144
138
  criticalApprovalMode = 'auto';
145
139
  editGuardMode = 'display-edits';
@@ -185,7 +179,6 @@ export class InteractiveShell {
185
179
  streamingOutputSuppressed = false;
186
180
  aiRuntimeStart = null;
187
181
  aiRuntimeTotalMs = 0;
188
- streamingFormatter = null;
189
182
  streamingThoughtBuffer = '';
190
183
  statusLineState = null;
191
184
  statusMessageOverride = null;
@@ -210,7 +203,6 @@ export class InteractiveShell {
210
203
  this.sessionPreferences = loadSessionPreferences();
211
204
  this.thinkingMode = this.sessionPreferences.thinkingMode;
212
205
  this.autosaveEnabled = this.sessionPreferences.autosave;
213
- this.autoContinueEnabled = this.sessionPreferences.autoContinue;
214
206
  this.criticalApprovalMode = this.sessionPreferences.criticalApprovalMode;
215
207
  const featureFlags = loadFeatureFlags();
216
208
  this.verificationEnabled = featureFlags.verification === true;
@@ -269,11 +261,6 @@ export class InteractiveShell {
269
261
  description: 'Switch between auto and approval mode for high-impact actions',
270
262
  category: 'configuration',
271
263
  });
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
- });
277
264
  this.slashCommands.push({
278
265
  command: '/offsec',
279
266
  description: 'AlphaZero offensive security run (start/status/next)',
@@ -292,6 +279,10 @@ export class InteractiveShell {
292
279
  this.uiAdapter.setToolStatusCallback((status) => {
293
280
  this.updateStatusMessage(status ?? null);
294
281
  });
282
+ // Set up activity callback to update activity line with streaming bash output
283
+ this.uiAdapter.setActivityCallback((activity) => {
284
+ this.renderer?.setActivity(activity);
285
+ });
295
286
  this.skillRepository = new SkillRepository({
296
287
  workingDir: this.workingDir,
297
288
  env: process.env,
@@ -305,20 +296,18 @@ export class InteractiveShell {
305
296
  onQueue: (text) => this.handleQueuedInput(text),
306
297
  onCtrlC: ({ hadBuffer }) => this.handleCtrlCPress(hadBuffer),
307
298
  onInterrupt: () => this.handleInterrupt(),
308
- onExit: () => this.shutdown(),
309
- onExpandToolResult: () => display.expandLastToolResult?.(),
299
+ onExit: () => this.handleExit(),
310
300
  onChange: ({ text, cursor }) => this.handleInputChange(text, cursor, 'renderer'),
311
301
  onEditModeChange: (mode) => this.handleEditModeChange(mode),
312
302
  onToggleVerify: () => this.toggleVerificationMode(),
313
- onToggleAutoContinue: () => this.toggleAutoContinueMode(),
314
303
  onToggleThinking: () => this.cycleThinkingMode(),
315
304
  onClearContext: () => this.handleClearContext(),
316
305
  onToggleCriticalApproval: () => this.toggleCriticalApprovalMode('shortcut'),
306
+ onExpandToolResult: () => this.expandLastToolResult(),
317
307
  });
318
308
  // Share renderer with Display so all output flows through the unified queue
319
309
  this.renderer = this.terminalInput.getRenderer();
320
310
  display.setRenderer(this.renderer);
321
- this.uiAdapter.attachRenderer(this.renderer);
322
311
  display.setInlinePanelHandler((content) => {
323
312
  if (!this.shouldCaptureInlinePanel()) {
324
313
  return false;
@@ -338,7 +327,6 @@ export class InteractiveShell {
338
327
  // The control bar will be refreshed after the welcome banner
339
328
  // Allow planning tools (e.g., ProposePlan) to open the interactive approval UI just like Codex CLI
340
329
  this.registerPlanApprovalBridge();
341
- this.registerPlanUpdateBridge();
342
330
  // Set up command autocomplete with all slash commands
343
331
  this.setupCommandAutocomplete();
344
332
  this.rebuildAgent();
@@ -459,18 +447,21 @@ export class InteractiveShell {
459
447
  const top = `╭${'─'.repeat(labelLeft)}${label}${'─'.repeat(labelRight)}╮`;
460
448
  const bottom = `╰${'─'.repeat(contentWidth)}╯`;
461
449
  const userName = process.env['USER'] || 'there';
462
- const logo = clamp('⟣╍◎╍⟢');
450
+ const logo = '⟣╍◎╍⟢';
463
451
  const versionLabel = version ? clamp(`${brand} · v${version}`) : brand;
464
452
  const modelLine = clamp(model);
465
453
  const workspaceLine = clamp(workspace);
466
- const welcomeLine = clamp(`Welcome back ${userName}!`);
454
+ // Compact welcome line with logo on same line
455
+ const welcomeLine = clamp(`${logo} Welcome back ${userName}! ${logo}`);
456
+ // Quick reference hints
457
+ const hintsLine = clamp('/model · /secrets · /help · ? shortcuts');
467
458
  const lines = [
468
459
  frame(top),
469
460
  frame(`│${padCenter(welcomeLine, contentWidth)}│`),
470
- frame(`│${padCenter(logo, contentWidth)}│`),
471
461
  frame(`│${padCenter(modelLine, contentWidth)}│`),
472
462
  frame(`│${padCenter(versionLabel, contentWidth)}│`),
473
463
  frame(`│${padCenter(workspaceLine, contentWidth)}│`),
464
+ frame(`│${padCenter(hintsLine, contentWidth)}│`),
474
465
  frame(bottom),
475
466
  ].join('\n');
476
467
  if (this.renderer) {
@@ -481,8 +472,6 @@ export class InteractiveShell {
481
472
  else {
482
473
  display.stream(`\n${lines}\n`);
483
474
  }
484
- // Check for updates asynchronously (non-blocking)
485
- void this.checkAndShowUpdates();
486
475
  // Keep UI pinned; no scrollback banners
487
476
  this.requestPromptRefresh(true);
488
477
  }
@@ -520,22 +509,32 @@ export class InteractiveShell {
520
509
  }
521
510
  async checkAndShowUpdates() {
522
511
  try {
523
- const { checkForUpdates, formatUpdateNotification, maybeAutoUpdate } = await import('../core/updateChecker.js');
512
+ const { checkForUpdates, getUpdateDecision, formatUpdateBanner, performBackgroundUpdate, readAutoUpdateState, shouldShowUpdateNotification, } = await import('../core/updateChecker.js');
524
513
  const currentVersion = this.version || this.getPackageInfo().version || '1.7.458';
525
514
  const updateInfo = await checkForUpdates(currentVersion);
526
515
  if (!updateInfo || !updateInfo.updateAvailable) {
527
516
  return;
528
517
  }
529
- const updateResult = await maybeAutoUpdate(currentVersion, {
530
- updateInfo,
531
- logger: (message) => this.streamEventBlock(message),
532
- });
533
- if (updateResult?.updated) {
518
+ // Check if we should show notification (respects quiet period)
519
+ const state = readAutoUpdateState();
520
+ if (!shouldShowUpdateNotification(state)) {
521
+ return;
522
+ }
523
+ // Get user's preference-based decision (no interactive prompt)
524
+ const sessionPrefs = loadSessionPreferences();
525
+ const decision = getUpdateDecision(sessionPrefs.autoUpdate);
526
+ if (decision === 'skip') {
527
+ // User chose to always skip - silently ignore updates
534
528
  return;
535
529
  }
536
- const note = this.describeUpdateSkipReason(updateResult);
537
- const notification = formatUpdateNotification(updateInfo, note);
538
- this.streamEventBlock(notification);
530
+ // Show non-interactive banner notification
531
+ this.streamEventBlock(formatUpdateBanner(updateInfo, decision));
532
+ if (decision === 'auto') {
533
+ // User chose to always update - perform background update
534
+ await performBackgroundUpdate(updateInfo, (message) => this.streamEventBlock(message));
535
+ }
536
+ // If decision === 'ask', we just show the banner with instructions
537
+ // User can run /update check or /update auto to update
539
538
  }
540
539
  catch {
541
540
  // Silently fail - don't interrupt user experience
@@ -619,6 +618,8 @@ export class InteractiveShell {
619
618
  this.pushUiEvent('raw', block);
620
619
  }
621
620
  async start(initialPrompt) {
621
+ // Check for updates BEFORE terminal input starts (to avoid keypress conflicts)
622
+ await this.checkAndShowUpdates();
622
623
  // Initialize the renderer before emitting the banner so we don't render the prompt twice
623
624
  this.terminalInput.start();
624
625
  this.resetRendererStreamingMode();
@@ -627,8 +628,9 @@ export class InteractiveShell {
627
628
  this.refreshControlBar();
628
629
  // Now sync renderer and control bar state
629
630
  this.syncRendererInput();
630
- // Ensure the prompt/control bar is rendered after the welcome banner
631
- this.ensureReadlineReady();
631
+ // CRITICAL: Force prompt render after banner - flushEvents should have done this,
632
+ // but explicitly ensure the prompt area is visible
633
+ this.renderer?.render();
632
634
  if (initialPrompt) {
633
635
  await this.enqueuePromptForProcessing(initialPrompt, 'programmatic');
634
636
  return;
@@ -636,7 +638,7 @@ export class InteractiveShell {
636
638
  this.showLaunchCommandPalette();
637
639
  // Ensure the terminal input is visible
638
640
  this.syncRendererInput();
639
- this.ensureReadlineReady();
641
+ this.renderer?.render();
640
642
  }
641
643
  showLaunchCommandPalette() {
642
644
  // Disabled: Quick commands palette takes up too much space
@@ -704,6 +706,10 @@ export class InteractiveShell {
704
706
  */
705
707
  async runPromptJob(job) {
706
708
  try {
709
+ // Show thinking indicator instead of "processing queued prompt"
710
+ display.showThinking('Thinking…');
711
+ // Re-echo the prompt to make it clear what's being processed
712
+ this.renderer?.emitPrompt(job.text);
707
713
  await this.processInputBlock(job.text);
708
714
  job.resolve();
709
715
  }
@@ -799,7 +805,6 @@ export class InteractiveShell {
799
805
  '/output-style',
800
806
  // Mode toggles
801
807
  '/thinking',
802
- '/autocontinue',
803
808
  // Discovery and plugins
804
809
  '/local', '/discover',
805
810
  '/plugins',
@@ -889,25 +894,22 @@ export class InteractiveShell {
889
894
  : '🛡️ High-impact actions now require your approval. (Ctrl+Shift+A to toggle; use /approvals ask to persist)';
890
895
  display.showSystemMessage(message);
891
896
  }
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') {
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.');
904
905
  return;
905
906
  }
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.`);
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`);
911
913
  }
912
914
  /**
913
915
  * Cycle through thinking modes (Tab shortcut).
@@ -1061,6 +1063,13 @@ export class InteractiveShell {
1061
1063
  }
1062
1064
  this.ctrlCHandledThisPress = false;
1063
1065
  }
1066
+ /**
1067
+ * Handle exit request (Ctrl+C with empty buffer in idle mode)
1068
+ */
1069
+ handleExit() {
1070
+ display.showSystemMessage('\nGoodbye!\n');
1071
+ this.shutdown();
1072
+ }
1064
1073
  /**
1065
1074
  * Gracefully tear down the shell and exit
1066
1075
  */
@@ -1070,7 +1079,7 @@ export class InteractiveShell {
1070
1079
  }
1071
1080
  this.shuttingDown = true;
1072
1081
  // Stop any active spinner to prevent process hang
1073
- display.stopThinking();
1082
+ display.stopThinking(false);
1074
1083
  this.stopStreamingHeartbeat('quit', { quiet: true });
1075
1084
  this.endAiRuntime();
1076
1085
  this.uiUpdates.dispose();
@@ -1082,7 +1091,6 @@ export class InteractiveShell {
1082
1091
  this.pendingCleanup = null;
1083
1092
  // Unregister plan approval bridge
1084
1093
  setPlanApprovalCallback(null);
1085
- setPlanUpdateCallback(null);
1086
1094
  // Dispose terminal input handler
1087
1095
  this.terminalInput.dispose();
1088
1096
  // Dispose unified UI adapter
@@ -1103,38 +1111,6 @@ export class InteractiveShell {
1103
1111
  });
1104
1112
  this.planApprovalBridgeRegistered = true;
1105
1113
  }
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
- }
1138
1114
  /**
1139
1115
  * Update status bar message
1140
1116
  */
@@ -1663,9 +1639,7 @@ export class InteractiveShell {
1663
1639
  refreshControlBar() {
1664
1640
  this.terminalInput.setModeToggles({
1665
1641
  verificationEnabled: this.verificationEnabled,
1666
- autoContinueEnabled: this.autoContinueEnabled,
1667
1642
  verificationHotkey: 'ctrl+shift+v',
1668
- autoContinueHotkey: 'ctrl+shift+c',
1669
1643
  thinkingModeLabel: (this.thinkingMode || 'off').toString(),
1670
1644
  thinkingHotkey: 'tab',
1671
1645
  criticalApprovalMode: this.criticalApprovalMode,
@@ -1756,15 +1730,14 @@ export class InteractiveShell {
1756
1730
  */
1757
1731
  refreshStatusLine(forceRender = false) {
1758
1732
  const elapsedSeconds = this.getAiRuntimeSeconds();
1759
- const thinkingMs = display.isSpinnerActive() ? display.getThinkingElapsedMs() : null;
1760
1733
  const tokensUsed = this.latestTokenUsage.used;
1761
1734
  const tokenLimit = this.latestTokenUsage.limit ?? this.activeContextWindowTokens;
1762
1735
  this.terminalInput.setMetaStatus({
1763
1736
  elapsedSeconds,
1764
1737
  tokensUsed,
1765
1738
  tokenLimit,
1766
- thinkingMs,
1767
- thinkingHasContent: display.isSpinnerActive(),
1739
+ thinkingMs: null,
1740
+ thinkingHasContent: false,
1768
1741
  });
1769
1742
  // Keep model/provider visible in the controls bar
1770
1743
  this.terminalInput.setModelContext({
@@ -1940,12 +1913,16 @@ export class InteractiveShell {
1940
1913
  enterStreamingMode();
1941
1914
  // Set up scroll region for streaming content
1942
1915
  this.uiUpdates.setMode('streaming');
1916
+ // Show activity status with animated spinner - use provided label or default
1917
+ const activityLabel = label || 'Thinking';
1918
+ this.renderer?.setActivity(activityLabel);
1943
1919
  this.streamingHeartbeatStart = Date.now();
1944
1920
  this.streamingContentSeen = false;
1945
1921
  this.streamingStatusText = null;
1946
1922
  this.streamingStatusLastUpdate = null;
1947
- // Suppress raw streaming lines; show only a single summary/status + final response
1948
- this.streamingOutputSuppressed = true;
1923
+ this.streamingTokenCount = 0; // Reset token count for new prompt
1924
+ // Show raw streaming output in real-time (Claude Code style)
1925
+ this.streamingOutputSuppressed = false;
1949
1926
  const initialLabel = this.isMeaningfulStreamingSnippet(label)
1950
1927
  ? this.truncateStreamingLabel(label)
1951
1928
  : this.getStreamingFallbackLabel();
@@ -1989,6 +1966,8 @@ export class InteractiveShell {
1989
1966
  this.streamingStatusLastUpdate = null;
1990
1967
  this.streamingStatusText = null;
1991
1968
  this.streamingOutputSuppressed = false;
1969
+ // Clear activity status when streaming ends
1970
+ this.renderer?.setActivity(null);
1992
1971
  // Emit a streaming note for stop/quit so the status stays inside the stream
1993
1972
  if (reason === 'stop' || reason === 'quit') {
1994
1973
  const note = reason === 'quit' ? 'Session closed.' : 'Stream stopped.';
@@ -2005,11 +1984,15 @@ export class InteractiveShell {
2005
1984
  // Buffer for accumulating partial <thinking> tags during streaming
2006
1985
  thinkingTagBuffer = '';
2007
1986
  insideThinkingBlock = false;
1987
+ streamingTokenCount = 0;
2008
1988
  handleStreamChunk(chunk, type = 'content') {
2009
1989
  if (!chunk) {
2010
1990
  return;
2011
1991
  }
2012
1992
  const isReasoning = type === 'reasoning';
1993
+ // Approximate token count (roughly 4 chars per token)
1994
+ this.streamingTokenCount += Math.ceil(chunk.length / 4);
1995
+ this.renderer?.updateStreamingTokens(this.streamingTokenCount);
2013
1996
  // Keep pinned status updated for all streaming chunks
2014
1997
  this.updateStreamingStatusFromChunk(chunk);
2015
1998
  // Handle <thinking> tags as separate events in the queue
@@ -2018,37 +2001,32 @@ export class InteractiveShell {
2018
2001
  if (!processed.contentChunk && !processed.thinkingChunk) {
2019
2002
  return;
2020
2003
  }
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
- }
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.
2025
2007
  // Process regular content (skip if no content after extracting thinking)
2026
2008
  const contentChunk = processed.contentChunk;
2027
2009
  if (!contentChunk) {
2028
2010
  return;
2029
2011
  }
2030
2012
  // Suppress raw streaming output in scrollback; keep only status + final message.
2031
- // Reasoning chunks bypass this so the thought process stays visible.
2032
- if (this.streamingOutputSuppressed && !isReasoning) {
2013
+ // Reasoning tokens should also be suppressed and shown only in the indicator.
2014
+ if (this.streamingOutputSuppressed) {
2033
2015
  this.captureStreamingThought(contentChunk);
2034
2016
  this.streamingContentSeen = true;
2017
+ // Update thinking indicator with a snippet of the content
2018
+ const prefix = isReasoning ? '○ ' : '';
2019
+ const snippet = contentChunk.replace(/\s+/g, ' ').trim().slice(0, 60);
2020
+ if (snippet) {
2021
+ display.updateThinking(prefix + snippet + (contentChunk.length > 60 ? '…' : ''));
2022
+ }
2035
2023
  return;
2036
2024
  }
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);
2025
+ // Buffer all streaming content - rendered as complete block via onAssistantMessage
2026
+ // This prevents fragmented display and ensures thinking tags are properly processed
2045
2027
  this.captureStreamingThought(contentChunk);
2046
- if (formatted) {
2047
- if (formatted.trim().length > 0) {
2048
- this.streamingContentSeen = true;
2049
- }
2050
- this.pushUiEvent('streaming', formatted);
2051
- }
2028
+ // Activity: show what the model is doing (stable, informative)
2029
+ this.renderer?.setActivity(type === 'reasoning' ? 'Reasoning' : 'Writing');
2052
2030
  }
2053
2031
  /**
2054
2032
  * Process streaming content to extract <thinking> blocks as separate events.
@@ -2096,22 +2074,12 @@ export class InteractiveShell {
2096
2074
  }
2097
2075
  return { thinkingChunk: thinkingContent, contentChunk: remainingContent };
2098
2076
  }
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
- }
2077
+ finishStreamingFormatter(_note, options) {
2078
+ // Flush any remaining buffered thoughts
2110
2079
  if (this.streamingThoughtBuffer.trim()) {
2111
2080
  this.ui.controller.recordAssistantThought(this.streamingThoughtBuffer.trim());
2112
2081
  }
2113
2082
  this.streamingThoughtBuffer = '';
2114
- this.streamingFormatter = null;
2115
2083
  if (options?.refreshPrompt ?? true) {
2116
2084
  this.requestPromptRefresh(true);
2117
2085
  }
@@ -2403,9 +2371,6 @@ export class InteractiveShell {
2403
2371
  case '/approvals':
2404
2372
  this.handleApprovalsCommand(input);
2405
2373
  break;
2406
- case '/plan':
2407
- this.handlePlanCommand(input);
2408
- break;
2409
2374
  case '/learn':
2410
2375
  this.showLearningStatus(input);
2411
2376
  break;
@@ -2453,9 +2418,6 @@ export class InteractiveShell {
2453
2418
  case '/thinking':
2454
2419
  this.handleThinkingCommand(input);
2455
2420
  break;
2456
- case '/autocontinue':
2457
- this.handleAutoContinueCommand(input);
2458
- break;
2459
2421
  case '/shortcuts':
2460
2422
  case '/keys':
2461
2423
  this.handleShortcutsCommand();
@@ -2559,6 +2521,9 @@ export class InteractiveShell {
2559
2521
  case '/permissions':
2560
2522
  this.handlePermissionsCommand();
2561
2523
  break;
2524
+ case '/update':
2525
+ await this.handleUpdateCommand(input);
2526
+ break;
2562
2527
  case '/init':
2563
2528
  this.handleInitCommand(input);
2564
2529
  break;
@@ -2616,7 +2581,6 @@ export class InteractiveShell {
2616
2581
  theme.bold(' Mode Toggles'),
2617
2582
  ` ${theme.info('Shift+Tab')} ${theme.ui.muted('Toggle edit mode (auto/ask)')}`,
2618
2583
  ` ${theme.info('Option+V')} ${theme.ui.muted('Toggle verification')}`,
2619
- ` ${theme.info('Option+C')} ${theme.ui.muted('Toggle auto-continue')}`,
2620
2584
  ` ${theme.info('Ctrl+Shift+A')} ${theme.ui.muted('Toggle approvals for high-impact actions')}`,
2621
2585
  ` ${theme.info('Option+T')} ${theme.ui.muted('Cycle thinking mode')}`,
2622
2586
  ` ${theme.info('Option+E')} ${theme.ui.muted('Toggle edit permission mode')}`,
@@ -4330,20 +4294,6 @@ export class InteractiveShell {
4330
4294
  clearAutosaveSnapshot(this.profile);
4331
4295
  display.showInfo('Cleared autosave history.');
4332
4296
  }
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
- }
4347
4297
  // ==================== Erosolar-CLI Style Commands ====================
4348
4298
  async handleRewindCommand(_input) {
4349
4299
  const lines = [];
@@ -4575,7 +4525,6 @@ export class InteractiveShell {
4575
4525
  lines.push(`${theme.primary('Provider/Model')}: ${theme.info(`${this.providerLabel(this.sessionState.provider)} · ${this.sessionState.model}`)}`);
4576
4526
  lines.push(`${theme.primary('Workspace')}: ${theme.ui.muted(this.abbreviatePath(this.workingDir))}`);
4577
4527
  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')}`);
4579
4528
  lines.push(`${theme.primary('Verification')}: ${this.verificationEnabled ? theme.success('on') : theme.ui.muted('off')}`);
4580
4529
  lines.push(`${theme.primary('Approvals')}: ${theme.ui.muted(this.describeEditGuardMode())}`);
4581
4530
  lines.push(`${theme.primary('Critical approvals')}: ${this.criticalApprovalMode === 'approval' ? theme.warning('ask') : theme.ui.muted('auto')}`);
@@ -4711,52 +4660,6 @@ export class InteractiveShell {
4711
4660
  }
4712
4661
  display.showWarning('Usage: /approvals [auto|ask|status]');
4713
4662
  }
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
- }
4760
4663
  handlePermissionsCommand() {
4761
4664
  const lines = [];
4762
4665
  lines.push(theme.bold('Tool Permissions'));
@@ -4777,6 +4680,67 @@ export class InteractiveShell {
4777
4680
  lines.push(theme.ui.muted('Use /tools to manage which tool suites are enabled.'));
4778
4681
  display.showSystemMessage(lines.join('\n'));
4779
4682
  }
4683
+ async handleUpdateCommand(input) {
4684
+ const tokens = input.split(/\s+/).slice(1);
4685
+ const subcommand = tokens[0]?.toLowerCase();
4686
+ const prefs = loadSessionPreferences();
4687
+ const currentPref = prefs.autoUpdate;
4688
+ const prefLabel = currentPref === true ? 'always update' : currentPref === false ? 'always skip' : 'notify only';
4689
+ // Show status or help
4690
+ if (!subcommand || subcommand === 'status') {
4691
+ const lines = [];
4692
+ lines.push(theme.bold('Update Settings'));
4693
+ lines.push('');
4694
+ lines.push(`Current preference: ${theme.info(prefLabel)}`);
4695
+ lines.push('');
4696
+ lines.push(theme.secondary('Commands:'));
4697
+ lines.push(' /update check - Check for updates and install immediately');
4698
+ lines.push(' /update auto - Always auto-update in background');
4699
+ lines.push(' /update skip - Never auto-update (silent)');
4700
+ lines.push(' /update notify - Show notification only (default)');
4701
+ display.showSystemMessage(lines.join('\n'));
4702
+ return;
4703
+ }
4704
+ if (subcommand === 'check') {
4705
+ // Force check and perform update immediately (non-interactive)
4706
+ try {
4707
+ const { checkForUpdates, performUpdate } = await import('../core/updateChecker.js');
4708
+ const currentVersion = this.version || this.getPackageInfo().version || '1.7.458';
4709
+ const updateInfo = await checkForUpdates(currentVersion);
4710
+ if (!updateInfo) {
4711
+ display.showWarning('Unable to check for updates (network issue or timeout).');
4712
+ return;
4713
+ }
4714
+ if (!updateInfo.updateAvailable) {
4715
+ display.showSuccess(`You're on the latest version (v${updateInfo.current}).`);
4716
+ return;
4717
+ }
4718
+ // Show update info and perform update immediately (no interactive prompt)
4719
+ display.showInfo(`Update available: v${updateInfo.current} → v${updateInfo.latest}`);
4720
+ await performUpdate(updateInfo, (msg) => this.streamEventBlock(msg));
4721
+ }
4722
+ catch (error) {
4723
+ display.showError(`Update check failed: ${error instanceof Error ? error.message : String(error)}`);
4724
+ }
4725
+ return;
4726
+ }
4727
+ if (subcommand === 'auto' || subcommand === 'always') {
4728
+ saveSessionPreferences({ autoUpdate: true });
4729
+ display.showSuccess('Auto-update enabled. Updates will install automatically.');
4730
+ return;
4731
+ }
4732
+ if (subcommand === 'skip' || subcommand === 'never' || subcommand === 'off') {
4733
+ saveSessionPreferences({ autoUpdate: false });
4734
+ display.showInfo('Auto-update disabled. Updates will be skipped silently.');
4735
+ return;
4736
+ }
4737
+ if (subcommand === 'ask' || subcommand === 'prompt' || subcommand === 'reset' || subcommand === 'notify') {
4738
+ saveSessionPreferences({ autoUpdate: null });
4739
+ display.showInfo('Update preference reset. You will see a notification when updates are available.');
4740
+ return;
4741
+ }
4742
+ display.showWarning('Usage: /update [check|auto|skip|notify|status]');
4743
+ }
4780
4744
  handleInitCommand(input) {
4781
4745
  const tokens = input.split(/\s+/).slice(1);
4782
4746
  const confirm = tokens[0]?.toLowerCase() === 'confirm';
@@ -5801,6 +5765,7 @@ export class InteractiveShell {
5801
5765
  }
5802
5766
  this.isProcessing = true;
5803
5767
  this.uiUpdates.setMode('processing');
5768
+ this.streamingTokenCount = 0; // Reset token counter for new request
5804
5769
  this.terminalInput.setStreaming(true);
5805
5770
  // Keep the persistent input/control bar active as we transition into streaming.
5806
5771
  this.syncRendererInput();
@@ -5815,6 +5780,7 @@ export class InteractiveShell {
5815
5780
  this.currentTaskType = classifyTaskType(request);
5816
5781
  this.currentToolCalls = [];
5817
5782
  this.clearToolUsageMeta();
5783
+ this.renderer?.setActivity('Starting...');
5818
5784
  this.uiAdapter.startProcessing('Working on your request');
5819
5785
  this.setProcessingStatus();
5820
5786
  this.beginAiRuntime();
@@ -5870,8 +5836,6 @@ export class InteractiveShell {
5870
5836
  clearActionHistory();
5871
5837
  this.lastFailure = null;
5872
5838
  }
5873
- // Run post-hoc AI flow sanity check to catch "looks right but wrong" responses
5874
- this.analyzeAiFlowForRun(request, responseText, requestStartTime);
5875
5839
  }
5876
5840
  catch (error) {
5877
5841
  const handled = this.handleProviderError(error, () => this.processRequest(request));
@@ -5891,7 +5855,7 @@ export class InteractiveShell {
5891
5855
  this.responseRendered = true;
5892
5856
  }
5893
5857
  this.finishStreamingFormatter(undefined, { refreshPrompt: false, mode: 'complete' });
5894
- display.stopThinking();
5858
+ display.stopThinking(false);
5895
5859
  this.uiUpdates.setMode('processing');
5896
5860
  this.stopStreamingHeartbeat('complete', { quiet: true });
5897
5861
  this.endAiRuntime();
@@ -5942,6 +5906,7 @@ export class InteractiveShell {
5942
5906
  this.clearToolUsageMeta();
5943
5907
  this.isProcessing = true;
5944
5908
  this.uiUpdates.setMode('processing');
5909
+ this.streamingTokenCount = 0; // Reset token counter for new request
5945
5910
  this.terminalInput.setStreaming(true);
5946
5911
  if (this.suppressNextNetworkReset) {
5947
5912
  this.suppressNextNetworkReset = false;
@@ -6107,7 +6072,7 @@ What's the next action?`;
6107
6072
  await new Promise(resolve => setTimeout(resolve, 500));
6108
6073
  }
6109
6074
  catch (error) {
6110
- display.stopThinking();
6075
+ display.stopThinking(false);
6111
6076
  // Handle context overflow specially - the agent should auto-recover
6112
6077
  // but if it propagates here, we continue the loop
6113
6078
  if (this.isContextOverflowError(error)) {
@@ -6349,43 +6314,6 @@ What's the next action?`;
6349
6314
  const parts = candidates.filter((part) => typeof part === 'string' && part.trim().length > 0);
6350
6315
  return parts.join('\n').trim();
6351
6316
  }
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
- }
6389
6317
  runAutoQualityChecks(trigger, assistantResponse, verificationContext) {
6390
6318
  if (!this.verificationEnabled) {
6391
6319
  return;
@@ -6766,21 +6694,41 @@ Return ONLY JSON array:
6766
6694
  maxTokens: this.sessionState.maxTokens,
6767
6695
  systemPrompt: this.buildSystemPrompt(),
6768
6696
  reasoningEffort: this.sessionState.reasoningEffort,
6769
- autoContinue: this.autoContinueEnabled,
6770
6697
  };
6771
6698
  this.agent = this.runtimeSession.createAgent(selection, {
6699
+ onRequestReceived: (requestPreview) => {
6700
+ const normalized = requestPreview?.trim();
6701
+ const activity = normalized ? `Working: ${normalized}` : 'Working';
6702
+ this.renderer?.setActivity(activity);
6703
+ },
6704
+ onBeforeFirstToolCall: (toolNames) => {
6705
+ const primaryTool = toolNames[0];
6706
+ if (primaryTool) {
6707
+ this.renderer?.setActivity(`Running ${primaryTool}`);
6708
+ }
6709
+ return undefined;
6710
+ },
6772
6711
  onStreamChunk: (chunk, type) => {
6773
6712
  this.handleStreamChunk(chunk, type ?? 'content');
6774
6713
  },
6775
- onStreamFallback: (info) => this.handleStreamingFallback(info),
6776
6714
  onAssistantMessage: (content, metadata) => {
6777
6715
  const enriched = this.buildDisplayMetadata(metadata);
6778
6716
  const streamedVisible = metadata.wasStreamed && this.streamingContentSeen && !this.streamingOutputSuppressed;
6779
6717
  let renderedFinal = false;
6718
+ // Update streaming token count from usage info (more accurate than chunk counting)
6719
+ if (metadata.usage) {
6720
+ const totalTokens = this.totalTokens(metadata.usage);
6721
+ if (totalTokens !== null && totalTokens > this.streamingTokenCount) {
6722
+ this.streamingTokenCount = totalTokens;
6723
+ this.renderer?.updateStreamingTokens(this.streamingTokenCount);
6724
+ }
6725
+ }
6780
6726
  // Update spinner based on message type
6781
6727
  if (metadata.isFinal) {
6782
6728
  const parsed = this.splitThinkingResponse(content);
6783
- const finalContent = parsed?.response?.trim() || content;
6729
+ // If we successfully parsed thinking, use the parsed response (may be empty)
6730
+ // Don't fall back to original content with raw <thinking> tags
6731
+ const finalContent = parsed ? parsed.response?.trim() : content;
6784
6732
  const thoughtContent = parsed?.thinking?.trim() || null;
6785
6733
  // Show the response if it wasn't already rendered during streaming
6786
6734
  if (!streamedVisible) {
@@ -6883,12 +6831,6 @@ Return ONLY JSON array:
6883
6831
  this.updateStatusMessage('Retrying with reduced context...');
6884
6832
  this.syncRendererInput();
6885
6833
  },
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
- },
6892
6834
  onCancelled: () => {
6893
6835
  // Update UI to show operation was cancelled
6894
6836
  display.showWarning('Operation cancelled.');
@@ -6897,10 +6839,42 @@ Return ONLY JSON array:
6897
6839
  this.updateStatusMessage(null);
6898
6840
  this.terminalInput.setStreaming(false);
6899
6841
  },
6842
+ onToolExecution: (toolName, isStart, args) => {
6843
+ // Update activity status to show what tool is being executed
6844
+ if (isStart) {
6845
+ // Show more specific activity for long-running tools
6846
+ let activity = `Running ${toolName}`;
6847
+ if (toolName === 'execute_bash' && args?.['command']) {
6848
+ const cmd = String(args['command']).slice(0, 40);
6849
+ activity = `$ ${cmd}${String(args['command']).length > 40 ? '...' : ''}`;
6850
+ }
6851
+ else if (toolName === 'read_file' && args?.['file_path']) {
6852
+ const path = String(args['file_path']).split('/').pop() || args['file_path'];
6853
+ activity = `Reading ${path}`;
6854
+ }
6855
+ this.renderer?.setActivity(activity);
6856
+ // Estimate tokens for tool call (~50 tokens per call)
6857
+ this.streamingTokenCount += 50;
6858
+ this.renderer?.updateStreamingTokens(this.streamingTokenCount);
6859
+ }
6860
+ else {
6861
+ // Tool finished - estimate result tokens (~100 per result)
6862
+ this.streamingTokenCount += 100;
6863
+ this.renderer?.updateStreamingTokens(this.streamingTokenCount);
6864
+ // Reset to thinking state while model generates next response
6865
+ this.renderer?.setActivity('Thinking');
6866
+ }
6867
+ },
6900
6868
  onVerificationNeeded: (response, context) => {
6901
6869
  this.lastAssistantResponse = response;
6902
6870
  void this.runAutoQualityChecks('verification', response, context);
6903
6871
  },
6872
+ // Retry notification for transient errors
6873
+ onRetrying: (attempt, maxAttempts, error) => {
6874
+ const shortError = error.message.slice(0, 100);
6875
+ display.showSystemMessage(`⚡ Retry ${attempt}/${maxAttempts}: ${shortError}${error.message.length > 100 ? '...' : ''}`);
6876
+ this.renderer?.setActivity(`Retrying (${attempt}/${maxAttempts})...`);
6877
+ },
6904
6878
  });
6905
6879
  // Register global AI enhancer for explore tool - uses active model by default
6906
6880
  this.registerExploreAIEnhancer();
@@ -6995,20 +6969,30 @@ Return ONLY JSON array:
6995
6969
  return lines.join('\n').trim();
6996
6970
  }
6997
6971
  buildThinkingDirective() {
6972
+ // Base requirement: ALWAYS think before acting (applies to all modes)
6973
+ const baseRequirement = [
6974
+ 'CRITICAL: Before calling ANY tool, ALWAYS output a <thinking>...</thinking> block explaining:',
6975
+ '1. What you understand the user is asking',
6976
+ '2. Your approach/plan to solve it',
6977
+ '3. Which tools you will use and why',
6978
+ 'This thinking block MUST appear before your first tool call in every response.',
6979
+ ].join('\n');
6998
6980
  switch (this.thinkingMode) {
6999
6981
  case 'extended':
7000
6982
  return [
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>',
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>.',
7008
6988
  ].join('\n');
7009
6989
  case 'balanced':
7010
6990
  default:
7011
- return 'Balanced thinking mode: include a short <thinking>...</thinking> block before <response> when the reasoning is non-trivial; skip it for simple answers.';
6991
+ return [
6992
+ baseRequirement,
6993
+ '',
6994
+ 'Balanced mode: Keep thinking concise (2-4 sentences) but always present.',
6995
+ ].join('\n');
7012
6996
  }
7013
6997
  }
7014
6998
  buildDisplayMetadata(metadata) {
@@ -7099,13 +7083,14 @@ Return ONLY JSON array:
7099
7083
  });
7100
7084
  cleanupOverlayActive = true;
7101
7085
  const triggerReason = trigger?.reason ?? 'Context optimization';
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(' '));
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
+ }
7109
7094
  const result = await contextManager.intelligentCompact(history);
7110
7095
  let afterStats = contextManager.getStats(result.compacted);
7111
7096
  let appliedHistory = result.compacted;
@@ -7132,10 +7117,18 @@ Return ONLY JSON array:
7132
7117
  }
7133
7118
  }
7134
7119
  if (!changed) {
7120
+ // Hide compacting status before showing info message
7121
+ if (this.renderer) {
7122
+ this.renderer.hideCompactingStatus();
7123
+ }
7135
7124
  display.showInfo('Context compaction completed but no changes were applied.');
7136
7125
  return;
7137
7126
  }
7138
7127
  if (!this.hasMeaningfulCompaction(changed, bestTokenSavings, bestPercentSavings, trigger?.forced)) {
7128
+ // Hide compacting status before showing info message
7129
+ if (this.renderer) {
7130
+ this.renderer.hideCompactingStatus();
7131
+ }
7139
7132
  display.showInfo('Auto-compaction completed but did not meaningfully reduce context size. Keeping existing history.');
7140
7133
  return;
7141
7134
  }
@@ -7151,15 +7144,18 @@ Return ONLY JSON array:
7151
7144
  this.updateContextUsage(newPercentUsed, CONTEXT_AUTOCOMPACT_PERCENT);
7152
7145
  this.lastContextWarningLevel = this.getContextWarningLevel(newPercentUsed);
7153
7146
  this.refreshStatusLine(true);
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(' '));
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
+ }
7163
7159
  this.recordContextCompaction({
7164
7160
  timestamp: Date.now(),
7165
7161
  source: trigger?.source ?? 'auto',
@@ -7173,12 +7169,20 @@ Return ONLY JSON array:
7173
7169
  });
7174
7170
  }
7175
7171
  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
+ }
7182
7186
  this.cleanupInProgress = false;
7183
7187
  this.contextCompactionInFlight = false;
7184
7188
  }
@@ -7480,28 +7484,6 @@ Return ONLY JSON array:
7480
7484
  const message = error instanceof Error ? error.message : String(error);
7481
7485
  display.showError(message);
7482
7486
  }
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
- }
7505
7487
  handleProviderError(error, retryAction) {
7506
7488
  const promptBlock = detectPromptBlockError(error);
7507
7489
  if (promptBlock) {