erosolar-cli 1.7.385 → 1.7.386

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 +211 -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') {
@@ -1255,10 +1275,18 @@ export class InteractiveShell {
1255
1275
  * Log user prompt to the scroll region so it's part of the conversation flow.
1256
1276
  */
1257
1277
  logUserPrompt(text) {
1258
- if (!text.trim())
1278
+ const normalized = text.trim();
1279
+ if (!normalized)
1259
1280
  return;
1281
+ // Skip duplicate renders of the exact same prompt if they happen back-to-back.
1282
+ const now = Date.now();
1283
+ if (this.lastLoggedPrompt === normalized && now - this.lastLoggedPromptAt < 2000) {
1284
+ return;
1285
+ }
1286
+ this.lastLoggedPrompt = normalized;
1287
+ this.lastLoggedPromptAt = now;
1260
1288
  // Format with user prompt prefix and write to scroll region
1261
- const formatted = `${theme.user('>')} ${text}\n`;
1289
+ const formatted = `${theme.user('>')} ${normalized}\n`;
1262
1290
  this.terminalInput.writeToScrollRegion(formatted);
1263
1291
  }
1264
1292
  requestPromptRefresh(force = false) {
@@ -1280,17 +1308,49 @@ export class InteractiveShell {
1280
1308
  this.promptRefreshTimer = null;
1281
1309
  }
1282
1310
  }
1311
+ resetStreamBuffer() {
1312
+ this.pendingStreamBuffer = '';
1313
+ }
1314
+ captureStreamChunk(chunk) {
1315
+ if (!this.batchedOutputMode || !chunk) {
1316
+ return;
1317
+ }
1318
+ this.pendingStreamBuffer += chunk;
1319
+ }
1320
+ consumeStreamBuffer(fallback) {
1321
+ if (!this.batchedOutputMode) {
1322
+ return fallback;
1323
+ }
1324
+ const buffered = this.pendingStreamBuffer;
1325
+ this.pendingStreamBuffer = '';
1326
+ return buffered || fallback;
1327
+ }
1328
+ currentRunElapsedMs() {
1329
+ if (!this.streamingHeartbeatStart) {
1330
+ return undefined;
1331
+ }
1332
+ return Date.now() - this.streamingHeartbeatStart;
1333
+ }
1283
1334
  startStreamingHeartbeat(label = 'Streaming') {
1284
1335
  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');
1336
+ this.resetStreamBuffer();
1337
+ this.lastStreamingElapsedSeconds = null;
1290
1338
  this.streamingHeartbeatStart = Date.now();
1291
1339
  this.streamingHeartbeatFrame = 0;
1292
1340
  const initialFrame = STREAMING_SPINNER_FRAMES[this.streamingHeartbeatFrame];
1293
1341
  this.streamingStatusLabel = this.buildStreamingStatus(`${initialFrame} ${label}`, 0);
1342
+ if (this.batchedOutputMode) {
1343
+ this.streamingUiActive = false;
1344
+ this.uiUpdates.setMode('processing');
1345
+ }
1346
+ else {
1347
+ // Enter global streaming mode - blocks all non-streaming UI output
1348
+ enterStreamingMode();
1349
+ // Set up scroll region for streaming content
1350
+ this.terminalInput.enterStreamingScrollRegion();
1351
+ this.streamingUiActive = true;
1352
+ this.uiUpdates.setMode('streaming');
1353
+ }
1294
1354
  display.updateStreamingStatus(this.streamingStatusLabel);
1295
1355
  this.refreshStatusLine(true);
1296
1356
  // Periodically refresh the pinned input/status region while streaming so
@@ -1320,18 +1380,21 @@ export class InteractiveShell {
1320
1380
  });
1321
1381
  }
1322
1382
  stopStreamingHeartbeat() {
1323
- // Exit global streaming mode - allows UI to render again
1324
- exitStreamingMode();
1325
1383
  // Preserve final elapsed time before clearing heartbeat start
1326
1384
  if (this.streamingHeartbeatStart) {
1327
1385
  this.lastStreamingElapsedSeconds = Math.max(0, Math.floor((Date.now() - this.streamingHeartbeatStart) / 1000));
1328
1386
  }
1329
- // Exit scroll region mode
1330
- this.terminalInput.exitStreamingScrollRegion();
1387
+ if (this.streamingUiActive) {
1388
+ // Exit global streaming mode - allows UI to render again
1389
+ exitStreamingMode();
1390
+ // Exit scroll region mode
1391
+ this.terminalInput.exitStreamingScrollRegion();
1392
+ }
1331
1393
  this.uiUpdates.stopHeartbeat('streaming');
1332
1394
  this.streamingHeartbeatStart = null;
1333
1395
  this.streamingHeartbeatFrame = 0;
1334
1396
  this.streamingStatusLabel = null;
1397
+ this.streamingUiActive = false;
1335
1398
  // Clear streaming label specifically (keeps override and main status if set)
1336
1399
  this.terminalInput.setStreamingLabel(null);
1337
1400
  // Clear streaming status from display
@@ -1499,6 +1562,10 @@ export class InteractiveShell {
1499
1562
  return false;
1500
1563
  }
1501
1564
  switch (this.pendingInteraction.type) {
1565
+ case 'model-loading':
1566
+ display.showInfo('Still fetching model options. Please wait a moment.');
1567
+ this.terminalInput.render();
1568
+ return true;
1502
1569
  case 'model-provider':
1503
1570
  await this.handleModelProviderSelection(input);
1504
1571
  return true;
@@ -3633,28 +3700,25 @@ export class InteractiveShell {
3633
3700
  return lines.join('\n');
3634
3701
  }
3635
3702
  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;
3703
+ // Hold input immediately so numeric selections don't get queued as prompts while we fetch
3704
+ this.pendingInteraction = { type: 'model-loading' };
3705
+ this.showInlineMenu('Model provider', [theme.ui.muted('Fetching latest models from providers...')], 'info');
3706
+ try {
3707
+ // Fetch live models from all configured providers
3708
+ const providerStatuses = await quickCheckProviders();
3709
+ const providerOptions = this.buildProviderOptionsWithDiscovery(providerStatuses);
3710
+ if (!providerOptions.length) {
3711
+ this.pendingInteraction = null;
3712
+ this.showInlineMenu('Model provider', [theme.warning('No providers are available.')], 'warning');
3713
+ return;
3714
+ }
3715
+ this.pendingInteraction = { type: 'model-provider', options: providerOptions };
3716
+ this.renderProviderMenu(providerOptions);
3717
+ }
3718
+ catch (error) {
3719
+ this.pendingInteraction = null;
3720
+ this.showInlineMenu('Model provider', [theme.error('Failed to load model list. Try again in a moment.')], 'error');
3643
3721
  }
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
3722
  }
3659
3723
  buildProviderOptions() {
3660
3724
  const counts = new Map();
@@ -3744,6 +3808,21 @@ export class InteractiveShell {
3744
3808
  };
3745
3809
  });
3746
3810
  }
3811
+ renderProviderMenu(options, hint) {
3812
+ const lines = [
3813
+ theme.bold('Select a provider:'),
3814
+ ...options.map((option, index) => {
3815
+ const isCurrent = option.provider === this.sessionState.provider;
3816
+ const countLabel = `${option.modelCount} model${option.modelCount === 1 ? '' : 's'}`;
3817
+ const latestLabel = option.latestModel ? theme.success(` (latest: ${option.latestModel})`) : '';
3818
+ const label = this.colorizeDropdownLine(`${index + 1}. ${option.label} — ${countLabel}${latestLabel}`, index);
3819
+ const suffix = isCurrent ? ` ${theme.primary('• current')}` : '';
3820
+ return `${label}${suffix}`;
3821
+ }),
3822
+ 'Type the number of the provider to continue, or type "cancel".',
3823
+ ];
3824
+ this.showInlineMenu('Model provider', lines, 'info', hint);
3825
+ }
3747
3826
  showProviderModels(option) {
3748
3827
  // Start with static presets
3749
3828
  const staticModels = MODEL_PRESETS.filter((preset) => preset.provider === option.provider);
@@ -3783,13 +3862,17 @@ export class InteractiveShell {
3783
3862
  }
3784
3863
  }
3785
3864
  if (!allModels.length) {
3786
- display.showWarning(`No models available for ${option.label}.`);
3865
+ this.showInlineMenu('Model selection', [theme.warning(`No models available for ${option.label}.`)], 'warning');
3787
3866
  this.pendingInteraction = null;
3788
3867
  return;
3789
3868
  }
3869
+ this.renderModelSelection(allModels, option, null);
3870
+ this.pendingInteraction = { type: 'model', provider: option.provider, options: allModels };
3871
+ }
3872
+ renderModelSelection(models, option, hint) {
3790
3873
  const lines = [
3791
3874
  theme.bold(`Select a model from ${option.label}:`),
3792
- ...allModels.map((preset, index) => {
3875
+ ...models.map((preset, index) => {
3793
3876
  const isCurrent = preset.id === this.sessionState.model;
3794
3877
  const isLatest = preset.id === option.latestModel;
3795
3878
  const latestBadge = isLatest ? theme.success(' ā˜… LATEST') : '';
@@ -3800,11 +3883,23 @@ export class InteractiveShell {
3800
3883
  }),
3801
3884
  'Type the number of the model to select it, type "back" to change provider, or type "cancel".',
3802
3885
  ];
3803
- display.showSystemMessage(lines.join('\n'));
3804
- this.pendingInteraction = { type: 'model', provider: option.provider, options: allModels };
3886
+ this.showInlineMenu('Model selection', lines, 'info', hint ?? undefined);
3887
+ }
3888
+ buildProviderContext(provider, models) {
3889
+ return {
3890
+ provider,
3891
+ label: this.providerLabel(provider),
3892
+ modelCount: models.length,
3893
+ latestModel: models[0]?.id,
3894
+ discoveredModels: [],
3895
+ };
3805
3896
  }
3806
3897
  showSecretsMenu() {
3807
3898
  const definitions = listSecretDefinitions();
3899
+ this.pendingInteraction = { type: 'secret-select', options: definitions };
3900
+ this.renderSecretsMenu(definitions);
3901
+ }
3902
+ renderSecretsMenu(definitions, hint) {
3808
3903
  const lines = [
3809
3904
  theme.bold('Manage Secrets:'),
3810
3905
  ...definitions.map((definition, index) => {
@@ -3816,8 +3911,18 @@ export class InteractiveShell {
3816
3911
  }),
3817
3912
  'Enter the number to update a key, or type "cancel".',
3818
3913
  ];
3819
- display.showSystemMessage(lines.join('\n'));
3820
- this.pendingInteraction = { type: 'secret-select', options: definitions };
3914
+ this.showInlineMenu('Secrets', lines, 'info', hint);
3915
+ }
3916
+ renderSecretInput(secret, hint) {
3917
+ const value = getSecretValue(secret.id);
3918
+ const status = value ? maskSecret(value) : theme.warning('not set');
3919
+ const providers = secret.providers.map((id) => this.providerLabel(id)).join(', ');
3920
+ const lines = [
3921
+ `${secret.label} (${providers})`,
3922
+ `Current: ${status}`,
3923
+ 'Enter a new value or type "cancel".',
3924
+ ];
3925
+ this.showInlineMenu('Update secret', lines, 'info', hint);
3821
3926
  }
3822
3927
  showToolsMenu() {
3823
3928
  const options = getToolToggleOptions();
@@ -4197,26 +4302,23 @@ export class InteractiveShell {
4197
4302
  }
4198
4303
  const trimmed = input.trim();
4199
4304
  if (!trimmed) {
4200
- display.showWarning('Enter a number or type cancel.');
4201
- this.terminalInput.render();
4305
+ this.renderProviderMenu(pending.options, 'Enter a number or type cancel.');
4202
4306
  return;
4203
4307
  }
4204
4308
  if (trimmed.toLowerCase() === 'cancel') {
4205
4309
  this.pendingInteraction = null;
4206
- display.showInfo('Model selection cancelled.');
4207
- this.terminalInput.render();
4310
+ this.clearInlineMenu();
4311
+ this.updateStatusMessage('Model selection cancelled.');
4208
4312
  return;
4209
4313
  }
4210
4314
  const choice = Number.parseInt(trimmed, 10);
4211
4315
  if (!Number.isFinite(choice)) {
4212
- display.showWarning('Please enter a valid number.');
4213
- this.terminalInput.render();
4316
+ this.renderProviderMenu(pending.options, 'Please enter a valid number.');
4214
4317
  return;
4215
4318
  }
4216
4319
  const option = pending.options[choice - 1];
4217
4320
  if (!option) {
4218
- display.showWarning('That option is not available.');
4219
- this.terminalInput.render();
4321
+ this.renderProviderMenu(pending.options, 'That option is not available.');
4220
4322
  return;
4221
4323
  }
4222
4324
  this.showProviderModels(option);
@@ -4227,10 +4329,10 @@ export class InteractiveShell {
4227
4329
  if (!pending || pending.type !== 'model') {
4228
4330
  return;
4229
4331
  }
4332
+ const providerContext = this.buildProviderContext(pending.provider, pending.options);
4230
4333
  const trimmed = input.trim();
4231
4334
  if (!trimmed) {
4232
- display.showWarning('Enter a number, type "back", or type "cancel".');
4233
- this.terminalInput.render();
4335
+ this.renderModelSelection(pending.options, providerContext, 'Enter a number, type "back", or type "cancel".');
4234
4336
  return;
4235
4337
  }
4236
4338
  if (trimmed.toLowerCase() === 'back') {
@@ -4240,20 +4342,18 @@ export class InteractiveShell {
4240
4342
  }
4241
4343
  if (trimmed.toLowerCase() === 'cancel') {
4242
4344
  this.pendingInteraction = null;
4243
- display.showInfo('Model selection cancelled.');
4244
- this.terminalInput.render();
4345
+ this.clearInlineMenu();
4346
+ this.updateStatusMessage('Model selection cancelled.');
4245
4347
  return;
4246
4348
  }
4247
4349
  const choice = Number.parseInt(trimmed, 10);
4248
4350
  if (!Number.isFinite(choice)) {
4249
- display.showWarning('Please enter a valid number.');
4250
- this.terminalInput.render();
4351
+ this.renderModelSelection(pending.options, providerContext, 'Please enter a valid number.');
4251
4352
  return;
4252
4353
  }
4253
4354
  const preset = pending.options[choice - 1];
4254
4355
  if (!preset) {
4255
- display.showWarning('That option is not available.');
4256
- this.terminalInput.render();
4356
+ this.renderModelSelection(pending.options, providerContext, 'That option is not available.');
4257
4357
  return;
4258
4358
  }
4259
4359
  this.pendingInteraction = null;
@@ -4277,11 +4377,14 @@ export class InteractiveShell {
4277
4377
  };
4278
4378
  this.applyPresetReasoningDefaults();
4279
4379
  if (this.rebuildAgent()) {
4280
- display.showInfo(`Switched to ${preset.label}.`);
4380
+ this.showInlineMenu('Model updated', [`Switched to ${preset.label}.`], 'success');
4281
4381
  this.refreshBannerSessionInfo();
4282
4382
  this.persistSessionPreference();
4283
4383
  this.resetChatBoxAfterModelSwap();
4284
4384
  }
4385
+ else {
4386
+ this.showInlineMenu('Model updated', [`Using ${preset.label}.`], 'info');
4387
+ }
4285
4388
  }
4286
4389
  async handleSecretSelection(input) {
4287
4390
  const pending = this.pendingInteraction;
@@ -4290,30 +4393,27 @@ export class InteractiveShell {
4290
4393
  }
4291
4394
  const trimmed = input.trim();
4292
4395
  if (!trimmed) {
4293
- display.showWarning('Enter a number or type cancel.');
4294
- this.terminalInput.render();
4396
+ this.renderSecretsMenu(pending.options, 'Enter a number or type cancel.');
4295
4397
  return;
4296
4398
  }
4297
4399
  if (trimmed.toLowerCase() === 'cancel') {
4298
4400
  this.pendingInteraction = null;
4299
- display.showInfo('Secret management cancelled.');
4300
- this.terminalInput.render();
4401
+ this.clearInlineMenu();
4402
+ this.updateStatusMessage('Secret management cancelled.');
4301
4403
  return;
4302
4404
  }
4303
4405
  const choice = Number.parseInt(trimmed, 10);
4304
4406
  if (!Number.isFinite(choice)) {
4305
- display.showWarning('Please enter a valid number.');
4306
- this.terminalInput.render();
4407
+ this.renderSecretsMenu(pending.options, 'Please enter a valid number.');
4307
4408
  return;
4308
4409
  }
4309
4410
  const secret = pending.options[choice - 1];
4310
4411
  if (!secret) {
4311
- display.showWarning('That option is not available.');
4312
- this.terminalInput.render();
4412
+ this.renderSecretsMenu(pending.options, 'That option is not available.');
4313
4413
  return;
4314
4414
  }
4315
- display.showSystemMessage(`Enter a new value for ${secret.label} or type "cancel".`);
4316
4415
  this.pendingInteraction = { type: 'secret-input', secret };
4416
+ this.renderSecretInput(secret);
4317
4417
  this.terminalInput.render();
4318
4418
  }
4319
4419
  async handleSecretInput(input) {
@@ -4323,20 +4423,20 @@ export class InteractiveShell {
4323
4423
  }
4324
4424
  const trimmed = input.trim();
4325
4425
  if (!trimmed) {
4326
- display.showWarning('Enter a value or type cancel.');
4327
- this.terminalInput.render();
4426
+ this.renderSecretInput(pending.secret, 'Enter a value or type cancel.');
4328
4427
  return;
4329
4428
  }
4330
4429
  if (trimmed.toLowerCase() === 'cancel') {
4331
4430
  this.pendingInteraction = null;
4332
4431
  this.pendingSecretRetry = null;
4333
- display.showInfo('Secret unchanged.');
4432
+ this.clearInlineMenu();
4433
+ this.updateStatusMessage('Secret unchanged.');
4334
4434
  this.terminalInput.render();
4335
4435
  return;
4336
4436
  }
4337
4437
  try {
4338
4438
  setSecretValue(pending.secret.id, trimmed);
4339
- display.showInfo(`${pending.secret.label} updated.`);
4439
+ this.showInlineMenu('Secret updated', [`${pending.secret.label} saved.`], 'success');
4340
4440
  this.pendingInteraction = null;
4341
4441
  const deferred = this.pendingSecretRetry;
4342
4442
  this.pendingSecretRetry = null;
@@ -4351,7 +4451,7 @@ export class InteractiveShell {
4351
4451
  }
4352
4452
  catch (error) {
4353
4453
  const message = error instanceof Error ? error.message : String(error);
4354
- display.showError(message);
4454
+ this.showInlineMenu('Secret error', [message], 'error');
4355
4455
  this.pendingInteraction = null;
4356
4456
  this.pendingSecretRetry = null;
4357
4457
  }
@@ -4370,6 +4470,7 @@ export class InteractiveShell {
4370
4470
  if (!agent) {
4371
4471
  return;
4372
4472
  }
4473
+ this.logUserPrompt(request);
4373
4474
  this.isProcessing = true;
4374
4475
  this.uiUpdates.setMode('processing');
4375
4476
  this.terminalInput.setStreaming(true);
@@ -4389,7 +4490,8 @@ export class InteractiveShell {
4389
4490
  let responseText = '';
4390
4491
  try {
4391
4492
  // Start streaming - no header needed, the input area already provides context
4392
- this.startStreamingHeartbeat('Streaming response');
4493
+ const heartbeatLabel = this.batchedOutputMode ? 'Processing response' : 'Streaming response';
4494
+ this.startStreamingHeartbeat(heartbeatLabel);
4393
4495
  responseText = await agent.send(request, true);
4394
4496
  await this.awaitPendingCleanup();
4395
4497
  this.captureHistorySnapshot();
@@ -4505,7 +4607,8 @@ export class InteractiveShell {
4505
4607
  this.uiAdapter.startProcessing('Continuous execution mode');
4506
4608
  this.setProcessingStatus();
4507
4609
  // No streaming header - just start streaming directly
4508
- this.startStreamingHeartbeat('Streaming');
4610
+ const continuousLabel = this.batchedOutputMode ? 'Processing' : 'Streaming';
4611
+ this.startStreamingHeartbeat(continuousLabel);
4509
4612
  let iteration = 0;
4510
4613
  let lastResponse = '';
4511
4614
  let consecutiveNoProgress = 0;
@@ -4533,9 +4636,10 @@ When truly finished with ALL tasks, explicitly state "TASK_FULLY_COMPLETE".`;
4533
4636
  display.showSystemMessage(`\nšŸ“ Iteration ${iteration}/${MAX_ITERATIONS}`);
4534
4637
  this.updateStatusMessage(`Working on iteration ${iteration}...`);
4535
4638
  try {
4536
- // Send the request and capture the response (streaming disabled)
4639
+ // Send the request and capture the response as a single block
4537
4640
  display.showThinking('Responding...');
4538
4641
  this.refreshStatusLine(true);
4642
+ this.resetStreamBuffer();
4539
4643
  const response = await agent.send(currentPrompt, true);
4540
4644
  await this.awaitPendingCleanup();
4541
4645
  this.captureHistorySnapshot();
@@ -5036,6 +5140,7 @@ What's the next action?`;
5036
5140
  // Send the error to the agent for fixing
5037
5141
  display.showThinking('Analyzing build errors');
5038
5142
  this.refreshStatusLine(true);
5143
+ this.resetStreamBuffer();
5039
5144
  const response = await this.agent.send(prompt, true);
5040
5145
  display.stopThinking();
5041
5146
  this.refreshStatusLine(true);
@@ -5065,27 +5170,33 @@ What's the next action?`;
5065
5170
  };
5066
5171
  this.agent = this.runtimeSession.createAgent(selection, {
5067
5172
  onStreamChunk: (chunk) => {
5173
+ if (this.batchedOutputMode) {
5174
+ this.captureStreamChunk(chunk);
5175
+ return;
5176
+ }
5068
5177
  // Stream output using clean streamContent() - chat box floats below
5069
5178
  this.terminalInput.streamContent(chunk);
5070
5179
  },
5071
5180
  onStreamFallback: (info) => this.handleStreamingFallback(info),
5072
5181
  onAssistantMessage: (content, metadata) => {
5073
5182
  const enriched = this.buildDisplayMetadata(metadata);
5183
+ const alreadyStreamedToUi = !this.batchedOutputMode && metadata.wasStreamed;
5184
+ const bufferedContent = this.consumeStreamBuffer(content);
5074
5185
  // Update spinner based on message type
5075
5186
  if (metadata.isFinal) {
5076
5187
  // Skip display if content was already streamed to avoid double-display
5077
- if (!metadata.wasStreamed) {
5078
- const parsed = this.splitThinkingResponse(content);
5188
+ if (!alreadyStreamedToUi) {
5189
+ const parsed = this.splitThinkingResponse(bufferedContent);
5079
5190
  if (parsed?.thinking) {
5080
5191
  const summary = this.extractThoughtSummary(parsed.thinking);
5081
5192
  if (summary) {
5082
5193
  display.updateThinking(`šŸ’­ ${summary}`);
5083
5194
  }
5084
- display.showAssistantMessage(parsed.thinking, { ...enriched, isFinal: false });
5195
+ display.showThinkingBlock(parsed.thinking, enriched.elapsedMs ?? this.currentRunElapsedMs());
5085
5196
  }
5086
- const finalContent = parsed?.response?.trim() || content;
5197
+ const finalContent = parsed?.response?.trim() || bufferedContent.trim();
5087
5198
  if (finalContent) {
5088
- display.showAssistantMessage(finalContent, enriched);
5199
+ display.showAssistantMessage(finalContent, { ...enriched, isFinal: true });
5089
5200
  }
5090
5201
  }
5091
5202
  // Status shown in mode controls bar - no separate status line needed
@@ -5107,8 +5218,13 @@ What's the next action?`;
5107
5218
  // Stop spinner and show the narrative text directly
5108
5219
  display.stopThinking();
5109
5220
  // Skip display if content was already streamed to avoid double-display
5110
- if (!metadata.wasStreamed) {
5111
- display.showNarrative(content.trim());
5221
+ if (!alreadyStreamedToUi) {
5222
+ const parsed = this.splitThinkingResponse(bufferedContent);
5223
+ const thoughtContent = parsed?.thinking ?? parsed?.response ?? bufferedContent;
5224
+ const trimmed = thoughtContent.trim();
5225
+ if (trimmed) {
5226
+ display.showThinkingBlock(trimmed, enriched.elapsedMs ?? this.currentRunElapsedMs());
5227
+ }
5112
5228
  }
5113
5229
  // The isProcessing flag already shows "ā³ Processing..." - no need for duplicate status
5114
5230
  this.requestPromptRefresh();
@@ -5667,7 +5783,12 @@ What's the next action?`;
5667
5783
  const reason = info.reason ? ` (${info.reason.replace(/-/g, ' ')})` : '';
5668
5784
  const partialNote = info.partialResponse ? ' Received partial stream before failure.' : '';
5669
5785
  display.showWarning(`Streaming failed${reason}, retrying without streaming.${detail}${partialNote}`);
5670
- this.startStreamingHeartbeat('Fallback in progress');
5786
+ const bufferedPartial = this.consumeStreamBuffer(info.partialResponse ?? '');
5787
+ if (this.batchedOutputMode && bufferedPartial.trim()) {
5788
+ display.showThinkingBlock(bufferedPartial.trim(), this.currentRunElapsedMs());
5789
+ }
5790
+ const fallbackLabel = this.batchedOutputMode ? 'Retrying (batched)' : 'Fallback in progress';
5791
+ this.startStreamingHeartbeat(fallbackLabel);
5671
5792
  this.requestPromptRefresh(true);
5672
5793
  }
5673
5794
  handleProviderError(error, retryAction) {
@@ -5678,9 +5799,9 @@ What's the next action?`;
5678
5799
  this.handleApiKeyIssue(apiKeyIssue, retryAction);
5679
5800
  return true;
5680
5801
  }
5681
- handleApiKeyIssue(info, retryAction) {
5802
+ handleApiKeyIssue(info, retryAction, contextLabel) {
5682
5803
  const secret = info.secret ?? null;
5683
- const providerLabel = info.provider ? this.providerLabel(info.provider) : 'the selected provider';
5804
+ const providerLabel = contextLabel ?? (info.provider ? this.providerLabel(info.provider) : 'the selected provider');
5684
5805
  if (!secret) {
5685
5806
  this.pendingSecretRetry = null;
5686
5807
  const guidance = 'Run "/secrets" to configure the required API key or export it (e.g., EXPORT KEY=value) before launching the CLI.';
@@ -5702,6 +5823,10 @@ What's the next action?`;
5702
5823
  this.pendingInteraction = { type: 'secret-input', secret };
5703
5824
  this.showSecretGuidance(secret, isMissing);
5704
5825
  }
5826
+ handleToolApiKeyIssue(info, call) {
5827
+ const label = call?.name ? `${call.name} tool` : 'web tools';
5828
+ this.handleApiKeyIssue(info, undefined, label);
5829
+ }
5705
5830
  showSecretGuidance(secret, promptForInput) {
5706
5831
  const lines = [];
5707
5832
  if (promptForInput) {
@@ -5711,7 +5836,7 @@ What's the next action?`;
5711
5836
  lines.push(`Update the stored value for ${secret.label} or type "cancel".`);
5712
5837
  }
5713
5838
  lines.push(`Tip: run "/secrets" anytime to manage credentials or export ${secret.envVar}=<value> before launching the CLI.`);
5714
- display.showSystemMessage(lines.join('\n'));
5839
+ this.showInlineMenu('Secrets', lines, promptForInput ? 'warning' : 'info');
5715
5840
  }
5716
5841
  colorizeDropdownLine(text, index) {
5717
5842
  if (!DROPDOWN_COLORS.length) {