erosolar-cli 1.7.385 → 1.7.387

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 (59) hide show
  1. package/dist/core/secretStore.d.ts +1 -0
  2. package/dist/core/secretStore.d.ts.map +1 -1
  3. package/dist/core/secretStore.js +3 -0
  4. package/dist/core/secretStore.js.map +1 -1
  5. package/dist/core/toolRuntime.d.ts +6 -0
  6. package/dist/core/toolRuntime.d.ts.map +1 -1
  7. package/dist/core/toolRuntime.js +7 -0
  8. package/dist/core/toolRuntime.js.map +1 -1
  9. package/dist/runtime/agentHost.d.ts +3 -1
  10. package/dist/runtime/agentHost.d.ts.map +1 -1
  11. package/dist/runtime/agentHost.js +3 -0
  12. package/dist/runtime/agentHost.js.map +1 -1
  13. package/dist/runtime/agentSession.d.ts +2 -1
  14. package/dist/runtime/agentSession.d.ts.map +1 -1
  15. package/dist/runtime/agentSession.js +1 -0
  16. package/dist/runtime/agentSession.js.map +1 -1
  17. package/dist/runtime/universal.d.ts +2 -1
  18. package/dist/runtime/universal.d.ts.map +1 -1
  19. package/dist/runtime/universal.js +1 -0
  20. package/dist/runtime/universal.js.map +1 -1
  21. package/dist/shell/interactiveShell.d.ts +22 -0
  22. package/dist/shell/interactiveShell.d.ts.map +1 -1
  23. package/dist/shell/interactiveShell.js +213 -86
  24. package/dist/shell/interactiveShell.js.map +1 -1
  25. package/dist/shell/shellApp.d.ts.map +1 -1
  26. package/dist/shell/shellApp.js +5 -0
  27. package/dist/shell/shellApp.js.map +1 -1
  28. package/dist/shell/systemPrompt.d.ts.map +1 -1
  29. package/dist/shell/systemPrompt.js +4 -0
  30. package/dist/shell/systemPrompt.js.map +1 -1
  31. package/dist/shell/terminalInput.d.ts +23 -0
  32. package/dist/shell/terminalInput.d.ts.map +1 -1
  33. package/dist/shell/terminalInput.js +167 -26
  34. package/dist/shell/terminalInput.js.map +1 -1
  35. package/dist/shell/terminalInputAdapter.d.ts +9 -0
  36. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  37. package/dist/shell/terminalInputAdapter.js +8 -2
  38. package/dist/shell/terminalInputAdapter.js.map +1 -1
  39. package/dist/tools/fileTools.d.ts.map +1 -1
  40. package/dist/tools/fileTools.js +29 -5
  41. package/dist/tools/fileTools.js.map +1 -1
  42. package/dist/tools/grepTools.d.ts.map +1 -1
  43. package/dist/tools/grepTools.js +22 -4
  44. package/dist/tools/grepTools.js.map +1 -1
  45. package/dist/tools/searchTools.d.ts.map +1 -1
  46. package/dist/tools/searchTools.js +47 -13
  47. package/dist/tools/searchTools.js.map +1 -1
  48. package/dist/tools/webTools.d.ts.map +1 -1
  49. package/dist/tools/webTools.js +36 -9
  50. package/dist/tools/webTools.js.map +1 -1
  51. package/dist/ui/display.d.ts +1 -0
  52. package/dist/ui/display.d.ts.map +1 -1
  53. package/dist/ui/display.js +50 -11
  54. package/dist/ui/display.js.map +1 -1
  55. package/dist/ui/layout.js +8 -7
  56. package/dist/ui/layout.js.map +1 -1
  57. package/dist/ui/unified/layout.js +2 -2
  58. package/dist/ui/unified/layout.js.map +1 -1
  59. package/package.json +1 -1
@@ -102,6 +102,8 @@ export class InteractiveShell {
102
102
  pendingCleanup = null;
103
103
  cleanupInProgress = false;
104
104
  slashPreviewVisible = false;
105
+ lastLoggedPrompt = null;
106
+ lastLoggedPromptAt = 0;
105
107
  skillRepository;
106
108
  skillToolHandlers = new Map();
107
109
  thinkingMode = 'balanced';
@@ -151,9 +153,12 @@ export class InteractiveShell {
151
153
  autoBuildInFlight = false;
152
154
  lastAutoBuildRun = null;
153
155
  // Streaming UX tracking
156
+ batchedOutputMode;
154
157
  streamingHeartbeatStart = null;
155
158
  streamingHeartbeatFrame = 0;
156
159
  streamingStatusLabel = null;
160
+ streamingUiActive = false;
161
+ pendingStreamBuffer = '';
157
162
  lastStreamingElapsedSeconds = null; // Preserve final elapsed time
158
163
  statusLineState = null;
159
164
  statusMessageOverride = null;
@@ -175,6 +180,8 @@ export class InteractiveShell {
175
180
  this.sessionRestoreConfig = config.sessionRestore ?? { mode: 'none' };
176
181
  this._enabledPlugins = config.enabledPlugins ?? [];
177
182
  this.version = config.version ?? '0.0.0';
183
+ const streamingEnv = process.env['EROSOLAR_STREAMING_UI']?.toLowerCase();
184
+ this.batchedOutputMode = streamingEnv !== 'on' && streamingEnv !== 'true' && streamingEnv !== '1';
178
185
  // Alternate screen disabled - use terminal-native mode for proper scrollback and text selection
179
186
  this.alternateScreenEnabled = false;
180
187
  this.initializeSessionHistory();
@@ -277,7 +284,7 @@ export class InteractiveShell {
277
284
  }
278
285
  // Stream banner first - this sets up scroll region dynamically
279
286
  const banner = this.buildBanner();
280
- this.terminalInput.streamContent(banner + '\n\n');
287
+ this.terminalInput.streamContent(banner + '\n');
281
288
  // Render chat box after banner is streamed
282
289
  this.refreshControlBar();
283
290
  this.terminalInput.forceRender();
@@ -371,7 +378,6 @@ export class InteractiveShell {
371
378
  void maybeOfferCliUpdate(this.version);
372
379
  }
373
380
  if (initialPrompt) {
374
- this.logUserPrompt(initialPrompt);
375
381
  await this.processInputBlock(initialPrompt);
376
382
  return;
377
383
  }
@@ -399,7 +405,6 @@ export class InteractiveShell {
399
405
  }
400
406
  // DON'T clear the input here - keep it visible while streaming.
401
407
  // The input will be cleared after streaming completes in the finally block.
402
- this.logUserPrompt(approved);
403
408
  void this.processInputBlock(approved).catch((err) => {
404
409
  display.showError(err instanceof Error ? err.message : String(err), err);
405
410
  });
@@ -779,6 +784,21 @@ export class InteractiveShell {
779
784
  // During streaming we still want the spinner prefix; when idle force a fast refresh.
780
785
  this.refreshStatusLine(!this.isProcessing);
781
786
  }
787
+ /**
788
+ * Inline menu/panel helpers (rendered in TerminalInput, not in scrollback).
789
+ */
790
+ showInlineMenu(title, lines, tone = 'info', hint) {
791
+ const merged = [...lines];
792
+ if (hint) {
793
+ merged.push('', tone === 'error' ? theme.error(hint) : theme.warning(hint));
794
+ }
795
+ this.terminalInput.setInlinePanel({ title, lines: merged, tone });
796
+ this.terminalInput.render();
797
+ }
798
+ clearInlineMenu() {
799
+ this.terminalInput.setInlinePanel(null);
800
+ this.terminalInput.render();
801
+ }
782
802
  async handleToolSettingsInput(input) {
783
803
  const pending = this.pendingInteraction;
784
804
  if (!pending || pending.type !== 'tool-settings') {
@@ -1089,6 +1109,8 @@ export class InteractiveShell {
1089
1109
  this.statusTracker.reset();
1090
1110
  }
1091
1111
  setIdleStatus(detail) {
1112
+ // Reset streaming timer display when returning to idle so elapsed doesn't linger
1113
+ this.lastStreamingElapsedSeconds = null;
1092
1114
  this.statusTracker.setBase('Ready for prompts', {
1093
1115
  detail: this.describeStatusDetail(detail),
1094
1116
  tone: 'success',
@@ -1255,10 +1277,18 @@ export class InteractiveShell {
1255
1277
  * Log user prompt to the scroll region so it's part of the conversation flow.
1256
1278
  */
1257
1279
  logUserPrompt(text) {
1258
- if (!text.trim())
1280
+ const normalized = text.trim();
1281
+ if (!normalized)
1259
1282
  return;
1283
+ // Skip duplicate renders of the exact same prompt if they happen back-to-back.
1284
+ const now = Date.now();
1285
+ if (this.lastLoggedPrompt === normalized && now - this.lastLoggedPromptAt < 2000) {
1286
+ return;
1287
+ }
1288
+ this.lastLoggedPrompt = normalized;
1289
+ this.lastLoggedPromptAt = now;
1260
1290
  // Format with user prompt prefix and write to scroll region
1261
- const formatted = `${theme.user('>')} ${text}\n`;
1291
+ const formatted = `${theme.user('>')} ${normalized}\n`;
1262
1292
  this.terminalInput.writeToScrollRegion(formatted);
1263
1293
  }
1264
1294
  requestPromptRefresh(force = false) {
@@ -1280,17 +1310,49 @@ export class InteractiveShell {
1280
1310
  this.promptRefreshTimer = null;
1281
1311
  }
1282
1312
  }
1313
+ resetStreamBuffer() {
1314
+ this.pendingStreamBuffer = '';
1315
+ }
1316
+ captureStreamChunk(chunk) {
1317
+ if (!this.batchedOutputMode || !chunk) {
1318
+ return;
1319
+ }
1320
+ this.pendingStreamBuffer += chunk;
1321
+ }
1322
+ consumeStreamBuffer(fallback) {
1323
+ if (!this.batchedOutputMode) {
1324
+ return fallback;
1325
+ }
1326
+ const buffered = this.pendingStreamBuffer;
1327
+ this.pendingStreamBuffer = '';
1328
+ return buffered || fallback;
1329
+ }
1330
+ currentRunElapsedMs() {
1331
+ if (!this.streamingHeartbeatStart) {
1332
+ return undefined;
1333
+ }
1334
+ return Date.now() - this.streamingHeartbeatStart;
1335
+ }
1283
1336
  startStreamingHeartbeat(label = 'Streaming') {
1284
1337
  this.stopStreamingHeartbeat();
1285
- // Enter global streaming mode - blocks all non-streaming UI output
1286
- enterStreamingMode();
1287
- // Set up scroll region for streaming content
1288
- this.terminalInput.enterStreamingScrollRegion();
1289
- this.uiUpdates.setMode('streaming');
1338
+ this.resetStreamBuffer();
1339
+ this.lastStreamingElapsedSeconds = null;
1290
1340
  this.streamingHeartbeatStart = Date.now();
1291
1341
  this.streamingHeartbeatFrame = 0;
1292
1342
  const initialFrame = STREAMING_SPINNER_FRAMES[this.streamingHeartbeatFrame];
1293
1343
  this.streamingStatusLabel = this.buildStreamingStatus(`${initialFrame} ${label}`, 0);
1344
+ if (this.batchedOutputMode) {
1345
+ this.streamingUiActive = false;
1346
+ this.uiUpdates.setMode('processing');
1347
+ }
1348
+ else {
1349
+ // Enter global streaming mode - blocks all non-streaming UI output
1350
+ enterStreamingMode();
1351
+ // Set up scroll region for streaming content
1352
+ this.terminalInput.enterStreamingScrollRegion();
1353
+ this.streamingUiActive = true;
1354
+ this.uiUpdates.setMode('streaming');
1355
+ }
1294
1356
  display.updateStreamingStatus(this.streamingStatusLabel);
1295
1357
  this.refreshStatusLine(true);
1296
1358
  // Periodically refresh the pinned input/status region while streaming so
@@ -1320,18 +1382,21 @@ export class InteractiveShell {
1320
1382
  });
1321
1383
  }
1322
1384
  stopStreamingHeartbeat() {
1323
- // Exit global streaming mode - allows UI to render again
1324
- exitStreamingMode();
1325
1385
  // Preserve final elapsed time before clearing heartbeat start
1326
1386
  if (this.streamingHeartbeatStart) {
1327
1387
  this.lastStreamingElapsedSeconds = Math.max(0, Math.floor((Date.now() - this.streamingHeartbeatStart) / 1000));
1328
1388
  }
1329
- // Exit scroll region mode
1330
- this.terminalInput.exitStreamingScrollRegion();
1389
+ if (this.streamingUiActive) {
1390
+ // Exit global streaming mode - allows UI to render again
1391
+ exitStreamingMode();
1392
+ // Exit scroll region mode
1393
+ this.terminalInput.exitStreamingScrollRegion();
1394
+ }
1331
1395
  this.uiUpdates.stopHeartbeat('streaming');
1332
1396
  this.streamingHeartbeatStart = null;
1333
1397
  this.streamingHeartbeatFrame = 0;
1334
1398
  this.streamingStatusLabel = null;
1399
+ this.streamingUiActive = false;
1335
1400
  // Clear streaming label specifically (keeps override and main status if set)
1336
1401
  this.terminalInput.setStreamingLabel(null);
1337
1402
  // Clear streaming status from display
@@ -1499,6 +1564,10 @@ export class InteractiveShell {
1499
1564
  return false;
1500
1565
  }
1501
1566
  switch (this.pendingInteraction.type) {
1567
+ case 'model-loading':
1568
+ display.showInfo('Still fetching model options. Please wait a moment.');
1569
+ this.terminalInput.render();
1570
+ return true;
1502
1571
  case 'model-provider':
1503
1572
  await this.handleModelProviderSelection(input);
1504
1573
  return true;
@@ -3633,28 +3702,25 @@ export class InteractiveShell {
3633
3702
  return lines.join('\n');
3634
3703
  }
3635
3704
  async showModelMenu() {
3636
- display.showSystemMessage(theme.ui.muted('Fetching latest models from providers...'));
3637
- // Fetch live models from all configured providers
3638
- const providerStatuses = await quickCheckProviders();
3639
- const providerOptions = this.buildProviderOptionsWithDiscovery(providerStatuses);
3640
- if (!providerOptions.length) {
3641
- display.showWarning('No providers are available.');
3642
- return;
3705
+ // Hold input immediately so numeric selections don't get queued as prompts while we fetch
3706
+ this.pendingInteraction = { type: 'model-loading' };
3707
+ this.showInlineMenu('Model provider', [theme.ui.muted('Fetching latest models from providers...')], 'info');
3708
+ try {
3709
+ // Fetch live models from all configured providers
3710
+ const providerStatuses = await quickCheckProviders();
3711
+ const providerOptions = this.buildProviderOptionsWithDiscovery(providerStatuses);
3712
+ if (!providerOptions.length) {
3713
+ this.pendingInteraction = null;
3714
+ this.showInlineMenu('Model provider', [theme.warning('No providers are available.')], 'warning');
3715
+ return;
3716
+ }
3717
+ this.pendingInteraction = { type: 'model-provider', options: providerOptions };
3718
+ this.renderProviderMenu(providerOptions);
3719
+ }
3720
+ catch (error) {
3721
+ this.pendingInteraction = null;
3722
+ this.showInlineMenu('Model provider', [theme.error('Failed to load model list. Try again in a moment.')], 'error');
3643
3723
  }
3644
- const lines = [
3645
- theme.bold('Select a provider:'),
3646
- ...providerOptions.map((option, index) => {
3647
- const isCurrent = option.provider === this.sessionState.provider;
3648
- const countLabel = `${option.modelCount} model${option.modelCount === 1 ? '' : 's'}`;
3649
- const latestLabel = option.latestModel ? theme.success(` (latest: ${option.latestModel})`) : '';
3650
- const label = this.colorizeDropdownLine(`${index + 1}. ${option.label} — ${countLabel}${latestLabel}`, index);
3651
- const suffix = isCurrent ? ` ${theme.primary('• current')}` : '';
3652
- return `${label}${suffix}`;
3653
- }),
3654
- 'Type the number of the provider to continue, or type "cancel".',
3655
- ];
3656
- display.showSystemMessage(lines.join('\n'));
3657
- this.pendingInteraction = { type: 'model-provider', options: providerOptions };
3658
3724
  }
3659
3725
  buildProviderOptions() {
3660
3726
  const counts = new Map();
@@ -3744,6 +3810,21 @@ export class InteractiveShell {
3744
3810
  };
3745
3811
  });
3746
3812
  }
3813
+ renderProviderMenu(options, hint) {
3814
+ const lines = [
3815
+ theme.bold('Select a provider:'),
3816
+ ...options.map((option, index) => {
3817
+ const isCurrent = option.provider === this.sessionState.provider;
3818
+ const countLabel = `${option.modelCount} model${option.modelCount === 1 ? '' : 's'}`;
3819
+ const latestLabel = option.latestModel ? theme.success(` (latest: ${option.latestModel})`) : '';
3820
+ const label = this.colorizeDropdownLine(`${index + 1}. ${option.label} — ${countLabel}${latestLabel}`, index);
3821
+ const suffix = isCurrent ? ` ${theme.primary('• current')}` : '';
3822
+ return `${label}${suffix}`;
3823
+ }),
3824
+ 'Type the number of the provider to continue, or type "cancel".',
3825
+ ];
3826
+ this.showInlineMenu('Model provider', lines, 'info', hint);
3827
+ }
3747
3828
  showProviderModels(option) {
3748
3829
  // Start with static presets
3749
3830
  const staticModels = MODEL_PRESETS.filter((preset) => preset.provider === option.provider);
@@ -3783,13 +3864,17 @@ export class InteractiveShell {
3783
3864
  }
3784
3865
  }
3785
3866
  if (!allModels.length) {
3786
- display.showWarning(`No models available for ${option.label}.`);
3867
+ this.showInlineMenu('Model selection', [theme.warning(`No models available for ${option.label}.`)], 'warning');
3787
3868
  this.pendingInteraction = null;
3788
3869
  return;
3789
3870
  }
3871
+ this.renderModelSelection(allModels, option, null);
3872
+ this.pendingInteraction = { type: 'model', provider: option.provider, options: allModels };
3873
+ }
3874
+ renderModelSelection(models, option, hint) {
3790
3875
  const lines = [
3791
3876
  theme.bold(`Select a model from ${option.label}:`),
3792
- ...allModels.map((preset, index) => {
3877
+ ...models.map((preset, index) => {
3793
3878
  const isCurrent = preset.id === this.sessionState.model;
3794
3879
  const isLatest = preset.id === option.latestModel;
3795
3880
  const latestBadge = isLatest ? theme.success(' ā˜… LATEST') : '';
@@ -3800,11 +3885,23 @@ export class InteractiveShell {
3800
3885
  }),
3801
3886
  'Type the number of the model to select it, type "back" to change provider, or type "cancel".',
3802
3887
  ];
3803
- display.showSystemMessage(lines.join('\n'));
3804
- this.pendingInteraction = { type: 'model', provider: option.provider, options: allModels };
3888
+ this.showInlineMenu('Model selection', lines, 'info', hint ?? undefined);
3889
+ }
3890
+ buildProviderContext(provider, models) {
3891
+ return {
3892
+ provider,
3893
+ label: this.providerLabel(provider),
3894
+ modelCount: models.length,
3895
+ latestModel: models[0]?.id,
3896
+ discoveredModels: [],
3897
+ };
3805
3898
  }
3806
3899
  showSecretsMenu() {
3807
3900
  const definitions = listSecretDefinitions();
3901
+ this.pendingInteraction = { type: 'secret-select', options: definitions };
3902
+ this.renderSecretsMenu(definitions);
3903
+ }
3904
+ renderSecretsMenu(definitions, hint) {
3808
3905
  const lines = [
3809
3906
  theme.bold('Manage Secrets:'),
3810
3907
  ...definitions.map((definition, index) => {
@@ -3816,8 +3913,18 @@ export class InteractiveShell {
3816
3913
  }),
3817
3914
  'Enter the number to update a key, or type "cancel".',
3818
3915
  ];
3819
- display.showSystemMessage(lines.join('\n'));
3820
- this.pendingInteraction = { type: 'secret-select', options: definitions };
3916
+ this.showInlineMenu('Secrets', lines, 'info', hint);
3917
+ }
3918
+ renderSecretInput(secret, hint) {
3919
+ const value = getSecretValue(secret.id);
3920
+ const status = value ? maskSecret(value) : theme.warning('not set');
3921
+ const providers = secret.providers.map((id) => this.providerLabel(id)).join(', ');
3922
+ const lines = [
3923
+ `${secret.label} (${providers})`,
3924
+ `Current: ${status}`,
3925
+ 'Enter a new value or type "cancel".',
3926
+ ];
3927
+ this.showInlineMenu('Update secret', lines, 'info', hint);
3821
3928
  }
3822
3929
  showToolsMenu() {
3823
3930
  const options = getToolToggleOptions();
@@ -4197,26 +4304,23 @@ export class InteractiveShell {
4197
4304
  }
4198
4305
  const trimmed = input.trim();
4199
4306
  if (!trimmed) {
4200
- display.showWarning('Enter a number or type cancel.');
4201
- this.terminalInput.render();
4307
+ this.renderProviderMenu(pending.options, 'Enter a number or type cancel.');
4202
4308
  return;
4203
4309
  }
4204
4310
  if (trimmed.toLowerCase() === 'cancel') {
4205
4311
  this.pendingInteraction = null;
4206
- display.showInfo('Model selection cancelled.');
4207
- this.terminalInput.render();
4312
+ this.clearInlineMenu();
4313
+ this.updateStatusMessage('Model selection cancelled.');
4208
4314
  return;
4209
4315
  }
4210
4316
  const choice = Number.parseInt(trimmed, 10);
4211
4317
  if (!Number.isFinite(choice)) {
4212
- display.showWarning('Please enter a valid number.');
4213
- this.terminalInput.render();
4318
+ this.renderProviderMenu(pending.options, 'Please enter a valid number.');
4214
4319
  return;
4215
4320
  }
4216
4321
  const option = pending.options[choice - 1];
4217
4322
  if (!option) {
4218
- display.showWarning('That option is not available.');
4219
- this.terminalInput.render();
4323
+ this.renderProviderMenu(pending.options, 'That option is not available.');
4220
4324
  return;
4221
4325
  }
4222
4326
  this.showProviderModels(option);
@@ -4227,10 +4331,10 @@ export class InteractiveShell {
4227
4331
  if (!pending || pending.type !== 'model') {
4228
4332
  return;
4229
4333
  }
4334
+ const providerContext = this.buildProviderContext(pending.provider, pending.options);
4230
4335
  const trimmed = input.trim();
4231
4336
  if (!trimmed) {
4232
- display.showWarning('Enter a number, type "back", or type "cancel".');
4233
- this.terminalInput.render();
4337
+ this.renderModelSelection(pending.options, providerContext, 'Enter a number, type "back", or type "cancel".');
4234
4338
  return;
4235
4339
  }
4236
4340
  if (trimmed.toLowerCase() === 'back') {
@@ -4240,20 +4344,18 @@ export class InteractiveShell {
4240
4344
  }
4241
4345
  if (trimmed.toLowerCase() === 'cancel') {
4242
4346
  this.pendingInteraction = null;
4243
- display.showInfo('Model selection cancelled.');
4244
- this.terminalInput.render();
4347
+ this.clearInlineMenu();
4348
+ this.updateStatusMessage('Model selection cancelled.');
4245
4349
  return;
4246
4350
  }
4247
4351
  const choice = Number.parseInt(trimmed, 10);
4248
4352
  if (!Number.isFinite(choice)) {
4249
- display.showWarning('Please enter a valid number.');
4250
- this.terminalInput.render();
4353
+ this.renderModelSelection(pending.options, providerContext, 'Please enter a valid number.');
4251
4354
  return;
4252
4355
  }
4253
4356
  const preset = pending.options[choice - 1];
4254
4357
  if (!preset) {
4255
- display.showWarning('That option is not available.');
4256
- this.terminalInput.render();
4358
+ this.renderModelSelection(pending.options, providerContext, 'That option is not available.');
4257
4359
  return;
4258
4360
  }
4259
4361
  this.pendingInteraction = null;
@@ -4277,11 +4379,14 @@ export class InteractiveShell {
4277
4379
  };
4278
4380
  this.applyPresetReasoningDefaults();
4279
4381
  if (this.rebuildAgent()) {
4280
- display.showInfo(`Switched to ${preset.label}.`);
4382
+ this.showInlineMenu('Model updated', [`Switched to ${preset.label}.`], 'success');
4281
4383
  this.refreshBannerSessionInfo();
4282
4384
  this.persistSessionPreference();
4283
4385
  this.resetChatBoxAfterModelSwap();
4284
4386
  }
4387
+ else {
4388
+ this.showInlineMenu('Model updated', [`Using ${preset.label}.`], 'info');
4389
+ }
4285
4390
  }
4286
4391
  async handleSecretSelection(input) {
4287
4392
  const pending = this.pendingInteraction;
@@ -4290,30 +4395,27 @@ export class InteractiveShell {
4290
4395
  }
4291
4396
  const trimmed = input.trim();
4292
4397
  if (!trimmed) {
4293
- display.showWarning('Enter a number or type cancel.');
4294
- this.terminalInput.render();
4398
+ this.renderSecretsMenu(pending.options, 'Enter a number or type cancel.');
4295
4399
  return;
4296
4400
  }
4297
4401
  if (trimmed.toLowerCase() === 'cancel') {
4298
4402
  this.pendingInteraction = null;
4299
- display.showInfo('Secret management cancelled.');
4300
- this.terminalInput.render();
4403
+ this.clearInlineMenu();
4404
+ this.updateStatusMessage('Secret management cancelled.');
4301
4405
  return;
4302
4406
  }
4303
4407
  const choice = Number.parseInt(trimmed, 10);
4304
4408
  if (!Number.isFinite(choice)) {
4305
- display.showWarning('Please enter a valid number.');
4306
- this.terminalInput.render();
4409
+ this.renderSecretsMenu(pending.options, 'Please enter a valid number.');
4307
4410
  return;
4308
4411
  }
4309
4412
  const secret = pending.options[choice - 1];
4310
4413
  if (!secret) {
4311
- display.showWarning('That option is not available.');
4312
- this.terminalInput.render();
4414
+ this.renderSecretsMenu(pending.options, 'That option is not available.');
4313
4415
  return;
4314
4416
  }
4315
- display.showSystemMessage(`Enter a new value for ${secret.label} or type "cancel".`);
4316
4417
  this.pendingInteraction = { type: 'secret-input', secret };
4418
+ this.renderSecretInput(secret);
4317
4419
  this.terminalInput.render();
4318
4420
  }
4319
4421
  async handleSecretInput(input) {
@@ -4323,20 +4425,20 @@ export class InteractiveShell {
4323
4425
  }
4324
4426
  const trimmed = input.trim();
4325
4427
  if (!trimmed) {
4326
- display.showWarning('Enter a value or type cancel.');
4327
- this.terminalInput.render();
4428
+ this.renderSecretInput(pending.secret, 'Enter a value or type cancel.');
4328
4429
  return;
4329
4430
  }
4330
4431
  if (trimmed.toLowerCase() === 'cancel') {
4331
4432
  this.pendingInteraction = null;
4332
4433
  this.pendingSecretRetry = null;
4333
- display.showInfo('Secret unchanged.');
4434
+ this.clearInlineMenu();
4435
+ this.updateStatusMessage('Secret unchanged.');
4334
4436
  this.terminalInput.render();
4335
4437
  return;
4336
4438
  }
4337
4439
  try {
4338
4440
  setSecretValue(pending.secret.id, trimmed);
4339
- display.showInfo(`${pending.secret.label} updated.`);
4441
+ this.showInlineMenu('Secret updated', [`${pending.secret.label} saved.`], 'success');
4340
4442
  this.pendingInteraction = null;
4341
4443
  const deferred = this.pendingSecretRetry;
4342
4444
  this.pendingSecretRetry = null;
@@ -4351,7 +4453,7 @@ export class InteractiveShell {
4351
4453
  }
4352
4454
  catch (error) {
4353
4455
  const message = error instanceof Error ? error.message : String(error);
4354
- display.showError(message);
4456
+ this.showInlineMenu('Secret error', [message], 'error');
4355
4457
  this.pendingInteraction = null;
4356
4458
  this.pendingSecretRetry = null;
4357
4459
  }
@@ -4370,6 +4472,7 @@ export class InteractiveShell {
4370
4472
  if (!agent) {
4371
4473
  return;
4372
4474
  }
4475
+ this.logUserPrompt(request);
4373
4476
  this.isProcessing = true;
4374
4477
  this.uiUpdates.setMode('processing');
4375
4478
  this.terminalInput.setStreaming(true);
@@ -4389,7 +4492,8 @@ export class InteractiveShell {
4389
4492
  let responseText = '';
4390
4493
  try {
4391
4494
  // Start streaming - no header needed, the input area already provides context
4392
- this.startStreamingHeartbeat('Streaming response');
4495
+ const heartbeatLabel = this.batchedOutputMode ? 'Processing response' : 'Streaming response';
4496
+ this.startStreamingHeartbeat(heartbeatLabel);
4393
4497
  responseText = await agent.send(request, true);
4394
4498
  await this.awaitPendingCleanup();
4395
4499
  this.captureHistorySnapshot();
@@ -4505,7 +4609,8 @@ export class InteractiveShell {
4505
4609
  this.uiAdapter.startProcessing('Continuous execution mode');
4506
4610
  this.setProcessingStatus();
4507
4611
  // No streaming header - just start streaming directly
4508
- this.startStreamingHeartbeat('Streaming');
4612
+ const continuousLabel = this.batchedOutputMode ? 'Processing' : 'Streaming';
4613
+ this.startStreamingHeartbeat(continuousLabel);
4509
4614
  let iteration = 0;
4510
4615
  let lastResponse = '';
4511
4616
  let consecutiveNoProgress = 0;
@@ -4533,9 +4638,10 @@ When truly finished with ALL tasks, explicitly state "TASK_FULLY_COMPLETE".`;
4533
4638
  display.showSystemMessage(`\nšŸ“ Iteration ${iteration}/${MAX_ITERATIONS}`);
4534
4639
  this.updateStatusMessage(`Working on iteration ${iteration}...`);
4535
4640
  try {
4536
- // Send the request and capture the response (streaming disabled)
4641
+ // Send the request and capture the response as a single block
4537
4642
  display.showThinking('Responding...');
4538
4643
  this.refreshStatusLine(true);
4644
+ this.resetStreamBuffer();
4539
4645
  const response = await agent.send(currentPrompt, true);
4540
4646
  await this.awaitPendingCleanup();
4541
4647
  this.captureHistorySnapshot();
@@ -5036,6 +5142,7 @@ What's the next action?`;
5036
5142
  // Send the error to the agent for fixing
5037
5143
  display.showThinking('Analyzing build errors');
5038
5144
  this.refreshStatusLine(true);
5145
+ this.resetStreamBuffer();
5039
5146
  const response = await this.agent.send(prompt, true);
5040
5147
  display.stopThinking();
5041
5148
  this.refreshStatusLine(true);
@@ -5065,27 +5172,33 @@ What's the next action?`;
5065
5172
  };
5066
5173
  this.agent = this.runtimeSession.createAgent(selection, {
5067
5174
  onStreamChunk: (chunk) => {
5175
+ if (this.batchedOutputMode) {
5176
+ this.captureStreamChunk(chunk);
5177
+ return;
5178
+ }
5068
5179
  // Stream output using clean streamContent() - chat box floats below
5069
5180
  this.terminalInput.streamContent(chunk);
5070
5181
  },
5071
5182
  onStreamFallback: (info) => this.handleStreamingFallback(info),
5072
5183
  onAssistantMessage: (content, metadata) => {
5073
5184
  const enriched = this.buildDisplayMetadata(metadata);
5185
+ const alreadyStreamedToUi = !this.batchedOutputMode && metadata.wasStreamed;
5186
+ const bufferedContent = this.consumeStreamBuffer(content);
5074
5187
  // Update spinner based on message type
5075
5188
  if (metadata.isFinal) {
5076
5189
  // Skip display if content was already streamed to avoid double-display
5077
- if (!metadata.wasStreamed) {
5078
- const parsed = this.splitThinkingResponse(content);
5190
+ if (!alreadyStreamedToUi) {
5191
+ const parsed = this.splitThinkingResponse(bufferedContent);
5079
5192
  if (parsed?.thinking) {
5080
5193
  const summary = this.extractThoughtSummary(parsed.thinking);
5081
5194
  if (summary) {
5082
5195
  display.updateThinking(`šŸ’­ ${summary}`);
5083
5196
  }
5084
- display.showAssistantMessage(parsed.thinking, { ...enriched, isFinal: false });
5197
+ display.showThinkingBlock(parsed.thinking, enriched.elapsedMs ?? this.currentRunElapsedMs());
5085
5198
  }
5086
- const finalContent = parsed?.response?.trim() || content;
5199
+ const finalContent = parsed?.response?.trim() || bufferedContent.trim();
5087
5200
  if (finalContent) {
5088
- display.showAssistantMessage(finalContent, enriched);
5201
+ display.showAssistantMessage(finalContent, { ...enriched, isFinal: true });
5089
5202
  }
5090
5203
  }
5091
5204
  // Status shown in mode controls bar - no separate status line needed
@@ -5107,8 +5220,13 @@ What's the next action?`;
5107
5220
  // Stop spinner and show the narrative text directly
5108
5221
  display.stopThinking();
5109
5222
  // Skip display if content was already streamed to avoid double-display
5110
- if (!metadata.wasStreamed) {
5111
- display.showNarrative(content.trim());
5223
+ if (!alreadyStreamedToUi) {
5224
+ const parsed = this.splitThinkingResponse(bufferedContent);
5225
+ const thoughtContent = parsed?.thinking ?? parsed?.response ?? bufferedContent;
5226
+ const trimmed = thoughtContent.trim();
5227
+ if (trimmed) {
5228
+ display.showThinkingBlock(trimmed, enriched.elapsedMs ?? this.currentRunElapsedMs());
5229
+ }
5112
5230
  }
5113
5231
  // The isProcessing flag already shows "ā³ Processing..." - no need for duplicate status
5114
5232
  this.requestPromptRefresh();
@@ -5667,7 +5785,12 @@ What's the next action?`;
5667
5785
  const reason = info.reason ? ` (${info.reason.replace(/-/g, ' ')})` : '';
5668
5786
  const partialNote = info.partialResponse ? ' Received partial stream before failure.' : '';
5669
5787
  display.showWarning(`Streaming failed${reason}, retrying without streaming.${detail}${partialNote}`);
5670
- this.startStreamingHeartbeat('Fallback in progress');
5788
+ const bufferedPartial = this.consumeStreamBuffer(info.partialResponse ?? '');
5789
+ if (this.batchedOutputMode && bufferedPartial.trim()) {
5790
+ display.showThinkingBlock(bufferedPartial.trim(), this.currentRunElapsedMs());
5791
+ }
5792
+ const fallbackLabel = this.batchedOutputMode ? 'Retrying (batched)' : 'Fallback in progress';
5793
+ this.startStreamingHeartbeat(fallbackLabel);
5671
5794
  this.requestPromptRefresh(true);
5672
5795
  }
5673
5796
  handleProviderError(error, retryAction) {
@@ -5678,9 +5801,9 @@ What's the next action?`;
5678
5801
  this.handleApiKeyIssue(apiKeyIssue, retryAction);
5679
5802
  return true;
5680
5803
  }
5681
- handleApiKeyIssue(info, retryAction) {
5804
+ handleApiKeyIssue(info, retryAction, contextLabel) {
5682
5805
  const secret = info.secret ?? null;
5683
- const providerLabel = info.provider ? this.providerLabel(info.provider) : 'the selected provider';
5806
+ const providerLabel = contextLabel ?? (info.provider ? this.providerLabel(info.provider) : 'the selected provider');
5684
5807
  if (!secret) {
5685
5808
  this.pendingSecretRetry = null;
5686
5809
  const guidance = 'Run "/secrets" to configure the required API key or export it (e.g., EXPORT KEY=value) before launching the CLI.';
@@ -5702,6 +5825,10 @@ What's the next action?`;
5702
5825
  this.pendingInteraction = { type: 'secret-input', secret };
5703
5826
  this.showSecretGuidance(secret, isMissing);
5704
5827
  }
5828
+ handleToolApiKeyIssue(info, call) {
5829
+ const label = call?.name ? `${call.name} tool` : 'web tools';
5830
+ this.handleApiKeyIssue(info, undefined, label);
5831
+ }
5705
5832
  showSecretGuidance(secret, promptForInput) {
5706
5833
  const lines = [];
5707
5834
  if (promptForInput) {
@@ -5711,7 +5838,7 @@ What's the next action?`;
5711
5838
  lines.push(`Update the stored value for ${secret.label} or type "cancel".`);
5712
5839
  }
5713
5840
  lines.push(`Tip: run "/secrets" anytime to manage credentials or export ${secret.envVar}=<value> before launching the CLI.`);
5714
- display.showSystemMessage(lines.join('\n'));
5841
+ this.showInlineMenu('Secrets', lines, promptForInput ? 'warning' : 'info');
5715
5842
  }
5716
5843
  colorizeDropdownLine(text, index) {
5717
5844
  if (!DROPDOWN_COLORS.length) {