erosolar-cli 1.7.410 โ†’ 1.7.412

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 (202) hide show
  1. package/README.md +6 -6
  2. package/dist/StringUtils.d.ts +1 -4
  3. package/dist/StringUtils.d.ts.map +1 -1
  4. package/dist/StringUtils.js +2 -8
  5. package/dist/StringUtils.js.map +1 -1
  6. package/dist/browser/BrowserSessionManager.d.ts +1 -3
  7. package/dist/browser/BrowserSessionManager.d.ts.map +1 -1
  8. package/dist/browser/BrowserSessionManager.js +4 -24
  9. package/dist/browser/BrowserSessionManager.js.map +1 -1
  10. package/dist/capabilities/askUserCapability.d.ts.map +1 -1
  11. package/dist/capabilities/askUserCapability.js +64 -10
  12. package/dist/capabilities/askUserCapability.js.map +1 -1
  13. package/dist/capabilities/toolRegistry.d.ts +2 -0
  14. package/dist/capabilities/toolRegistry.d.ts.map +1 -1
  15. package/dist/capabilities/toolRegistry.js +40 -5
  16. package/dist/capabilities/toolRegistry.js.map +1 -1
  17. package/dist/contracts/agent-profiles.schema.json +5 -5
  18. package/dist/contracts/agent-schemas.json +6 -16
  19. package/dist/contracts/schemas/agent.schema.json +1 -5
  20. package/dist/contracts/schemas/tool-selection.schema.json +1 -7
  21. package/dist/contracts/tools.schema.json +80 -207
  22. package/dist/contracts/unified-schema.json +4 -5
  23. package/dist/contracts/v1/agent.d.ts +0 -3
  24. package/dist/contracts/v1/agent.d.ts.map +1 -1
  25. package/dist/contracts/v1/provider.d.ts +1 -2
  26. package/dist/contracts/v1/provider.d.ts.map +1 -1
  27. package/dist/contracts/v1/toolAccess.d.ts +1 -1
  28. package/dist/contracts/v1/toolAccess.d.ts.map +1 -1
  29. package/dist/core/agent.d.ts +1 -7
  30. package/dist/core/agent.d.ts.map +1 -1
  31. package/dist/core/agent.js +2 -131
  32. package/dist/core/agent.js.map +1 -1
  33. package/dist/core/alphaZeroEngine.d.ts +0 -8
  34. package/dist/core/alphaZeroEngine.d.ts.map +1 -1
  35. package/dist/core/alphaZeroEngine.js +35 -149
  36. package/dist/core/alphaZeroEngine.js.map +1 -1
  37. package/dist/core/alphaZeroOrchestrator.d.ts +0 -17
  38. package/dist/core/alphaZeroOrchestrator.d.ts.map +1 -1
  39. package/dist/core/alphaZeroOrchestrator.js +8 -95
  40. package/dist/core/alphaZeroOrchestrator.js.map +1 -1
  41. package/dist/core/claudeCodeFeatures.d.ts +2 -1
  42. package/dist/core/claudeCodeFeatures.d.ts.map +1 -1
  43. package/dist/core/claudeCodeFeatures.js +2 -1
  44. package/dist/core/claudeCodeFeatures.js.map +1 -1
  45. package/dist/core/cliTestHarness.d.ts +0 -5
  46. package/dist/core/cliTestHarness.d.ts.map +1 -1
  47. package/dist/core/cliTestHarness.js +3 -14
  48. package/dist/core/cliTestHarness.js.map +1 -1
  49. package/dist/core/contextManager.d.ts +0 -30
  50. package/dist/core/contextManager.d.ts.map +1 -1
  51. package/dist/core/contextManager.js +5 -87
  52. package/dist/core/contextManager.js.map +1 -1
  53. package/dist/core/contextWindow.d.ts +4 -4
  54. package/dist/core/contextWindow.js +9 -9
  55. package/dist/core/contextWindow.js.map +1 -1
  56. package/dist/core/modelDiscovery.js +3 -3
  57. package/dist/core/modelDiscovery.js.map +1 -1
  58. package/dist/core/preferences.d.ts +2 -3
  59. package/dist/core/preferences.d.ts.map +1 -1
  60. package/dist/core/preferences.js +11 -18
  61. package/dist/core/preferences.js.map +1 -1
  62. package/dist/core/secretStore.d.ts.map +1 -1
  63. package/dist/core/secretStore.js +31 -0
  64. package/dist/core/secretStore.js.map +1 -1
  65. package/dist/core/toolPreconditions.d.ts +1 -0
  66. package/dist/core/toolPreconditions.d.ts.map +1 -1
  67. package/dist/core/toolPreconditions.js +13 -64
  68. package/dist/core/toolPreconditions.js.map +1 -1
  69. package/dist/core/toolRuntime.d.ts.map +1 -1
  70. package/dist/core/toolRuntime.js +0 -17
  71. package/dist/core/toolRuntime.js.map +1 -1
  72. package/dist/core/types.d.ts +1 -1
  73. package/dist/core/types.d.ts.map +1 -1
  74. package/dist/headless/headlessApp.d.ts.map +1 -1
  75. package/dist/headless/headlessApp.js +6 -22
  76. package/dist/headless/headlessApp.js.map +1 -1
  77. package/dist/plugins/providers/google/index.js +3 -2
  78. package/dist/plugins/providers/google/index.js.map +1 -1
  79. package/dist/providers/openaiChatCompletionsProvider.d.ts.map +1 -1
  80. package/dist/providers/openaiChatCompletionsProvider.js +6 -60
  81. package/dist/providers/openaiChatCompletionsProvider.js.map +1 -1
  82. package/dist/runtime/agentController.d.ts.map +1 -1
  83. package/dist/runtime/agentController.js +6 -27
  84. package/dist/runtime/agentController.js.map +1 -1
  85. package/dist/shell/interactiveShell.d.ts +32 -98
  86. package/dist/shell/interactiveShell.d.ts.map +1 -1
  87. package/dist/shell/interactiveShell.js +733 -1645
  88. package/dist/shell/interactiveShell.js.map +1 -1
  89. package/dist/shell/shellApp.d.ts.map +1 -1
  90. package/dist/shell/shellApp.js +41 -15
  91. package/dist/shell/shellApp.js.map +1 -1
  92. package/dist/shell/systemPrompt.d.ts.map +1 -1
  93. package/dist/shell/systemPrompt.js +0 -1
  94. package/dist/shell/systemPrompt.js.map +1 -1
  95. package/dist/shell/terminalInput.d.ts +21 -85
  96. package/dist/shell/terminalInput.d.ts.map +1 -1
  97. package/dist/shell/terminalInput.js +62 -519
  98. package/dist/shell/terminalInput.js.map +1 -1
  99. package/dist/shell/terminalInputAdapter.d.ts +16 -37
  100. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  101. package/dist/shell/terminalInputAdapter.js +22 -44
  102. package/dist/shell/terminalInputAdapter.js.map +1 -1
  103. package/dist/shell/updateManager.d.ts.map +1 -1
  104. package/dist/shell/updateManager.js +17 -1
  105. package/dist/shell/updateManager.js.map +1 -1
  106. package/dist/tools/buildTools.d.ts.map +1 -1
  107. package/dist/tools/buildTools.js +76 -19
  108. package/dist/tools/buildTools.js.map +1 -1
  109. package/dist/tools/editTools.js +1 -1
  110. package/dist/tools/editTools.js.map +1 -1
  111. package/dist/tools/enhancedCodeIntelligenceTools.js +2 -1
  112. package/dist/tools/enhancedCodeIntelligenceTools.js.map +1 -1
  113. package/dist/tools/fileTools.js +0 -3
  114. package/dist/tools/fileTools.js.map +1 -1
  115. package/dist/tools/frontendTestingTools.js +1 -1
  116. package/dist/tools/frontendTestingTools.js.map +1 -1
  117. package/dist/tools/interactionTools.d.ts.map +1 -1
  118. package/dist/tools/interactionTools.js +82 -15
  119. package/dist/tools/interactionTools.js.map +1 -1
  120. package/dist/tools/learnTools.d.ts +0 -2
  121. package/dist/tools/learnTools.d.ts.map +1 -1
  122. package/dist/tools/learnTools.js +81 -29
  123. package/dist/tools/learnTools.js.map +1 -1
  124. package/dist/tools/localExplore.d.ts.map +1 -1
  125. package/dist/tools/localExplore.js +1 -0
  126. package/dist/tools/localExplore.js.map +1 -1
  127. package/dist/tools/notebookEditTools.js.map +1 -1
  128. package/dist/tools/repoChecksTools.js +3 -4
  129. package/dist/tools/repoChecksTools.js.map +1 -1
  130. package/dist/tools/searchTools.js +0 -4
  131. package/dist/tools/searchTools.js.map +1 -1
  132. package/dist/tools/softwareEngineeringTools.d.ts.map +1 -1
  133. package/dist/tools/softwareEngineeringTools.js +0 -1
  134. package/dist/tools/softwareEngineeringTools.js.map +1 -1
  135. package/dist/tools/webTools.d.ts.map +1 -1
  136. package/dist/tools/webTools.js.map +1 -1
  137. package/dist/ui/ShellUIAdapter.d.ts +13 -52
  138. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  139. package/dist/ui/ShellUIAdapter.js +72 -373
  140. package/dist/ui/ShellUIAdapter.js.map +1 -1
  141. package/dist/ui/display.d.ts +38 -19
  142. package/dist/ui/display.d.ts.map +1 -1
  143. package/dist/ui/display.js +310 -96
  144. package/dist/ui/display.js.map +1 -1
  145. package/dist/ui/orchestration/UIUpdateCoordinator.d.ts.map +1 -1
  146. package/dist/ui/orchestration/UIUpdateCoordinator.js +3 -5
  147. package/dist/ui/orchestration/UIUpdateCoordinator.js.map +1 -1
  148. package/dist/ui/shortcutsHelp.d.ts +1 -1
  149. package/dist/ui/shortcutsHelp.d.ts.map +1 -1
  150. package/dist/ui/shortcutsHelp.js +6 -11
  151. package/dist/ui/shortcutsHelp.js.map +1 -1
  152. package/dist/ui/streamingFormatter.d.ts +17 -0
  153. package/dist/ui/streamingFormatter.d.ts.map +1 -0
  154. package/dist/ui/streamingFormatter.js +71 -0
  155. package/dist/ui/streamingFormatter.js.map +1 -0
  156. package/dist/ui/theme.d.ts +100 -100
  157. package/dist/ui/theme.d.ts.map +1 -1
  158. package/dist/ui/theme.js.map +1 -1
  159. package/dist/ui/toolDisplay.d.ts.map +1 -1
  160. package/dist/ui/toolDisplay.js +1 -7
  161. package/dist/ui/toolDisplay.js.map +1 -1
  162. package/dist/ui/unified/index.d.ts +3 -2
  163. package/dist/ui/unified/index.d.ts.map +1 -1
  164. package/dist/ui/unified/index.js +1 -0
  165. package/dist/ui/unified/index.js.map +1 -1
  166. package/dist/ui/unified/layout.d.ts +0 -14
  167. package/dist/ui/unified/layout.d.ts.map +1 -1
  168. package/dist/ui/unified/layout.js +1 -67
  169. package/dist/ui/unified/layout.js.map +1 -1
  170. package/package.json +24 -37
  171. package/dist/core/alphaZeroConfig.d.ts +0 -11
  172. package/dist/core/alphaZeroConfig.d.ts.map +0 -1
  173. package/dist/core/alphaZeroConfig.js +0 -59
  174. package/dist/core/alphaZeroConfig.js.map +0 -1
  175. package/dist/core/alphaZeroEnhanced.d.ts +0 -125
  176. package/dist/core/alphaZeroEnhanced.d.ts.map +0 -1
  177. package/dist/core/alphaZeroEnhanced.js +0 -386
  178. package/dist/core/alphaZeroEnhanced.js.map +0 -1
  179. package/dist/core/autonomousVerification.d.ts +0 -103
  180. package/dist/core/autonomousVerification.d.ts.map +0 -1
  181. package/dist/core/autonomousVerification.js +0 -583
  182. package/dist/core/autonomousVerification.js.map +0 -1
  183. package/dist/core/offsecAlphaZeroEnhanced.d.ts +0 -98
  184. package/dist/core/offsecAlphaZeroEnhanced.d.ts.map +0 -1
  185. package/dist/core/offsecAlphaZeroEnhanced.js +0 -441
  186. package/dist/core/offsecAlphaZeroEnhanced.js.map +0 -1
  187. package/dist/core/parallelAgentOrchestrator.d.ts +0 -171
  188. package/dist/core/parallelAgentOrchestrator.d.ts.map +0 -1
  189. package/dist/core/parallelAgentOrchestrator.js +0 -459
  190. package/dist/core/parallelAgentOrchestrator.js.map +0 -1
  191. package/dist/index.d.ts +0 -5
  192. package/dist/index.d.ts.map +0 -1
  193. package/dist/index.js +0 -3
  194. package/dist/index.js.map +0 -1
  195. package/dist/tools/detectCommands.d.ts +0 -8
  196. package/dist/tools/detectCommands.d.ts.map +0 -1
  197. package/dist/tools/detectCommands.js +0 -183
  198. package/dist/tools/detectCommands.js.map +0 -1
  199. package/dist/ui/assistantBlockRenderer.d.ts +0 -30
  200. package/dist/ui/assistantBlockRenderer.d.ts.map +0 -1
  201. package/dist/ui/assistantBlockRenderer.js +0 -121
  202. package/dist/ui/assistantBlockRenderer.js.map +0 -1
@@ -1,18 +1,19 @@
1
1
  import { stdin as input, stdout as output, exit } from 'node:process';
2
2
  import { exec } from 'node:child_process';
3
3
  import { promisify } from 'node:util';
4
- import { AssistantBlockRenderer } from '../ui/assistantBlockRenderer.js';
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
5
6
  import { display } from '../ui/display.js';
6
- import { compactRenderer } from '../ui/compactRenderer.js';
7
- import { theme, icons } from '../ui/theme.js';
7
+ import { isPlainOutputMode } from '../ui/outputMode.js';
8
+ import { theme } from '../ui/theme.js';
9
+ import { StreamingResponseFormatter } from '../ui/streamingFormatter.js';
8
10
  import { getContextWindowTokens } from '../core/contextWindow.js';
9
- import { ensureSecretForProvider, getSecretDefinitionForProvider, getSecretValue, listSecretDefinitions, MissingSecretError, maskSecret, setSecretValue, } from '../core/secretStore.js';
11
+ import { ensureSecretForProvider, getSecretDefinitionForProvider, getSecretValue, listSecretDefinitions, maskSecret, setSecretValue, } from '../core/secretStore.js';
10
12
  import { saveActiveProfilePreference, saveModelPreference, loadToolSettings, saveToolSettings, clearToolSettings, clearActiveProfilePreference, loadSessionPreferences, saveSessionPreferences, loadFeatureFlags, saveFeatureFlags, toggleFeatureFlag, FEATURE_FLAG_INFO, } from '../core/preferences.js';
11
13
  import { getLearningSummary, getRecentLearning, commitLearning, exportAllLearning, getLearningDir, } from '../core/learningPersistence.js';
12
14
  import { buildEnabledToolSet, evaluateToolPermissions, getToolToggleOptions, } from '../capabilities/toolRegistry.js';
13
15
  import { detectApiKeyError } from '../core/errors/apiKeyErrors.js';
14
16
  import { buildWorkspaceContext } from '../workspace.js';
15
- import { detectBuildCommand, detectTestCommand } from '../tools/detectCommands.js';
16
17
  import { buildInteractiveSystemPrompt } from './systemPrompt.js';
17
18
  import { getTaskCompletionDetector, resetTaskCompletionDetector, } from './taskCompletionDetector.js';
18
19
  import { discoverAllModels, quickCheckProviders, getCachedDiscoveredModels, sortModelsByPriority } from '../core/modelDiscovery.js';
@@ -36,14 +37,11 @@ import { analyzeTokenUsage, discoverModularTargets, getModularStatusDisplay, gen
36
37
  import { startOffsecRun, resumeOffsecRun, recordOffsecOutcome, getOffsecNextActions, simulateOffsecRollout, formatOffsecStatus, listOffsecRuns, } from '../core/offsecAlphaZero.js';
37
38
  import { generateTestFlows, detectBugs, detectUIUpdates, saveTestFlows, saveBugReports, saveUIUpdates, getTestFlowStatus, } from '../core/intelligentTestFlows.js';
38
39
  import { TerminalInputAdapter } from './terminalInputAdapter.js';
39
- import { renderSessionFrame } from '../ui/unified/layout.js';
40
- import { isUpdateInProgress, maybeOfferCliUpdate } from './updateManager.js';
41
40
  import { writeLock } from '../ui/writeLock.js';
42
41
  import { enterStreamingMode, exitStreamingMode } from '../ui/globalWriteLock.js';
43
42
  import { setGlobalAIEnhancer } from '../tools/localExplore.js';
44
43
  import { createProvider } from '../providers/providerFactory.js';
45
44
  import { getParallelAgentManager } from '../subagents/parallelAgentManager.js';
46
- import { formatThinkingContent } from '../ui/textHighlighter.js';
47
45
  const execAsync = promisify(exec);
48
46
  const DROPDOWN_COLORS = [
49
47
  theme.primary,
@@ -113,19 +111,15 @@ export class InteractiveShell {
113
111
  thinkingMode = 'balanced';
114
112
  agentMenu;
115
113
  slashCommands;
116
- bannerSessionState = null;
117
114
  statusTracker;
118
115
  ui;
119
116
  uiAdapter;
120
117
  uiUpdates;
121
- assistantBlocksEnabled;
122
- assistantBlockRenderer;
123
118
  _fileChangeTracker = new FileChangeTracker(); // Reserved for future file tracking features
124
119
  alphaZeroMetrics; // Alpha Zero 2 performance tracking
125
120
  statusSubscription = null;
126
121
  followUpQueue = [];
127
122
  isDrainingQueue = false;
128
- apiKeyGateActive = false;
129
123
  activeContextWindowTokens = null;
130
124
  latestTokenUsage = { used: null, limit: null };
131
125
  planApprovalBridgeRegistered = false;
@@ -134,9 +128,7 @@ export class InteractiveShell {
134
128
  sessionPreferences;
135
129
  autosaveEnabled;
136
130
  autoContinueEnabled;
137
- verificationEnabled = true;
138
- alphaZeroModeEnabled;
139
- alphaZeroVerificationSnapshot = null;
131
+ verificationEnabled = false;
140
132
  editGuardMode = 'display-edits';
141
133
  pendingPermissionInput = null;
142
134
  pendingHistoryLoad = null;
@@ -148,51 +140,36 @@ export class InteractiveShell {
148
140
  customCommandMap;
149
141
  sessionRestoreConfig;
150
142
  _enabledPlugins;
151
- autoVerificationInFlight = false;
143
+ // Cached provider status for unified status bar display after streaming
144
+ cachedProviderStatus = [];
152
145
  // Auto-test tracking
153
146
  autoTestInFlight = false;
154
147
  // AlphaZero learning tracking
155
148
  currentTaskType = 'general';
156
149
  currentToolCalls = [];
157
150
  lastUserQuery = '';
151
+ lastAssistantResponse = null;
158
152
  lastFailure = null;
159
153
  lastAutoTestRun = null;
160
- runIdCounter = 0;
161
- lastRunLog = null;
162
- lastReflectedRunId = null;
163
- skipNextAutoReflection = false;
164
- alphaZeroAutoImproveActive = false;
165
- alphaZeroAutoImproveIterations = 0;
166
- alphaZeroAutoImproveMaxIterations = 6;
167
154
  // Auto-build tracking
168
155
  autoBuildInFlight = false;
156
+ autoBuildPromise = null;
169
157
  lastAutoBuildRun = null;
158
+ lastBuildSucceededAt = null;
170
159
  // Offsec AlphaZero tracking
171
160
  offsecRunId = null;
172
161
  // Streaming UX tracking
173
162
  streamingHeartbeatStart = null;
174
163
  streamingHeartbeatFrame = 0;
175
164
  streamingStatusLabel = null;
176
- streamingStatusBase = null;
177
- streamingStatusDetail = null;
178
165
  lastStreamingElapsedSeconds = null; // Preserve final elapsed time
166
+ streamingFormatter = null;
179
167
  statusLineState = null;
180
168
  statusMessageOverride = null;
181
- latestThoughtSummary = null;
182
- hasShownThoughtProcess = false;
183
169
  promptRefreshTimer = null;
184
170
  launchPaletteShown = false;
185
171
  version;
186
172
  alternateScreenEnabled;
187
- inputInitialized = false;
188
- assistantStreamActive = false;
189
- assistantStreamPhase = null;
190
- assistantStreamHeaderShown = false;
191
- assistantStreamBuffer = '';
192
- assistantStreamMetadata;
193
- assistantStreamHadContent = false;
194
- lastRequestStartedAt = null;
195
- lastToolSummaryRenderedAt = null;
196
173
  constructor(config) {
197
174
  this.profile = config.profile;
198
175
  this.profileLabel = config.profileLabel;
@@ -204,10 +181,10 @@ export class InteractiveShell {
204
181
  this.thinkingMode = this.sessionPreferences.thinkingMode;
205
182
  this.autosaveEnabled = this.sessionPreferences.autosave;
206
183
  this.autoContinueEnabled = this.sessionPreferences.autoContinue;
207
- this.alphaZeroModeEnabled = this.sessionPreferences.alphaZeroMode;
184
+ const featureFlags = loadFeatureFlags();
185
+ this.verificationEnabled = featureFlags.verification === true;
208
186
  this.sessionRestoreConfig = config.sessionRestore ?? { mode: 'none' };
209
187
  this._enabledPlugins = config.enabledPlugins ?? [];
210
- this.assistantBlocksEnabled = config.assistantBlocksEnabled ?? true;
211
188
  this.version = config.version ?? '0.0.0';
212
189
  // Alternate screen disabled - use terminal-native mode for proper scrollback and text selection
213
190
  this.alternateScreenEnabled = false;
@@ -220,11 +197,6 @@ export class InteractiveShell {
220
197
  reasoningEffort: config.initialModel.reasoningEffort,
221
198
  };
222
199
  this.applyPresetReasoningDefaults();
223
- // The welcome banner only includes model + provider on launch, so mark that as the initial state.
224
- this.bannerSessionState = {
225
- model: this.sessionState.model,
226
- provider: this.sessionState.provider,
227
- };
228
200
  this.agentMenu = config.agentSelection ?? null;
229
201
  this.slashCommands = [...BASE_SLASH_COMMANDS];
230
202
  if (this.agentMenu) {
@@ -256,21 +228,11 @@ export class InteractiveShell {
256
228
  description: 'Show available and loaded plugins',
257
229
  category: 'configuration',
258
230
  });
259
- this.slashCommands.push({
260
- command: '/contextlog',
261
- description: 'Show recent context compaction summaries',
262
- category: 'context',
263
- });
264
231
  this.slashCommands.push({
265
232
  command: '/offsec',
266
233
  description: 'AlphaZero offensive security run (start/status/next)',
267
234
  category: 'automation',
268
235
  });
269
- this.slashCommands.push({
270
- command: '/alphazero',
271
- description: 'Toggle AlphaZero RL mode with full-cycle verification',
272
- category: 'mode',
273
- });
274
236
  this.statusTracker = config.statusTracker;
275
237
  this.ui = config.ui;
276
238
  this.uiAdapter = config.ui.adapter;
@@ -282,11 +244,7 @@ export class InteractiveShell {
282
244
  });
283
245
  // Set up tool status callback to update status during tool execution
284
246
  this.uiAdapter.setToolStatusCallback((status) => {
285
- const statusText = status?.text ?? null;
286
- if (statusText) {
287
- this.terminalInput.recordRecentAction(`[tool] ${statusText}`);
288
- }
289
- this.updateStatusMessage(statusText, { logRecent: false });
247
+ this.updateStatusMessage(status ?? null);
290
248
  });
291
249
  this.skillRepository = new SkillRepository({
292
250
  workingDir: this.workingDir,
@@ -306,19 +264,14 @@ export class InteractiveShell {
306
264
  onToggleVerify: () => this.toggleVerificationMode(),
307
265
  onToggleAutoContinue: () => this.toggleAutoContinueMode(),
308
266
  onToggleThinking: () => this.cycleThinkingMode(),
309
- onToggleAlphaZero: () => this.toggleAlphaZeroMode('shortcut'),
310
267
  onClearContext: () => this.handleClearContext(),
311
268
  });
312
- this.assistantBlockRenderer = this.assistantBlocksEnabled
313
- ? new AssistantBlockRenderer({
314
- write: (text) => this.writeAssistantBlock(text),
315
- updates: this.uiUpdates,
316
- })
317
- : null;
318
269
  // Initialize Alpha Zero 2 metrics tracking
319
270
  this.alphaZeroMetrics = new MetricsTracker(`${this.profile}-${Date.now()}`);
320
271
  this.setupStatusTracking();
321
272
  this.refreshContextGauge();
273
+ // Start terminal input (sets up handlers)
274
+ this.terminalInput.start();
322
275
  // Allow planning tools (e.g., ProposePlan) to open the interactive approval UI just like Codex CLI
323
276
  this.registerPlanApprovalBridge();
324
277
  // Capture display output into the scrollback/chat log so system messages
@@ -333,12 +286,12 @@ export class InteractiveShell {
333
286
  else if (output.isTTY) {
334
287
  this.terminalInput.clearScreen();
335
288
  }
336
- // Stream banner first - this sets up scroll region dynamically
337
- const banner = this.buildBanner();
338
- this.terminalInput.streamContent(banner + '\n\n');
289
+ // Render chat box immediately using the streaming UI lifecycle
290
+ this.refreshControlBar();
291
+ this.terminalInput.forceRender();
339
292
  this.rebuildAgent();
340
293
  this.setupHandlers();
341
- this.refreshBannerSessionInfo();
294
+ this.refreshSessionContext();
342
295
  // Subscribe to parallel agent manager events
343
296
  this.setupParallelAgentTracking();
344
297
  }
@@ -350,13 +303,7 @@ export class InteractiveShell {
350
303
  this.parallelAgentDisplayLines = manager.formatDisplay();
351
304
  // Trigger UI refresh if streaming
352
305
  if (this.streamingHeartbeatStart) {
353
- this.uiUpdates.enqueue({
354
- lane: 'stream',
355
- mode: ['streaming', 'processing'],
356
- coalesceKey: 'parallel-agents',
357
- description: 'parallel agent status',
358
- run: () => this.displayParallelAgents(),
359
- });
306
+ this.displayParallelAgents();
360
307
  }
361
308
  };
362
309
  manager.on('agent:started', updateDisplay);
@@ -427,56 +374,13 @@ export class InteractiveShell {
427
374
  this.sessionResumeNotice = null;
428
375
  }
429
376
  async start(initialPrompt) {
430
- await this.runStartupUpdatePrompt();
431
- this.ensureInputInitialized();
432
377
  if (initialPrompt) {
433
378
  await this.processInputBlock(initialPrompt);
434
379
  return;
435
380
  }
436
381
  this.showLaunchCommandPalette();
437
382
  // Ensure the terminal input is visible
438
- this.renderPromptArea();
439
- }
440
- async runStartupUpdatePrompt() {
441
- if (this.version) {
442
- try {
443
- await maybeOfferCliUpdate(this.version);
444
- }
445
- catch {
446
- // Ignore update check failures at startup
447
- }
448
- }
449
- this.showStartupSecretGuidance();
450
- }
451
- ensureInputInitialized() {
452
- if (this.inputInitialized) {
453
- return;
454
- }
455
- this.terminalInput.start();
456
- this.refreshControlBar();
457
- this.renderPromptArea(true);
458
- this.inputInitialized = true;
459
- }
460
- showStartupSecretGuidance() {
461
- const definitions = listSecretDefinitions();
462
- const providerSecret = definitions.find((definition) => definition.providers.includes(this.sessionState.provider));
463
- const missingProviderKey = providerSecret ? !getSecretValue(providerSecret.id) : false;
464
- const hasSearchKey = Boolean(getSecretValue('TAVILY_API_KEY')) ||
465
- Boolean(getSecretValue('BRAVE_SEARCH_API_KEY')) ||
466
- Boolean(getSecretValue('SERPAPI_API_KEY'));
467
- if (!missingProviderKey && hasSearchKey) {
468
- return;
469
- }
470
- const lines = [];
471
- lines.push(theme.gradient.primary('Quick setup needed:'));
472
- if (missingProviderKey && providerSecret) {
473
- lines.push(`${theme.primary('โ€ข')} Set ${providerSecret.label} to use ${this.providerLabel(this.sessionState.provider)}.`);
474
- }
475
- if (!hasSearchKey) {
476
- lines.push(`${theme.primary('โ€ข')} Add a web search API key (Tavily recommended) for web tools.`);
477
- }
478
- lines.push(theme.ui.muted('Run /secrets to configure keys now. Stored values override env vars.'));
479
- display.showSystemMessage(lines.join('\n'));
383
+ this.terminalInput.render();
480
384
  }
481
385
  showLaunchCommandPalette() {
482
386
  // Disabled: Quick commands palette takes up too much space
@@ -487,10 +391,6 @@ export class InteractiveShell {
487
391
  * TerminalInputAdapter submit handler
488
392
  */
489
393
  processInput(text) {
490
- // Guard: Don't process input if CLI update is in progress (we're about to exit)
491
- if (isUpdateInProgress()) {
492
- return;
493
- }
494
394
  const approved = this.resolveEditPermission(text);
495
395
  if (!approved) {
496
396
  this.handleInputChange('');
@@ -506,10 +406,6 @@ export class InteractiveShell {
506
406
  * TerminalInputAdapter queue handler (streaming mode)
507
407
  */
508
408
  handleQueuedInput(text) {
509
- // Guard: Don't queue input if CLI update is in progress (we're about to exit)
510
- if (isUpdateInProgress()) {
511
- return;
512
- }
513
409
  if (this.isExitCommand(text)) {
514
410
  // Allow immediate exits even while streaming
515
411
  this.terminalInput.dequeue();
@@ -534,8 +430,7 @@ export class InteractiveShell {
534
430
  // Keep adapter queue trimmed so hints stay accurate
535
431
  this.terminalInput.dequeue();
536
432
  this.followUpQueue.push({ type: 'request', text });
537
- // Record the queued action in the pinned recent strip instead of the scroll log
538
- this.terminalInput.recordRecentAction(`queued: ${text}`);
433
+ display.showInfo(`Queued: "${text}"`);
539
434
  this.refreshQueueIndicators();
540
435
  this.scheduleQueueProcessing();
541
436
  this.handleInputChange('');
@@ -574,7 +469,6 @@ export class InteractiveShell {
574
469
  // Mode toggles
575
470
  '/thinking',
576
471
  '/autocontinue',
577
- '/alphazero',
578
472
  // Discovery and plugins
579
473
  '/local', '/discover',
580
474
  '/plugins',
@@ -596,30 +490,15 @@ export class InteractiveShell {
596
490
  * Execute a command immediately during streaming.
597
491
  */
598
492
  async executeImmediateCommand(text) {
599
- const label = text.trim() || 'command';
600
- // Keep the action visible in the pinned input area instead of the scroll log
601
- this.terminalInput.recordRecentAction(label);
602
- // Surface a lightweight inline status while the command runs
603
- const previousOverride = this.statusMessageOverride;
604
- this.statusMessageOverride = `Running ${label}`;
605
- this.refreshStatusLine(true);
606
- try {
607
- await this.processSlashCommand(text);
608
- }
609
- finally {
610
- this.statusMessageOverride = previousOverride;
611
- this.refreshStatusLine(true);
612
- }
493
+ // Pause streaming display briefly to show command output
494
+ display.showInfo(`Running command during stream: ${text}`);
495
+ await this.processSlashCommand(text);
613
496
  }
614
497
  /**
615
498
  * TerminalInputAdapter change handler
616
499
  */
617
500
  handleInputChange(text) {
618
- const previous = this.currentInput;
619
501
  this.currentInput = text;
620
- if (previous !== text) {
621
- this.terminalInput.clearInlineCommandPanel();
622
- }
623
502
  if (text.length > 0) {
624
503
  this.resetCtrlCSequence();
625
504
  }
@@ -641,7 +520,7 @@ export class InteractiveShell {
641
520
  display.showSystemMessage('โœ๏ธ Display edits mode enabled.');
642
521
  }
643
522
  }
644
- this.renderPromptArea();
523
+ this.terminalInput.render();
645
524
  }
646
525
  toggleVerificationMode() {
647
526
  this.setVerificationMode(!this.verificationEnabled, 'shortcut');
@@ -654,8 +533,8 @@ export class InteractiveShell {
654
533
  return;
655
534
  }
656
535
  const message = enabled
657
- ? 'โœ… Verification on. Auto-tests will run after edits. (Ctrl+Shift+V to toggle)'
658
- : 'โญ๏ธ Verification off. Skipping auto-tests until re-enabled. (Ctrl+Shift+V to toggle)';
536
+ ? 'โœ… Verification on for this session. Auto-tests will run after edits. (Ctrl+Shift+V to toggle; use /features verification on and restart to make this the default)'
537
+ : 'โญ๏ธ Verification off. Skipping auto-tests until re-enabled. (Ctrl+Shift+V to toggle; defaults to off unless enabled via /features)';
659
538
  display.showSystemMessage(message);
660
539
  }
661
540
  toggleAutoContinueMode() {
@@ -678,54 +557,21 @@ export class InteractiveShell {
678
557
  : 'The model will not be auto-prompted to continue.') +
679
558
  ' Toggle with Ctrl+Shift+C.');
680
559
  }
681
- toggleAlphaZeroMode(source = 'shortcut') {
682
- this.setAlphaZeroMode(!this.alphaZeroModeEnabled, source);
683
- }
684
- setAlphaZeroMode(enabled, source) {
685
- const changed = this.alphaZeroModeEnabled !== enabled;
686
- this.alphaZeroModeEnabled = enabled;
687
- saveSessionPreferences({ alphaZeroMode: this.alphaZeroModeEnabled });
688
- // Force verification on while AlphaZero mode is active to guarantee deep checks
689
- if (enabled) {
690
- if (this.alphaZeroVerificationSnapshot === null) {
691
- this.alphaZeroVerificationSnapshot = this.verificationEnabled;
692
- }
693
- if (!this.verificationEnabled) {
694
- this.setVerificationMode(true, 'command');
695
- }
696
- }
697
- else if (this.alphaZeroVerificationSnapshot !== null) {
698
- this.setVerificationMode(this.alphaZeroVerificationSnapshot, 'command');
699
- this.alphaZeroVerificationSnapshot = null;
700
- }
701
- this.refreshControlBar();
702
- if (!changed && source === 'shortcut') {
703
- return;
704
- }
705
- if (enabled) {
706
- display.showSystemMessage('โ™ž AlphaZero RL mode enabled. Difficult prompts will use the duel/self-critique playbook with full lifecycle verification.');
707
- }
708
- else {
709
- display.showInfo('AlphaZero RL mode disabled. Returning to standard flow.');
710
- }
711
- }
712
560
  /**
713
- * Cycle through thinking modes (Ctrl+Shift+T keyboard shortcut).
561
+ * Cycle through thinking modes (Tab shortcut).
714
562
  */
715
563
  cycleThinkingMode() {
716
- const modes = ['concise', 'balanced', 'extended'];
717
- const currentIndex = modes.indexOf(this.thinkingMode);
718
- const nextIndex = (currentIndex + 1) % modes.length;
719
- const nextMode = modes[nextIndex];
564
+ const nextMode = this.thinkingMode === 'balanced' ? 'extended' : 'balanced';
720
565
  this.thinkingMode = nextMode;
721
566
  saveSessionPreferences({ thinkingMode: this.thinkingMode });
722
567
  this.refreshControlBar();
723
- const descriptions = {
724
- concise: 'Minimal reasoning, faster responses',
725
- balanced: 'Default reasoning depth',
726
- extended: 'Deep reasoning, thorough analysis',
727
- };
728
- display.showInfo(`Thinking mode: ${theme.info(nextMode)} - ${descriptions[nextMode]}. (Alt+T to cycle)`);
568
+ const headline = nextMode === 'extended'
569
+ ? `${theme.info('Thinking on')} (Tab to toggle)`
570
+ : `${theme.info('Thinking off')} (Tab to toggle)`;
571
+ const detail = nextMode === 'extended'
572
+ ? 'Longer reasoning enabled; expect extra usage for deeper answers.'
573
+ : 'Balanced (default) reasoning restored.';
574
+ display.showSystemMessage([headline, theme.ui.muted(detail)].join('\n'));
729
575
  }
730
576
  /**
731
577
  * Handle context clear/compact request (Alt+X keyboard shortcut).
@@ -744,27 +590,17 @@ export class InteractiveShell {
744
590
  */
745
591
  async performContextCompaction() {
746
592
  try {
747
- const sourceHistory = this.agent ? this.agent.getHistory() : this.cachedHistory;
748
- const oldLength = sourceHistory.length;
593
+ // For now, just clear the history and show a message
594
+ // A full implementation would summarize the conversation
595
+ const oldLength = this.cachedHistory.length;
749
596
  if (oldLength === 0) {
750
597
  display.showInfo('Context is already empty.');
751
598
  return;
752
599
  }
753
- // Preserve the first system message (base prompt) and the last few user/assistant turns.
754
- const systemMessage = sourceHistory.find((message) => message.role === 'system') ?? null;
755
- const conversationalMessages = sourceHistory.filter((message) => message.role !== 'system');
756
- const keepCount = Math.min(4, conversationalMessages.length);
757
- const tail = keepCount > 0 ? conversationalMessages.slice(-keepCount) : [];
758
- const compacted = systemMessage ? [systemMessage, ...tail] : tail;
759
- this.cachedHistory = compacted;
760
- if (this.agent) {
761
- this.agent.loadHistory(compacted);
762
- }
763
- // Reset tracked context usage so tools and status bars stay in sync post-compaction.
764
- this.latestTokenUsage = { used: null, limit: this.activeContextWindowTokens ?? null };
765
- this.updateContextUsage(0, CONTEXT_AUTOCOMPACT_PERCENT);
766
- this.lastContextWarningLevel = null;
767
- display.showSuccess(`Context compacted: ${oldLength} messages reduced to ${compacted.length}. ` +
600
+ // Keep the last few messages for continuity
601
+ const keepCount = Math.min(4, oldLength);
602
+ this.cachedHistory = this.cachedHistory.slice(-keepCount);
603
+ display.showSuccess(`Context compacted: ${oldLength} messages reduced to ${keepCount}. ` +
768
604
  `Context usage reset. Continue your conversation.`);
769
605
  this.refreshControlBar();
770
606
  }
@@ -796,7 +632,7 @@ export class InteractiveShell {
796
632
  if (['n', 'no', 'cancel', '/cancel'].includes(lower)) {
797
633
  this.pendingPermissionInput = null;
798
634
  display.showInfo('Request cancelled.');
799
- this.renderPromptArea();
635
+ this.terminalInput.render();
800
636
  return null;
801
637
  }
802
638
  // Treat any other input as a replacement request that also needs confirmation
@@ -814,7 +650,7 @@ export class InteractiveShell {
814
650
  ]
815
651
  .filter(Boolean)
816
652
  .join('\n'));
817
- this.renderPromptArea();
653
+ this.terminalInput.render();
818
654
  }
819
655
  /**
820
656
  * Handle Ctrl+C presses in three stages:
@@ -828,8 +664,8 @@ export class InteractiveShell {
828
664
  if (this.ctrlCPressCount === 1) {
829
665
  this.clearChatInput();
830
666
  const prefix = hadBuffer ? 'Input cleared.' : 'Nothing to clear.';
831
- display.showSystemMessage(`${prefix} Press Ctrl+C again to pause the AI; a third time quits.`);
832
- this.renderPromptArea();
667
+ display.showSystemMessage(`${prefix} Press Ctrl+C again to pause the AI.`);
668
+ this.terminalInput.render();
833
669
  return;
834
670
  }
835
671
  if (this.ctrlCPressCount === 2) {
@@ -841,10 +677,10 @@ export class InteractiveShell {
841
677
  pauseAiExecution() {
842
678
  if (this.isProcessing && this.agent) {
843
679
  this.agent.requestCancellation();
844
- display.showWarning('Pausing AI execution... (Press Ctrl+C again to quit)');
680
+ display.showWarning('Pausing AI execution...');
845
681
  return;
846
682
  }
847
- display.showInfo('No active AI execution to pause. Press Ctrl+C again to quit.');
683
+ display.showInfo('No active AI execution to pause.');
848
684
  }
849
685
  resetCtrlCSequence() {
850
686
  this.ctrlCPressCount = 0;
@@ -877,7 +713,7 @@ export class InteractiveShell {
877
713
  : null;
878
714
  // Stop any active spinner to prevent process hang
879
715
  display.stopThinking(false);
880
- this.stopStreamingHeartbeat({ skipRender: true });
716
+ this.stopStreamingHeartbeat();
881
717
  this.uiUpdates.dispose();
882
718
  this.clearPromptRefreshTimer();
883
719
  this.teardownStatusTracking();
@@ -886,9 +722,7 @@ export class InteractiveShell {
886
722
  // Clear any pending cleanup to prevent hanging
887
723
  this.pendingCleanup = null;
888
724
  // Reset terminal state before disposing adapters
889
- if (this.terminalInput.isScrollRegionActive()) {
890
- this.terminalInput.exitStreamingScrollRegion({ skipRender: true });
891
- }
725
+ this.terminalInput.exitStreamingScrollRegion();
892
726
  if (this.alternateScreenEnabled) {
893
727
  this.terminalInput.exitAlternateScreen();
894
728
  }
@@ -901,9 +735,7 @@ export class InteractiveShell {
901
735
  if (scrollbackSnapshot && scrollbackSnapshot.length > 0) {
902
736
  this.restoreScrollbackSnapshot(scrollbackSnapshot);
903
737
  }
904
- console.log(theme.ui.muted('โ”'.repeat(44)));
905
- console.log(theme.ui.muted(' Goodbye! ยท support@ero.solar'));
906
- console.log(theme.ui.muted('โ”'.repeat(44)));
738
+ display.stream(`\n${theme.ui.muted('Session closed.')}\n`);
907
739
  exit(0);
908
740
  }
909
741
  restoreScrollbackSnapshot(lines) {
@@ -913,8 +745,8 @@ export class InteractiveShell {
913
745
  const transcript = lines.join('\n');
914
746
  const separator = theme.ui.muted('โ”€'.repeat(44));
915
747
  const header = theme.ui.muted('Restored scrollback from this session:');
916
- // Write directly to stdout after exiting alternate screen to preserve the transcript
917
- process.stdout.write(`\n${separator}\n${header}\n${transcript}\n${separator}\n`);
748
+ // Stream the restored transcript so it stays within the in-stream UI flow
749
+ display.stream(`\n${separator}\n${header}\n${transcript}\n${separator}\n`);
918
750
  }
919
751
  /**
920
752
  * Wire the planning tool suite to the interactive plan approval UI so ProposePlan behaves like Codex CLI.
@@ -931,11 +763,8 @@ export class InteractiveShell {
931
763
  /**
932
764
  * Update status bar message
933
765
  */
934
- updateStatusMessage(message, options = {}) {
766
+ updateStatusMessage(message) {
935
767
  this.statusMessageOverride = message;
936
- if (message && options.logRecent !== false) {
937
- this.terminalInput.recordRecentAction(`[status] ${message}`);
938
- }
939
768
  // During streaming we still want the spinner prefix; when idle force a fast refresh.
940
769
  this.refreshStatusLine(!this.isProcessing);
941
770
  }
@@ -947,26 +776,26 @@ export class InteractiveShell {
947
776
  const trimmed = input.trim();
948
777
  if (!trimmed) {
949
778
  display.showWarning('Enter a number, "save", "defaults", or "cancel".');
950
- this.renderPromptArea();
779
+ this.terminalInput.render();
951
780
  return;
952
781
  }
953
782
  const normalized = trimmed.toLowerCase();
954
783
  if (normalized === 'cancel') {
955
784
  this.pendingInteraction = null;
956
785
  display.showInfo('Tool selection cancelled.');
957
- this.renderPromptArea();
786
+ this.terminalInput.render();
958
787
  return;
959
788
  }
960
789
  if (normalized === 'defaults') {
961
790
  pending.selection = buildEnabledToolSet(null);
962
791
  this.renderToolMenu(pending);
963
- this.renderPromptArea();
792
+ this.terminalInput.render();
964
793
  return;
965
794
  }
966
795
  if (normalized === 'save') {
967
796
  await this.persistToolSelection(pending);
968
797
  this.pendingInteraction = null;
969
- this.renderPromptArea();
798
+ this.terminalInput.render();
970
799
  return;
971
800
  }
972
801
  const choice = Number.parseInt(trimmed, 10);
@@ -976,19 +805,24 @@ export class InteractiveShell {
976
805
  display.showWarning('That option is not available.');
977
806
  }
978
807
  else {
979
- if (pending.selection.has(option.id)) {
980
- pending.selection.delete(option.id);
808
+ if (option.locked) {
809
+ display.showInfo('The default tool package is always enabled and cannot be toggled.');
981
810
  }
982
811
  else {
983
- pending.selection.add(option.id);
812
+ if (pending.selection.has(option.id)) {
813
+ pending.selection.delete(option.id);
814
+ }
815
+ else {
816
+ pending.selection.add(option.id);
817
+ }
984
818
  }
985
819
  this.renderToolMenu(pending);
986
820
  }
987
- this.renderPromptArea();
821
+ this.terminalInput.render();
988
822
  return;
989
823
  }
990
824
  display.showWarning('Enter a number, "save", "defaults", or "cancel".');
991
- this.renderPromptArea();
825
+ this.terminalInput.render();
992
826
  }
993
827
  async persistToolSelection(interaction) {
994
828
  if (setsEqual(interaction.selection, interaction.initialSelection)) {
@@ -998,10 +832,11 @@ export class InteractiveShell {
998
832
  const defaults = buildEnabledToolSet(null);
999
833
  if (setsEqual(interaction.selection, defaults)) {
1000
834
  clearToolSettings();
1001
- display.showInfo('Tool settings cleared. Defaults will be used on the next launch. Next steps: enable any optional suites from the Tools menu or preferences (restart to apply) if you want them available by default.');
835
+ display.showInfo('Tool settings cleared. Defaults will be used on the next launch.');
1002
836
  return;
1003
837
  }
1004
838
  const ordered = interaction.options
839
+ .filter((option) => !option.locked)
1005
840
  .map((option) => option.id)
1006
841
  .filter((id) => interaction.selection.has(id));
1007
842
  saveToolSettings({ enabledTools: ordered });
@@ -1015,36 +850,36 @@ export class InteractiveShell {
1015
850
  if (!this.agentMenu) {
1016
851
  this.pendingInteraction = null;
1017
852
  display.showWarning('Agent selection is unavailable in this CLI.');
1018
- this.renderPromptArea();
853
+ this.terminalInput.render();
1019
854
  return;
1020
855
  }
1021
856
  const trimmed = input.trim();
1022
857
  if (!trimmed) {
1023
858
  display.showWarning('Enter a number or type "cancel".');
1024
- this.renderPromptArea();
859
+ this.terminalInput.render();
1025
860
  return;
1026
861
  }
1027
862
  if (trimmed.toLowerCase() === 'cancel') {
1028
863
  this.pendingInteraction = null;
1029
864
  display.showInfo('Agent selection cancelled.');
1030
- this.renderPromptArea();
865
+ this.terminalInput.render();
1031
866
  return;
1032
867
  }
1033
868
  const choice = Number.parseInt(trimmed, 10);
1034
869
  if (!Number.isFinite(choice)) {
1035
870
  display.showWarning('Please enter a valid number.');
1036
- this.renderPromptArea();
871
+ this.terminalInput.render();
1037
872
  return;
1038
873
  }
1039
874
  const option = pending.options[choice - 1];
1040
875
  if (!option) {
1041
876
  display.showWarning('That option is not available.');
1042
- this.renderPromptArea();
877
+ this.terminalInput.render();
1043
878
  return;
1044
879
  }
1045
880
  await this.persistAgentSelection(option.name);
1046
881
  this.pendingInteraction = null;
1047
- this.renderPromptArea();
882
+ this.terminalInput.render();
1048
883
  }
1049
884
  async persistAgentSelection(profileName) {
1050
885
  if (!this.agentMenu) {
@@ -1117,7 +952,7 @@ export class InteractiveShell {
1117
952
  lines.push(` ${theme.primary('[text]')} Submit your own solution instead`);
1118
953
  lines.push('');
1119
954
  display.showSystemMessage(lines.join('\n'));
1120
- this.renderPromptArea();
955
+ this.terminalInput.render();
1121
956
  }
1122
957
  async handlePlanApprovalInput(input) {
1123
958
  const pending = this.pendingInteraction;
@@ -1126,7 +961,7 @@ export class InteractiveShell {
1126
961
  const trimmed = input.trim();
1127
962
  if (!trimmed) {
1128
963
  display.showWarning('Enter a command or your own solution.');
1129
- this.renderPromptArea();
964
+ this.terminalInput.render();
1130
965
  return;
1131
966
  }
1132
967
  const lower = trimmed.toLowerCase();
@@ -1134,7 +969,7 @@ export class InteractiveShell {
1134
969
  if (lower === 'cancel' || lower === 'c') {
1135
970
  this.pendingInteraction = null;
1136
971
  display.showInfo('Plan cancelled. You can continue with a different approach.');
1137
- this.renderPromptArea();
972
+ this.terminalInput.render();
1138
973
  return;
1139
974
  }
1140
975
  // Select all
@@ -1154,7 +989,7 @@ export class InteractiveShell {
1154
989
  const selectedSteps = pending.steps.filter(s => pending.selectedSteps.has(s.id));
1155
990
  if (selectedSteps.length === 0) {
1156
991
  display.showWarning('No steps selected. Select steps or enter your own solution.');
1157
- this.renderPromptArea();
992
+ this.terminalInput.render();
1158
993
  return;
1159
994
  }
1160
995
  this.pendingInteraction = null;
@@ -1187,18 +1022,15 @@ export class InteractiveShell {
1187
1022
  return;
1188
1023
  }
1189
1024
  display.showWarning('Invalid input. Enter a step number, command (go/cancel/all/none), or your own solution.');
1190
- this.renderPromptArea();
1025
+ this.terminalInput.render();
1191
1026
  }
1192
1027
  setupHandlers() {
1193
1028
  // Handle terminal resize
1194
1029
  output.on('resize', () => {
1195
- if (!this.inputInitialized) {
1196
- return;
1197
- }
1198
- this.terminalInput.resetContentPosition();
1199
1030
  this.terminalInput.handleResize();
1200
- this.terminalInput.forceRender();
1201
1031
  });
1032
+ // Show initial input UI
1033
+ this.terminalInput.render();
1202
1034
  }
1203
1035
  /**
1204
1036
  * Set up command autocomplete with all available slash commands.
@@ -1229,14 +1061,6 @@ export class InteractiveShell {
1229
1061
  catch {
1230
1062
  // Custom commands are optional
1231
1063
  }
1232
- // Add manual commands that are not yet in the schema
1233
- if (!commands.some((cmd) => cmd.command === '/alphazero')) {
1234
- commands.push({
1235
- command: '/alphazero',
1236
- description: 'Toggle AlphaZero RL mode',
1237
- category: 'mode',
1238
- });
1239
- }
1240
1064
  // Sort commands alphabetically
1241
1065
  commands.sort((a, b) => a.command.localeCompare(b.command));
1242
1066
  this.terminalInput.setAvailableCommands(commands);
@@ -1315,312 +1139,25 @@ export class InteractiveShell {
1315
1139
  autoContinueEnabled: this.autoContinueEnabled,
1316
1140
  verificationHotkey: 'ctrl+shift+v',
1317
1141
  autoContinueHotkey: 'ctrl+shift+c',
1318
- thinkingModeLabel: this.thinkingMode,
1319
- thinkingHotkey: 'ctrl+shift+t',
1320
- alphaZeroEnabled: this.alphaZeroModeEnabled,
1321
- alphaZeroHotkey: 'ctrl+shift+a',
1322
- alphaZeroLabel: 'AlphaZero RL',
1142
+ thinkingModeLabel: this.thinkingMode === 'extended' ? 'on' : 'off',
1143
+ thinkingHotkey: 'tab',
1323
1144
  });
1324
- this.refreshFeatureStatusDisplay();
1325
1145
  this.refreshStatusLine();
1326
- this.renderPromptArea();
1327
- }
1328
- refreshFeatureStatusDisplay() {
1329
- this.terminalInput.setFeatureStatus(this.buildFeatureStatusSnapshot());
1146
+ this.terminalInput.render();
1330
1147
  }
1331
1148
  writeLocked(content) {
1332
1149
  if (!content) {
1333
1150
  return;
1334
1151
  }
1335
- // Route through display stream so scroll regions and streaming locks stay in sync
1336
- display.stream(content);
1337
- }
1338
- resetAssistantStreamTracking() {
1339
- this.assistantStreamActive = false;
1340
- this.assistantStreamPhase = null;
1341
- this.assistantStreamHeaderShown = false;
1342
- this.assistantStreamBuffer = '';
1343
- this.assistantStreamMetadata = undefined;
1344
- this.assistantStreamHadContent = false;
1345
- }
1346
- startAssistantStream(metadata) {
1347
- if (this.assistantStreamActive) {
1348
- this.assistantStreamMetadata = metadata ?? this.assistantStreamMetadata;
1152
+ // If lock is already held, write directly - we're in a protected context
1153
+ // This prevents queuing issues where content gets delayed
1154
+ if (writeLock.isLocked()) {
1155
+ process.stdout.write(content);
1349
1156
  return;
1350
1157
  }
1351
- this.assistantStreamActive = true;
1352
- // Default to thought phase so the first output after a prompt is the assistant's thinking.
1353
- this.assistantStreamPhase = 'thought';
1354
- this.assistantStreamHeaderShown = false;
1355
- this.assistantStreamBuffer = '';
1356
- this.assistantStreamMetadata = metadata;
1357
- this.assistantStreamHadContent = false;
1358
- }
1359
- streamAssistantChunk(chunk, channel) {
1360
- if (!chunk) {
1361
- return;
1362
- }
1363
- if (!this.assistantStreamActive) {
1364
- this.startAssistantStream({
1365
- contextWindowTokens: this.activeContextWindowTokens,
1366
- });
1367
- }
1368
- // Agent wraps reasoning chunks in <thinking> tags via openThinking()/closeThinking()
1369
- // Always use tag parsing to properly handle the tags
1370
- this.processAssistantStreamChunk(chunk);
1371
- }
1372
- finalizeAssistantStream() {
1373
- if (!this.assistantStreamActive) {
1374
- return;
1375
- }
1376
- if (this.assistantStreamBuffer) {
1377
- this.writeAssistantStream(this.assistantStreamBuffer);
1378
- this.assistantStreamBuffer = '';
1379
- }
1380
- this.assistantStreamActive = false;
1381
- this.assistantStreamPhase = null;
1382
- this.assistantStreamHeaderShown = false;
1383
- this.assistantStreamMetadata = undefined;
1384
- }
1385
- processAssistantStreamChunk(chunk) {
1386
- this.assistantStreamBuffer += chunk;
1387
- const tagRegex = /<(\/?)(thinking|response)>/i;
1388
- while (this.assistantStreamBuffer.length > 0) {
1389
- const match = tagRegex.exec(this.assistantStreamBuffer);
1390
- if (!match || match.index === undefined) {
1391
- // No more full tags - flush what we can but keep any partial tag prefix buffered
1392
- const { flushable, remainder } = this.splitAssistantStreamRemainder(this.assistantStreamBuffer);
1393
- if (flushable) {
1394
- this.writeAssistantStream(flushable);
1395
- }
1396
- this.assistantStreamBuffer = remainder;
1397
- return;
1398
- }
1399
- const tagIndex = match.index;
1400
- const beforeTag = this.assistantStreamBuffer.slice(0, tagIndex);
1401
- if (beforeTag) {
1402
- this.writeAssistantStream(beforeTag);
1403
- }
1404
- // Advance buffer past the tag
1405
- this.assistantStreamBuffer = this.assistantStreamBuffer.slice(tagIndex + match[0].length);
1406
- const isClosing = match[1] === '/';
1407
- const rawTagType = (match[2] ?? '').toLowerCase();
1408
- // Map 'thinking' to 'thought' for AssistantBlockType compatibility
1409
- const tagType = (rawTagType === 'thinking' ? 'thought' : rawTagType);
1410
- if (isClosing) {
1411
- if (this.assistantStreamPhase === tagType) {
1412
- this.assistantStreamPhase = null;
1413
- this.assistantStreamHeaderShown = false;
1414
- }
1415
- }
1416
- else {
1417
- this.setAssistantStreamPhase(tagType);
1418
- }
1419
- }
1420
- }
1421
- writeAssistantStream(content) {
1422
- if (!content) {
1423
- return;
1424
- }
1425
- if (content.trim().length > 0) {
1426
- this.assistantStreamHadContent = true;
1427
- }
1428
- // If no explicit phase has been set yet, keep the stream labeled as "thought"
1429
- // until a <response> tag arrives so operators see thinking first.
1430
- this.setAssistantStreamPhase(this.assistantStreamPhase ?? 'thought');
1431
- if (this.assistantStreamPhase) {
1432
- this.renderAssistantStreamHeader(this.assistantStreamPhase);
1433
- }
1434
- let output = content;
1435
- if (this.assistantStreamPhase === 'thought') {
1436
- output = this.formatThoughtDisplay(content, { streaming: true });
1437
- }
1438
- this.enqueueAssistantStream(output);
1439
- }
1440
- setAssistantStreamPhase(phase) {
1441
- if (this.assistantStreamPhase === phase) {
1442
- return;
1443
- }
1444
- this.assistantStreamPhase = phase;
1445
- this.assistantStreamHeaderShown = false;
1446
- if (phase) {
1447
- this.renderAssistantStreamHeader(phase);
1448
- }
1449
- }
1450
- renderAssistantStreamHeader(type) {
1451
- if (this.assistantStreamHeaderShown) {
1452
- return;
1453
- }
1454
- const header = this.formatAssistantHeader(type, this.assistantStreamMetadata);
1455
- if (header.trim()) {
1456
- this.enqueueAssistantStream(`\n${header}\n`);
1457
- }
1458
- this.assistantStreamHeaderShown = true;
1459
- }
1460
- splitAssistantStreamRemainder(buffer) {
1461
- if (!buffer) {
1462
- return { flushable: '', remainder: '' };
1463
- }
1464
- const tags = ['<thinking>', '</thinking>', '<response>', '</response>'];
1465
- let longest = '';
1466
- for (const tag of tags) {
1467
- for (let i = 1; i < tag.length; i += 1) {
1468
- const prefix = tag.slice(0, i);
1469
- if (buffer.endsWith(prefix) && prefix.length > longest.length) {
1470
- longest = prefix;
1471
- }
1472
- }
1473
- }
1474
- if (!longest) {
1475
- return { flushable: buffer, remainder: '' };
1476
- }
1477
- return {
1478
- flushable: buffer.slice(0, -longest.length),
1479
- remainder: longest,
1480
- };
1481
- }
1482
- enqueueAssistantStream(content) {
1483
- if (!content) {
1484
- return;
1485
- }
1486
- const run = () => this.writeAssistantBlock(content);
1487
- this.uiUpdates.enqueue({
1488
- lane: 'stream',
1489
- mode: ['streaming', 'processing', 'idle'],
1490
- priority: 'high',
1491
- description: 'assistant stream flush',
1492
- run,
1493
- });
1494
- }
1495
- writeAssistantBlock(content) {
1496
- if (!content) {
1497
- return;
1498
- }
1499
- // Ensure assistant block writes are serialized with terminal input renders
1500
- writeLock.safeWrite(() => {
1501
- this.terminalInput.streamContent(content);
1502
- });
1503
- }
1504
- renderAssistantBlock(type, content, metadata) {
1505
- if (!this.assistantBlocksEnabled || !this.assistantBlockRenderer) {
1506
- return;
1507
- }
1508
- this.assistantBlockRenderer.renderBlock(type, content, metadata);
1509
- // Ensure the prompt stays pinned below freshly written blocks
1510
- this.renderPromptArea();
1511
- }
1512
- renderAssistantContent(type, content, metadata) {
1513
- const normalized = content.replace(/\r\n/g, '\n').trim();
1514
- if (!normalized) {
1515
- return;
1516
- }
1517
- const formatted = type === 'thought' ? this.formatThoughtDisplay(normalized) : normalized;
1518
- if (this.assistantBlocksEnabled && this.assistantBlockRenderer) {
1519
- this.renderAssistantBlock(type, formatted, metadata);
1520
- return;
1521
- }
1522
- this.renderAssistantFallback(type, formatted, metadata);
1523
- }
1524
- renderAssistantFallback(type, content, metadata) {
1525
- const header = this.formatAssistantHeader(type, metadata);
1526
- const compact = content.trimEnd().replace(/\n{3,}/g, '\n\n');
1527
- const block = `\n${header}\n${compact}\n\n`;
1528
- this.enqueueAssistantStream(block);
1529
- }
1530
- formatThoughtDisplay(content, options = {}) {
1531
- const normalized = content.replace(/\r\n/g, '\n');
1532
- if (options.streaming) {
1533
- return formatThinkingContent(normalized);
1534
- }
1535
- const lines = normalized.split('\n');
1536
- return lines
1537
- .map((line, index) => {
1538
- const prefix = index === 0 ? theme.info(icons.action) : theme.ui.muted(icons.subaction);
1539
- const trimmed = line.trim();
1540
- const body = trimmed ? formatThinkingContent(trimmed) : '';
1541
- return body ? `${prefix} ${body}` : `${prefix}`;
1542
- })
1543
- .join('\n');
1544
- }
1545
- formatAssistantHeader(type, metadata) {
1546
- if (this.assistantBlocksEnabled && this.assistantBlockRenderer) {
1547
- return this.assistantBlockRenderer.formatHeader(type, metadata);
1548
- }
1549
- const badges = [];
1550
- badges.push(this.buildAssistantTypeBadge(type));
1551
- const timestamp = new Date().toLocaleTimeString('en-US', {
1552
- hour12: false,
1553
- hour: '2-digit',
1554
- minute: '2-digit',
1555
- second: '2-digit',
1556
- });
1557
- badges.push({ text: timestamp, style: 'muted', icon: '๐Ÿ•‘' });
1558
- const total = metadata?.usage ? this.totalTokens(metadata.usage) : null;
1559
- const windowTokens = metadata?.contextWindowTokens;
1560
- if (typeof total === 'number') {
1561
- badges.push({ text: `${total} tok`, style: 'muted', icon: 'โŽ' });
1562
- }
1563
- if (typeof total === 'number' && typeof windowTokens === 'number' && windowTokens > 0) {
1564
- const percentage = Math.round((total / windowTokens) * 100);
1565
- const style = percentage > 85 ? 'error' : percentage > 70 ? 'warning' : 'info';
1566
- badges.push({ text: `${percentage}% ctx`, style, icon: 'โŠ›' });
1567
- }
1568
- const elapsedBadge = this.buildElapsedBadge(metadata);
1569
- if (elapsedBadge) {
1570
- badges.push(elapsedBadge);
1571
- }
1572
- return compactRenderer.formatBadges(badges, theme.ui.muted(' โ”‚ '));
1573
- }
1574
- buildAssistantTypeBadge(type) {
1575
- switch (type) {
1576
- case 'thought':
1577
- return { text: 'Thought', style: 'info', icon: '๐Ÿ’ญ' };
1578
- case 'tools':
1579
- return { text: 'Tools', style: 'primary', icon: '๐Ÿ› ' };
1580
- case 'response':
1581
- default:
1582
- return { text: 'Response', style: 'success', icon: '๐Ÿ’ฌ' };
1583
- }
1584
- }
1585
- buildElapsedBadge(metadata) {
1586
- const elapsed = metadata?.elapsedMs;
1587
- if (typeof elapsed !== 'number' || elapsed <= 0) {
1588
- return null;
1589
- }
1590
- const formatted = elapsed < 1000 ? `${Math.round(elapsed)}ms` : `${Math.round(elapsed / 1000)}s`;
1591
- return { text: formatted, style: 'muted', icon: 'โฑ' };
1592
- }
1593
- isStreamingUiActive() {
1594
- return this.streamingHeartbeatStart !== null;
1595
- }
1596
- /**
1597
- * Render the prompt/control bar. During streaming, rely on the streaming frame
1598
- * renderer and enqueue through the UIUpdateCoordinator to avoid fighting the
1599
- * scroll region or duplicating the prompt.
1600
- */
1601
- renderPromptArea(force = false) {
1602
- if (this.isStreamingUiActive()) {
1603
- this.uiUpdates.enqueue({
1604
- lane: 'prompt',
1605
- mode: ['streaming', 'processing'],
1606
- coalesceKey: 'prompt:streaming-frame',
1607
- description: 'render streaming prompt frame',
1608
- run: () => {
1609
- if (force) {
1610
- this.terminalInput.renderStreamingFrame(true);
1611
- return;
1612
- }
1613
- this.terminalInput.renderStreamingFrame();
1614
- },
1615
- });
1616
- return;
1617
- }
1618
- if (force) {
1619
- this.terminalInput.forceRender();
1620
- }
1621
- else {
1622
- this.terminalInput.render();
1623
- }
1158
+ writeLock.withLock(() => {
1159
+ process.stdout.write(content);
1160
+ }, 'interactiveShell.stdout');
1624
1161
  }
1625
1162
  /**
1626
1163
  * Refresh the status line in the persistent input area.
@@ -1638,23 +1175,19 @@ export class InteractiveShell {
1638
1175
  // Surface meta header (elapsed + context usage) above the divider
1639
1176
  // Use streaming elapsed time if available, otherwise fall back to status line state
1640
1177
  let elapsedSeconds = null;
1641
- const shouldShowElapsed = this.streamingHeartbeatStart !== null || this.isProcessing;
1642
- if (this.streamingHeartbeatStart && shouldShowElapsed) {
1178
+ if (this.streamingHeartbeatStart) {
1643
1179
  // Actively streaming - compute live elapsed
1644
1180
  elapsedSeconds = Math.max(0, Math.floor((Date.now() - this.streamingHeartbeatStart) / 1000));
1645
1181
  }
1646
- else if (shouldShowElapsed && this.lastStreamingElapsedSeconds !== null) {
1182
+ else if (this.lastStreamingElapsedSeconds !== null) {
1647
1183
  // Just finished streaming - use preserved final time
1648
1184
  elapsedSeconds = this.lastStreamingElapsedSeconds;
1649
1185
  }
1650
- else if (shouldShowElapsed && this.statusLineState) {
1186
+ else if (this.statusLineState) {
1651
1187
  // Fallback to status line state elapsed
1652
1188
  elapsedSeconds = Math.max(0, Math.floor((Date.now() - this.statusLineState.startedAt) / 1000));
1653
1189
  }
1654
- const hasThoughtSummary = !!this.latestThoughtSummary;
1655
- const thinkingMs = hasThoughtSummary && display.isSpinnerActive()
1656
- ? display.getThinkingElapsedMs()
1657
- : null;
1190
+ const thinkingMs = display.isSpinnerActive() ? display.getThinkingElapsedMs() : null;
1658
1191
  const tokensUsed = this.latestTokenUsage.used;
1659
1192
  const tokenLimit = this.latestTokenUsage.limit ?? this.activeContextWindowTokens;
1660
1193
  this.terminalInput.setMetaStatus({
@@ -1662,7 +1195,7 @@ export class InteractiveShell {
1662
1195
  tokensUsed,
1663
1196
  tokenLimit,
1664
1197
  thinkingMs,
1665
- thinkingHasContent: hasThoughtSummary,
1198
+ thinkingHasContent: display.isSpinnerActive(),
1666
1199
  });
1667
1200
  // Keep model/provider visible in the controls bar
1668
1201
  this.terminalInput.setModelContext({
@@ -1670,7 +1203,7 @@ export class InteractiveShell {
1670
1203
  provider: this.providerLabel(this.sessionState.provider),
1671
1204
  });
1672
1205
  if (forceRender) {
1673
- this.renderPromptArea(true);
1206
+ this.terminalInput.render();
1674
1207
  }
1675
1208
  }
1676
1209
  /**
@@ -1707,7 +1240,7 @@ export class InteractiveShell {
1707
1240
  * Ensure the terminal input is ready for interactive input.
1708
1241
  */
1709
1242
  ensureReadlineReady() {
1710
- this.renderPromptArea();
1243
+ this.terminalInput.render();
1711
1244
  }
1712
1245
  /**
1713
1246
  * Log user prompt to the scroll region so it's part of the conversation flow.
@@ -1729,7 +1262,7 @@ export class InteractiveShell {
1729
1262
  }
1730
1263
  requestPromptRefresh(force = false) {
1731
1264
  if (force) {
1732
- this.renderPromptArea(true);
1265
+ this.terminalInput.forceRender();
1733
1266
  return;
1734
1267
  }
1735
1268
  if (this.promptRefreshTimer) {
@@ -1737,7 +1270,7 @@ export class InteractiveShell {
1737
1270
  }
1738
1271
  this.promptRefreshTimer = setTimeout(() => {
1739
1272
  this.promptRefreshTimer = null;
1740
- this.renderPromptArea();
1273
+ this.terminalInput.render();
1741
1274
  }, 48);
1742
1275
  }
1743
1276
  clearPromptRefreshTimer() {
@@ -1746,53 +1279,35 @@ export class InteractiveShell {
1746
1279
  this.promptRefreshTimer = null;
1747
1280
  }
1748
1281
  }
1749
- async withStreamingUi(label, run) {
1750
- if (this.isStreamingUiActive()) {
1751
- return run();
1752
- }
1753
- this.resetAssistantStreamTracking();
1754
- this.terminalInput.setStreaming(true);
1755
- this.startStreamingHeartbeat(label);
1756
- try {
1757
- return await run();
1758
- }
1759
- finally {
1760
- this.stopStreamingHeartbeat();
1761
- this.terminalInput.setStreaming(false);
1762
- const nextMode = this.isProcessing ? 'processing' : 'idle';
1763
- this.uiUpdates.setMode(nextMode);
1764
- if (nextMode === 'processing') {
1765
- queueMicrotask(() => this.uiUpdates.setMode('idle'));
1766
- }
1767
- }
1768
- }
1769
1282
  startStreamingHeartbeat(label = 'Streaming') {
1770
- this.stopStreamingHeartbeat({ skipRender: true });
1283
+ this.stopStreamingHeartbeat();
1771
1284
  // Enter global streaming mode - blocks all non-streaming UI output
1772
1285
  enterStreamingMode();
1773
- this.streamingStatusBase = label;
1774
- this.streamingStatusDetail = null;
1775
- this.latestThoughtSummary = null;
1776
- this.lastStreamingElapsedSeconds = null;
1777
1286
  // Set up scroll region for streaming content
1778
1287
  this.terminalInput.enterStreamingScrollRegion();
1779
1288
  this.uiUpdates.setMode('streaming');
1780
1289
  this.streamingHeartbeatStart = Date.now();
1781
1290
  this.streamingHeartbeatFrame = 0;
1782
- this.rebuildStreamingStatusLabel();
1291
+ const initialFrame = STREAMING_SPINNER_FRAMES[this.streamingHeartbeatFrame];
1292
+ this.streamingStatusLabel = this.buildStreamingStatus(`${initialFrame} ${label}`, 0);
1293
+ display.updateStreamingStatus(this.streamingStatusLabel);
1783
1294
  this.refreshStatusLine(true);
1784
1295
  // Periodically refresh the pinned input/status region while streaming so
1785
1296
  // elapsed time remains visible without interrupting the scroll region.
1786
1297
  this.uiUpdates.startHeartbeat('streaming', {
1787
1298
  intervalMs: 1000,
1788
1299
  lane: 'heartbeat',
1789
- priority: 'high',
1790
1300
  mode: ['streaming', 'processing'],
1791
1301
  coalesceKey: 'streaming:heartbeat',
1792
1302
  run: () => {
1303
+ const elapsedSeconds = this.streamingHeartbeatStart
1304
+ ? Math.max(0, Math.floor((Date.now() - this.streamingHeartbeatStart) / 1000))
1305
+ : 0;
1793
1306
  this.streamingHeartbeatFrame =
1794
1307
  (this.streamingHeartbeatFrame + 1) % STREAMING_SPINNER_FRAMES.length;
1795
- this.rebuildStreamingStatusLabel();
1308
+ const frame = STREAMING_SPINNER_FRAMES[this.streamingHeartbeatFrame];
1309
+ this.streamingStatusLabel = this.buildStreamingStatus(`${frame} ${label}`, elapsedSeconds);
1310
+ display.updateStreamingStatus(this.streamingStatusLabel);
1796
1311
  // Update parallel agent display during streaming
1797
1312
  const manager = getParallelAgentManager();
1798
1313
  if (manager.isRunning()) {
@@ -1803,13 +1318,7 @@ export class InteractiveShell {
1803
1318
  },
1804
1319
  });
1805
1320
  }
1806
- stopStreamingHeartbeat(options = {}) {
1807
- const skipRender = !!options.skipRender;
1808
- const streamingActive = this.isStreamingUiActive();
1809
- const scrollRegionActive = this.terminalInput.isScrollRegionActive();
1810
- if (!streamingActive && !scrollRegionActive) {
1811
- return;
1812
- }
1321
+ stopStreamingHeartbeat() {
1813
1322
  // Exit global streaming mode - allows UI to render again
1814
1323
  exitStreamingMode();
1815
1324
  // Preserve final elapsed time before clearing heartbeat start
@@ -1817,44 +1326,51 @@ export class InteractiveShell {
1817
1326
  this.lastStreamingElapsedSeconds = Math.max(0, Math.floor((Date.now() - this.streamingHeartbeatStart) / 1000));
1818
1327
  }
1819
1328
  // Exit scroll region mode
1820
- this.terminalInput.exitStreamingScrollRegion({ skipRender });
1329
+ this.terminalInput.exitStreamingScrollRegion();
1821
1330
  this.uiUpdates.stopHeartbeat('streaming');
1822
1331
  this.streamingHeartbeatStart = null;
1823
1332
  this.streamingHeartbeatFrame = 0;
1824
1333
  this.streamingStatusLabel = null;
1825
- this.streamingStatusBase = null;
1826
- this.streamingStatusDetail = null;
1827
- this.latestThoughtSummary = null;
1828
1334
  // Clear streaming label specifically (keeps override and main status if set)
1829
1335
  this.terminalInput.setStreamingLabel(null);
1830
1336
  // Clear streaming status from display
1831
1337
  display.updateStreamingStatus(null);
1832
1338
  // Force refresh to update the input area now that streaming has ended
1833
- if (!skipRender) {
1834
- this.refreshStatusLine(true);
1835
- }
1339
+ this.refreshStatusLine(true);
1836
1340
  }
1837
- buildStreamingStatus(label, _elapsedSeconds) {
1838
- // Model + elapsed time already live in the pinned meta header; keep the streaming
1839
- // status focused on the activity and most recent thought summary.
1840
- const prefix = theme.info('โบ');
1841
- const parts = [label.trim()];
1842
- if (this.streamingStatusDetail) {
1843
- const detail = this.streamingStatusDetail.length > 52
1844
- ? `${this.streamingStatusDetail.slice(0, 51)}โ€ฆ`
1845
- : this.streamingStatusDetail;
1846
- parts.push(theme.ui.muted(detail));
1341
+ handleStreamChunk(chunk) {
1342
+ if (!chunk) {
1343
+ return;
1344
+ }
1345
+ // Preserve raw output in plain/CI modes or non-TTY environments
1346
+ if (isPlainOutputMode() || !output.isTTY) {
1347
+ this.terminalInput.streamContent(chunk);
1348
+ return;
1349
+ }
1350
+ if (!this.streamingFormatter) {
1351
+ this.streamingFormatter = new StreamingResponseFormatter(output.columns ?? undefined);
1352
+ this.terminalInput.streamContent(this.streamingFormatter.header());
1353
+ }
1354
+ const formatted = this.streamingFormatter.formatChunk(chunk);
1355
+ if (formatted) {
1356
+ this.terminalInput.streamContent(formatted);
1847
1357
  }
1848
- return `${prefix} ${parts.join(' ยท ')}`.trim();
1849
1358
  }
1850
- rebuildStreamingStatusLabel() {
1851
- if (this.streamingHeartbeatStart === null) {
1359
+ finishStreamingFormatter(note) {
1360
+ if (!this.streamingFormatter) {
1852
1361
  return;
1853
1362
  }
1854
- const frame = STREAMING_SPINNER_FRAMES[this.streamingHeartbeatFrame];
1855
- const base = this.streamingStatusBase ?? 'Streaming';
1856
- this.streamingStatusLabel = this.buildStreamingStatus(`${frame} ${base}`);
1857
- display.updateStreamingStatus(this.streamingStatusLabel);
1363
+ const closing = this.streamingFormatter.finish(note);
1364
+ if (closing) {
1365
+ this.terminalInput.streamContent(closing);
1366
+ }
1367
+ this.streamingFormatter = null;
1368
+ }
1369
+ buildStreamingStatus(label, _elapsedSeconds) {
1370
+ // Model + elapsed time already live in the pinned meta header; keep the streaming
1371
+ // status focused on the activity to avoid duplicate info.
1372
+ const prefix = theme.info('โบ');
1373
+ return `${prefix} ${label}`.trim();
1858
1374
  }
1859
1375
  formatElapsedShort(seconds) {
1860
1376
  if (seconds < 60) {
@@ -1871,7 +1387,7 @@ export class InteractiveShell {
1871
1387
  else {
1872
1388
  this.setIdleStatus();
1873
1389
  }
1874
- this.renderPromptArea();
1390
+ this.terminalInput.render();
1875
1391
  }
1876
1392
  enqueueFollowUpAction(action) {
1877
1393
  this.followUpQueue.push(action);
@@ -1890,21 +1406,14 @@ export class InteractiveShell {
1890
1406
  this.refreshQueueIndicators();
1891
1407
  this.scheduleQueueProcessing();
1892
1408
  // Re-show the prompt so user can continue typing more follow-ups
1893
- this.renderPromptArea();
1409
+ this.terminalInput.render();
1894
1410
  }
1895
1411
  scheduleQueueProcessing() {
1896
1412
  if (!this.followUpQueue.length) {
1897
1413
  this.refreshQueueIndicators();
1898
1414
  return;
1899
1415
  }
1900
- if (this.apiKeyGateActive) {
1901
- this.refreshQueueIndicators();
1902
- return;
1903
- }
1904
1416
  queueMicrotask(() => {
1905
- if (this.apiKeyGateActive) {
1906
- return;
1907
- }
1908
1417
  void this.processQueuedActions();
1909
1418
  });
1910
1419
  }
@@ -1912,12 +1421,12 @@ export class InteractiveShell {
1912
1421
  * Process queued follow-up actions.
1913
1422
  */
1914
1423
  async processQueuedActions() {
1915
- if (this.apiKeyGateActive || this.isDrainingQueue || this.isProcessing || !this.followUpQueue.length) {
1424
+ if (this.isDrainingQueue || this.isProcessing || !this.followUpQueue.length) {
1916
1425
  return;
1917
1426
  }
1918
1427
  this.isDrainingQueue = true;
1919
1428
  try {
1920
- while (!this.isProcessing && !this.apiKeyGateActive && this.followUpQueue.length) {
1429
+ while (!this.isProcessing && this.followUpQueue.length) {
1921
1430
  const next = this.followUpQueue.shift();
1922
1431
  const remaining = this.followUpQueue.length;
1923
1432
  const label = next.type === 'continuous' ? 'continuous command' : 'follow-up';
@@ -1952,12 +1461,12 @@ export class InteractiveShell {
1952
1461
  }
1953
1462
  if (lower === 'clear') {
1954
1463
  display.clear();
1955
- this.renderPromptArea();
1464
+ this.terminalInput.render();
1956
1465
  return;
1957
1466
  }
1958
1467
  if (lower === 'help') {
1959
1468
  this.showHelp();
1960
- this.renderPromptArea();
1469
+ this.terminalInput.render();
1961
1470
  return;
1962
1471
  }
1963
1472
  if (trimmed.startsWith('/')) {
@@ -1967,12 +1476,12 @@ export class InteractiveShell {
1967
1476
  // Check for continuous/infinite loop commands
1968
1477
  if (this.isContinuousCommand(trimmed)) {
1969
1478
  await this.processContinuousRequest(trimmed);
1970
- this.renderPromptArea();
1479
+ this.terminalInput.render();
1971
1480
  return;
1972
1481
  }
1973
1482
  // Direct execution for all inputs, including multi-line pastes
1974
1483
  await this.processRequest(trimmed);
1975
- this.renderPromptArea();
1484
+ this.terminalInput.render();
1976
1485
  }
1977
1486
  /**
1978
1487
  * Check if the command is a continuous/infinite loop command
@@ -2010,153 +1519,6 @@ export class InteractiveShell {
2010
1519
  ];
2011
1520
  return patterns.some(pattern => pattern.test(lower));
2012
1521
  }
2013
- isDifficultProblem(input) {
2014
- const normalized = input.toLowerCase();
2015
- const wordCount = normalized.split(/\s+/).filter(Boolean).length;
2016
- if (normalized.length > 600 || wordCount > 80) {
2017
- return true;
2018
- }
2019
- const signals = [
2020
- 'root cause',
2021
- 'postmortem',
2022
- 'crash',
2023
- 'incident',
2024
- 'outage',
2025
- 'optimiz',
2026
- 'performance',
2027
- 'throughput',
2028
- 'latency',
2029
- 'scalab',
2030
- 'architecture',
2031
- 'rewrite',
2032
- 'migration',
2033
- 'refactor',
2034
- 'reverse engineer',
2035
- 'security',
2036
- 'exploit',
2037
- 'injection',
2038
- 'vulnerability',
2039
- 'compliance',
2040
- 'multi-step',
2041
- 'complex',
2042
- 'difficult',
2043
- 'hard problem',
2044
- 'debug',
2045
- 'trace',
2046
- 'profil',
2047
- 'bottleneck',
2048
- ];
2049
- return signals.some((signal) => normalized.includes(signal));
2050
- }
2051
- buildAlphaZeroPrompt(request, flaggedDifficult) {
2052
- const playbook = [
2053
- 'AlphaZero RL MODE is ACTIVE. Operate as a self-play reinforcement loop.',
2054
- flaggedDifficult
2055
- ? 'Treat this as a difficult, high-risk task and over-verify the result.'
2056
- : 'Apply the reinforcement loop even if the task looks small.',
2057
- 'Follow this closed-loop playbook:',
2058
- '- Draft two competing solution strategies and merge the strongest ideas before executing.',
2059
- '- Execute with tools while logging decisions and evidence.',
2060
- '- Self-critique and repair until the quality is excellent (aim โ‰ฅ90/100).',
2061
- '- Run full-lifecycle verification like a human reviewer: build/tests, manual sanity checks, edge cases, performance/safety/security probes, docs/UX/readiness notes.',
2062
- '- Keep a verification ledger: each check with PASS/FAIL, evidence, and remaining risks. If anything fails, fix and re-verify before claiming completion.',
2063
- 'Finish with a concise sign-off that lists what was achieved and the proof of completion.',
2064
- ];
2065
- return `${playbook.join('\n')}\n\nPrimary user request:\n${request.trim()}`;
2066
- }
2067
- buildRunLogExcerpt(startIndex) {
2068
- const buffer = this.terminalInput.getScrollbackBuffer();
2069
- const sinceStart = buffer.slice(Math.max(0, startIndex));
2070
- const excerpt = sinceStart.slice(-200); // Cap prompt size
2071
- return excerpt.join('\n').trim();
2072
- }
2073
- buildRunLogEntry(request, response, scrollbackStartIndex, meta) {
2074
- return {
2075
- id: ++this.runIdCounter,
2076
- request,
2077
- response,
2078
- timestamp: new Date().toISOString(),
2079
- alphaZero: meta.alphaZeroEngaged,
2080
- difficult: meta.alphaZeroDifficult,
2081
- failureType: meta.failureType,
2082
- outputExcerpt: this.buildRunLogExcerpt(scrollbackStartIndex),
2083
- };
2084
- }
2085
- buildAlphaZeroReflectionPrompt(runLog, options) {
2086
- const lines = [];
2087
- lines.push('AlphaZero Post-Run Self-Reflection (erosolar-cli)');
2088
- lines.push(`Timestamp: ${runLog.timestamp}`);
2089
- lines.push(`AlphaZero: ${runLog.alphaZero ? 'on' : 'off'}${runLog.difficult ? ' | difficult' : ''}${runLog.failureType ? ` | signal: ${runLog.failureType}` : ''}`);
2090
- lines.push('');
2091
- lines.push('User request:');
2092
- lines.push(runLog.request);
2093
- lines.push('');
2094
- lines.push('Previous run log excerpt:');
2095
- lines.push(runLog.outputExcerpt || '[empty]');
2096
- lines.push('');
2097
- lines.push('Instructions:');
2098
- lines.push('- Reflect on the log to spot erosolar-cli bugs, UX issues, or reliability gaps.');
2099
- lines.push('- Propose and apply targeted fixes in this repository only (no user workspace edits).');
2100
- lines.push('- Prefer small, test-backed changes; run any relevant checks you invoke.');
2101
- lines.push('- Keep notes concise and finish with applied changes plus follow-ups.');
2102
- if (options.autoChain) {
2103
- lines.push('- Continue iterating automatically while meaningful improvements remain.');
2104
- lines.push('- When no further improvements are possible, reply with NO_MORE_IMPROVEMENTS on its own line.');
2105
- }
2106
- return lines.join('\n');
2107
- }
2108
- maybeQueueAlphaZeroSelfReflection(runLog, alphaZeroEngaged, allowAutoChain) {
2109
- if (!alphaZeroEngaged) {
2110
- return;
2111
- }
2112
- if (!isErosolarRepo(this.workingDir)) {
2113
- return;
2114
- }
2115
- if (!runLog.outputExcerpt) {
2116
- return;
2117
- }
2118
- if (this.lastReflectedRunId === runLog.id) {
2119
- return;
2120
- }
2121
- if (allowAutoChain) {
2122
- if (!this.autoContinueEnabled) {
2123
- return;
2124
- }
2125
- if (this.alphaZeroAutoImproveIterations >= this.alphaZeroAutoImproveMaxIterations) {
2126
- display.showInfo('AlphaZero auto-improvement limit reached; stopping.');
2127
- this.alphaZeroAutoImproveActive = false;
2128
- this.alphaZeroAutoImproveIterations = 0;
2129
- return;
2130
- }
2131
- if (!this.alphaZeroAutoImproveActive) {
2132
- this.alphaZeroAutoImproveIterations = 0;
2133
- }
2134
- this.alphaZeroAutoImproveActive = true;
2135
- this.alphaZeroAutoImproveIterations++;
2136
- }
2137
- const prompt = this.buildAlphaZeroReflectionPrompt(runLog, { autoChain: allowAutoChain });
2138
- this.lastReflectedRunId = runLog.id;
2139
- this.skipNextAutoReflection = !allowAutoChain; // Prevent reflection-on-reflection unless auto-chaining
2140
- this.enqueueFollowUpAction({ type: 'request', text: prompt });
2141
- display.showInfo(allowAutoChain
2142
- ? 'Auto AlphaZero self-improvement queued (auto-continue enabled).'
2143
- : 'Queued AlphaZero self-reflection to improve erosolar-cli from the latest run log.');
2144
- }
2145
- shouldStopAlphaZeroAutoImprove(responseText, allowAutoChain) {
2146
- if (!this.alphaZeroAutoImproveActive) {
2147
- return false;
2148
- }
2149
- if (!allowAutoChain) {
2150
- return true;
2151
- }
2152
- if (!responseText) {
2153
- return false;
2154
- }
2155
- const normalized = responseText.toLowerCase();
2156
- return (normalized.includes('no_more_improvements') ||
2157
- normalized.includes('no more improvements') ||
2158
- normalized.includes('stop_auto_improve'));
2159
- }
2160
1522
  async handlePendingInteraction(input) {
2161
1523
  if (!this.pendingInteraction) {
2162
1524
  return false;
@@ -2164,7 +1526,7 @@ export class InteractiveShell {
2164
1526
  switch (this.pendingInteraction.type) {
2165
1527
  case 'model-loading':
2166
1528
  display.showInfo('Still fetching model options. Please wait a moment.');
2167
- this.renderPromptArea();
1529
+ this.terminalInput.render();
2168
1530
  return true;
2169
1531
  case 'model-provider':
2170
1532
  await this.handleModelProviderSelection(input);
@@ -2195,213 +1557,166 @@ export class InteractiveShell {
2195
1557
  const [command] = input.split(/\s+/);
2196
1558
  if (!command) {
2197
1559
  display.showWarning('Enter a slash command.');
2198
- this.renderPromptArea();
1560
+ this.terminalInput.render();
2199
1561
  return;
2200
1562
  }
2201
- // Keep the slash action visible in the pinned recent strip
2202
- this.terminalInput.recordRecentAction(command);
2203
- const runCommand = async () => {
2204
- switch (command) {
2205
- case '/help':
2206
- case '/?':
2207
- this.showHelp();
2208
- break;
2209
- case '/features':
2210
- this.showFeaturesMenu(input);
2211
- break;
2212
- case '/learn':
2213
- this.showLearningStatus(input);
2214
- break;
2215
- case '/improve':
2216
- void this.handleImprovementCommand(input);
2217
- break;
2218
- case '/model':
2219
- this.showModelMenu();
2220
- break;
2221
- case '/exit':
2222
- case '/quit':
2223
- case '/q':
2224
- this.shutdown();
2225
- break;
2226
- case '/secrets':
2227
- this.showSecretsMenu();
2228
- break;
2229
- case '/tools':
2230
- this.showToolsMenu();
2231
- break;
2232
- case '/mcp':
2233
- await this.showMcpStatus();
2234
- break;
2235
- case '/doctor':
2236
- this.runDoctor();
2237
- break;
2238
- case '/checks':
2239
- await this.runRepoChecksCommand();
2240
- break;
2241
- case '/context':
2242
- await this.refreshWorkspaceContextCommand(input);
2243
- break;
2244
- case '/contextlog':
2245
- this.showContextSummaryLog(input);
2246
- break;
2247
- case '/agents':
2248
- this.showAgentsMenu();
2249
- break;
2250
- case '/sessions':
2251
- await this.handleSessionCommand(input);
2252
- break;
2253
- case '/skills':
2254
- await this.handleSkillsCommand(input);
2255
- break;
2256
- case '/thinking':
2257
- this.handleThinkingCommand(input);
2258
- break;
2259
- case '/autocontinue':
2260
- this.handleAutoContinueCommand(input);
2261
- break;
2262
- case '/alphazero':
2263
- this.handleAlphaZeroCommand(input);
2264
- break;
2265
- case '/shortcuts':
2266
- case '/keys':
2267
- this.handleShortcutsCommand();
2268
- break;
2269
- case '/changes':
2270
- case '/summary':
2271
- this.showFileChangeSummary();
2272
- break;
2273
- case '/metrics':
2274
- case '/stats':
2275
- case '/perf':
2276
- this.showAlphaZeroMetrics();
2277
- break;
2278
- case '/suggestions':
2279
- case '/improve':
2280
- this.showImprovementSuggestions();
2281
- break;
2282
- case '/plugins':
2283
- this.showPluginStatus();
2284
- break;
2285
- case '/evolve':
2286
- void this.handleEvolveCommand(input);
2287
- break;
2288
- case '/modular':
2289
- case '/a0':
2290
- void this.handleModularCommand(input);
2291
- break;
2292
- case '/offsec':
2293
- void this.handleOffsecCommand(input);
2294
- break;
2295
- case '/test':
2296
- case '/tests':
2297
- void this.handleTestCommand(input);
2298
- break;
2299
- case '/provider':
2300
- await this.handleProviderCommand(input);
2301
- break;
2302
- case '/providers':
2303
- this.showConfiguredProviders();
2304
- break;
2305
- case '/local':
2306
- await this.handleLocalCommand(input);
2307
- break;
2308
- case '/discover':
2309
- await this.discoverModelsCommand();
2310
- break;
2311
- // Claude Code style commands
2312
- case '/rewind':
2313
- await this.handleRewindCommand(input);
2314
- break;
2315
- case '/memory':
2316
- this.handleMemoryCommand(input);
2317
- break;
2318
- case '/vim':
2319
- this.handleVimCommand();
2320
- break;
2321
- case '/output-style':
2322
- this.handleOutputStyleCommand(input);
2323
- break;
2324
- case '/cost':
2325
- this.handleCostCommand();
2326
- break;
2327
- case '/usage':
2328
- this.handleUsageCommand();
2329
- break;
2330
- case '/update':
2331
- await this.handleUpdateCommand();
2332
- break;
2333
- case '/clear':
2334
- this.handleClearCommand();
2335
- break;
2336
- case '/resume':
2337
- await this.handleResumeCommand(input);
2338
- break;
2339
- case '/export':
2340
- this.handleExportCommand(input);
2341
- break;
2342
- case '/review':
2343
- await this.handleReviewCommand();
2344
- break;
2345
- case '/security-review':
2346
- await this.handleSecurityReviewCommand();
2347
- break;
2348
- case '/bug':
2349
- this.handleBugCommand();
2350
- break;
2351
- case '/terminal-setup':
2352
- this.handleTerminalSetupCommand();
2353
- break;
2354
- case '/permissions':
2355
- this.handlePermissionsCommand();
2356
- break;
2357
- case '/init':
2358
- this.handleInitCommand();
2359
- break;
2360
- case '/compact':
2361
- await this.handleCompactCommand();
2362
- break;
2363
- default:
2364
- if (!(await this.tryCustomSlashCommand(command, input))) {
2365
- display.showWarning(`Unknown command "${command}".`);
2366
- }
2367
- break;
2368
- }
2369
- };
2370
- const streamingUiActive = this.isStreamingUiActive();
2371
- const captureOptions = streamingUiActive
2372
- ? { includeStreaming: false, suppressTypes: ['normal'] }
2373
- : undefined;
2374
- let capturedOutput = '';
2375
- try {
2376
- const { output: outputBuffer } = await display.captureOutput(runCommand, captureOptions);
2377
- capturedOutput = outputBuffer;
2378
- }
2379
- catch (error) {
2380
- capturedOutput = error?.capturedOutput ?? capturedOutput;
2381
- if (streamingUiActive) {
2382
- const message = error instanceof Error ? error.message : String(error);
2383
- const { output: errorOutput } = await display.captureOutput(() => display.showError(message, error), { includeStreaming: false, suppressTypes: ['normal'] });
2384
- capturedOutput = capturedOutput || errorOutput || message;
2385
- }
2386
- else {
2387
- display.showError(error instanceof Error ? error.message : String(error), error);
2388
- }
2389
- }
2390
- const panelContent = this.buildInlineCommandPanel(command, capturedOutput);
2391
- this.terminalInput.setInlineCommandPanel(panelContent);
2392
- this.renderPromptArea();
2393
- }
2394
- buildInlineCommandPanel(command, output) {
2395
- const normalized = output ? output.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trimEnd() : '';
2396
- if (!normalized) {
2397
- return null;
2398
- }
2399
- const header = theme.ui.muted(command);
2400
- const body = normalized.split('\n');
2401
- if (body.length === 1) {
2402
- return [`${header} ${body[0] ?? ''}`.trimEnd()];
1563
+ switch (command) {
1564
+ case '/help':
1565
+ case '/?':
1566
+ this.showHelp();
1567
+ break;
1568
+ case '/features':
1569
+ this.showFeaturesMenu(input);
1570
+ break;
1571
+ case '/learn':
1572
+ this.showLearningStatus(input);
1573
+ break;
1574
+ case '/improve':
1575
+ void this.handleImprovementCommand(input);
1576
+ break;
1577
+ case '/model':
1578
+ this.showModelMenu();
1579
+ break;
1580
+ case '/exit':
1581
+ case '/quit':
1582
+ case '/q':
1583
+ this.shutdown();
1584
+ break;
1585
+ case '/secrets':
1586
+ this.showSecretsMenu();
1587
+ break;
1588
+ case '/tools':
1589
+ this.showToolsMenu();
1590
+ break;
1591
+ case '/mcp':
1592
+ await this.showMcpStatus();
1593
+ break;
1594
+ case '/doctor':
1595
+ this.runDoctor();
1596
+ break;
1597
+ case '/checks':
1598
+ await this.runRepoChecksCommand();
1599
+ break;
1600
+ case '/context':
1601
+ await this.refreshWorkspaceContextCommand(input);
1602
+ break;
1603
+ case '/agents':
1604
+ this.showAgentsMenu();
1605
+ break;
1606
+ case '/sessions':
1607
+ await this.handleSessionCommand(input);
1608
+ break;
1609
+ case '/skills':
1610
+ await this.handleSkillsCommand(input);
1611
+ break;
1612
+ case '/thinking':
1613
+ this.handleThinkingCommand(input);
1614
+ break;
1615
+ case '/autocontinue':
1616
+ this.handleAutoContinueCommand(input);
1617
+ break;
1618
+ case '/shortcuts':
1619
+ case '/keys':
1620
+ this.handleShortcutsCommand();
1621
+ break;
1622
+ case '/changes':
1623
+ case '/summary':
1624
+ this.showFileChangeSummary();
1625
+ break;
1626
+ case '/metrics':
1627
+ case '/stats':
1628
+ case '/perf':
1629
+ this.showAlphaZeroMetrics();
1630
+ break;
1631
+ case '/suggestions':
1632
+ case '/improve':
1633
+ this.showImprovementSuggestions();
1634
+ break;
1635
+ case '/plugins':
1636
+ this.showPluginStatus();
1637
+ break;
1638
+ case '/evolve':
1639
+ void this.handleEvolveCommand(input);
1640
+ break;
1641
+ case '/modular':
1642
+ case '/a0':
1643
+ void this.handleModularCommand(input);
1644
+ break;
1645
+ case '/offsec':
1646
+ void this.handleOffsecCommand(input);
1647
+ break;
1648
+ case '/test':
1649
+ case '/tests':
1650
+ void this.handleTestCommand(input);
1651
+ break;
1652
+ case '/provider':
1653
+ await this.handleProviderCommand(input);
1654
+ break;
1655
+ case '/providers':
1656
+ this.showConfiguredProviders();
1657
+ break;
1658
+ case '/local':
1659
+ await this.handleLocalCommand(input);
1660
+ break;
1661
+ case '/discover':
1662
+ await this.discoverModelsCommand();
1663
+ break;
1664
+ // Claude Code style commands
1665
+ case '/rewind':
1666
+ await this.handleRewindCommand(input);
1667
+ break;
1668
+ case '/memory':
1669
+ this.handleMemoryCommand(input);
1670
+ break;
1671
+ case '/vim':
1672
+ this.handleVimCommand();
1673
+ break;
1674
+ case '/output-style':
1675
+ this.handleOutputStyleCommand(input);
1676
+ break;
1677
+ case '/cost':
1678
+ this.handleCostCommand();
1679
+ break;
1680
+ case '/usage':
1681
+ this.handleUsageCommand();
1682
+ break;
1683
+ case '/clear':
1684
+ this.handleClearCommand();
1685
+ break;
1686
+ case '/resume':
1687
+ await this.handleResumeCommand(input);
1688
+ break;
1689
+ case '/export':
1690
+ this.handleExportCommand(input);
1691
+ break;
1692
+ case '/review':
1693
+ await this.handleReviewCommand();
1694
+ break;
1695
+ case '/security-review':
1696
+ await this.handleSecurityReviewCommand();
1697
+ break;
1698
+ case '/bug':
1699
+ this.handleBugCommand();
1700
+ break;
1701
+ case '/terminal-setup':
1702
+ this.handleTerminalSetupCommand();
1703
+ break;
1704
+ case '/permissions':
1705
+ this.handlePermissionsCommand();
1706
+ break;
1707
+ case '/init':
1708
+ this.handleInitCommand();
1709
+ break;
1710
+ case '/compact':
1711
+ await this.handleCompactCommand();
1712
+ break;
1713
+ default:
1714
+ if (!(await this.tryCustomSlashCommand(command, input))) {
1715
+ display.showWarning(`Unknown command "${command}".`);
1716
+ }
1717
+ break;
2403
1718
  }
2404
- return [header, ...body];
1719
+ this.terminalInput.render();
2405
1720
  }
2406
1721
  async tryCustomSlashCommand(command, fullInput) {
2407
1722
  const custom = this.customCommandMap.get(command);
@@ -2438,13 +1753,11 @@ export class InteractiveShell {
2438
1753
  ` ${theme.info('Option+V')} ${theme.ui.muted('Toggle verification')}`,
2439
1754
  ` ${theme.info('Option+C')} ${theme.ui.muted('Toggle auto-continue')}`,
2440
1755
  ` ${theme.info('Option+T')} ${theme.ui.muted('Cycle thinking mode')}`,
2441
- ` ${theme.info('Option+A')} ${theme.ui.muted('Toggle AlphaZero RL mode')}`,
2442
1756
  ` ${theme.info('Option+E')} ${theme.ui.muted('Toggle edit permission mode')}`,
2443
1757
  ` ${theme.info('Option+X')} ${theme.ui.muted('Clear/compact context')}`,
2444
1758
  '',
2445
1759
  theme.bold(' Navigation'),
2446
1760
  ` ${theme.info('PageUp/PageDown')} ${theme.ui.muted('Scroll through output')}`,
2447
- ` ${theme.info('Escape')} ${theme.ui.muted('Stop streaming')}`,
2448
1761
  ` ${theme.info('Ctrl+C')} ${theme.ui.muted('Clear input or interrupt')}`,
2449
1762
  ` ${theme.info('Up/Down')} ${theme.ui.muted('Navigate command history')}`,
2450
1763
  '',
@@ -2531,76 +1844,16 @@ export class InteractiveShell {
2531
1844
  }
2532
1845
  display.showInfo('Refreshing workspace snapshot...');
2533
1846
  const context = buildWorkspaceContext(this.workingDir, this.workspaceOptions);
2534
- const profileConfig = this.runtimeSession.refreshWorkspaceContext(context);
2535
- const tools = this.runtimeSession.toolRuntime.listProviderTools();
2536
- this.baseSystemPrompt = buildInteractiveSystemPrompt(profileConfig.systemPrompt, profileConfig.label, tools);
2537
- if (this.rebuildAgent()) {
2538
- display.showInfo(`Workspace snapshot refreshed (${this.describeWorkspaceOptions()}).`);
2539
- this.resetChatBoxAfterModelSwap();
2540
- }
2541
- else {
2542
- display.showWarning('Workspace snapshot refreshed, but the agent failed to rebuild. Run /doctor for details.');
2543
- }
2544
- }
2545
- showContextSummaryLog(input) {
2546
- const agent = this.agent;
2547
- if (!agent) {
2548
- display.showWarning('No active agent session. Start one to inspect context summaries.');
2549
- return;
2550
- }
2551
- const contextManager = agent.getContextManager();
2552
- if (!contextManager?.getSummaryLog) {
2553
- display.showWarning('Context summary logging is unavailable in this session.');
2554
- return;
2555
- }
2556
- const tokens = input.trim().split(/\s+/).slice(1);
2557
- let limit = 5;
2558
- for (const token of tokens) {
2559
- if (!token)
2560
- continue;
2561
- const normalized = token.toLowerCase();
2562
- if (/^\d+$/.test(token)) {
2563
- limit = Math.min(Math.max(parseInt(token, 10), 1), 20);
2564
- }
2565
- else if (normalized.startsWith('limit=')) {
2566
- const value = parseInt(normalized.split('=')[1] ?? '', 10);
2567
- if (!Number.isNaN(value)) {
2568
- limit = Math.min(Math.max(value, 1), 20);
2569
- }
2570
- }
2571
- }
2572
- const entries = contextManager.getSummaryLog(limit);
2573
- if (!entries.length) {
2574
- display.showInfo('No context summaries captured yet.');
2575
- return;
1847
+ const profileConfig = this.runtimeSession.refreshWorkspaceContext(context);
1848
+ const tools = this.runtimeSession.toolRuntime.listProviderTools();
1849
+ this.baseSystemPrompt = buildInteractiveSystemPrompt(profileConfig.systemPrompt, profileConfig.label, tools);
1850
+ if (this.rebuildAgent()) {
1851
+ display.showInfo(`Workspace snapshot refreshed (${this.describeWorkspaceOptions()}).`);
1852
+ this.resetChatBoxAfterModelSwap();
2576
1853
  }
2577
- const lines = [];
2578
- lines.push(`${theme.primary('Context summaries')} ${theme.ui.muted(`(latest ${entries.length})`)}`);
2579
- for (const entry of entries) {
2580
- const timestamp = new Date(entry.timestamp).toLocaleString();
2581
- const headerParts = [
2582
- theme.ui.muted(timestamp),
2583
- theme.info(entry.method),
2584
- `removed ${theme.error(String(entry.removed))}`,
2585
- `kept ${theme.success(String(entry.preserved))}`,
2586
- ];
2587
- if (entry.reason) {
2588
- headerParts.push(theme.ui.muted(entry.reason));
2589
- }
2590
- if (entry.model) {
2591
- headerParts.push(theme.ui.muted(`model=${entry.model}`));
2592
- }
2593
- lines.push(headerParts.join(' ยท '));
2594
- if (entry.summary) {
2595
- const trimmed = entry.summary.length > 600 ? `${entry.summary.slice(0, 600)}โ€ฆ` : entry.summary;
2596
- lines.push(trimmed);
2597
- }
2598
- else {
2599
- lines.push(theme.ui.muted('(no summary text โ€” simple prune)'));
2600
- }
2601
- lines.push(''); // spacer
1854
+ else {
1855
+ display.showWarning('Workspace snapshot refreshed, but the agent failed to rebuild. Run /doctor for details.');
2602
1856
  }
2603
- display.showSystemMessage(lines.join('\n'));
2604
1857
  }
2605
1858
  parseContextOverrideTokens(input) {
2606
1859
  const overrides = {};
@@ -2761,11 +2014,12 @@ export class InteractiveShell {
2761
2014
  handleThinkingCommand(input) {
2762
2015
  const value = input.slice('/thinking'.length).trim().toLowerCase();
2763
2016
  if (!value) {
2764
- display.showInfo(`Thinking mode is currently ${theme.info(this.thinkingMode)}. Usage: /thinking [concise|balanced|extended]`);
2017
+ display.showInfo(`Thinking mode is currently ${theme.info(this.thinkingMode)}. Usage: /thinking [balanced|extended].` +
2018
+ ' Press Tab any time to toggle.');
2765
2019
  return;
2766
2020
  }
2767
- if (value !== 'concise' && value !== 'balanced' && value !== 'extended') {
2768
- display.showWarning('Usage: /thinking [concise|balanced|extended]');
2021
+ if (value !== 'balanced' && value !== 'extended') {
2022
+ display.showWarning('Usage: /thinking [balanced|extended]');
2769
2023
  return;
2770
2024
  }
2771
2025
  if (this.isProcessing) {
@@ -2777,30 +2031,15 @@ export class InteractiveShell {
2777
2031
  if (this.rebuildAgent()) {
2778
2032
  this.resetChatBoxAfterModelSwap();
2779
2033
  }
2034
+ this.refreshControlBar();
2780
2035
  const descriptions = {
2781
- concise: 'Hides internal reasoning and responds directly.',
2782
- balanced: 'Shows short thoughts only when helpful.',
2783
- extended: 'Always emits a <thinking> block before the final response.',
2036
+ balanced: 'Balanced (default) reasoning with short thoughts only when helpful.',
2037
+ extended: 'Longer reasoning enabled; expect extra usage for deeper answers.',
2784
2038
  };
2785
- display.showInfo(`Thinking mode set to ${theme.info(value)} โ€“ ${descriptions[this.thinkingMode]}`);
2786
- }
2787
- handleAlphaZeroCommand(input) {
2788
- const value = input.slice('/alphazero'.length).trim().toLowerCase();
2789
- if (!value || value === 'status') {
2790
- const status = this.alphaZeroModeEnabled ? theme.success('on') : theme.ui.muted('off');
2791
- const verification = this.verificationEnabled ? 'verification locked on' : 'verification optional';
2792
- display.showInfo(`AlphaZero RL mode is ${status}. When enabled, difficult prompts use duel/self-critique and human-style verification (${verification}).`);
2793
- return;
2794
- }
2795
- if (['on', 'enable', 'enabled'].includes(value)) {
2796
- this.setAlphaZeroMode(true, 'command');
2797
- return;
2798
- }
2799
- if (['off', 'disable', 'disabled'].includes(value)) {
2800
- this.setAlphaZeroMode(false, 'command');
2801
- return;
2802
- }
2803
- display.showWarning('Usage: /alphazero [on|off|status]');
2039
+ const headline = this.thinkingMode === 'extended'
2040
+ ? `${theme.info('Thinking on')} (Tab to toggle)`
2041
+ : `${theme.info('Thinking off')} (Tab to toggle)`;
2042
+ display.showSystemMessage(`${headline}\n${theme.ui.muted(descriptions[this.thinkingMode])}`);
2804
2043
  }
2805
2044
  handleShortcutsCommand() {
2806
2045
  // Display keyboard shortcuts help (Claude Code style)
@@ -2889,7 +2128,6 @@ export class InteractiveShell {
2889
2128
  const updated = toggleFeatureFlag(matchedKey, newValue);
2890
2129
  const status = updated[matchedKey] ? theme.success('enabled') : theme.ui.muted('disabled');
2891
2130
  display.showInfo(`Feature "${FEATURE_FLAG_INFO[matchedKey].label}" is now ${status}.`);
2892
- this.refreshFeatureStatusDisplay();
2893
2131
  display.showInfo('Changes will take effect on next launch or after /features refresh.');
2894
2132
  return;
2895
2133
  }
@@ -2902,7 +2140,6 @@ export class InteractiveShell {
2902
2140
  }
2903
2141
  saveFeatureFlags(updated);
2904
2142
  display.showInfo(`All features ${newValue ? theme.success('enabled') : theme.ui.muted('disabled')}.`);
2905
- this.refreshFeatureStatusDisplay();
2906
2143
  return;
2907
2144
  }
2908
2145
  else {
@@ -4075,7 +3312,9 @@ export class InteractiveShell {
4075
3312
  }
4076
3313
  display.showInfo(`Deleted session "${summary.title}".`);
4077
3314
  if (this.activeSessionId === summary.id) {
4078
- this.updateActiveSession(null, true);
3315
+ this.activeSessionId = null;
3316
+ this.activeSessionTitle = null;
3317
+ saveSessionPreferences({ lastSessionId: null });
4079
3318
  }
4080
3319
  }
4081
3320
  newSessionCommand(title) {
@@ -4095,7 +3334,6 @@ export class InteractiveShell {
4095
3334
  clearAutosaveSnapshot(this.profile);
4096
3335
  display.showInfo('Started a new empty session.');
4097
3336
  this.refreshContextGauge();
4098
- this.refreshFeatureStatusDisplay();
4099
3337
  }
4100
3338
  toggleAutosaveCommand(value) {
4101
3339
  if (!value) {
@@ -4150,6 +3388,7 @@ export class InteractiveShell {
4150
3388
  lines.push(' /rewind code Rewind code only (keep conversation)');
4151
3389
  lines.push(' /rewind conv Rewind conversation only (keep code)');
4152
3390
  lines.push('');
3391
+ lines.push(theme.ui.muted('Press Esc+Esc for quick access to the rewind menu'));
4153
3392
  display.showSystemMessage(lines.join('\n'));
4154
3393
  }
4155
3394
  handleMemoryCommand(input) {
@@ -4168,6 +3407,7 @@ export class InteractiveShell {
4168
3407
  lines.push(' - Use # prefix to quickly add notes to project memory');
4169
3408
  lines.push(' - Import other files with @./relative/path syntax');
4170
3409
  lines.push('');
3410
+ lines.push(theme.ui.muted('Create EROSOLAR.md with project coding standards for better results'));
4171
3411
  display.showSystemMessage(lines.join('\n'));
4172
3412
  }
4173
3413
  handleVimCommand() {
@@ -4248,20 +3488,6 @@ export class InteractiveShell {
4248
3488
  }
4249
3489
  display.showSystemMessage(lines.join('\n'));
4250
3490
  }
4251
- async handleUpdateCommand() {
4252
- display.showInfo('Checking for updates...');
4253
- const lines = [];
4254
- lines.push(theme.bold('Update Check'));
4255
- lines.push('');
4256
- lines.push(`Current version: ${this.version ?? 'unknown'}`);
4257
- lines.push('');
4258
- lines.push(theme.secondary('To update:'));
4259
- lines.push(' npm install -g erosolar-cli@latest');
4260
- lines.push(' or: npm update erosolar-cli');
4261
- lines.push('');
4262
- lines.push(theme.ui.muted('Auto-updates will be checked periodically.'));
4263
- display.showSystemMessage(lines.join('\n'));
4264
- }
4265
3491
  handleClearCommand() {
4266
3492
  if (this.agent) {
4267
3493
  this.agent.clearHistory();
@@ -4270,7 +3496,7 @@ export class InteractiveShell {
4270
3496
  display.clear();
4271
3497
  clearAutosaveSnapshot(this.profile);
4272
3498
  display.showInfo('Conversation cleared. Starting fresh.');
4273
- this.renderPromptArea();
3499
+ this.terminalInput.render();
4274
3500
  }
4275
3501
  async handleResumeCommand(input) {
4276
3502
  const tokens = input.split(/\s+/).slice(1);
@@ -4411,7 +3637,6 @@ export class InteractiveShell {
4411
3637
  if (remember) {
4412
3638
  saveSessionPreferences({ lastSessionId: summary?.id ?? null });
4413
3639
  }
4414
- this.refreshFeatureStatusDisplay();
4415
3640
  }
4416
3641
  resolveSessionBySelector(selector) {
4417
3642
  const sessions = listSessions(this.profile);
@@ -4580,7 +3805,7 @@ export class InteractiveShell {
4580
3805
  if (!providerOptions.length) {
4581
3806
  display.showWarning('No providers are available.');
4582
3807
  this.pendingInteraction = null;
4583
- this.renderPromptArea();
3808
+ this.terminalInput.render();
4584
3809
  return;
4585
3810
  }
4586
3811
  const lines = [
@@ -4601,7 +3826,7 @@ export class InteractiveShell {
4601
3826
  catch (error) {
4602
3827
  display.showError('Failed to load model list. Try again in a moment.', error);
4603
3828
  this.pendingInteraction = null;
4604
- this.renderPromptArea();
3829
+ this.terminalInput.render();
4605
3830
  }
4606
3831
  }
4607
3832
  buildProviderOptions() {
@@ -4785,7 +4010,7 @@ export class InteractiveShell {
4785
4010
  }
4786
4011
  renderToolMenu(interaction) {
4787
4012
  const lines = [
4788
- theme.bold('Select which tools are enabled (changes apply on next launch):'),
4013
+ theme.bold('Select which tools are enabled (changes apply on next launch; default package is locked on).'),
4789
4014
  ...interaction.options.map((option, index) => this.formatToolOptionLine(option, index, interaction.selection)),
4790
4015
  '',
4791
4016
  'Enter the number to toggle, "save" to persist, "defaults" to restore recommended tools, or "cancel".',
@@ -4794,13 +4019,23 @@ export class InteractiveShell {
4794
4019
  }
4795
4020
  formatToolOptionLine(option, index, selection) {
4796
4021
  const enabled = selection.has(option.id);
4797
- const checkbox = enabled ? theme.primary('[x]') : theme.ui.muted('[ ]');
4022
+ const checkbox = option.locked
4023
+ ? theme.primary('[โœ“]')
4024
+ : enabled
4025
+ ? theme.primary('[x]')
4026
+ : theme.ui.muted('[ ]');
4798
4027
  const details = [option.description];
4799
4028
  if (option.requiresSecret) {
4800
4029
  const hasSecret = Boolean(getSecretValue(option.requiresSecret));
4801
4030
  const status = hasSecret ? theme.success('API key set') : theme.warning('API key missing');
4802
4031
  details.push(status);
4803
4032
  }
4033
+ if (option.locked) {
4034
+ details.push(theme.ui.muted('Locked default package'));
4035
+ }
4036
+ if (option.restartRequired) {
4037
+ details.push(theme.ui.muted('Restart required'));
4038
+ }
4804
4039
  const numberLabel = this.colorizeDropdownLine(`${index + 1}.`, index);
4805
4040
  const optionLabel = this.colorizeDropdownLine(option.label, index);
4806
4041
  const detailLine = this.colorizeDropdownLine(` ${details.join(' โ€ข ')}`, index);
@@ -5146,29 +4381,29 @@ export class InteractiveShell {
5146
4381
  const trimmed = input.trim();
5147
4382
  if (!trimmed) {
5148
4383
  display.showWarning('Enter a number or type cancel.');
5149
- this.renderPromptArea();
4384
+ this.terminalInput.render();
5150
4385
  return;
5151
4386
  }
5152
4387
  if (trimmed.toLowerCase() === 'cancel') {
5153
4388
  this.pendingInteraction = null;
5154
4389
  display.showInfo('Model selection cancelled.');
5155
- this.renderPromptArea();
4390
+ this.terminalInput.render();
5156
4391
  return;
5157
4392
  }
5158
4393
  const choice = Number.parseInt(trimmed, 10);
5159
4394
  if (!Number.isFinite(choice)) {
5160
4395
  display.showWarning('Please enter a valid number.');
5161
- this.renderPromptArea();
4396
+ this.terminalInput.render();
5162
4397
  return;
5163
4398
  }
5164
4399
  const option = pending.options[choice - 1];
5165
4400
  if (!option) {
5166
4401
  display.showWarning('That option is not available.');
5167
- this.renderPromptArea();
4402
+ this.terminalInput.render();
5168
4403
  return;
5169
4404
  }
5170
4405
  this.showProviderModels(option);
5171
- this.renderPromptArea();
4406
+ this.terminalInput.render();
5172
4407
  }
5173
4408
  async handleModelSelection(input) {
5174
4409
  const pending = this.pendingInteraction;
@@ -5178,35 +4413,35 @@ export class InteractiveShell {
5178
4413
  const trimmed = input.trim();
5179
4414
  if (!trimmed) {
5180
4415
  display.showWarning('Enter a number, type "back", or type "cancel".');
5181
- this.renderPromptArea();
4416
+ this.terminalInput.render();
5182
4417
  return;
5183
4418
  }
5184
4419
  if (trimmed.toLowerCase() === 'back') {
5185
4420
  this.showModelMenu();
5186
- this.renderPromptArea();
4421
+ this.terminalInput.render();
5187
4422
  return;
5188
4423
  }
5189
4424
  if (trimmed.toLowerCase() === 'cancel') {
5190
4425
  this.pendingInteraction = null;
5191
4426
  display.showInfo('Model selection cancelled.');
5192
- this.renderPromptArea();
4427
+ this.terminalInput.render();
5193
4428
  return;
5194
4429
  }
5195
4430
  const choice = Number.parseInt(trimmed, 10);
5196
4431
  if (!Number.isFinite(choice)) {
5197
4432
  display.showWarning('Please enter a valid number.');
5198
- this.renderPromptArea();
4433
+ this.terminalInput.render();
5199
4434
  return;
5200
4435
  }
5201
4436
  const preset = pending.options[choice - 1];
5202
4437
  if (!preset) {
5203
4438
  display.showWarning('That option is not available.');
5204
- this.renderPromptArea();
4439
+ this.terminalInput.render();
5205
4440
  return;
5206
4441
  }
5207
4442
  this.pendingInteraction = null;
5208
4443
  await this.applyModelPreset(preset);
5209
- this.renderPromptArea();
4444
+ this.terminalInput.render();
5210
4445
  }
5211
4446
  async applyModelPreset(preset) {
5212
4447
  try {
@@ -5226,7 +4461,7 @@ export class InteractiveShell {
5226
4461
  this.applyPresetReasoningDefaults();
5227
4462
  if (this.rebuildAgent()) {
5228
4463
  display.showInfo(`Switched to ${preset.label}.`);
5229
- this.refreshBannerSessionInfo();
4464
+ this.refreshSessionContext();
5230
4465
  this.persistSessionPreference();
5231
4466
  this.resetChatBoxAfterModelSwap();
5232
4467
  }
@@ -5239,30 +4474,30 @@ export class InteractiveShell {
5239
4474
  const trimmed = input.trim();
5240
4475
  if (!trimmed) {
5241
4476
  display.showWarning('Enter a number or type cancel.');
5242
- this.renderPromptArea();
4477
+ this.terminalInput.render();
5243
4478
  return;
5244
4479
  }
5245
4480
  if (trimmed.toLowerCase() === 'cancel') {
5246
4481
  this.pendingInteraction = null;
5247
4482
  display.showInfo('Secret management cancelled.');
5248
- this.renderPromptArea();
4483
+ this.terminalInput.render();
5249
4484
  return;
5250
4485
  }
5251
4486
  const choice = Number.parseInt(trimmed, 10);
5252
4487
  if (!Number.isFinite(choice)) {
5253
4488
  display.showWarning('Please enter a valid number.');
5254
- this.renderPromptArea();
4489
+ this.terminalInput.render();
5255
4490
  return;
5256
4491
  }
5257
4492
  const secret = pending.options[choice - 1];
5258
4493
  if (!secret) {
5259
4494
  display.showWarning('That option is not available.');
5260
- this.renderPromptArea();
4495
+ this.terminalInput.render();
5261
4496
  return;
5262
4497
  }
5263
4498
  display.showSystemMessage(`Enter a new value for ${secret.label} or type "cancel".`);
5264
4499
  this.pendingInteraction = { type: 'secret-input', secret };
5265
- this.renderPromptArea();
4500
+ this.terminalInput.render();
5266
4501
  }
5267
4502
  async handleSecretInput(input) {
5268
4503
  const pending = this.pendingInteraction;
@@ -5272,16 +4507,14 @@ export class InteractiveShell {
5272
4507
  const trimmed = input.trim();
5273
4508
  if (!trimmed) {
5274
4509
  display.showWarning('Enter a value or type cancel.');
5275
- this.renderPromptArea();
4510
+ this.terminalInput.render();
5276
4511
  return;
5277
4512
  }
5278
4513
  if (trimmed.toLowerCase() === 'cancel') {
5279
4514
  this.pendingInteraction = null;
5280
4515
  this.pendingSecretRetry = null;
5281
- this.apiKeyGateActive = false;
5282
4516
  display.showInfo('Secret unchanged.');
5283
- this.renderPromptArea();
5284
- this.scheduleQueueProcessing();
4517
+ this.terminalInput.render();
5285
4518
  return;
5286
4519
  }
5287
4520
  try {
@@ -5290,7 +4523,6 @@ export class InteractiveShell {
5290
4523
  this.pendingInteraction = null;
5291
4524
  const deferred = this.pendingSecretRetry;
5292
4525
  this.pendingSecretRetry = null;
5293
- this.apiKeyGateActive = false;
5294
4526
  if (pending.secret.providers.includes(this.sessionState.provider)) {
5295
4527
  if (this.rebuildAgent()) {
5296
4528
  this.resetChatBoxAfterModelSwap();
@@ -5305,14 +4537,12 @@ export class InteractiveShell {
5305
4537
  display.showError(message);
5306
4538
  this.pendingInteraction = null;
5307
4539
  this.pendingSecretRetry = null;
5308
- this.apiKeyGateActive = false;
5309
4540
  }
5310
- this.renderPromptArea();
5311
- this.scheduleQueueProcessing();
4541
+ this.terminalInput.render();
5312
4542
  }
5313
- async processRequest(userRequest) {
4543
+ async processRequest(request) {
5314
4544
  if (this.isProcessing) {
5315
- this.enqueueFollowUpAction({ type: 'request', text: userRequest });
4545
+ this.enqueueFollowUpAction({ type: 'request', text: request });
5316
4546
  return;
5317
4547
  }
5318
4548
  if (!this.agent && !this.rebuildAgent()) {
@@ -5323,57 +4553,29 @@ export class InteractiveShell {
5323
4553
  if (!agent) {
5324
4554
  return;
5325
4555
  }
5326
- this.hasShownThoughtProcess = false;
5327
- this.resetAssistantStreamTracking();
5328
- const alphaZeroEngaged = this.alphaZeroModeEnabled;
5329
- const alphaZeroDifficult = alphaZeroEngaged ? this.isDifficultProblem(userRequest) : false;
5330
- const requestForAgent = alphaZeroEngaged
5331
- ? this.buildAlphaZeroPrompt(userRequest, alphaZeroDifficult)
5332
- : userRequest;
5333
- const skipReflectionForThisRun = this.skipNextAutoReflection;
5334
- this.skipNextAutoReflection = false;
5335
- const scrollbackStartIndex = this.terminalInput.getScrollbackBuffer().length;
5336
- const alphaZeroStatusId = 'alpha-zero';
5337
- let alphaZeroStatusApplied = false;
5338
- let alphaZeroTaskStarted = false;
5339
- let alphaZeroTaskCompleted = false;
5340
- if (alphaZeroEngaged) {
5341
- const detail = alphaZeroDifficult ? 'Difficult request detected' : 'Reinforcement loop enabled';
5342
- this.statusTracker.pushOverride(alphaZeroStatusId, 'AlphaZero RL active', {
5343
- detail: `${detail} ยท full verification`,
5344
- tone: 'info',
5345
- });
5346
- alphaZeroStatusApplied = true;
5347
- display.showInfo(`AlphaZero RL mode engaged${alphaZeroDifficult ? ' for a difficult request' : ''}. Duel + self-critique with verification enabled.`);
5348
- this.alphaZeroMetrics.startAlphaZeroTask(userRequest);
5349
- alphaZeroTaskStarted = true;
5350
- }
5351
- this.logUserPrompt(userRequest);
4556
+ this.logUserPrompt(request);
5352
4557
  this.isProcessing = true;
5353
4558
  this.uiUpdates.setMode('processing');
5354
4559
  this.terminalInput.setStreaming(true);
5355
4560
  // Keep the persistent input/control bar active as we transition into streaming.
5356
- this.renderPromptArea(true);
4561
+ this.terminalInput.forceRender();
5357
4562
  const requestStartTime = Date.now(); // Alpha Zero 2 timing
5358
- this.lastRequestStartedAt = requestStartTime;
5359
- this.lastToolSummaryRenderedAt = null;
5360
4563
  // Clear previous parallel agents and start fresh for new request
5361
4564
  const parallelManager = getParallelAgentManager();
5362
4565
  parallelManager.clear();
5363
4566
  parallelManager.startBatch();
5364
4567
  // AlphaZero: Track task for learning
5365
- this.lastUserQuery = userRequest;
5366
- this.currentTaskType = classifyTaskType(userRequest);
4568
+ this.lastUserQuery = request;
4569
+ this.currentTaskType = classifyTaskType(request);
5367
4570
  this.currentToolCalls = [];
5368
4571
  this.uiAdapter.startProcessing('Working on your request');
5369
4572
  this.setProcessingStatus();
5370
4573
  let responseText = '';
5371
- let detectedFailure = null;
5372
- let hadUnhandledError = false;
5373
4574
  try {
5374
4575
  // Start streaming - no header needed, the input area already provides context
5375
- this.startStreamingHeartbeat(alphaZeroEngaged ? 'AlphaZero RL' : 'Streaming response');
5376
- responseText = await agent.send(requestForAgent, true);
4576
+ this.startStreamingHeartbeat('Streaming response');
4577
+ responseText = await agent.send(request, true);
4578
+ this.finishStreamingFormatter();
5377
4579
  await this.awaitPendingCleanup();
5378
4580
  this.captureHistorySnapshot();
5379
4581
  this.autosaveIfEnabled();
@@ -5392,18 +4594,14 @@ export class InteractiveShell {
5392
4594
  duration: 0,
5393
4595
  }));
5394
4596
  // AlphaZero: Check for failure in response
5395
- detectedFailure = detectFailure(responseText, {
4597
+ const failure = detectFailure(responseText, {
5396
4598
  toolCalls: this.currentToolCalls,
5397
- userMessage: userRequest,
4599
+ userMessage: request,
5398
4600
  });
5399
- if (alphaZeroEngaged && alphaZeroTaskStarted && !alphaZeroTaskCompleted) {
5400
- this.alphaZeroMetrics.completeAlphaZeroTask(!detectedFailure);
5401
- alphaZeroTaskCompleted = true;
5402
- }
5403
- if (detectedFailure) {
5404
- this.lastFailure = detectedFailure;
4601
+ if (failure) {
4602
+ this.lastFailure = failure;
5405
4603
  // Check if we have a recovery strategy
5406
- const strategy = findRecoveryStrategy(detectedFailure);
4604
+ const strategy = findRecoveryStrategy(failure);
5407
4605
  if (strategy) {
5408
4606
  display.showSystemMessage(`๐Ÿ”„ Found recovery strategy for this type of issue (success rate: ${Math.round(strategy.successRate * 100)}%)`);
5409
4607
  }
@@ -5426,55 +4624,28 @@ export class InteractiveShell {
5426
4624
  }
5427
4625
  }
5428
4626
  catch (error) {
5429
- const handled = this.handleProviderError(error, () => this.processRequest(userRequest));
5430
- hadUnhandledError = !handled;
5431
- if (alphaZeroEngaged && alphaZeroTaskStarted && !alphaZeroTaskCompleted) {
5432
- this.alphaZeroMetrics.completeAlphaZeroTask(false);
5433
- alphaZeroTaskCompleted = true;
5434
- }
4627
+ const handled = this.handleProviderError(error, () => this.processRequest(request));
5435
4628
  if (!handled) {
5436
4629
  // Pass full error object for enhanced formatting with stack trace
5437
4630
  display.showError(error instanceof Error ? error.message : String(error), error);
5438
4631
  }
5439
4632
  }
5440
4633
  finally {
5441
- const runLogEntry = this.buildRunLogEntry(userRequest, responseText, scrollbackStartIndex, {
5442
- alphaZeroEngaged,
5443
- alphaZeroDifficult,
5444
- failureType: detectedFailure?.type ?? (hadUnhandledError ? 'unhandled-error' : null),
5445
- });
5446
- this.lastRunLog = runLogEntry;
5447
- const allowAutoChain = alphaZeroEngaged && this.autoContinueEnabled && isErosolarRepo(this.workingDir);
5448
- let shouldStopAuto = false;
5449
- if (this.alphaZeroAutoImproveActive) {
5450
- shouldStopAuto = this.shouldStopAlphaZeroAutoImprove(responseText, allowAutoChain);
5451
- if (shouldStopAuto) {
5452
- this.alphaZeroAutoImproveActive = false;
5453
- this.alphaZeroAutoImproveIterations = 0;
5454
- }
5455
- }
5456
- if (!skipReflectionForThisRun && !shouldStopAuto) {
5457
- this.maybeQueueAlphaZeroSelfReflection(runLogEntry, alphaZeroEngaged, allowAutoChain);
5458
- }
5459
- if (alphaZeroEngaged && alphaZeroStatusApplied) {
5460
- this.statusTracker.clearOverride(alphaZeroStatusId);
5461
- }
5462
- if (alphaZeroEngaged && alphaZeroTaskStarted && !alphaZeroTaskCompleted) {
5463
- this.alphaZeroMetrics.completeAlphaZeroTask(false);
5464
- }
4634
+ this.finishStreamingFormatter();
5465
4635
  display.stopThinking(false);
5466
4636
  this.uiUpdates.setMode('processing');
5467
- this.stopStreamingHeartbeat({ skipRender: true });
4637
+ this.stopStreamingHeartbeat();
5468
4638
  this.isProcessing = false;
5469
4639
  this.terminalInput.setStreaming(false);
5470
4640
  this.uiAdapter.endProcessing('Ready for prompts');
5471
4641
  this.setIdleStatus();
4642
+ display.newLine();
5472
4643
  this.updateStatusMessage(null);
5473
4644
  queueMicrotask(() => this.uiUpdates.setMode('idle'));
5474
4645
  // CRITICAL: Ensure readline prompt is active for user input
5475
4646
  // Claude Code style: New prompt naturally appears at bottom
5476
4647
  this.ensureReadlineReady();
5477
- this.renderPromptArea();
4648
+ this.terminalInput.render();
5478
4649
  this.scheduleQueueProcessing();
5479
4650
  this.refreshQueueIndicators();
5480
4651
  }
@@ -5505,7 +4676,6 @@ export class InteractiveShell {
5505
4676
  if (!agent) {
5506
4677
  return;
5507
4678
  }
5508
- this.hasShownThoughtProcess = false;
5509
4679
  this.isProcessing = true;
5510
4680
  this.uiUpdates.setMode('processing');
5511
4681
  this.terminalInput.setStreaming(true);
@@ -5546,8 +4716,6 @@ When truly finished with ALL tasks, explicitly state "TASK_FULLY_COMPLETE".`;
5546
4716
  }
5547
4717
  while (iteration < MAX_ITERATIONS) {
5548
4718
  iteration++;
5549
- this.hasShownThoughtProcess = false;
5550
- this.resetAssistantStreamTracking();
5551
4719
  display.showSystemMessage(`\n๐Ÿ“ Iteration ${iteration}/${MAX_ITERATIONS}`);
5552
4720
  this.updateStatusMessage(`Working on iteration ${iteration}...`);
5553
4721
  try {
@@ -5555,6 +4723,7 @@ When truly finished with ALL tasks, explicitly state "TASK_FULLY_COMPLETE".`;
5555
4723
  display.showThinking('Responding...');
5556
4724
  this.refreshStatusLine(true);
5557
4725
  const response = await agent.send(currentPrompt, true);
4726
+ this.finishStreamingFormatter();
5558
4727
  await this.awaitPendingCleanup();
5559
4728
  this.captureHistorySnapshot();
5560
4729
  this.autosaveIfEnabled();
@@ -5687,6 +4856,7 @@ What's the next action?`;
5687
4856
  }
5688
4857
  }
5689
4858
  finally {
4859
+ this.finishStreamingFormatter();
5690
4860
  const totalElapsed = Date.now() - overallStartTime;
5691
4861
  const minutes = Math.floor(totalElapsed / 60000);
5692
4862
  const seconds = Math.floor((totalElapsed % 60000) / 1000);
@@ -5700,6 +4870,10 @@ What's the next action?`;
5700
4870
  this.uiAdapter.endProcessing('Ready for prompts');
5701
4871
  this.setIdleStatus();
5702
4872
  this.updateStatusMessage(null);
4873
+ display.newLine();
4874
+ // Claude Code style: Show unified status bar before prompt
4875
+ // This creates consistent UI between startup and post-streaming
4876
+ this.showUnifiedStatusBar();
5703
4877
  queueMicrotask(() => this.uiUpdates.setMode('idle'));
5704
4878
  // CRITICAL: Ensure readline prompt is active for user input
5705
4879
  // Claude Code style: New prompt naturally appears at bottom
@@ -5878,25 +5052,7 @@ What's the next action?`;
5878
5052
  }
5879
5053
  if (name === 'bash' || name === 'execute_bash') {
5880
5054
  const command = String(entry.args['command'] ?? '').toLowerCase();
5881
- const patterns = [
5882
- 'npm test',
5883
- 'yarn test',
5884
- 'pnpm test',
5885
- 'bun test',
5886
- 'go test',
5887
- 'cargo test',
5888
- 'pytest',
5889
- 'python -m pytest',
5890
- 'tox',
5891
- 'mvn test',
5892
- './mvnw test',
5893
- 'gradle test',
5894
- './gradlew test',
5895
- 'dotnet test',
5896
- 'make test',
5897
- 'swift test',
5898
- ];
5899
- return patterns.some((pattern) => command.includes(pattern));
5055
+ return command.includes('npm test') || command.includes('yarn test') || command.includes('pnpm test');
5900
5056
  }
5901
5057
  return false;
5902
5058
  }
@@ -5914,170 +5070,310 @@ What's the next action?`;
5914
5070
  const parts = [error?.stdout, error?.stderr, error?.message].filter((part) => typeof part === 'string' && part.trim());
5915
5071
  return parts.join('\n').trim();
5916
5072
  }
5917
- async enforceAutoTests(trigger, commandInfo) {
5918
- if (this.autoTestInFlight || !this.verificationEnabled) {
5919
- return 'skipped';
5073
+ runAutoQualityChecks(trigger, assistantResponse, verificationContext) {
5074
+ if (!this.verificationEnabled) {
5075
+ return;
5076
+ }
5077
+ void (async () => {
5078
+ try {
5079
+ const buildOk = await this.enforceAutoBuild(trigger);
5080
+ if (!buildOk) {
5081
+ return;
5082
+ }
5083
+ await this.enforceAutoTests(trigger, assistantResponse, verificationContext);
5084
+ }
5085
+ catch (error) {
5086
+ const message = error instanceof Error ? error.message : String(error);
5087
+ display.showWarning(`Auto quality checks failed: ${message}`);
5088
+ }
5089
+ })();
5090
+ }
5091
+ async enforceAutoTests(trigger, assistantResponse, verificationContext) {
5092
+ if (this.autoTestInFlight) {
5093
+ return;
5094
+ }
5095
+ if (!this.verificationEnabled) {
5096
+ return;
5920
5097
  }
5921
5098
  const latestChange = this.getLatestFileChangeTimestamp();
5922
5099
  if (!latestChange) {
5923
- return 'skipped';
5100
+ return;
5924
5101
  }
5925
5102
  const latestTest = this.getLatestTestTimestamp();
5926
5103
  if (latestTest && latestChange <= latestTest) {
5927
- return 'skipped';
5104
+ return;
5928
5105
  }
5929
- const detected = commandInfo ?? detectTestCommand(this.workingDir);
5930
- if (!detected) {
5931
- this.lastAutoTestRun = Date.now();
5932
- display.showSystemMessage('โ„น๏ธ Skipping auto-tests: no test command detected for this repo.');
5933
- return 'skipped';
5106
+ if (!this.lastBuildSucceededAt || this.lastBuildSucceededAt < latestChange) {
5107
+ display.showSystemMessage('โญ๏ธ Skipping auto-tests because no successful build is recorded for the latest changes.');
5108
+ return;
5934
5109
  }
5935
5110
  this.autoTestInFlight = true;
5936
- const command = detected.command;
5937
- const reasonSuffix = detected.reason ? ` [${detected.reason}]` : '';
5938
- display.showSystemMessage(`๐Ÿงช Auto-testing recent changes (${trigger}) with "${command}"${reasonSuffix}...`);
5111
+ const command = 'npm test -- --runInBand';
5112
+ display.showSystemMessage(`๐Ÿงช Auto-testing recent changes (${trigger}) with "${command}"...`);
5939
5113
  this.updateStatusMessage('Running tests automatically...');
5114
+ let combinedOutput = '';
5940
5115
  try {
5941
5116
  const { stdout, stderr } = await execAsync(command, {
5942
5117
  cwd: this.workingDir,
5943
5118
  timeout: 10 * 60 * 1000,
5944
5119
  maxBuffer: 10 * 1024 * 1024,
5945
5120
  });
5946
- this.lastAutoTestRun = Date.now();
5947
- const outputText = [stdout, stderr].filter(Boolean).join('\n').trim();
5121
+ combinedOutput = [stdout, stderr].filter(Boolean).join('\n').trim();
5948
5122
  display.showSystemMessage('โœ… Auto-tests finished.');
5949
- if (outputText) {
5950
- this.writeLocked(`${outputText}\n`);
5951
- }
5952
5123
  this.statusTracker.clearOverride('tests');
5953
- return 'success';
5954
5124
  }
5955
5125
  catch (error) {
5956
- this.lastAutoTestRun = Date.now();
5957
- const message = this.formatCommandError(error);
5126
+ combinedOutput = this.formatCommandError(error);
5958
5127
  display.showWarning('โš ๏ธ Auto-tests failed. Review output below.');
5959
- if (message) {
5960
- this.writeLocked(`${message}\n`);
5961
- }
5962
5128
  this.statusTracker.pushOverride('tests', 'Tests failing', {
5963
- detail: `Auto-run ${command} failed`,
5129
+ detail: 'Auto-run npm test failed',
5964
5130
  tone: 'danger',
5965
5131
  });
5966
- return 'failed';
5967
5132
  }
5968
5133
  finally {
5134
+ if (combinedOutput) {
5135
+ this.writeLocked(`${combinedOutput}\n`);
5136
+ }
5137
+ try {
5138
+ await this.runAIDesignedTests(trigger, assistantResponse, verificationContext);
5139
+ }
5140
+ catch (error) {
5141
+ const message = error instanceof Error ? error.message : String(error);
5142
+ display.showWarning(`AI-designed tests failed: ${message}`);
5143
+ }
5144
+ this.lastAutoTestRun = Date.now();
5969
5145
  this.updateStatusMessage(null);
5970
5146
  this.autoTestInFlight = false;
5971
- this.terminalInput.resetContentPosition();
5972
- this.terminalInput.forceRender();
5973
5147
  }
5974
5148
  }
5975
- isBuildToolCall(entry) {
5976
- const name = entry.toolName.toLowerCase();
5977
- if (name === 'run_build' || name === 'build') {
5149
+ /**
5150
+ * Auto-build verification after file edits.
5151
+ * Runs `npm run build` to catch TypeScript errors and feeds failures back to the agent.
5152
+ */
5153
+ async enforceAutoBuild(trigger) {
5154
+ if (this.autoBuildPromise) {
5155
+ return this.autoBuildPromise;
5156
+ }
5157
+ if (!this.verificationEnabled) {
5158
+ return false;
5159
+ }
5160
+ const latestChange = this.getLatestFileChangeTimestamp();
5161
+ if (!latestChange) {
5978
5162
  return true;
5979
5163
  }
5980
- if (name === 'bash' || name === 'execute_bash') {
5981
- const command = String(entry.args['command'] ?? '').toLowerCase();
5982
- const makeBuild = command.startsWith('make') && !command.includes('make test') && !command.includes('make lint');
5983
- const patterns = [
5984
- 'npm run build',
5985
- 'yarn build',
5986
- 'pnpm build',
5987
- 'bun run build',
5988
- 'bun build',
5989
- 'tsc',
5990
- 'cargo build',
5991
- 'go build',
5992
- 'python -m build',
5993
- 'mvn -b -dskiptests package',
5994
- 'mvn package',
5995
- 'mvn install',
5996
- './mvnw -b -dskiptests package',
5997
- './mvnw package',
5998
- './mvnw install',
5999
- 'gradle build',
6000
- 'gradle assemble',
6001
- './gradlew build',
6002
- './gradlew assemble',
6003
- 'dotnet build',
6004
- 'swift build',
6005
- ];
6006
- return makeBuild || patterns.some((pattern) => command.includes(pattern));
5164
+ if (this.lastBuildSucceededAt && latestChange <= this.lastBuildSucceededAt) {
5165
+ return true;
6007
5166
  }
6008
- return false;
5167
+ const command = 'npm run build';
5168
+ const runner = (async () => {
5169
+ this.autoBuildInFlight = true;
5170
+ display.showSystemMessage(`๐Ÿ”จ Auto-building to verify changes (${trigger})...`);
5171
+ this.updateStatusMessage('Running build automatically...');
5172
+ try {
5173
+ const { stdout, stderr } = await execAsync(command, {
5174
+ cwd: this.workingDir,
5175
+ timeout: 5 * 60 * 1000,
5176
+ maxBuffer: 10 * 1024 * 1024,
5177
+ });
5178
+ const finishedAt = Date.now();
5179
+ this.lastAutoBuildRun = finishedAt;
5180
+ this.lastBuildSucceededAt = finishedAt;
5181
+ const outputText = [stdout, stderr].filter(Boolean).join('\n').trim();
5182
+ display.showSystemMessage('โœ… Build succeeded.');
5183
+ if (outputText && outputText.length < 500) {
5184
+ this.writeLocked(`${outputText}\n`);
5185
+ }
5186
+ this.statusTracker.clearOverride('build');
5187
+ return true;
5188
+ }
5189
+ catch (error) {
5190
+ const finishedAt = Date.now();
5191
+ this.lastAutoBuildRun = finishedAt;
5192
+ this.lastBuildSucceededAt = null;
5193
+ const errorOutput = this.formatCommandError(error);
5194
+ display.showWarning('โš ๏ธ Build failed. Feeding errors back to agent...');
5195
+ if (errorOutput) {
5196
+ this.writeLocked(`${errorOutput}\n`);
5197
+ }
5198
+ this.statusTracker.pushOverride('build', 'Build failing', {
5199
+ detail: 'Auto-run npm run build failed',
5200
+ tone: 'danger',
5201
+ });
5202
+ // Feed build errors back to the agent so it can fix them
5203
+ await this.feedBuildErrorsToAgent(errorOutput);
5204
+ return false;
5205
+ }
5206
+ finally {
5207
+ this.updateStatusMessage(null);
5208
+ this.autoBuildInFlight = false;
5209
+ this.autoBuildPromise = null;
5210
+ }
5211
+ })();
5212
+ this.autoBuildPromise = runner;
5213
+ return runner;
6009
5214
  }
6010
- getLatestBuildTimestamp() {
6011
- const history = this.runtimeSession.toolRuntime.getToolHistory?.() ?? [];
6012
- let latest = this.lastAutoBuildRun;
6013
- for (const entry of history) {
6014
- if (this.isBuildToolCall(entry)) {
6015
- latest = latest ? Math.max(latest, entry.timestamp) : entry.timestamp;
5215
+ describeRecentChanges() {
5216
+ const changes = this._fileChangeTracker.getAllChanges();
5217
+ if (!changes.length) {
5218
+ return 'No tracked changes';
5219
+ }
5220
+ const items = changes.slice(0, 8).map((change) => {
5221
+ const additions = change.additions ? `+${change.additions}` : '';
5222
+ const removals = change.removals ? `-${change.removals}` : '';
5223
+ const delta = [additions, removals].filter(Boolean).join('/');
5224
+ const deltaLabel = delta ? ` ${delta}` : '';
5225
+ return `${change.path} (${change.type}${deltaLabel})`;
5226
+ });
5227
+ return items.join('; ');
5228
+ }
5229
+ describePackageScripts() {
5230
+ try {
5231
+ const pkgPath = join(this.workingDir, 'package.json');
5232
+ if (!existsSync(pkgPath)) {
5233
+ return 'package.json not found';
5234
+ }
5235
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
5236
+ const scripts = pkg.scripts ? Object.keys(pkg.scripts) : [];
5237
+ if (!scripts.length) {
5238
+ return 'package.json present without scripts';
6016
5239
  }
5240
+ return `package.json scripts: ${scripts.slice(0, 8).join(', ')}`;
5241
+ }
5242
+ catch {
5243
+ return 'package.json present but unreadable';
6017
5244
  }
6018
- return latest;
6019
5245
  }
6020
- /**
6021
- * Auto-build verification after file edits.
6022
- * Detects the build command for the current repo and feeds failures back to the agent.
6023
- */
6024
- async enforceAutoBuild(trigger, commandInfo) {
6025
- if (this.autoBuildInFlight || !this.verificationEnabled) {
6026
- return 'skipped';
5246
+ parseAIDesignedTests(plan) {
5247
+ if (!plan?.trim()) {
5248
+ return [];
6027
5249
  }
6028
- const latestChange = this.getLatestFileChangeTimestamp();
6029
- if (!latestChange) {
6030
- return 'skipped';
6031
- }
6032
- const latestBuild = this.getLatestBuildTimestamp();
6033
- if (latestBuild && latestChange <= latestBuild) {
6034
- return 'skipped';
6035
- }
6036
- const detected = commandInfo ?? detectBuildCommand(this.workingDir);
6037
- if (!detected) {
6038
- this.lastAutoBuildRun = Date.now();
6039
- display.showSystemMessage('โ„น๏ธ Skipping auto-build: no build command detected for this repo.');
6040
- return 'skipped';
6041
- }
6042
- this.autoBuildInFlight = true;
6043
- const command = detected.command;
6044
- const reasonSuffix = detected.reason ? ` [${detected.reason}]` : '';
6045
- display.showSystemMessage(`๐Ÿ”จ Auto-building to verify changes (${trigger}) with "${command}"${reasonSuffix}...`);
6046
- this.updateStatusMessage('Running build automatically...');
5250
+ const match = plan.match(/\[[\s\S]*\]/);
5251
+ const payload = match ? match[0] : plan;
6047
5252
  try {
6048
- const { stdout, stderr } = await execAsync(command, {
6049
- cwd: this.workingDir,
6050
- timeout: 5 * 60 * 1000,
6051
- maxBuffer: 10 * 1024 * 1024,
5253
+ const parsed = JSON.parse(payload);
5254
+ if (!Array.isArray(parsed)) {
5255
+ return [];
5256
+ }
5257
+ return parsed
5258
+ .map((entry, index) => ({
5259
+ id: typeof entry.id === 'string' && entry.id.trim() ? entry.id.trim() : `ai-test-${index + 1}`,
5260
+ description: typeof entry.description === 'string' ? entry.description.trim() : `AI test ${index + 1}`,
5261
+ command: typeof entry.command === 'string' ? entry.command.trim() : '',
5262
+ expect: typeof entry.expect === 'string' ? entry.expect.trim() : undefined,
5263
+ timeoutMs: typeof entry.timeoutMs === 'number' ? entry.timeoutMs : undefined,
5264
+ }))
5265
+ .filter((test) => !!test.command);
5266
+ }
5267
+ catch {
5268
+ return [];
5269
+ }
5270
+ }
5271
+ isDangerousTestCommand(command) {
5272
+ const patterns = [
5273
+ /\brm\s+-/i,
5274
+ /\brm\s+/i,
5275
+ /\brmdir\b/i,
5276
+ /\bchmod\s+7/i,
5277
+ /\bsudo\b/i,
5278
+ /\bmkfs\b/i,
5279
+ /\bshutdown\b/i,
5280
+ /\breboot\b/i,
5281
+ /\bgit\s+(push|reset|checkout|clean)\b/i,
5282
+ /\bnpm\s+install\b/i,
5283
+ /\byarn\s+add\b/i,
5284
+ /\bpnpm\s+add\b/i,
5285
+ ];
5286
+ return patterns.some((pattern) => pattern.test(command));
5287
+ }
5288
+ async runAIDesignedTests(trigger, assistantResponse, verificationContext) {
5289
+ const userGoal = this.lastUserQuery?.trim() || 'Not provided';
5290
+ const implementationClaim = (assistantResponse ?? this.lastAssistantResponse ?? '').trim();
5291
+ const changeSummary = this.describeRecentChanges();
5292
+ const packageSummary = this.describePackageScripts();
5293
+ const recentConversation = verificationContext?.conversationHistory?.slice(-3).join('\n') ?? '';
5294
+ let provider;
5295
+ try {
5296
+ provider = createProvider({
5297
+ provider: this.sessionState.provider,
5298
+ model: this.sessionState.model,
5299
+ temperature: 0.15,
5300
+ maxTokens: 900,
6052
5301
  });
6053
- this.lastAutoBuildRun = Date.now();
6054
- const outputText = [stdout, stderr].filter(Boolean).join('\n').trim();
6055
- display.showSystemMessage('โœ… Build succeeded.');
6056
- if (outputText && outputText.length < 500) {
6057
- this.writeLocked(`${outputText}\n`);
6058
- }
6059
- this.statusTracker.clearOverride('build');
6060
- return 'success';
6061
5302
  }
6062
5303
  catch (error) {
6063
- this.lastAutoBuildRun = Date.now();
6064
- const errorOutput = this.formatCommandError(error);
6065
- display.showWarning('โš ๏ธ Build failed. Feeding errors back to agent...');
6066
- if (errorOutput) {
6067
- this.writeLocked(`${errorOutput}\n`);
6068
- }
6069
- this.statusTracker.pushOverride('build', 'Build failing', {
6070
- detail: `Auto-run ${command} failed`,
6071
- tone: 'danger',
6072
- });
6073
- // Feed build errors back to the agent so it can fix them
6074
- await this.feedBuildErrorsToAgent(errorOutput);
6075
- return 'failed';
5304
+ const message = error instanceof Error ? error.message : String(error);
5305
+ display.showWarning(`AI-designed tests skipped: ${message}`);
5306
+ return;
6076
5307
  }
6077
- finally {
6078
- this.updateStatusMessage(null);
6079
- this.autoBuildInFlight = false;
5308
+ const systemPrompt = 'You design high-value, repo-aware verification commands. Only emit JSON. Use read-only commands that run quickly.';
5309
+ const prompt = `Workspace: ${this.workingDir}
5310
+ User goal: ${userGoal.slice(0, 800)}
5311
+ Assistant claim: ${implementationClaim ? implementationClaim.slice(0, 1200) : 'No implementation summary captured.'}
5312
+ Recent file changes: ${changeSummary}
5313
+ Package signals: ${packageSummary}
5314
+ Recent conversation: ${recentConversation || 'n/a'}
5315
+
5316
+ Design 3-5 focused verification commands to confirm the requested functionality works after the successful build.
5317
+ Rules:
5318
+ - Commands must be safe and non-destructive (no rm, mv, chmod 7xx, sudo, package installs, or git mutations).
5319
+ - Prefer existing npm/yarn/pnpm scripts or targeted checks (node scripts, grep, curl localhost) that prove behavior.
5320
+ - Keep commands runnable from the repo root and finish quickly.
5321
+ Return ONLY JSON array:
5322
+ [{"id":"ai-test-1","description":"what it verifies","command":"npm test -- my-case","expect":"substring to find","timeoutMs":45000}]`;
5323
+ const plan = await provider.generate([
5324
+ { role: 'system', content: systemPrompt },
5325
+ { role: 'user', content: prompt },
5326
+ ], []);
5327
+ const planText = plan.type === 'message' ? plan.content : plan.content ?? '';
5328
+ const tests = this.parseAIDesignedTests(planText).slice(0, 5);
5329
+ if (!tests.length) {
5330
+ display.showWarning('AI-designed tests could not be generated. Skipping.');
5331
+ return;
5332
+ }
5333
+ const results = [];
5334
+ results.push(`๐Ÿค– Running ${tests.length} AI-designed verification tests (${trigger})...`);
5335
+ for (const test of tests) {
5336
+ const command = test.command.trim();
5337
+ if (!command) {
5338
+ continue;
5339
+ }
5340
+ if (this.isDangerousTestCommand(command)) {
5341
+ results.push(`โš ๏ธ ${test.id} blocked (unsafe command): ${command}`);
5342
+ continue;
5343
+ }
5344
+ try {
5345
+ const { stdout, stderr } = await execAsync(command, {
5346
+ cwd: this.workingDir,
5347
+ timeout: Math.min(Math.max(test.timeoutMs ?? 45000, 5000), 120000),
5348
+ maxBuffer: 10 * 1024 * 1024,
5349
+ });
5350
+ const output = [stdout, stderr].filter(Boolean).join('\n');
5351
+ const expected = test.expect?.trim();
5352
+ const success = expected ? output.toLowerCase().includes(expected.toLowerCase()) : true;
5353
+ const snippet = output.replace(/\s+/g, ' ').trim().slice(0, 240);
5354
+ results.push(`${success ? 'โœ…' : 'โŒ'} ${test.description || test.id}`);
5355
+ results.push(` cmd: ${command}`);
5356
+ if (expected) {
5357
+ results.push(` expect: ${expected}`);
5358
+ }
5359
+ if (snippet) {
5360
+ results.push(` output: ${snippet}`);
5361
+ }
5362
+ if (!success && !snippet) {
5363
+ results.push(' output: [no output captured]');
5364
+ }
5365
+ }
5366
+ catch (error) {
5367
+ const message = this.formatCommandError(error) || (error instanceof Error ? error.message : String(error));
5368
+ results.push(`โŒ ${test.description || test.id}`);
5369
+ results.push(` cmd: ${command}`);
5370
+ if (test.expect) {
5371
+ results.push(` expect: ${test.expect}`);
5372
+ }
5373
+ results.push(` error: ${message.slice(0, 240)}`);
5374
+ }
6080
5375
  }
5376
+ display.showSystemMessage(results.join('\n'));
6081
5377
  }
6082
5378
  /**
6083
5379
  * Feed build errors back to the agent conversation for automatic fixing.
@@ -6103,43 +5399,21 @@ What's the next action?`;
6103
5399
  // Send the error to the agent for fixing
6104
5400
  display.showThinking('Analyzing build errors');
6105
5401
  this.refreshStatusLine(true);
6106
- this.hasShownThoughtProcess = false;
6107
- await this.withStreamingUi('Fixing build errors', () => this.agent.send(prompt, true));
5402
+ const response = await this.agent.send(prompt, true);
5403
+ this.finishStreamingFormatter();
6108
5404
  display.stopThinking();
6109
5405
  this.refreshStatusLine(true);
5406
+ if (response) {
5407
+ display.showAssistantMessage(response, { isFinal: true });
5408
+ }
6110
5409
  // Recursively verify the fix worked
6111
5410
  await this.enforceAutoBuild('verification');
6112
5411
  }
6113
5412
  catch (agentError) {
6114
5413
  display.showWarning('Agent could not automatically fix build errors. Please review manually.');
6115
5414
  }
6116
- }
6117
- async enforceAutoVerification(trigger) {
6118
- if (this.autoVerificationInFlight || !this.verificationEnabled) {
6119
- return;
6120
- }
6121
- const latestChange = this.getLatestFileChangeTimestamp();
6122
- if (!latestChange) {
6123
- return;
6124
- }
6125
- this.autoVerificationInFlight = true;
6126
- try {
6127
- const buildInfo = detectBuildCommand(this.workingDir);
6128
- const testInfo = detectTestCommand(this.workingDir);
6129
- const buildResult = await this.enforceAutoBuild(trigger, buildInfo);
6130
- if (buildResult === 'failed') {
6131
- return;
6132
- }
6133
- if (testInfo) {
6134
- await this.enforceAutoTests(trigger, testInfo);
6135
- }
6136
- else {
6137
- this.lastAutoTestRun = Date.now();
6138
- display.showSystemMessage('โ„น๏ธ Skipping auto-tests: no test command detected for this repo.');
6139
- }
6140
- }
6141
5415
  finally {
6142
- this.autoVerificationInFlight = false;
5416
+ this.finishStreamingFormatter();
6143
5417
  }
6144
5418
  }
6145
5419
  rebuildAgent() {
@@ -6157,35 +5431,29 @@ What's the next action?`;
6157
5431
  autoContinue: this.autoContinueEnabled,
6158
5432
  };
6159
5433
  this.agent = this.runtimeSession.createAgent(selection, {
6160
- onStreamChunk: (chunk, channel) => {
6161
- this.streamAssistantChunk(chunk, channel);
5434
+ onStreamChunk: (chunk) => {
5435
+ this.handleStreamChunk(chunk);
6162
5436
  },
6163
5437
  onStreamFallback: (info) => this.handleStreamingFallback(info),
6164
5438
  onAssistantMessage: (content, metadata) => {
6165
5439
  const enriched = this.buildDisplayMetadata(metadata);
6166
- const blocksActive = this.assistantBlocksEnabled && this.assistantBlockRenderer !== null;
6167
- const parsed = this.splitThinkingResponse(content);
6168
- const thinking = parsed?.thinking ?? null;
6169
- const responseContent = parsed ? parsed.response?.trim() ?? '' : content.trim();
6170
- const narrativeContent = (parsed?.response ?? content).trim();
6171
- const streamRendered = metadata.wasStreamed && this.assistantStreamHadContent;
6172
- if (thinking) {
6173
- this.presentThoughtProcess(thinking, enriched, {
6174
- wasStreamed: metadata.wasStreamed,
6175
- // Always render the thought block immediately when present so it appears first.
6176
- renderWhenStreamed: true,
6177
- });
6178
- }
5440
+ // Update spinner based on message type
6179
5441
  if (metadata.isFinal) {
6180
- if (metadata.wasStreamed) {
6181
- this.finalizeAssistantStream();
6182
- }
6183
- const body = responseContent || content;
6184
- // Always render the final response body even if it already streamed.
6185
- if (body) {
6186
- this.renderAssistantContent('response', body, { ...enriched, isFinal: true });
5442
+ const parsed = this.splitThinkingResponse(content);
5443
+ const finalContent = parsed?.response?.trim() || content;
5444
+ // Skip display if content was already streamed to avoid double-display
5445
+ if (!metadata.wasStreamed) {
5446
+ if (parsed?.thinking) {
5447
+ const summary = this.extractThoughtSummary(parsed.thinking);
5448
+ if (summary) {
5449
+ display.updateThinking(`๐Ÿ’ญ ${summary}`);
5450
+ }
5451
+ display.showAssistantMessage(parsed.thinking, { ...enriched, isFinal: false });
5452
+ }
5453
+ if (finalContent) {
5454
+ display.showAssistantMessage(finalContent, enriched);
5455
+ }
6187
5456
  }
6188
- this.renderToolUsageSummary({ ...enriched, isFinal: true });
6189
5457
  // Status shown in mode controls bar - no separate status line needed
6190
5458
  display.stopThinking();
6191
5459
  // Update context usage for mode controls display
@@ -6196,22 +5464,19 @@ What's the next action?`;
6196
5464
  this.updateContextUsage(percentage);
6197
5465
  }
6198
5466
  }
5467
+ if (finalContent) {
5468
+ this.lastAssistantResponse = finalContent;
5469
+ }
6199
5470
  // Auto-verify changes: build first (catches type errors), then tests
6200
- void this.enforceAutoVerification('final-response');
5471
+ void this.runAutoQualityChecks('final-response', finalContent);
6201
5472
  }
6202
5473
  else {
6203
5474
  // Non-final message = narrative text before tool calls (Claude Code style)
6204
5475
  // Stop spinner and show the narrative text directly
6205
5476
  display.stopThinking();
6206
- if (metadata.wasStreamed) {
6207
- this.finalizeAssistantStream();
6208
- }
6209
- const shouldRenderNarrative = Boolean(narrativeContent && (!metadata.wasStreamed || !blocksActive || !streamRendered));
6210
- if (shouldRenderNarrative) {
6211
- this.renderAssistantContent('thought', narrativeContent, {
6212
- ...enriched,
6213
- isFinal: false,
6214
- });
5477
+ // Skip display if content was already streamed to avoid double-display
5478
+ if (!metadata.wasStreamed) {
5479
+ display.showNarrative(content.trim());
6215
5480
  }
6216
5481
  // The isProcessing flag already shows "โณ Processing..." - no need for duplicate status
6217
5482
  this.requestPromptRefresh();
@@ -6224,8 +5489,8 @@ What's the next action?`;
6224
5489
  this.requestPromptRefresh();
6225
5490
  },
6226
5491
  onContextSquishing: (message) => {
6227
- // Stream notification in UI when auto context squishing occurs
6228
- display.stream(`\n๐Ÿ”„ ${message}\n`);
5492
+ // Show notification in UI when auto context squishing occurs
5493
+ display.showSystemMessage(`๐Ÿ”„ ${message}`);
6229
5494
  this.statusTracker.pushOverride('context-squish', 'Auto-squishing context', {
6230
5495
  detail: 'Reducing conversation history to fit within token limits',
6231
5496
  tone: 'warning',
@@ -6233,7 +5498,7 @@ What's the next action?`;
6233
5498
  },
6234
5499
  onContextRecovery: (attempt, maxAttempts, message) => {
6235
5500
  // Show recovery progress in UI
6236
- display.stream(`\nโšก Context Recovery (${attempt}/${maxAttempts}): ${message}\n`);
5501
+ display.showSystemMessage(`โšก Context Recovery (${attempt}/${maxAttempts}): ${message}`);
6237
5502
  },
6238
5503
  onContextPruned: (removedCount, stats) => {
6239
5504
  // Clear squish overlay if active
@@ -6241,43 +5506,40 @@ What's the next action?`;
6241
5506
  // Show notification that context was pruned
6242
5507
  const method = stats['method'];
6243
5508
  const percentage = stats['percentage'];
6244
- const summarized = stats['summarized'] === true;
6245
5509
  if (method === 'emergency-recovery') {
6246
- display.stream(`\nโœ… Context recovery complete. Removed ${removedCount} messages. Context now at ${percentage ?? 'unknown'}%\n`);
5510
+ display.showSystemMessage(`โœ… Context recovery complete. Removed ${removedCount} messages. Context now at ${percentage ?? 'unknown'}%`);
6247
5511
  }
6248
5512
  // Update context usage in UI
6249
5513
  if (typeof percentage === 'number') {
6250
5514
  this.updateContextUsage(percentage);
6251
5515
  }
6252
- if (summarized) {
6253
- display.showSystemMessage('Context summary captured. View the latest summaries with /contextlog.');
6254
- }
6255
5516
  // Ensure prompt remains visible at bottom after context messages
6256
- this.renderPromptArea();
5517
+ this.terminalInput.render();
6257
5518
  },
6258
5519
  onContinueAfterRecovery: () => {
6259
5520
  // Update UI to show we're continuing after context recovery
6260
- display.stream(`\n๐Ÿ”„ Continuing after context recovery...\n`);
5521
+ display.showSystemMessage(`๐Ÿ”„ Continuing after context recovery...`);
6261
5522
  this.updateStatusMessage('Retrying with reduced context...');
6262
- this.renderPromptArea();
5523
+ this.terminalInput.render();
6263
5524
  },
6264
5525
  onAutoContinue: (attempt, maxAttempts, _message) => {
6265
5526
  // Show auto-continue progress in UI
6266
5527
  display.showSystemMessage(`๐Ÿ”„ Auto-continue (${attempt}/${maxAttempts}): Model expressed intent, prompting to act...`);
6267
5528
  this.updateStatusMessage('Auto-continuing...');
6268
- this.renderPromptArea();
5529
+ this.terminalInput.render();
6269
5530
  },
6270
5531
  onCancelled: () => {
6271
5532
  // Update UI to show operation was cancelled
6272
5533
  display.showWarning('Operation cancelled.');
6273
5534
  this.uiUpdates.setMode('processing');
6274
- this.stopStreamingHeartbeat({ skipRender: true });
5535
+ this.stopStreamingHeartbeat();
6275
5536
  this.updateStatusMessage(null);
6276
5537
  this.terminalInput.setStreaming(false);
6277
- this.renderPromptArea();
5538
+ this.terminalInput.render();
6278
5539
  },
6279
- onVerificationNeeded: () => {
6280
- void this.enforceAutoVerification('verification');
5540
+ onVerificationNeeded: (response, context) => {
5541
+ this.lastAssistantResponse = response;
5542
+ void this.runAutoQualityChecks('verification', response, context);
6281
5543
  },
6282
5544
  });
6283
5545
  // Register global AI enhancer for explore tool - uses active model by default
@@ -6311,7 +5573,7 @@ What's the next action?`;
6311
5573
  resetChatBoxAfterModelSwap() {
6312
5574
  this.updateStatusMessage(null);
6313
5575
  this.terminalInput.setStreaming(false);
6314
- this.renderPromptArea();
5576
+ this.terminalInput.render();
6315
5577
  this.ensureReadlineReady();
6316
5578
  }
6317
5579
  /**
@@ -6374,22 +5636,19 @@ What's the next action?`;
6374
5636
  }
6375
5637
  buildThinkingDirective() {
6376
5638
  switch (this.thinkingMode) {
6377
- case 'concise':
6378
- return 'Concise thinking mode: respond directly with the final answer and skip <thinking> blocks unless the user explicitly asks for reasoning.';
6379
5639
  case 'extended':
6380
5640
  return [
6381
- 'Extended thinking mode: include a <thinking> block followed by a <response> block.',
5641
+ 'Extended thinking mode is enabled. Format every reply as:',
6382
5642
  '<thinking>',
6383
- 'Brief, structured reasoning (cite tools/files when relevant; no secrets).',
5643
+ 'Detailed multi-step reasoning (reference tool runs/files when relevant, keep secrets redacted, no code blocks unless citing filenames).',
6384
5644
  '</thinking>',
6385
5645
  '<response>',
6386
- 'Final answer with requested code/commands and next steps.',
5646
+ 'Final answer with actionable next steps and any code/commands requested.',
6387
5647
  '</response>',
6388
5648
  ].join('\n');
6389
5649
  case 'balanced':
6390
5650
  default:
6391
- // Balanced mode: show thinking for complex reasoning, planning, or multi-step tasks
6392
- return 'When reasoning through complex tasks, planning approaches, or making decisions, wrap your thought process in <thinking> tags before your response. Keep thinking concise and focused on the decision-making process.';
5651
+ return 'Balanced thinking mode: include a short <thinking>...</thinking> block before <response> when the reasoning is non-trivial; skip it for simple answers.';
6393
5652
  }
6394
5653
  }
6395
5654
  buildDisplayMetadata(metadata) {
@@ -6398,58 +5657,6 @@ What's the next action?`;
6398
5657
  contextWindowTokens: this.activeContextWindowTokens,
6399
5658
  };
6400
5659
  }
6401
- presentThoughtProcess(thinking, metadata, options) {
6402
- if (!thinking || this.hasShownThoughtProcess) {
6403
- return;
6404
- }
6405
- const summary = this.extractThoughtSummary(thinking);
6406
- if (summary) {
6407
- display.updateThinking(`๐Ÿ’ญ ${summary}`);
6408
- this.latestThoughtSummary = summary;
6409
- this.streamingStatusDetail = summary;
6410
- this.terminalInput.recordRecentAction(`๐Ÿ’ญ ${summary}`);
6411
- this.rebuildStreamingStatusLabel();
6412
- this.refreshStatusLine(true);
6413
- }
6414
- const shouldRender = !options?.wasStreamed || options?.renderWhenStreamed;
6415
- if (shouldRender) {
6416
- this.renderAssistantContent('thought', thinking, { ...metadata, isFinal: false });
6417
- }
6418
- this.hasShownThoughtProcess = true;
6419
- }
6420
- renderToolUsageSummary(metadata) {
6421
- const since = this.lastToolSummaryRenderedAt ?? this.lastRequestStartedAt ?? Date.now();
6422
- const operations = this.uiAdapter.getToolOperationsSince(since);
6423
- if (!operations.length) {
6424
- return;
6425
- }
6426
- let summaryLine = compactRenderer.formatCompactLine(operations, {
6427
- showIcons: true,
6428
- showTimings: true,
6429
- });
6430
- const warnings = this.uiAdapter.getRecentWarnings(since);
6431
- if (warnings.length) {
6432
- const limited = warnings.slice(0, 3);
6433
- const formattedWarnings = limited
6434
- .map((warning) => `${icons.warning} ${warning.tool}: ${warning.text}`)
6435
- .join(' โ€ข ');
6436
- summaryLine = `${summaryLine} ${theme.warning('[warnings]')} ${formattedWarnings}`;
6437
- }
6438
- if (!summaryLine.trim()) {
6439
- this.lastToolSummaryRenderedAt = Date.now();
6440
- return;
6441
- }
6442
- this.lastToolSummaryRenderedAt =
6443
- operations[operations.length - 1]?.startedAt ?? Date.now();
6444
- const blockMetadata = { ...metadata, isFinal: true };
6445
- if (this.assistantBlocksEnabled && this.assistantBlockRenderer) {
6446
- this.renderAssistantBlock('tools', summaryLine, blockMetadata);
6447
- return;
6448
- }
6449
- const header = this.formatAssistantHeader('tools', blockMetadata);
6450
- const block = `\n${header}\n${summaryLine}\n\n`;
6451
- this.enqueueAssistantStream(block);
6452
- }
6453
5660
  handleContextTelemetry(metadata, displayMetadata) {
6454
5661
  if (!metadata.isFinal) {
6455
5662
  return null;
@@ -6810,7 +6017,7 @@ What's the next action?`;
6810
6017
  }
6811
6018
  handleAgentSetupError(error, retryAction, providerOverride) {
6812
6019
  this.pendingInteraction = null;
6813
- const provider = providerOverride === undefined ? this.sessionState.provider : providerOverride;
6020
+ const provider = providerOverride ?? this.sessionState.provider;
6814
6021
  const apiKeyIssue = detectApiKeyError(error, provider);
6815
6022
  if (apiKeyIssue) {
6816
6023
  this.handleApiKeyIssue(apiKeyIssue, retryAction);
@@ -6825,14 +6032,13 @@ What's the next action?`;
6825
6032
  const detail = detailText ? ` Error: ${detailText}` : '';
6826
6033
  const reason = info.reason ? ` (${info.reason.replace(/-/g, ' ')})` : '';
6827
6034
  const partialNote = info.partialResponse ? ' Received partial stream before failure.' : '';
6828
- this.finalizeAssistantStream();
6829
6035
  display.showWarning(`Streaming failed${reason}, retrying without streaming.${detail}${partialNote}`);
6036
+ this.finishStreamingFormatter('Stream interrupted - retrying without streaming');
6830
6037
  this.startStreamingHeartbeat('Fallback in progress');
6831
6038
  this.requestPromptRefresh(true);
6832
6039
  }
6833
6040
  handleProviderError(error, retryAction) {
6834
- const providerHint = error instanceof MissingSecretError ? null : this.sessionState.provider;
6835
- const apiKeyIssue = detectApiKeyError(error, providerHint);
6041
+ const apiKeyIssue = detectApiKeyError(error, this.sessionState.provider);
6836
6042
  if (!apiKeyIssue) {
6837
6043
  return false;
6838
6044
  }
@@ -6841,19 +6047,13 @@ What's the next action?`;
6841
6047
  }
6842
6048
  handleApiKeyIssue(info, retryAction) {
6843
6049
  const secret = info.secret ?? null;
6844
- const providerLabel = info.provider
6845
- ? this.providerLabel(info.provider)
6846
- : secret?.providers?.length
6847
- ? this.providerLabel(secret.providers[0])
6848
- : null;
6849
- const targetLabel = providerLabel ?? secret?.label ?? 'this tool';
6850
- this.apiKeyGateActive = !!secret;
6050
+ const providerLabel = info.provider ? this.providerLabel(info.provider) : 'the selected provider';
6851
6051
  if (!secret) {
6852
6052
  this.pendingSecretRetry = null;
6853
6053
  const guidance = 'Run "/secrets" to configure the required API key or export it (e.g., EXPORT KEY=value) before launching the CLI.';
6854
6054
  const baseMessage = info.type === 'missing'
6855
- ? `An API key is required before using ${targetLabel}.`
6856
- : `API authentication failed for ${targetLabel}.`;
6055
+ ? `An API key is required before using ${providerLabel}.`
6056
+ : `API authentication failed for ${providerLabel}.`;
6857
6057
  display.showWarning(`${baseMessage} ${guidance}`.trim());
6858
6058
  return;
6859
6059
  }
@@ -6862,8 +6062,8 @@ What's the next action?`;
6862
6062
  display.showWarning(info.message.trim());
6863
6063
  }
6864
6064
  const prefix = isMissing
6865
- ? `${secret.label} is required before you can use ${targetLabel}.`
6866
- : `${secret.label} appears to be invalid for ${targetLabel}.`;
6065
+ ? `${secret.label} is required before you can use ${providerLabel}.`
6066
+ : `${secret.label} appears to be invalid for ${providerLabel}.`;
6867
6067
  display.showWarning(prefix);
6868
6068
  this.pendingSecretRetry = retryAction ?? null;
6869
6069
  this.pendingInteraction = { type: 'secret-input', secret };
@@ -6899,138 +6099,12 @@ What's the next action?`;
6899
6099
  this.sessionState.reasoningEffort = preset.reasoningEffort;
6900
6100
  }
6901
6101
  }
6902
- /**
6903
- * Build the session banner with comprehensive feature status.
6904
- */
6905
- getSessionFrameProps() {
6906
- return {
6907
- profileLabel: this.profileLabel,
6908
- profileName: this.profile,
6909
- model: this.sessionState.model,
6910
- provider: this.sessionState.provider,
6911
- workspace: this.workingDir,
6912
- version: this.version,
6913
- };
6914
- }
6915
- buildBanner() {
6916
- const terminalWidth = output.columns ?? 100;
6917
- const width = Math.min(terminalWidth - 4, 110);
6918
- return renderSessionFrame({
6919
- ...this.getSessionFrameProps(),
6920
- width,
6921
- });
6922
- }
6923
- /**
6924
- * Collect tool categories for banner display.
6925
- */
6926
- collectToolCategories() {
6927
- const categories = [];
6928
- try {
6929
- const providerTools = this.runtimeSession.toolRuntime.listProviderTools();
6930
- if (providerTools.length > 0) {
6931
- // Group by category (first word of tool name or namespace)
6932
- const groups = new Map();
6933
- for (const tool of providerTools) {
6934
- const category = this.extractToolCategory(tool.name);
6935
- groups.set(category, (groups.get(category) || 0) + 1);
6936
- }
6937
- // Convert to array sorted by count
6938
- const sorted = Array.from(groups.entries())
6939
- .sort((a, b) => b[1] - a[1])
6940
- .slice(0, 5); // Top 5 categories
6941
- for (const [name, count] of sorted) {
6942
- categories.push({ name, count, icon: this.getToolCategoryIcon(name) });
6943
- }
6944
- }
6945
- }
6946
- catch {
6947
- // Ignore errors in tool collection
6948
- }
6949
- return categories;
6950
- }
6951
- buildFeatureStatusSnapshot() {
6952
- const featureFlags = loadFeatureFlags();
6953
- const toolCategories = this.collectToolCategories();
6954
- const toolCount = toolCategories.reduce((sum, cat) => sum + cat.count, 0);
6955
- const pluginCount = this._enabledPlugins.length;
6956
- return {
6957
- pluginCount: pluginCount > 0 ? pluginCount : undefined,
6958
- toolCount: toolCount > 0 ? toolCount : undefined,
6959
- sessionId: this.activeSessionId,
6960
- mcpEnabled: featureFlags.mcpEnabled,
6961
- metricsEnabled: featureFlags.metrics,
6962
- autoCompact: featureFlags.autoCompact,
6963
- dualMode: featureFlags.alphaZeroDual,
6964
- };
6965
- }
6966
- /**
6967
- * Extract category from tool name.
6968
- */
6969
- extractToolCategory(toolName) {
6970
- // Common tool prefixes
6971
- const prefixMap = {
6972
- git: 'Git',
6973
- npm: 'NPM',
6974
- bash: 'Shell',
6975
- file: 'Files',
6976
- read: 'Files',
6977
- write: 'Files',
6978
- edit: 'Files',
6979
- search: 'Search',
6980
- glob: 'Search',
6981
- grep: 'Search',
6982
- web: 'Web',
6983
- fetch: 'Web',
6984
- test: 'Testing',
6985
- build: 'Build',
6986
- deploy: 'Deploy',
6987
- cloud: 'Cloud',
6988
- browser: 'Browser',
6989
- };
6990
- const lower = toolName.toLowerCase();
6991
- for (const [prefix, category] of Object.entries(prefixMap)) {
6992
- if (lower.startsWith(prefix)) {
6993
- return category;
6994
- }
6995
- }
6996
- // Default to first word capitalized
6997
- const firstWord = toolName.split(/[_\-\s]/)[0] || 'Other';
6998
- return firstWord.charAt(0).toUpperCase() + firstWord.slice(1).toLowerCase();
6999
- }
7000
- /**
7001
- * Get icon for tool category.
7002
- */
7003
- getToolCategoryIcon(category) {
7004
- const icons = {
7005
- Git: 'โއ',
7006
- NPM: '๐Ÿ“ฆ',
7007
- Shell: 'โŒ˜',
7008
- Files: '๐Ÿ“',
7009
- Search: '๐Ÿ”',
7010
- Web: '๐ŸŒ',
7011
- Testing: '๐Ÿงช',
7012
- Build: '๐Ÿ”ง',
7013
- Deploy: '๐Ÿš€',
7014
- Cloud: 'โ˜',
7015
- Browser: '๐ŸŒ',
7016
- };
7017
- return icons[category] || 'โš™';
7018
- }
7019
- refreshBannerSessionInfo() {
7020
- const nextState = {
7021
- model: this.sessionState.model,
7022
- provider: this.sessionState.provider,
7023
- };
7024
- const previous = this.bannerSessionState;
7025
- if (previous && previous.model === nextState.model && previous.provider === nextState.provider) {
7026
- return;
7027
- }
6102
+ refreshSessionContext() {
7028
6103
  this.refreshContextGauge();
7029
- // Banner is streamed once at launch; keep the control bar up to date without re-rendering it
7030
6104
  if (!this.isProcessing) {
7031
6105
  this.setIdleStatus();
7032
6106
  }
7033
- this.bannerSessionState = nextState;
6107
+ this.refreshStatusLine(true);
7034
6108
  }
7035
6109
  providerLabel(id) {
7036
6110
  return PROVIDER_LABELS[id] ?? id;
@@ -7137,7 +6211,7 @@ What's the next action?`;
7137
6211
  // Rebuild agent with new provider
7138
6212
  if (this.rebuildAgent()) {
7139
6213
  this.persistSessionPreference();
7140
- this.refreshBannerSessionInfo();
6214
+ this.refreshSessionContext();
7141
6215
  display.showInfo(`Switched from ${this.providerLabel(oldProvider)}/${oldModel} to ${match.label}/${defaultModel.id}`);
7142
6216
  this.resetChatBoxAfterModelSwap();
7143
6217
  }
@@ -7331,6 +6405,20 @@ What's the next action?`;
7331
6405
  ];
7332
6406
  display.showSystemMessage(lines.join('\n'));
7333
6407
  }
6408
+ /**
6409
+ * Set the cached provider status for unified status bar display.
6410
+ * Called once at startup after checking providers.
6411
+ */
6412
+ setProviderStatus(providers) {
6413
+ this.cachedProviderStatus = providers;
6414
+ }
6415
+ /**
6416
+ * Show the unified status bar (Claude Code style).
6417
+ * Displays provider indicators and ready hints before the prompt.
6418
+ */
6419
+ showUnifiedStatusBar() {
6420
+ display.showUnifiedStatusBar(this.cachedProviderStatus);
6421
+ }
7334
6422
  }
7335
6423
  function setsEqual(first, second) {
7336
6424
  if (first.size !== second.size) {