erosolar-cli 1.7.190 → 1.7.192

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.
@@ -1,4 +1,3 @@
1
- import readline from 'node:readline';
2
1
  import { stdin as input, stdout as output, exit } from 'node:process';
3
2
  import { exec } from 'node:child_process';
4
3
  import { promisify } from 'node:util';
@@ -8,8 +7,6 @@ import { getContextWindowTokens } from '../core/contextWindow.js';
8
7
  import { ensureSecretForProvider, getSecretDefinitionForProvider, getSecretValue, listSecretDefinitions, maskSecret, setSecretValue, } from '../core/secretStore.js';
9
8
  import { saveActiveProfilePreference, saveModelPreference, loadToolSettings, saveToolSettings, clearToolSettings, clearActiveProfilePreference, loadSessionPreferences, saveSessionPreferences, } from '../core/preferences.js';
10
9
  import { buildEnabledToolSet, evaluateToolPermissions, getToolToggleOptions, } from '../capabilities/toolRegistry.js';
11
- import { BracketedPasteManager } from './bracketedPasteManager.js';
12
- import { ComposableMessageBuilder } from './composableMessage.js';
13
10
  import { detectApiKeyError } from '../core/errors/apiKeyErrors.js';
14
11
  import { buildWorkspaceContext } from '../workspace.js';
15
12
  import { buildInteractiveSystemPrompt } from './systemPrompt.js';
@@ -21,12 +18,10 @@ import { buildCustomCommandPrompt, loadCustomSlashCommands, } from '../core/cust
21
18
  import { SkillRepository } from '../skills/skillRepository.js';
22
19
  import { createSkillTools } from '../tools/skillTools.js';
23
20
  import { FileChangeTracker } from './fileChangeTracker.js';
24
- import { PersistentPrompt, PinnedChatBox } from '../ui/persistentPrompt.js';
25
21
  import { formatShortcutsHelp } from '../ui/shortcutsHelp.js';
26
22
  import { MetricsTracker } from '../alpha-zero/index.js';
27
23
  import { listAvailablePlugins } from '../plugins/index.js';
28
- import { getClaudeCodeStreamHandler } from './claudeCodeStreamHandler.js';
29
- import { getUnifiedChatBox } from './unifiedChatBox.js';
24
+ import { TerminalInputAdapter } from './terminalInputAdapter.js';
30
25
  const execAsync = promisify(exec);
31
26
  const DROPDOWN_COLORS = [
32
27
  theme.primary,
@@ -54,9 +49,6 @@ const BASE_SLASH_COMMANDS = getSlashCommands().map((cmd) => ({
54
49
  // Load PROVIDER_LABELS from centralized schema
55
50
  const PROVIDER_LABELS = Object.fromEntries(getProviders().map((provider) => [provider.id, provider.label]));
56
51
  // Allow enough time for paste detection to kick in before flushing buffered lines
57
- const MULTILINE_INPUT_FLUSH_DELAY_MS = 120;
58
- const BRACKETED_PASTE_ENABLE = '\u001b[?2004h';
59
- const BRACKETED_PASTE_DISABLE = '\u001b[?2004l';
60
52
  const CONTEXT_USAGE_THRESHOLD = 0.9;
61
53
  const CONTEXT_RECENT_MESSAGE_COUNT = 12;
62
54
  const CONTEXT_CLEANUP_CHARS_PER_CHUNK = 6000;
@@ -68,7 +60,6 @@ const CONTEXT_CLEANUP_SYSTEM_PROMPT = `You condense earlier IDE collaboration lo
68
60
  - Keep the response under roughly 200 words, prefer short bullet lists.
69
61
  - Never call tools or run shell commands; respond with plain Markdown text only.`;
70
62
  export class InteractiveShell {
71
- rl;
72
63
  agent = null;
73
64
  profile;
74
65
  profileLabel;
@@ -78,18 +69,14 @@ export class InteractiveShell {
78
69
  workspaceOptions;
79
70
  sessionState;
80
71
  isProcessing = false;
72
+ shuttingDown = false;
81
73
  pendingInteraction = null;
82
74
  pendingSecretRetry = null;
83
- bufferedInputLines = [];
84
- bufferedInputTimer = null;
85
- bracketedPaste;
86
- bracketedPasteEnabled = false;
87
- composableMessage = new ComposableMessageBuilder();
75
+ terminalInput;
76
+ currentInput = '';
88
77
  pendingCleanup = null;
89
78
  cleanupInProgress = false;
90
79
  slashPreviewVisible = false;
91
- keypressHandler = null;
92
- rawDataHandler = null;
93
80
  skillRepository;
94
81
  skillToolHandlers = new Map();
95
82
  thinkingMode = 'balanced';
@@ -99,7 +86,6 @@ export class InteractiveShell {
99
86
  statusTracker;
100
87
  uiAdapter;
101
88
  _fileChangeTracker = new FileChangeTracker(); // Reserved for future file tracking features
102
- persistentPrompt;
103
89
  alphaZeroMetrics; // Alpha Zero 2 performance tracking
104
90
  statusSubscription = null;
105
91
  followUpQueue = [];
@@ -117,11 +103,6 @@ export class InteractiveShell {
117
103
  customCommandMap;
118
104
  sessionRestoreConfig;
119
105
  _enabledPlugins;
120
- pinnedChatBox;
121
- readlineOutputSuppressed = false;
122
- originalStdoutWrite = null;
123
- streamHandler;
124
- unifiedChatBox;
125
106
  // Cached provider status for unified status bar display after streaming
126
107
  cachedProviderStatus = [];
127
108
  // Auto-test tracking
@@ -180,19 +161,10 @@ export class InteractiveShell {
180
161
  // Set up file change tracking callback
181
162
  this.uiAdapter.setFileChangeCallback((path, type, additions, removals) => {
182
163
  this._fileChangeTracker.recordChange(path, type, additions, removals);
183
- // Update persistent prompt status bar with file changes
184
- this.updatePersistentPromptFileChanges();
185
164
  });
186
- // Set up tool status callback to update pinned chat box during tool execution
165
+ // Set up tool status callback to update status during tool execution
187
166
  this.uiAdapter.setToolStatusCallback((status) => {
188
- if (status) {
189
- this.pinnedChatBox.setStatusMessage(status);
190
- }
191
- else {
192
- // Clear status but keep processing indicator if still processing
193
- this.pinnedChatBox.setStatusMessage(null);
194
- }
195
- this.pinnedChatBox.forceRender();
167
+ this.updateStatusMessage(status ?? null);
196
168
  });
197
169
  this.skillRepository = new SkillRepository({
198
170
  workingDir: this.workingDir,
@@ -201,62 +173,18 @@ export class InteractiveShell {
201
173
  for (const definition of createSkillTools({ repository: this.skillRepository })) {
202
174
  this.skillToolHandlers.set(definition.name, definition.handler);
203
175
  }
204
- this.rl = readline.createInterface({
205
- input,
206
- output,
207
- // Use '> ' as the standard prompt prefix
208
- prompt: formatUserPrompt(this.profileLabel || this.profile),
209
- terminal: true,
210
- historySize: 100, // Enable native readline history
211
- });
212
- // Wrap prompt() so we always re-arm stdin before showing it
213
- const originalPrompt = this.rl.prompt.bind(this.rl);
214
- this.rl.prompt = (preserveCursor) => {
215
- this.ensureReadlineReady();
216
- originalPrompt(preserveCursor);
217
- };
218
- // Initialize persistent prompt (Claude Code style)
219
- this.persistentPrompt = new PersistentPrompt(output, formatUserPrompt(this.profileLabel || this.profile));
220
- // Initialize pinned chat box for always-visible input during AI processing
221
- // Note: Pass empty string for promptText since render() adds its own '> ' prefix
222
- this.pinnedChatBox = new PinnedChatBox(output, '', // Don't pass formatUserPrompt - the render method adds '> ' prefix
223
- {
224
- onCommandQueued: (cmd) => {
225
- // Bridge to existing follow-up queue system
226
- this.followUpQueue.push({
227
- type: cmd.type === 'slash' ? 'request' : cmd.type,
228
- text: cmd.text,
229
- });
230
- this.refreshQueueIndicators();
231
- },
232
- onInputSubmit: (input) => {
233
- // Process the input through the normal flow
234
- this.processInputBlock(input).catch((err) => {
235
- display.showError(err instanceof Error ? err.message : String(err), err);
236
- });
237
- },
176
+ // Initialize unified terminal input handler
177
+ this.terminalInput = new TerminalInputAdapter(input, output, {
178
+ onSubmit: (text) => this.processInput(text),
179
+ onQueue: (text) => this.handleQueuedInput(text),
180
+ onInterrupt: () => this.handleInterrupt(),
181
+ onChange: (text) => this.handleInputChange(text),
238
182
  });
239
- // Register pinned chat box with display's output interceptor system
240
- // This ensures the pinned area is cleared before output and re-rendered after,
241
- // preventing ghost lines when the terminal scrolls
242
- this.pinnedChatBox.registerOutputInterceptor(display);
243
- // Initialize Claude Code style stream handler for natural output flow
244
- // Key insight: No cursor manipulation during streaming - output just appends
245
- this.streamHandler = getClaudeCodeStreamHandler();
246
- this.streamHandler.attachToReadline(this.rl);
247
- // Initialize UnifiedChatBox - Claude Code exact style
248
- // The chat box is always at the bottom naturally through readline
249
- // During streaming: output flows up, typed input queued invisibly
250
- // After streaming: new prompt appears at bottom naturally
251
- this.unifiedChatBox = getUnifiedChatBox();
252
- this.unifiedChatBox.attachToReadline(this.rl);
253
- this.unifiedChatBox.setPrompt(formatUserPrompt(this.profileLabel || this.profile));
254
183
  // Initialize Alpha Zero 2 metrics tracking
255
184
  this.alphaZeroMetrics = new MetricsTracker(`${this.profile}-${Date.now()}`);
256
185
  this.setupStatusTracking();
257
186
  this.refreshContextGauge();
258
- this.bracketedPasteEnabled = this.enableBracketedPasteMode();
259
- this.bracketedPaste = new BracketedPasteManager(this.bracketedPasteEnabled);
187
+ this.terminalInput.start();
260
188
  this.rebuildAgent();
261
189
  this.setupHandlers();
262
190
  this.refreshBannerSessionInfo();
@@ -303,17 +231,89 @@ export class InteractiveShell {
303
231
  this.sessionResumeNotice = null;
304
232
  }
305
233
  async start(initialPrompt) {
306
- // Claude Code style: Just use readline's prompt
307
- // The prompt naturally appears at the bottom
308
- // No complex UI rendering needed - terminal handles it naturally
309
234
  if (initialPrompt) {
310
235
  display.newLine();
311
236
  console.log(`${formatUserPrompt(this.profileLabel || this.profile)}${initialPrompt}`);
312
237
  await this.processInputBlock(initialPrompt);
313
238
  return;
314
239
  }
315
- // Show readline prompt - naturally at bottom of terminal
316
- this.rl.prompt();
240
+ // Ensure the terminal input is visible
241
+ this.terminalInput.render();
242
+ }
243
+ /**
244
+ * TerminalInputAdapter submit handler
245
+ */
246
+ processInput(text) {
247
+ this.handleInputChange('');
248
+ void this.processInputBlock(text).catch((err) => {
249
+ display.showError(err instanceof Error ? err.message : String(err), err);
250
+ });
251
+ }
252
+ /**
253
+ * TerminalInputAdapter queue handler (streaming mode)
254
+ */
255
+ handleQueuedInput(text) {
256
+ // Keep adapter queue trimmed so hints stay accurate
257
+ this.terminalInput.dequeue();
258
+ this.followUpQueue.push({ type: 'request', text });
259
+ display.showInfo(`Queued: "${text}"`);
260
+ this.refreshQueueIndicators();
261
+ this.scheduleQueueProcessing();
262
+ this.handleInputChange('');
263
+ }
264
+ /**
265
+ * TerminalInputAdapter change handler
266
+ */
267
+ handleInputChange(text) {
268
+ this.currentInput = text;
269
+ this.handleSlashCommandPreviewChange();
270
+ }
271
+ /**
272
+ * TerminalInputAdapter interrupt handler (Ctrl+C with empty buffer)
273
+ */
274
+ handleInterrupt() {
275
+ if (this.isProcessing && this.agent) {
276
+ this.agent.requestCancellation();
277
+ display.showWarning('Cancelling current operation... (Ctrl+C again to force quit)');
278
+ return;
279
+ }
280
+ this.shutdown();
281
+ }
282
+ /**
283
+ * Gracefully tear down the shell and exit
284
+ */
285
+ shutdown() {
286
+ if (this.shuttingDown) {
287
+ return;
288
+ }
289
+ this.shuttingDown = true;
290
+ // Stop any active spinner to prevent process hang
291
+ display.stopThinking(false);
292
+ this.teardownStatusTracking();
293
+ // Clear any pending cleanup to prevent hanging
294
+ this.pendingCleanup = null;
295
+ // Dispose terminal input handler
296
+ this.terminalInput.dispose();
297
+ // Dispose unified UI adapter
298
+ this.uiAdapter.dispose();
299
+ display.newLine();
300
+ const highlightedEmail = theme.info('support@ero.solar');
301
+ const infoMessage = [
302
+ 'Thank you to Anthropic for allowing me to use Claude Code to build erosolar-cli.',
303
+ 'https://www.anthropic.com/news/disrupting-AI-espionage',
304
+ '',
305
+ `Email ${highlightedEmail} with any bugs or feedback`,
306
+ 'GitHub: https://github.com/ErosolarAI/erosolar-by-bo',
307
+ 'npm: https://www.npmjs.com/package/erosolar-cli',
308
+ ].join('\n');
309
+ display.showInfo(infoMessage);
310
+ exit(0);
311
+ }
312
+ /**
313
+ * Update status bar message
314
+ */
315
+ updateStatusMessage(_message) {
316
+ this.terminalInput.render();
317
317
  }
318
318
  async handleToolSettingsInput(input) {
319
319
  const pending = this.pendingInteraction;
@@ -323,26 +323,26 @@ export class InteractiveShell {
323
323
  const trimmed = input.trim();
324
324
  if (!trimmed) {
325
325
  display.showWarning('Enter a number, "save", "defaults", or "cancel".');
326
- this.rl.prompt();
326
+ this.terminalInput.render();
327
327
  return;
328
328
  }
329
329
  const normalized = trimmed.toLowerCase();
330
330
  if (normalized === 'cancel') {
331
331
  this.pendingInteraction = null;
332
332
  display.showInfo('Tool selection cancelled.');
333
- this.rl.prompt();
333
+ this.terminalInput.render();
334
334
  return;
335
335
  }
336
336
  if (normalized === 'defaults') {
337
337
  pending.selection = buildEnabledToolSet(null);
338
338
  this.renderToolMenu(pending);
339
- this.rl.prompt();
339
+ this.terminalInput.render();
340
340
  return;
341
341
  }
342
342
  if (normalized === 'save') {
343
343
  await this.persistToolSelection(pending);
344
344
  this.pendingInteraction = null;
345
- this.rl.prompt();
345
+ this.terminalInput.render();
346
346
  return;
347
347
  }
348
348
  const choice = Number.parseInt(trimmed, 10);
@@ -360,11 +360,11 @@ export class InteractiveShell {
360
360
  }
361
361
  this.renderToolMenu(pending);
362
362
  }
363
- this.rl.prompt();
363
+ this.terminalInput.render();
364
364
  return;
365
365
  }
366
366
  display.showWarning('Enter a number, "save", "defaults", or "cancel".');
367
- this.rl.prompt();
367
+ this.terminalInput.render();
368
368
  }
369
369
  async persistToolSelection(interaction) {
370
370
  if (setsEqual(interaction.selection, interaction.initialSelection)) {
@@ -391,36 +391,36 @@ export class InteractiveShell {
391
391
  if (!this.agentMenu) {
392
392
  this.pendingInteraction = null;
393
393
  display.showWarning('Agent selection is unavailable in this CLI.');
394
- this.rl.prompt();
394
+ this.terminalInput.render();
395
395
  return;
396
396
  }
397
397
  const trimmed = input.trim();
398
398
  if (!trimmed) {
399
399
  display.showWarning('Enter a number or type "cancel".');
400
- this.rl.prompt();
400
+ this.terminalInput.render();
401
401
  return;
402
402
  }
403
403
  if (trimmed.toLowerCase() === 'cancel') {
404
404
  this.pendingInteraction = null;
405
405
  display.showInfo('Agent selection cancelled.');
406
- this.rl.prompt();
406
+ this.terminalInput.render();
407
407
  return;
408
408
  }
409
409
  const choice = Number.parseInt(trimmed, 10);
410
410
  if (!Number.isFinite(choice)) {
411
411
  display.showWarning('Please enter a valid number.');
412
- this.rl.prompt();
412
+ this.terminalInput.render();
413
413
  return;
414
414
  }
415
415
  const option = pending.options[choice - 1];
416
416
  if (!option) {
417
417
  display.showWarning('That option is not available.');
418
- this.rl.prompt();
418
+ this.terminalInput.render();
419
419
  return;
420
420
  }
421
421
  await this.persistAgentSelection(option.name);
422
422
  this.pendingInteraction = null;
423
- this.rl.prompt();
423
+ this.terminalInput.render();
424
424
  }
425
425
  async persistAgentSelection(profileName) {
426
426
  if (!this.agentMenu) {
@@ -442,380 +442,12 @@ export class InteractiveShell {
442
442
  display.showInfo(`${this.agentMenuLabel(profileName)} will load the next time you start the CLI. Restart to switch now.`);
443
443
  }
444
444
  setupHandlers() {
445
- // Set up raw data interception for bracketed paste
446
- this.setupRawPasteHandler();
447
- this.rl.on('line', (line) => {
448
- // If we're capturing raw paste data, ignore readline line events
449
- // (they've already been handled by the raw data handler)
450
- if (this.bracketedPaste.isCapturingRaw()) {
451
- this.resetBufferedInputLines();
452
- // Clear the line that readline just echoed - move up and clear
453
- output.write('\x1b[A\r\x1b[K');
454
- // Show paste progress (this will update in place)
455
- this.showMultiLinePastePreview(this.bracketedPaste.getRawBufferLineCount(), this.bracketedPaste.getRawBufferPreview());
456
- return;
457
- }
458
- // If a paste was just captured via raw handler, ignore readline's line events
459
- // (readline still emits line events after we've already captured the paste)
460
- if (this.bracketedPaste.shouldIgnoreLineEvent()) {
461
- this.resetBufferedInputLines();
462
- // Clear the echoed line that readline wrote
463
- output.write('\x1b[A\r\x1b[K');
464
- return;
465
- }
466
- const normalized = this.bracketedPaste.process(line);
467
- if (normalized.handled) {
468
- // Clear any partially buffered lines so we don't auto-submit the first line of a paste
469
- this.resetBufferedInputLines();
470
- // If still accumulating multi-line paste, show preview
471
- if (normalized.isPending) {
472
- // Clear the line that readline just echoed - move up and clear
473
- output.write('\x1b[A\r\x1b[K');
474
- this.showMultiLinePastePreview(normalized.lineCount || 0, normalized.preview);
475
- return;
476
- }
477
- // Paste complete, store or submit the full content
478
- if (typeof normalized.result === 'string') {
479
- this.clearMultiLinePastePreview();
480
- const lineCount = normalized.lineCount ?? normalized.result.split('\n').length;
481
- // All pastes (single or multi-line) are captured for confirmation before submit
482
- void this.capturePaste(normalized.result, lineCount);
483
- return;
484
- }
485
- return;
486
- }
487
- this.enqueueUserInput(line);
488
- // Clear input in persistent prompt after submission
489
- this.persistentPrompt.updateInput('', 0);
490
- });
491
- this.rl.on('close', () => {
492
- // Stop any active spinner to prevent process hang
493
- display.stopThinking(false);
494
- this.disableBracketedPasteMode();
495
- this.teardownStatusTracking();
496
- // Remove keypress listener to prevent memory leaks
497
- const inputStream = input;
498
- if (inputStream && this.keypressHandler) {
499
- inputStream.off('keypress', this.keypressHandler);
500
- this.keypressHandler = null;
501
- }
502
- // Restore original stdin emit (cleanup from paste interception)
503
- if (this.rawDataHandler) {
504
- this.rawDataHandler(); // This restores the original emit function
505
- this.rawDataHandler = null;
506
- }
507
- // Clear any pending cleanup to prevent hanging
508
- this.pendingCleanup = null;
509
- // Dispose persistent prompt
510
- this.persistentPrompt.dispose();
511
- // Dispose Claude Code stream handler
512
- this.streamHandler.dispose();
513
- // Dispose unified chat box
514
- this.unifiedChatBox.dispose();
515
- // Dispose unified UI adapter
516
- this.uiAdapter.dispose();
517
- display.newLine();
518
- const highlightedEmail = theme.info('support@ero.solar');
519
- const infoMessage = [
520
- 'Thank you to Anthropic for allowing me to use Claude Code to build erosolar-cli.',
521
- '',
522
- `Email ${highlightedEmail} with any bugs or feedback`,
523
- 'GitHub: https://github.com/ErosolarAI/erosolar-by-bo',
524
- 'npm: https://www.npmjs.com/package/erosolar-cli',
525
- ].join('\n');
526
- display.showInfo(infoMessage);
527
- exit(0);
528
- });
529
- // Handle SIGINT (Ctrl+C) - clear text if present, otherwise close
530
- // This prevents readline from auto-closing when there's text to clear
531
- this.rl.on('SIGINT', () => {
532
- // During processing, check pinnedChatBox input first
533
- if (this.isProcessing) {
534
- const chatBoxInput = this.pinnedChatBox.getInput();
535
- if (chatBoxInput.length > 0) {
536
- // Clear the chat box input instead of exiting
537
- this.pinnedChatBox.clearPastedBlockState();
538
- this.pinnedChatBox.updatePersistentInput();
539
- return;
540
- }
541
- // No text in chat box during processing - cancel the agent
542
- if (this.agent) {
543
- this.agent.requestCancellation();
544
- output.write('\n');
545
- display.showWarning('Cancelling current operation... (Ctrl+C again to force quit)');
546
- return;
547
- }
548
- }
549
- // Not processing - check readline input
550
- const currentLine = this.rl.line || '';
551
- if (currentLine.length > 0) {
552
- // Clear the input buffer instead of exiting
553
- this.rl.line = '';
554
- this.rl.cursor = 0;
555
- this.persistentPrompt.updateInput('', 0);
556
- this.pinnedChatBox.setInput('');
557
- this.pinnedChatBox.clearPastedBlockState();
558
- // Clear the displayed line and show fresh prompt
559
- output.write('\r\x1b[K'); // Clear current line
560
- output.write('^C\n'); // Show ^C indicator
561
- this.rl.prompt(); // Re-show prompt
562
- return;
563
- }
564
- // No text anywhere - close readline to trigger exit
565
- this.rl.close();
566
- });
567
445
  // Handle terminal resize
568
446
  output.on('resize', () => {
569
- this.persistentPrompt.handleResize();
570
- this.pinnedChatBox.handleResize();
571
- });
572
- this.setupSlashCommandPreviewHandler();
573
- // Show initial persistent prompt
574
- this.persistentPrompt.show();
575
- }
576
- /**
577
- * Set up raw stdin data interception for bracketed paste mode.
578
- * This intercepts data before readline processes it, allowing us to
579
- * capture complete multi-line pastes without readline splitting them.
580
- */
581
- setupRawPasteHandler() {
582
- if (!this.bracketedPasteEnabled) {
583
- return;
584
- }
585
- const inputStream = input;
586
- if (!inputStream || !inputStream.isTTY) {
587
- return;
588
- }
589
- // Set up callback for when a complete paste is captured
590
- this.bracketedPaste.setRawPasteCallback((content) => {
591
- this.clearMultiLinePastePreview();
592
- const lines = content.split('\n');
593
- const lineCount = lines.length;
594
- // When streaming, route pastes into the pinned chat box instead of readline
595
- if (this.isProcessing) {
596
- this.pinnedChatBox.handlePaste(content);
597
- this.pinnedChatBox.updatePersistentInput();
598
- return;
599
- }
600
- // All pastes (single or multi-line) are captured for confirmation before submit
601
- void this.capturePaste(content, lineCount);
447
+ this.terminalInput.handleResize();
602
448
  });
603
- // Set up raw data interception to catch bracketed paste before readline processes it.
604
- // We need to actually PREVENT readline from seeing the paste content to avoid echo.
605
- // Strategy: Replace stdin's 'data' event emission during paste capture.
606
- const originalEmit = inputStream.emit.bind(inputStream);
607
- inputStream.emit = (event, ...args) => {
608
- if (event === 'data' && args[0]) {
609
- const data = args[0];
610
- const str = typeof data === 'string' ? data : data.toString();
611
- const result = this.bracketedPaste.processRawData(str);
612
- if (result.consumed) {
613
- // Data was consumed by paste handler - don't pass to readline
614
- // If there's passThrough data, emit that instead
615
- if (result.passThrough) {
616
- return originalEmit('data', Buffer.from(result.passThrough));
617
- }
618
- return true; // Event "handled" but not passed to other listeners
619
- }
620
- }
621
- // Pass through all other events and non-paste data normally
622
- return originalEmit(event, ...args);
623
- };
624
- // Store reference for cleanup
625
- this.rawDataHandler = () => {
626
- inputStream.emit = originalEmit;
627
- };
628
- }
629
- setupSlashCommandPreviewHandler() {
630
- const inputStream = input;
631
- if (!inputStream || typeof inputStream.on !== 'function' || !inputStream.isTTY) {
632
- return;
633
- }
634
- if (inputStream.listenerCount('keypress') === 0) {
635
- readline.emitKeypressEvents(inputStream, this.rl);
636
- }
637
- // Ensure raw mode for keypress events
638
- if (inputStream.setRawMode && !inputStream.isRaw) {
639
- inputStream.setRawMode(true);
640
- }
641
- this.keypressHandler = (_str, key) => {
642
- // Handle special keys
643
- if (key) {
644
- // Shift+Tab: Cycle paste preview options if paste blocks exist, otherwise profile switching
645
- if (key.name === 'tab' && key.shift) {
646
- // Check if there are paste blocks to cycle through
647
- const pasteBlockCount = this.composableMessage.getPasteBlockCount();
648
- if (pasteBlockCount > 0) {
649
- this.cyclePastePreview();
650
- return;
651
- }
652
- // Fall through to profile switching if no paste blocks
653
- if (this.agentMenu) {
654
- this.showProfileSwitcher();
655
- return;
656
- }
657
- }
658
- // Tab: Autocomplete slash commands
659
- if (key.name === 'tab' && !key.shift && !key.ctrl) {
660
- this.handleTabCompletion();
661
- return;
662
- }
663
- // Escape: Cancel current operation if agent is running
664
- if (key.name === 'escape') {
665
- if (this.isProcessing && this.agent) {
666
- this.agent.requestCancellation();
667
- output.write('\n');
668
- display.showWarning('Cancelling current operation... (waiting for safe stop point)');
669
- return;
670
- }
671
- // If not processing, clear any queued follow-ups
672
- if (this.followUpQueue.length > 0) {
673
- const cleared = this.followUpQueue.length;
674
- this.followUpQueue.length = 0;
675
- this.refreshQueueIndicators();
676
- output.write('\n');
677
- display.showInfo(`Cleared ${cleared} queued follow-up${cleared === 1 ? '' : 's'}.`);
678
- this.rl.prompt();
679
- return;
680
- }
681
- }
682
- // Ctrl+C: Clear input first, then cancel operation if no input
683
- if (key.ctrl && key.name === 'c') {
684
- // During processing, check pinnedChatBox input first
685
- if (this.isProcessing) {
686
- const chatBoxInput = this.pinnedChatBox.getInput();
687
- if (chatBoxInput.length > 0) {
688
- // Clear the chat box input instead of cancelling
689
- this.pinnedChatBox.clearPastedBlockState();
690
- this.pinnedChatBox.updatePersistentInput();
691
- return;
692
- }
693
- // No text in chat box, cancel the agent
694
- if (this.agent) {
695
- this.agent.requestCancellation();
696
- output.write('\n');
697
- display.showWarning('Cancelling current operation... (Ctrl+C again to force quit)');
698
- return;
699
- }
700
- }
701
- // Not processing - check readline input
702
- const currentLine = this.rl.line || '';
703
- if (currentLine.length > 0) {
704
- // Clear the input buffer instead of exiting
705
- this.rl.line = '';
706
- this.rl.cursor = 0;
707
- this.persistentPrompt.updateInput('', 0);
708
- this.pinnedChatBox.setInput('');
709
- this.pinnedChatBox.clearPastedBlockState();
710
- // Clear the displayed line and show fresh prompt
711
- output.write('\r\x1b[K'); // Clear current line
712
- output.write('^C\n'); // Show ^C indicator
713
- this.rl.prompt(); // Re-show prompt
714
- return;
715
- }
716
- // If no text in buffer, let default Ctrl+C behavior exit
717
- }
718
- // Ctrl+G: Edit pasted content blocks
719
- if (key.ctrl && key.name === 'g') {
720
- this.showPasteBlockEditor();
721
- return;
722
- }
723
- }
724
- // Claude Code approach: During processing, route input to PinnedChatBox
725
- // - User can type and see their input in the visible chat box at bottom
726
- // - Input is queued until streaming ends
727
- // - After each keystroke, we re-render the bottom area to show the input
728
- if (this.isProcessing) {
729
- // Handle paste: if _str contains newlines, it's a paste - don't auto-submit
730
- const isPaste = typeof _str === 'string' && (_str.length > 1 || _str.includes('\n'));
731
- if (key) {
732
- if (key.name === 'backspace') {
733
- this.pinnedChatBox.handleBackspace();
734
- }
735
- else if (key.name === 'delete') {
736
- this.pinnedChatBox.handleDelete();
737
- }
738
- else if (key.name === 'left') {
739
- if (key.meta || key.alt) {
740
- this.pinnedChatBox.handleWordLeft();
741
- }
742
- else {
743
- this.pinnedChatBox.handleCursorLeft();
744
- }
745
- }
746
- else if (key.name === 'right') {
747
- if (key.meta || key.alt) {
748
- this.pinnedChatBox.handleWordRight();
749
- }
750
- else {
751
- this.pinnedChatBox.handleCursorRight();
752
- }
753
- }
754
- else if (key.name === 'up') {
755
- this.pinnedChatBox.handleHistoryUp();
756
- }
757
- else if (key.name === 'down') {
758
- this.pinnedChatBox.handleHistoryDown();
759
- }
760
- else if (key.name === 'home' || (key.ctrl && key.name === 'a')) {
761
- this.pinnedChatBox.handleHome();
762
- }
763
- else if (key.name === 'end' || (key.ctrl && key.name === 'e')) {
764
- this.pinnedChatBox.handleEnd();
765
- }
766
- else if (key.ctrl && key.name === 'u') {
767
- this.pinnedChatBox.handleDeleteToStart();
768
- }
769
- else if (key.ctrl && key.name === 'k') {
770
- this.pinnedChatBox.handleDeleteToEnd();
771
- }
772
- else if (key.ctrl && key.name === 'w') {
773
- this.pinnedChatBox.handleDeleteWord();
774
- }
775
- else if (key.name === 'return' && (key.shift || key.meta || key.ctrl)) {
776
- // Shift+Enter or Option+Enter: Insert newline for multi-line input
777
- this.pinnedChatBox.handleNewline();
778
- }
779
- else if (key.name === 'return' && !isPaste) {
780
- // Queue the command - but NOT if this is part of a paste
781
- const result = this.pinnedChatBox.handleSubmit();
782
- if (result) {
783
- const preview = result.replace(/\s+/g, ' ').trim();
784
- const displayText = preview.length > 50 ? `${preview.slice(0, 47)}...` : preview;
785
- display.showInfo(`📝 Queued: "${displayText}"`);
786
- }
787
- }
788
- else if (_str && !key.ctrl && !key.meta) {
789
- // Route raw input (including multi-line paste) to the pinned chat box
790
- this.pinnedChatBox.handleInput(_str, { allowNewlines: true, isPaste });
791
- }
792
- }
793
- else if (_str) {
794
- // String without key info (paste) - pass through for summary handling
795
- this.pinnedChatBox.handleInput(_str, { allowNewlines: true, isPaste: true });
796
- }
797
- // Re-render the bottom chat box to show the updated input
798
- this.pinnedChatBox.updatePersistentInput();
799
- return;
800
- }
801
- // Readline handles all keyboard input natively (history, shortcuts, etc.)
802
- // We just sync the current state to our display components
803
- // Use setImmediate to get the updated line after readline processes the key
804
- setImmediate(() => {
805
- const currentLine = this.rl.line || '';
806
- const cursorPos = this.rl.cursor || 0;
807
- this.persistentPrompt.updateInput(currentLine, cursorPos);
808
- // Sync to pinned chat box for display only
809
- this.pinnedChatBox.setInput(currentLine, cursorPos);
810
- if (this.composableMessage.hasContent()) {
811
- this.composableMessage.setDraft(currentLine);
812
- this.updateComposeStatusSummary();
813
- }
814
- this.handleSlashCommandPreviewChange();
815
- });
816
- };
817
- // Use prependListener to handle before readline
818
- inputStream.prependListener('keypress', this.keypressHandler);
449
+ // Show initial input UI
450
+ this.terminalInput.render();
819
451
  }
820
452
  setupStatusTracking() {
821
453
  this.statusSubscription = this.statusTracker.subscribe((_state) => {
@@ -873,14 +505,10 @@ export class InteractiveShell {
873
505
  if (!shouldShow && this.slashPreviewVisible) {
874
506
  this.slashPreviewVisible = false;
875
507
  this.uiAdapter.hideSlashCommandPreview();
876
- // Show persistent prompt again after hiding overlay
877
- if (!this.isProcessing) {
878
- this.persistentPrompt.show();
879
- }
880
508
  }
881
509
  }
882
510
  shouldShowSlashCommandPreview() {
883
- const line = this.rl.line ?? '';
511
+ const line = this.currentInput ?? '';
884
512
  if (!line.trim()) {
885
513
  return false;
886
514
  }
@@ -889,642 +517,28 @@ export class InteractiveShell {
889
517
  }
890
518
  showSlashCommandPreview() {
891
519
  // Filter commands based on current input
892
- const line = this.rl.line ?? '';
520
+ const line = this.currentInput ?? '';
893
521
  const trimmed = line.trimStart();
894
522
  // Filter commands that match the current input
895
523
  const filtered = this.slashCommands.filter(cmd => cmd.command.startsWith(trimmed) || trimmed === '/');
896
- // Hide persistent prompt to avoid conflicts with overlay
897
- this.persistentPrompt.hide();
898
524
  // Show in the unified UI with dynamic overlay
899
525
  this.uiAdapter.showSlashCommandPreview(filtered);
900
526
  // Don't reprompt - this causes flickering
901
527
  }
902
- showProfileSwitcher() {
903
- if (!this.agentMenu) {
904
- return;
905
- }
906
- // Build profile options with current/next indicators
907
- const profiles = this.agentMenu.options.map((option, index) => {
908
- const badges = [];
909
- const nextProfile = this.agentMenu.persistedProfile ?? this.agentMenu.defaultProfile;
910
- if (option.name === this.profile) {
911
- badges.push('current');
912
- }
913
- if (option.name === nextProfile && option.name !== this.profile) {
914
- badges.push('next');
915
- }
916
- const badgeText = badges.length > 0 ? ` (${badges.join(', ')})` : '';
917
- return {
918
- command: `${index + 1}. ${option.label}${badgeText}`,
919
- description: `${this.providerLabel(option.defaultProvider)} • ${option.defaultModel}`,
920
- };
921
- });
922
- // Show profile switcher overlay
923
- this.uiAdapter.showProfileSwitcher(profiles, this.profileLabel);
924
- }
925
528
  /**
926
- * Ensure stdin is ready for interactive input before showing the prompt.
927
- * This recovers from cases where a nested readline/prompt or partial paste
928
- * left the stream paused, raw mode disabled, or keypress listeners detached.
529
+ * Ensure the terminal input is ready for interactive input.
929
530
  */
930
531
  ensureReadlineReady() {
931
- // Clear any stuck bracketed paste state so new input isn't dropped
932
- this.bracketedPaste.reset();
933
- // Always ensure the pinned chat box is visible when readline is ready
934
- this.pinnedChatBox.show();
935
- this.pinnedChatBox.forceRender();
936
- const inputStream = input;
937
- if (!inputStream) {
938
- return;
939
- }
940
- // Resume stdin if another consumer paused it
941
- if (typeof inputStream.isPaused === 'function' && inputStream.isPaused()) {
942
- inputStream.resume();
943
- }
944
- else if (inputStream.readableFlowing === false) {
945
- inputStream.resume();
946
- }
947
- // Restore raw mode if a nested readline turned it off
948
- if (inputStream.isTTY && typeof inputStream.isRaw === 'boolean') {
949
- const ttyStream = inputStream;
950
- if (!ttyStream.isRaw) {
951
- ttyStream.setRawMode(true);
952
- }
953
- }
954
- // Reattach keypress handler if it was removed
955
- if (this.keypressHandler) {
956
- const listeners = inputStream.listeners('keypress');
957
- const hasHandler = listeners.includes(this.keypressHandler);
958
- if (!hasHandler) {
959
- if (inputStream.listenerCount('keypress') === 0) {
960
- readline.emitKeypressEvents(inputStream, this.rl);
961
- }
962
- inputStream.on('keypress', this.keypressHandler);
963
- }
964
- }
965
- // Restore readline output if it was suppressed
966
- // Note: Stream handler manages its own state, this is for legacy compatibility
967
- if (!this.streamHandler.isStreaming()) {
968
- this.restoreReadlineOutput();
969
- }
970
- }
971
- /**
972
- * Suppress readline's character echo during streaming.
973
- * Characters typed will be captured but not echoed to the main output.
974
- * Instead, they appear only in the persistent input box at the bottom.
975
- *
976
- * @deprecated Use streamHandler.startStreaming() instead - kept for backwards compatibility
977
- * @internal Kept for legacy code paths that may still reference this method
978
- */
979
- // @ts-expect-error - Legacy method kept for backwards compatibility, unused in favor of streamHandler
980
- suppressReadlineOutput() {
981
- // Delegate to stream handler if it's not already streaming
982
- if (this.streamHandler.isStreaming()) {
983
- return;
984
- }
985
- if (this.readlineOutputSuppressed || !output.isTTY) {
986
- return;
987
- }
988
- this.originalStdoutWrite = output.write.bind(output);
989
- const self = this;
990
- // Replace stdout.write to filter readline echo
991
- // Readline writes single characters for echo - we filter those out
992
- // but allow multi-character writes (actual output) through
993
- output.write = function (chunk, encodingOrCallback, callback) {
994
- if (!self.originalStdoutWrite) {
995
- return true;
996
- }
997
- const str = typeof chunk === 'string' ? chunk : chunk.toString();
998
- // Filter out readline echo patterns:
999
- // - Single printable characters (user typing)
1000
- // - Cursor movement sequences for single chars
1001
- // - Backspace sequences
1002
- // - Prompt redraws
1003
- // But allow through:
1004
- // - Multi-character content (actual AI output)
1005
- // - Newlines and control sequences for formatting
1006
- // If it's a single printable char, suppress it (user typing)
1007
- if (str.length === 1 && str.charCodeAt(0) >= 32 && str.charCodeAt(0) < 127) {
1008
- return true;
1009
- }
1010
- // Suppress backspace sequences (readline's delete char)
1011
- if (str === '\b \b' || str === '\x1b[D \x1b[D' || str === '\b' || str === '\x7f') {
1012
- return true;
1013
- }
1014
- // Suppress cursor movement sequences (readline cursor positioning)
1015
- if (/^\x1b\[\d*[ABCD]$/.test(str)) {
1016
- return true;
1017
- }
1018
- // Suppress readline prompt redraw patterns (starts with \r or cursor home)
1019
- if (/^\r/.test(str) && str.length < 20 && /^[\r\x1b\[\dGK> ]+$/.test(str)) {
1020
- return true;
1021
- }
1022
- // Suppress clear line + prompt patterns from readline
1023
- if (/^\x1b\[\d*[GK]/.test(str) && str.length < 15) {
1024
- return true;
1025
- }
1026
- // Suppress short sequences that look like readline control (not AI content)
1027
- // AI content is typically longer or contains actual text
1028
- if (str.length <= 3 && /^[\x1b\[\]\d;GKJ]+$/.test(str)) {
1029
- return true;
1030
- }
1031
- // Allow everything else through (actual AI output)
1032
- if (typeof encodingOrCallback === 'function') {
1033
- return self.originalStdoutWrite(chunk, encodingOrCallback);
1034
- }
1035
- return self.originalStdoutWrite(chunk, encodingOrCallback, callback);
1036
- };
1037
- this.readlineOutputSuppressed = true;
1038
- }
1039
- /**
1040
- * Restore normal readline output after streaming completes.
1041
- *
1042
- * @deprecated Use streamHandler.endStreaming() instead
1043
- */
1044
- restoreReadlineOutput() {
1045
- if (!this.readlineOutputSuppressed || !this.originalStdoutWrite) {
1046
- return;
1047
- }
1048
- output.write = this.originalStdoutWrite;
1049
- this.originalStdoutWrite = null;
1050
- this.readlineOutputSuppressed = false;
1051
- }
1052
- enqueueUserInput(line, flushImmediately = false) {
1053
- this.bufferedInputLines.push(line);
1054
- if (flushImmediately) {
1055
- if (this.bufferedInputTimer) {
1056
- clearTimeout(this.bufferedInputTimer);
1057
- this.bufferedInputTimer = null;
1058
- }
1059
- void this.flushBufferedInput();
1060
- return;
1061
- }
1062
- if (this.bufferedInputTimer) {
1063
- clearTimeout(this.bufferedInputTimer);
1064
- }
1065
- this.bufferedInputTimer = setTimeout(() => {
1066
- void this.flushBufferedInput();
1067
- }, MULTILINE_INPUT_FLUSH_DELAY_MS);
1068
- }
1069
- /**
1070
- * Clear any buffered input lines and pending flush timers.
1071
- * Prevents partial multi-line pastes from being auto-submitted.
1072
- */
1073
- resetBufferedInputLines() {
1074
- if (this.bufferedInputTimer) {
1075
- clearTimeout(this.bufferedInputTimer);
1076
- this.bufferedInputTimer = null;
1077
- }
1078
- if (this.bufferedInputLines.length) {
1079
- this.bufferedInputLines = [];
1080
- }
1081
- }
1082
- /** Track paste preview state to properly clear/update display */
1083
- lastPastePreviewLineCount = 0;
1084
- inPasteCapture = false;
1085
- /**
1086
- * Show a preview indicator while accumulating multi-line paste.
1087
- * The line handler has already cleared readline's echoed line, so we just
1088
- * write our preview in place using carriage return.
1089
- */
1090
- showMultiLinePastePreview(lineCount, preview) {
1091
- // Only update if we're actually accumulating (avoid spam during rapid paste)
1092
- if (lineCount === 0 && !preview) {
1093
- return;
1094
- }
1095
- // Mark that we're in paste capture mode
1096
- this.inPasteCapture = true;
1097
- // Throttle updates - only update if line count changed to avoid flicker
1098
- if (lineCount === this.lastPastePreviewLineCount) {
1099
- return;
1100
- }
1101
- this.lastPastePreviewLineCount = lineCount;
1102
- // Clear current line and write preview (the line handler already moved cursor up)
1103
- output.write('\r\x1b[K');
1104
- const statusText = preview
1105
- ? `${theme.ui.muted('📋 Pasting:')} ${theme.ui.muted(preview.slice(0, 50))}${preview.length > 50 ? '...' : ''}`
1106
- : `${theme.ui.muted(`📋 Pasting ${lineCount} line${lineCount !== 1 ? 's' : ''}...`)}`;
1107
- output.write(statusText);
1108
- }
1109
- /**
1110
- * Clear the multi-line paste preview
1111
- */
1112
- clearMultiLinePastePreview() {
1113
- if (!this.inPasteCapture) {
1114
- return;
1115
- }
1116
- // Clear current line
1117
- output.write('\r\x1b[K');
1118
- // Reset tracking state
1119
- this.lastPastePreviewLineCount = 0;
1120
- this.inPasteCapture = false;
1121
- }
1122
- /**
1123
- * Capture any paste (single or multi-line) without immediately submitting it.
1124
- * - Short pastes (1-2 lines) are displayed inline like normal typed text
1125
- * - Longer pastes (3+ lines) show as collapsed block chips
1126
- * Supports multiple pastes - user can paste multiple times before submitting.
1127
- */
1128
- async capturePaste(content, lineCount) {
1129
- this.resetBufferedInputLines();
1130
- // Short pastes (1-2 lines) display inline like normal text
1131
- const isShortPaste = lineCount <= 2;
1132
- if (isShortPaste) {
1133
- // For short pastes, display inline like normal typed text
1134
- // No composableMessage storage - just treat as typed input
1135
- // For 2-line pastes, join with a visual newline indicator
1136
- const displayContent = lineCount === 1
1137
- ? content
1138
- : content.replace(/\n/g, ' ↵ '); // Visual newline indicator for 2-line pastes
1139
- // Clear any echoed content first
1140
- output.write('\r\x1b[K');
1141
- // Get current readline content and append paste
1142
- const currentLine = this.rl.line || '';
1143
- const cursorPos = this.rl.cursor || 0;
1144
- // Insert paste at cursor position
1145
- const before = currentLine.slice(0, cursorPos);
1146
- const after = currentLine.slice(cursorPos);
1147
- const newLine = before + displayContent + after;
1148
- const newCursor = cursorPos + displayContent.length;
1149
- // Update readline buffer - write directly without storing in composableMessage
1150
- // This allows short pastes to flow through as normal typed text
1151
- this.rl.write(null, { ctrl: true, name: 'u' }); // Clear line
1152
- this.rl.write(newLine); // Write new content
1153
- // Update persistent prompt display
1154
- this.persistentPrompt.updateInput(newLine, newCursor);
1155
- // NOTE: Don't clear pasteJustCaptured here - the counter-based logic in shouldIgnoreLineEvent()
1156
- // will decrement for each readline line event and auto-clear when all are processed.
1157
- // Clearing prematurely causes the remaining readline-echoed lines to pass through.
1158
- // Re-prompt to show the inline content
1159
- this.rl.prompt(true);
1160
- return;
1161
- }
1162
- // For longer pastes (3+ lines), store as a composable block
1163
- // Check size limits first
1164
- const { ComposableMessageBuilder } = await import('./composableMessage.js');
1165
- const sizeCheck = ComposableMessageBuilder.checkPasteSize(content);
1166
- if (!sizeCheck.ok) {
1167
- // Paste rejected - show error and abort
1168
- display.showError(sizeCheck.error || 'Paste rejected due to size limits');
1169
- this.rl.prompt();
1170
- return;
1171
- }
1172
- // Show warning for large (but acceptable) pastes
1173
- if (sizeCheck.warning) {
1174
- display.showWarning(`⚠️ ${sizeCheck.warning}`);
1175
- }
1176
- const pasteId = this.composableMessage.addPaste(content);
1177
- if (!pasteId) {
1178
- // Should not happen since we checked above, but handle defensively
1179
- display.showError('Failed to store paste block');
1180
- this.rl.prompt();
1181
- return;
1182
- }
1183
- // Clear remaining echoed lines from terminal
1184
- output.write('\r\x1b[K');
1185
- // Build the paste chips to show inline with prompt
1186
- // Format: [Pasted text #1 +104 lines] [Pasted text #2 +50 lines]
1187
- const pasteChips = this.composableMessage.formatPasteChips();
1188
- // Update status bar - minimal hint, no confirmation required
1189
- this.persistentPrompt.updateStatusBar({
1190
- message: 'Press Enter to send',
1191
- });
1192
- // Set the prompt to show paste chips, then position cursor after them
1193
- // The user can type additional text after the chips
1194
- this.persistentPrompt.updateInput(pasteChips + ' ', pasteChips.length + 1);
1195
- // Update readline's line buffer to include the chips as prefix
1196
- // This ensures typed text appears after the chips
1197
- if (this.rl.line !== undefined) {
1198
- this.rl.line = pasteChips + ' ';
1199
- this.rl.cursor = pasteChips.length + 1;
1200
- }
1201
- // NOTE: Don't clear pasteJustCaptured here - the counter-based logic in shouldIgnoreLineEvent()
1202
- // will decrement for each readline line event (one per pasted line) and auto-clear when done.
1203
- // Clearing prematurely causes remaining readline-echoed lines to pass through and get displayed.
1204
- this.rl.prompt(true); // preserveCursor=true to keep position after chips
1205
- }
1206
- /**
1207
- * Update the status bar to reflect any pending composed message parts
1208
- */
1209
- updateComposeStatusSummary() {
1210
- if (this.composableMessage.hasContent()) {
1211
- // Chips are shown inline - minimal status hint
1212
- this.persistentPrompt.updateStatusBar({
1213
- message: 'Press Enter to send',
1214
- });
1215
- }
1216
- else {
1217
- this.persistentPrompt.updateStatusBar({ message: undefined });
1218
- }
1219
- }
1220
- /**
1221
- * Show paste block editor (Ctrl+G)
1222
- * Allows viewing and editing captured paste blocks before sending
1223
- *
1224
- * Features:
1225
- * - View block content with line numbers
1226
- * - Edit blocks inline (replace content)
1227
- * - Remove individual blocks
1228
- * - Undo/redo paste operations
1229
- * - Clear all blocks
1230
- */
1231
- showPasteBlockEditor() {
1232
- const state = this.composableMessage.getState();
1233
- const pasteBlocks = state.parts.filter((p) => p.type === 'paste');
1234
- if (pasteBlocks.length === 0) {
1235
- display.showInfo('No pasted content blocks to edit. Paste some content first.');
1236
- this.rl.prompt();
1237
- return;
1238
- }
1239
- output.write('\n');
1240
- display.showSystemMessage('📋 Paste Block Editor');
1241
- output.write('\n');
1242
- // Display each paste block with preview
1243
- pasteBlocks.forEach((block, index) => {
1244
- const lines = block.content.split('\n');
1245
- const preview = lines.slice(0, 3).map((l) => ` ${l.slice(0, 60)}${l.length > 60 ? '...' : ''}`).join('\n');
1246
- const moreLines = lines.length > 3 ? `\n ... +${lines.length - 3} more lines` : '';
1247
- const editedFlag = block.edited ? theme.warning(' [edited]') : '';
1248
- output.write(theme.ui.muted(`[${index + 1}] `) + theme.info(`${block.lineCount} lines, ${block.content.length} chars`) + editedFlag + '\n');
1249
- output.write(theme.secondary(preview + moreLines) + '\n\n');
1250
- });
1251
- // Show undo/redo status
1252
- const canUndo = this.composableMessage.canUndo();
1253
- const canRedo = this.composableMessage.canRedo();
1254
- if (canUndo || canRedo) {
1255
- const undoStatus = canUndo ? theme.success('undo available') : theme.ui.muted('undo unavailable');
1256
- const redoStatus = canRedo ? theme.success('redo available') : theme.ui.muted('redo unavailable');
1257
- output.write(theme.ui.muted('History: ') + undoStatus + theme.ui.muted(' │ ') + redoStatus + '\n\n');
1258
- }
1259
- output.write(theme.ui.muted('Commands: ') + '\n');
1260
- output.write(theme.ui.muted(' • Enter number to view full block') + '\n');
1261
- output.write(theme.ui.muted(' • "edit N" to replace block N content') + '\n');
1262
- output.write(theme.ui.muted(' • "remove N" to remove block N') + '\n');
1263
- output.write(theme.ui.muted(' • "undo" / "redo" to undo/redo changes') + '\n');
1264
- output.write(theme.ui.muted(' • "clear" to remove all blocks') + '\n');
1265
- output.write(theme.ui.muted(' • Enter to return to prompt') + '\n');
1266
- output.write('\n');
1267
- // Set up interaction mode for paste editing
1268
- this.pendingInteraction = { type: 'paste-edit' };
1269
- this.rl.prompt();
1270
- }
1271
- /**
1272
- * Handle paste block editor input
1273
- */
1274
- async handlePasteEditInput(trimmedInput) {
1275
- const trimmed = trimmedInput.toLowerCase();
1276
- const state = this.composableMessage.getState();
1277
- const pasteBlocks = state.parts.filter((p) => p.type === 'paste');
1278
- if (!trimmed) {
1279
- // Exit paste edit mode
1280
- this.pendingInteraction = null;
1281
- display.showInfo('Returned to prompt.');
1282
- this.rl.prompt();
1283
- return;
1284
- }
1285
- // Handle undo command
1286
- if (trimmed === 'undo') {
1287
- if (this.composableMessage.undo()) {
1288
- this.updateComposeStatusSummary();
1289
- this.refreshPasteChipsDisplay();
1290
- display.showInfo('Undone.');
1291
- // Refresh the paste block view
1292
- this.showPasteBlockEditor();
1293
- }
1294
- else {
1295
- display.showWarning('Nothing to undo.');
1296
- this.rl.prompt();
1297
- }
1298
- return;
1299
- }
1300
- // Handle redo command
1301
- if (trimmed === 'redo') {
1302
- if (this.composableMessage.redo()) {
1303
- this.updateComposeStatusSummary();
1304
- this.refreshPasteChipsDisplay();
1305
- display.showInfo('Redone.');
1306
- // Refresh the paste block view
1307
- this.showPasteBlockEditor();
1308
- }
1309
- else {
1310
- display.showWarning('Nothing to redo.');
1311
- this.rl.prompt();
1312
- }
1313
- return;
1314
- }
1315
- if (trimmed === 'clear') {
1316
- this.composableMessage.clear();
1317
- this.updateComposeStatusSummary();
1318
- this.persistentPrompt.updateInput('', 0);
1319
- this.rl.line = '';
1320
- this.rl.cursor = 0;
1321
- this.pendingInteraction = null;
1322
- display.showInfo('Cleared all paste blocks.');
1323
- this.rl.prompt();
1324
- return;
1325
- }
1326
- // Handle "edit N" command - start inline editing mode
1327
- const editMatch = trimmed.match(/^edit\s+(\d+)$/);
1328
- if (editMatch) {
1329
- const blockNum = parseInt(editMatch[1], 10);
1330
- if (blockNum >= 1 && blockNum <= pasteBlocks.length) {
1331
- const block = pasteBlocks[blockNum - 1];
1332
- if (block) {
1333
- // Enter edit mode for this block
1334
- this.pendingInteraction = { type: 'paste-edit-block', blockId: block.id, blockNum };
1335
- output.write('\n');
1336
- display.showSystemMessage(`Editing Block ${blockNum}:`);
1337
- output.write(theme.ui.muted('Paste or type new content. Press Enter twice to finish, or "cancel" to abort.\n'));
1338
- output.write('\n');
1339
- // Show current content as reference
1340
- const preview = block.content.split('\n').slice(0, 5).map((l) => theme.ui.muted(` ${l.slice(0, 60)}`)).join('\n');
1341
- output.write(theme.ui.muted('Current content (first 5 lines):\n') + preview + '\n\n');
1342
- }
1343
- }
1344
- else {
1345
- display.showWarning(`Invalid block number. Use 1-${pasteBlocks.length}.`);
1346
- }
1347
- this.rl.prompt();
1348
- return;
1349
- }
1350
- // Handle "remove N" command
1351
- const removeMatch = trimmed.match(/^remove\s+(\d+)$/);
1352
- if (removeMatch) {
1353
- const blockNum = parseInt(removeMatch[1], 10);
1354
- if (blockNum >= 1 && blockNum <= pasteBlocks.length) {
1355
- const block = pasteBlocks[blockNum - 1];
1356
- if (block) {
1357
- this.composableMessage.removePart(block.id);
1358
- this.updateComposeStatusSummary();
1359
- this.refreshPasteChipsDisplay();
1360
- display.showInfo(`Removed block ${blockNum}.`);
1361
- // Check if any blocks remain
1362
- const remaining = this.composableMessage.getState().parts.filter(p => p.type === 'paste');
1363
- if (remaining.length === 0) {
1364
- this.pendingInteraction = null;
1365
- display.showInfo('No more paste blocks.');
1366
- }
1367
- }
1368
- }
1369
- else {
1370
- display.showWarning(`Invalid block number. Use 1-${pasteBlocks.length}.`);
1371
- }
1372
- this.rl.prompt();
1373
- return;
1374
- }
1375
- // Handle viewing a block by number
1376
- const blockNum = parseInt(trimmed, 10);
1377
- if (!Number.isNaN(blockNum) && blockNum >= 1 && blockNum <= pasteBlocks.length) {
1378
- const block = pasteBlocks[blockNum - 1];
1379
- if (block) {
1380
- output.write('\n');
1381
- display.showSystemMessage(`Block ${blockNum} Content:`);
1382
- output.write('\n');
1383
- // Show full content with line numbers
1384
- const lines = block.content.split('\n');
1385
- lines.forEach((line, i) => {
1386
- const lineNum = theme.ui.muted(`${String(i + 1).padStart(4)} │ `);
1387
- output.write(lineNum + line + '\n');
1388
- });
1389
- output.write('\n');
1390
- }
1391
- this.rl.prompt();
1392
- return;
1393
- }
1394
- display.showWarning('Enter a block number, "edit N", "remove N", "undo", "redo", "clear", or press Enter to return.');
1395
- this.rl.prompt();
1396
- }
1397
- /**
1398
- * Handle paste block inline editing mode
1399
- */
1400
- editBlockBuffer = [];
1401
- async handlePasteBlockEditInput(input, blockId, blockNum) {
1402
- // Check for cancel
1403
- if (input.toLowerCase() === 'cancel') {
1404
- this.editBlockBuffer = [];
1405
- this.pendingInteraction = { type: 'paste-edit' };
1406
- display.showInfo('Edit cancelled.');
1407
- this.showPasteBlockEditor();
1408
- return;
1409
- }
1410
- // Empty line - check if this is end of edit (double Enter)
1411
- if (!input.trim()) {
1412
- if (this.editBlockBuffer.length > 0) {
1413
- // Complete the edit
1414
- const newContent = this.editBlockBuffer.join('\n');
1415
- this.composableMessage.editPaste(blockId, newContent);
1416
- this.editBlockBuffer = [];
1417
- this.updateComposeStatusSummary();
1418
- this.refreshPasteChipsDisplay();
1419
- display.showInfo(`Block ${blockNum} updated (${newContent.split('\n').length} lines).`);
1420
- this.pendingInteraction = { type: 'paste-edit' };
1421
- this.showPasteBlockEditor();
1422
- return;
1423
- }
1424
- // First empty line with no buffer - just prompt again
1425
- this.rl.prompt();
1426
- return;
1427
- }
1428
- // Add line to edit buffer
1429
- this.editBlockBuffer.push(input);
1430
- this.rl.prompt();
1431
- }
1432
- /**
1433
- * Refresh paste chips display in prompt
1434
- */
1435
- refreshPasteChipsDisplay() {
1436
- const chips = this.composableMessage.formatPasteChips();
1437
- this.persistentPrompt.updateInput(chips ? chips + ' ' : '', chips ? chips.length + 1 : 0);
1438
- this.rl.line = chips ? chips + ' ' : '';
1439
- this.rl.cursor = chips ? chips.length + 1 : 0;
1440
- }
1441
- /**
1442
- * Cycle paste preview mode (Shift+Tab)
1443
- * Cycles through: collapsed → summary → expanded
1444
- */
1445
- pastePreviewMode = 'collapsed';
1446
- cyclePastePreview() {
1447
- const state = this.composableMessage.getState();
1448
- const pasteBlocks = state.parts.filter((p) => p.type === 'paste');
1449
- if (pasteBlocks.length === 0) {
1450
- return;
1451
- }
1452
- // Cycle through modes
1453
- const modes = ['collapsed', 'summary', 'expanded'];
1454
- const currentIndex = modes.indexOf(this.pastePreviewMode);
1455
- this.pastePreviewMode = modes[(currentIndex + 1) % modes.length];
1456
- output.write('\n');
1457
- switch (this.pastePreviewMode) {
1458
- case 'collapsed':
1459
- // Show just chips
1460
- display.showSystemMessage(`📋 Preview: Collapsed (${pasteBlocks.length} block${pasteBlocks.length > 1 ? 's' : ''})`);
1461
- output.write(theme.ui.muted(this.composableMessage.formatPasteChips()) + '\n');
1462
- break;
1463
- case 'summary':
1464
- // Show blocks with first line preview
1465
- display.showSystemMessage('📋 Preview: Summary');
1466
- pasteBlocks.forEach((block, i) => {
1467
- const preview = block.summary.length > 60 ? block.summary.slice(0, 57) + '...' : block.summary;
1468
- output.write(theme.ui.muted(`[${i + 1}] `) + theme.info(`${block.lineCount}L`) + theme.ui.muted(` ${preview}`) + '\n');
1469
- });
1470
- break;
1471
- case 'expanded':
1472
- // Show full content (up to 10 lines each)
1473
- display.showSystemMessage('📋 Preview: Expanded');
1474
- pasteBlocks.forEach((block, i) => {
1475
- output.write(theme.ui.muted(`[${i + 1}] `) + theme.info(`${block.lineCount} lines, ${block.content.length} chars`) + '\n');
1476
- const lines = block.content.split('\n').slice(0, 10);
1477
- lines.forEach((line, j) => {
1478
- const lineNum = theme.ui.muted(`${String(j + 1).padStart(3)} │ `);
1479
- output.write(lineNum + line.slice(0, 80) + (line.length > 80 ? '...' : '') + '\n');
1480
- });
1481
- if (block.lineCount > 10) {
1482
- output.write(theme.ui.muted(` ... +${block.lineCount - 10} more lines\n`));
1483
- }
1484
- output.write('\n');
1485
- });
1486
- break;
1487
- }
1488
- output.write(theme.ui.muted('(Shift+Tab to cycle preview modes)') + '\n\n');
1489
- this.rl.prompt(true);
1490
- }
1491
- async flushBufferedInput() {
1492
- if (!this.bufferedInputLines.length) {
1493
- this.bufferedInputTimer = null;
1494
- return;
1495
- }
1496
- const lineCount = this.bufferedInputLines.length;
1497
- const combined = this.bufferedInputLines.join('\n');
1498
- this.bufferedInputLines = [];
1499
- this.bufferedInputTimer = null;
1500
- try {
1501
- await this.processInputBlock(combined, lineCount > 1);
1502
- }
1503
- catch (error) {
1504
- // Pass full error object for enhanced formatting
1505
- display.showError(error instanceof Error ? error.message : String(error), error);
1506
- this.rl.prompt();
1507
- }
532
+ this.terminalInput.render();
1508
533
  }
1509
534
  refreshQueueIndicators() {
1510
- const queued = this.followUpQueue.length;
1511
- // Build status message based on processing state and queue
1512
- let message;
1513
- if (this.isProcessing) {
1514
- const queuePart = queued > 0 ? `${queued} queued · ` : '';
1515
- // Show helpful hints: Esc to cancel, type to queue
1516
- message = `⏳ Processing... ${queuePart}[Esc: cancel · Enter: queue follow-up]`;
1517
- }
1518
- else if (queued > 0) {
1519
- message = `${queued} follow-up${queued === 1 ? '' : 's'} queued · [Esc: clear queue]`;
1520
- }
1521
- this.persistentPrompt.updateStatusBar({ message });
1522
535
  if (this.isProcessing) {
1523
536
  this.setProcessingStatus();
1524
537
  }
1525
538
  else {
1526
539
  this.setIdleStatus();
1527
540
  }
541
+ this.terminalInput.render();
1528
542
  }
1529
543
  enqueueFollowUpAction(action) {
1530
544
  this.followUpQueue.push(action);
@@ -1533,23 +547,17 @@ export class InteractiveShell {
1533
547
  const preview = normalized.length > previewLimit ? `${normalized.slice(0, previewLimit - 3)}...` : normalized;
1534
548
  const position = this.followUpQueue.length === 1 ? 'to run next' : `#${this.followUpQueue.length} in queue`;
1535
549
  const label = action.type === 'continuous' ? 'continuous command' : 'follow-up';
1536
- const queueCount = this.followUpQueue.length;
1537
550
  // Show immediate acknowledgment with checkmark
1538
- const queueLabel = queueCount === 1 ? '1 queued' : `${queueCount} queued`;
1539
551
  if (preview) {
1540
552
  display.showInfo(`✓ ${theme.info(label)} ${position}: ${theme.ui.muted(preview)}`);
1541
553
  }
1542
554
  else {
1543
555
  display.showInfo(`✓ ${theme.info(label)} queued ${position}.`);
1544
556
  }
1545
- // Update status bar to show queue count
1546
- this.persistentPrompt.updateStatusBar({
1547
- message: `⏳ Processing... (${queueLabel})`
1548
- });
1549
557
  this.refreshQueueIndicators();
1550
558
  this.scheduleQueueProcessing();
1551
559
  // Re-show the prompt so user can continue typing more follow-ups
1552
- this.rl.prompt();
560
+ this.terminalInput.render();
1553
561
  }
1554
562
  scheduleQueueProcessing() {
1555
563
  if (!this.followUpQueue.length) {
@@ -1561,71 +569,8 @@ export class InteractiveShell {
1561
569
  });
1562
570
  }
1563
571
  /**
1564
- * Process any inputs that were queued during streaming.
1565
- * Claude Code style: User can type while streaming, input is stored in queue,
1566
- * processed after streaming ends.
1567
- *
1568
- * @deprecated Use processUnifiedChatBoxQueue() instead
1569
- */
1570
- // @ts-expect-error - Legacy method kept for backwards compatibility
1571
- processQueuedInputs() {
1572
- if (!this.streamHandler.hasQueuedInput()) {
1573
- return;
1574
- }
1575
- // Transfer queued inputs from stream handler to follow-up queue
1576
- while (this.streamHandler.hasQueuedInput()) {
1577
- const input = this.streamHandler.getNextQueuedInput();
1578
- if (!input) {
1579
- break;
1580
- }
1581
- // Handle interrupts specially
1582
- if (input.type === 'interrupt') {
1583
- // Interrupt was already processed during capture
1584
- continue;
1585
- }
1586
- // Add to follow-up queue for processing
1587
- const actionType = input.type === 'command' ? 'request' : 'request';
1588
- this.followUpQueue.push({
1589
- type: actionType,
1590
- text: input.text,
1591
- });
1592
- }
1593
- // Show queue summary if there are items
1594
- const summary = this.streamHandler.getQueueSummary();
1595
- if (summary) {
1596
- display.showInfo(`📝 ${summary}`);
1597
- }
1598
- }
1599
- /**
1600
- * Process any inputs that were queued in UnifiedChatBox during streaming.
1601
- * Claude Code style: User can type while streaming, input is stored invisibly,
1602
- * processed after streaming ends.
572
+ * Process queued follow-up actions.
1603
573
  */
1604
- processUnifiedChatBoxQueue() {
1605
- if (!this.unifiedChatBox.hasQueuedInput()) {
1606
- return;
1607
- }
1608
- // Transfer queued inputs from unified chat box to follow-up queue
1609
- while (this.unifiedChatBox.hasQueuedInput()) {
1610
- const input = this.unifiedChatBox.dequeue();
1611
- if (!input) {
1612
- break;
1613
- }
1614
- // Handle interrupts specially - they cancel current operation
1615
- if (input.type === 'interrupt') {
1616
- // Request cancellation if agent is running
1617
- if (this.agent) {
1618
- this.agent.requestCancellation();
1619
- }
1620
- continue;
1621
- }
1622
- // Add to follow-up queue for processing
1623
- this.followUpQueue.push({
1624
- type: 'request',
1625
- text: input.text,
1626
- });
1627
- }
1628
- }
1629
574
  async processQueuedActions() {
1630
575
  if (this.isDrainingQueue || this.isProcessing || !this.followUpQueue.length) {
1631
576
  return;
@@ -1659,85 +604,22 @@ export class InteractiveShell {
1659
604
  if (await this.handlePendingInteraction(trimmed)) {
1660
605
  return;
1661
606
  }
1662
- // If we have captured multi-line paste blocks, respect control commands before assembling
1663
- if (this.composableMessage.hasContent()) {
1664
- // Strip paste chip prefixes from input since actual content is in composableMessage
1665
- // Chips look like: [Pasted text #1 +X lines] [Pasted text #2 +Y lines]
1666
- const chipsPrefix = this.composableMessage.formatPasteChips();
1667
- let userText = trimmed;
1668
- if (chipsPrefix && trimmed.startsWith(chipsPrefix)) {
1669
- userText = trimmed.slice(chipsPrefix.length).trim();
1670
- }
1671
- const lower = userText.toLowerCase();
1672
- // Control commands that should NOT consume the captured paste
1673
- if (lower === '/cancel' || lower === 'cancel') {
1674
- this.composableMessage.clear();
1675
- this.updateComposeStatusSummary();
1676
- this.persistentPrompt.updateInput('', 0);
1677
- display.showInfo('Discarded captured paste.');
1678
- this.rl.prompt();
1679
- return;
1680
- }
1681
- if (lower === 'exit' || lower === 'quit') {
1682
- this.rl.close();
1683
- return;
1684
- }
1685
- if (lower === 'clear') {
1686
- display.clear();
1687
- this.rl.prompt();
1688
- return;
1689
- }
1690
- if (lower === 'help') {
1691
- this.showHelp();
1692
- this.rl.prompt();
1693
- return;
1694
- }
1695
- // Slash commands operate independently of captured paste
1696
- if (userText.startsWith('/')) {
1697
- await this.processSlashCommand(userText);
1698
- this.updateComposeStatusSummary();
1699
- return;
1700
- }
1701
- // If userText is empty OR it's additional content, assemble and send
1702
- // Empty enter sends the captured paste; non-empty content is appended first
1703
- if (userText) {
1704
- this.composableMessage.setDraft(userText);
1705
- this.composableMessage.commitDraft();
1706
- }
1707
- const assembled = this.composableMessage.assemble();
1708
- this.composableMessage.clear();
1709
- this.updateComposeStatusSummary();
1710
- this.persistentPrompt.updateInput('', 0);
1711
- if (!assembled) {
1712
- this.rl.prompt();
1713
- return;
1714
- }
1715
- // Check if assembled content is a continuous command
1716
- if (this.isContinuousCommand(assembled)) {
1717
- await this.processContinuousRequest(assembled);
1718
- this.rl.prompt();
1719
- return;
1720
- }
1721
- await this.processRequest(assembled);
1722
- this.rl.prompt();
1723
- return;
1724
- }
1725
607
  if (!trimmed) {
1726
- this.rl.prompt();
1727
608
  return;
1728
609
  }
1729
- if (trimmed.toLowerCase() === 'exit' || trimmed.toLowerCase() === 'quit') {
1730
- this.rl.close();
610
+ const lower = trimmed.toLowerCase();
611
+ if (lower === 'exit' || lower === 'quit') {
612
+ this.shutdown();
1731
613
  return;
1732
614
  }
1733
- if (trimmed.toLowerCase() === 'clear') {
615
+ if (lower === 'clear') {
1734
616
  display.clear();
1735
- this.rl.prompt();
617
+ this.terminalInput.render();
1736
618
  return;
1737
619
  }
1738
- if (trimmed.toLowerCase() === 'help') {
620
+ if (lower === 'help') {
1739
621
  this.showHelp();
1740
- this.rl.prompt();
622
+ this.terminalInput.render();
1741
623
  return;
1742
624
  }
1743
625
  if (trimmed.startsWith('/')) {
@@ -1747,12 +629,12 @@ export class InteractiveShell {
1747
629
  // Check for continuous/infinite loop commands
1748
630
  if (this.isContinuousCommand(trimmed)) {
1749
631
  await this.processContinuousRequest(trimmed);
1750
- this.rl.prompt();
632
+ this.terminalInput.render();
1751
633
  return;
1752
634
  }
1753
635
  // Direct execution for all inputs, including multi-line pastes
1754
636
  await this.processRequest(trimmed);
1755
- this.rl.prompt();
637
+ this.terminalInput.render();
1756
638
  }
1757
639
  /**
1758
640
  * Check if the command is a continuous/infinite loop command
@@ -1804,12 +686,6 @@ export class InteractiveShell {
1804
686
  case 'agent-selection':
1805
687
  await this.handleAgentSelectionInput(input);
1806
688
  return true;
1807
- case 'paste-edit':
1808
- await this.handlePasteEditInput(input);
1809
- return true;
1810
- case 'paste-edit-block':
1811
- await this.handlePasteBlockEditInput(input, this.pendingInteraction.blockId, this.pendingInteraction.blockNum);
1812
- return true;
1813
689
  default:
1814
690
  return false;
1815
691
  }
@@ -1818,7 +694,7 @@ export class InteractiveShell {
1818
694
  const [command] = input.split(/\s+/);
1819
695
  if (!command) {
1820
696
  display.showWarning('Enter a slash command.');
1821
- this.rl.prompt();
697
+ this.terminalInput.render();
1822
698
  return;
1823
699
  }
1824
700
  switch (command) {
@@ -1893,7 +769,7 @@ export class InteractiveShell {
1893
769
  }
1894
770
  break;
1895
771
  }
1896
- this.rl.prompt();
772
+ this.terminalInput.render();
1897
773
  }
1898
774
  async tryCustomSlashCommand(command, fullInput) {
1899
775
  const custom = this.customCommandMap.get(command);
@@ -1927,100 +803,6 @@ export class InteractiveShell {
1927
803
  ];
1928
804
  display.showSystemMessage(info.join('\n'));
1929
805
  }
1930
- /**
1931
- * Handle Tab key for slash command autocompletion
1932
- * Completes partial slash commands like /mo -> /model
1933
- */
1934
- handleTabCompletion() {
1935
- const currentLine = this.rl.line || '';
1936
- const cursorPos = this.rl.cursor || 0;
1937
- // Only complete if line starts with /
1938
- if (!currentLine.startsWith('/')) {
1939
- return;
1940
- }
1941
- // Get the partial command (from / to cursor)
1942
- const partial = currentLine.slice(0, cursorPos).toLowerCase();
1943
- // Available slash commands
1944
- const commands = [
1945
- '/help',
1946
- '/model',
1947
- '/secrets',
1948
- '/tools',
1949
- '/doctor',
1950
- '/clear',
1951
- '/cancel',
1952
- '/compact',
1953
- '/cost',
1954
- '/status',
1955
- '/undo',
1956
- '/redo',
1957
- ];
1958
- // Find matching commands
1959
- const matches = commands.filter(cmd => cmd.startsWith(partial));
1960
- if (matches.length === 0) {
1961
- // No matches - beep or do nothing
1962
- return;
1963
- }
1964
- if (matches.length === 1) {
1965
- // Single match - complete it
1966
- const completion = matches[0];
1967
- const suffix = currentLine.slice(cursorPos);
1968
- const newLine = completion + suffix;
1969
- const newCursor = completion.length;
1970
- // Update readline
1971
- this.rl.line = newLine;
1972
- this.rl.cursor = newCursor;
1973
- // Redraw
1974
- output.write('\r\x1b[K'); // Clear line
1975
- output.write(formatUserPrompt(this.profileLabel || this.profile));
1976
- output.write(newLine);
1977
- // Position cursor
1978
- if (suffix.length > 0) {
1979
- output.write(`\x1b[${suffix.length}D`);
1980
- }
1981
- // Sync to display components
1982
- this.persistentPrompt.updateInput(newLine, newCursor);
1983
- this.pinnedChatBox.setInput(newLine, newCursor);
1984
- }
1985
- else {
1986
- // Multiple matches - show them
1987
- output.write('\n');
1988
- output.write(theme.ui.muted('Completions: ') + matches.join(' ') + '\n');
1989
- this.rl.prompt();
1990
- // Find common prefix for partial completion
1991
- const commonPrefix = this.findCommonPrefix(matches);
1992
- if (commonPrefix.length > partial.length) {
1993
- const suffix = currentLine.slice(cursorPos);
1994
- const newLine = commonPrefix + suffix;
1995
- const newCursor = commonPrefix.length;
1996
- this.rl.line = newLine;
1997
- this.rl.cursor = newCursor;
1998
- this.persistentPrompt.updateInput(newLine, newCursor);
1999
- this.pinnedChatBox.setInput(newLine, newCursor);
2000
- }
2001
- }
2002
- }
2003
- /**
2004
- * Find the longest common prefix among strings
2005
- */
2006
- findCommonPrefix(strings) {
2007
- if (strings.length === 0)
2008
- return '';
2009
- if (strings.length === 1)
2010
- return strings[0];
2011
- let prefix = strings[0];
2012
- for (let i = 1; i < strings.length; i++) {
2013
- const str = strings[i];
2014
- let j = 0;
2015
- while (j < prefix.length && j < str.length && prefix[j] === str[j]) {
2016
- j++;
2017
- }
2018
- prefix = prefix.slice(0, j);
2019
- if (prefix.length === 0)
2020
- break;
2021
- }
2022
- return prefix;
2023
- }
2024
806
  runDoctor() {
2025
807
  const lines = [];
2026
808
  lines.push(theme.bold('Environment diagnostics'));
@@ -2914,29 +1696,29 @@ export class InteractiveShell {
2914
1696
  const trimmed = input.trim();
2915
1697
  if (!trimmed) {
2916
1698
  display.showWarning('Enter a number or type cancel.');
2917
- this.rl.prompt();
1699
+ this.terminalInput.render();
2918
1700
  return;
2919
1701
  }
2920
1702
  if (trimmed.toLowerCase() === 'cancel') {
2921
1703
  this.pendingInteraction = null;
2922
1704
  display.showInfo('Model selection cancelled.');
2923
- this.rl.prompt();
1705
+ this.terminalInput.render();
2924
1706
  return;
2925
1707
  }
2926
1708
  const choice = Number.parseInt(trimmed, 10);
2927
1709
  if (!Number.isFinite(choice)) {
2928
1710
  display.showWarning('Please enter a valid number.');
2929
- this.rl.prompt();
1711
+ this.terminalInput.render();
2930
1712
  return;
2931
1713
  }
2932
1714
  const option = pending.options[choice - 1];
2933
1715
  if (!option) {
2934
1716
  display.showWarning('That option is not available.');
2935
- this.rl.prompt();
1717
+ this.terminalInput.render();
2936
1718
  return;
2937
1719
  }
2938
1720
  this.showProviderModels(option);
2939
- this.rl.prompt();
1721
+ this.terminalInput.render();
2940
1722
  }
2941
1723
  async handleModelSelection(input) {
2942
1724
  const pending = this.pendingInteraction;
@@ -2946,35 +1728,35 @@ export class InteractiveShell {
2946
1728
  const trimmed = input.trim();
2947
1729
  if (!trimmed) {
2948
1730
  display.showWarning('Enter a number, type "back", or type "cancel".');
2949
- this.rl.prompt();
1731
+ this.terminalInput.render();
2950
1732
  return;
2951
1733
  }
2952
1734
  if (trimmed.toLowerCase() === 'back') {
2953
1735
  this.showModelMenu();
2954
- this.rl.prompt();
1736
+ this.terminalInput.render();
2955
1737
  return;
2956
1738
  }
2957
1739
  if (trimmed.toLowerCase() === 'cancel') {
2958
1740
  this.pendingInteraction = null;
2959
1741
  display.showInfo('Model selection cancelled.');
2960
- this.rl.prompt();
1742
+ this.terminalInput.render();
2961
1743
  return;
2962
1744
  }
2963
1745
  const choice = Number.parseInt(trimmed, 10);
2964
1746
  if (!Number.isFinite(choice)) {
2965
1747
  display.showWarning('Please enter a valid number.');
2966
- this.rl.prompt();
1748
+ this.terminalInput.render();
2967
1749
  return;
2968
1750
  }
2969
1751
  const preset = pending.options[choice - 1];
2970
1752
  if (!preset) {
2971
1753
  display.showWarning('That option is not available.');
2972
- this.rl.prompt();
1754
+ this.terminalInput.render();
2973
1755
  return;
2974
1756
  }
2975
1757
  this.pendingInteraction = null;
2976
1758
  await this.applyModelPreset(preset);
2977
- this.rl.prompt();
1759
+ this.terminalInput.render();
2978
1760
  }
2979
1761
  async applyModelPreset(preset) {
2980
1762
  try {
@@ -3007,30 +1789,30 @@ export class InteractiveShell {
3007
1789
  const trimmed = input.trim();
3008
1790
  if (!trimmed) {
3009
1791
  display.showWarning('Enter a number or type cancel.');
3010
- this.rl.prompt();
1792
+ this.terminalInput.render();
3011
1793
  return;
3012
1794
  }
3013
1795
  if (trimmed.toLowerCase() === 'cancel') {
3014
1796
  this.pendingInteraction = null;
3015
1797
  display.showInfo('Secret management cancelled.');
3016
- this.rl.prompt();
1798
+ this.terminalInput.render();
3017
1799
  return;
3018
1800
  }
3019
1801
  const choice = Number.parseInt(trimmed, 10);
3020
1802
  if (!Number.isFinite(choice)) {
3021
1803
  display.showWarning('Please enter a valid number.');
3022
- this.rl.prompt();
1804
+ this.terminalInput.render();
3023
1805
  return;
3024
1806
  }
3025
1807
  const secret = pending.options[choice - 1];
3026
1808
  if (!secret) {
3027
1809
  display.showWarning('That option is not available.');
3028
- this.rl.prompt();
1810
+ this.terminalInput.render();
3029
1811
  return;
3030
1812
  }
3031
1813
  display.showSystemMessage(`Enter a new value for ${secret.label} or type "cancel".`);
3032
1814
  this.pendingInteraction = { type: 'secret-input', secret };
3033
- this.rl.prompt();
1815
+ this.terminalInput.render();
3034
1816
  }
3035
1817
  async handleSecretInput(input) {
3036
1818
  const pending = this.pendingInteraction;
@@ -3040,14 +1822,14 @@ export class InteractiveShell {
3040
1822
  const trimmed = input.trim();
3041
1823
  if (!trimmed) {
3042
1824
  display.showWarning('Enter a value or type cancel.');
3043
- this.rl.prompt();
1825
+ this.terminalInput.render();
3044
1826
  return;
3045
1827
  }
3046
1828
  if (trimmed.toLowerCase() === 'cancel') {
3047
1829
  this.pendingInteraction = null;
3048
1830
  this.pendingSecretRetry = null;
3049
1831
  display.showInfo('Secret unchanged.');
3050
- this.rl.prompt();
1832
+ this.terminalInput.render();
3051
1833
  return;
3052
1834
  }
3053
1835
  try {
@@ -3071,7 +1853,7 @@ export class InteractiveShell {
3071
1853
  this.pendingInteraction = null;
3072
1854
  this.pendingSecretRetry = null;
3073
1855
  }
3074
- this.rl.prompt();
1856
+ this.terminalInput.render();
3075
1857
  }
3076
1858
  async processRequest(request) {
3077
1859
  if (this.isProcessing) {
@@ -3087,11 +1869,8 @@ export class InteractiveShell {
3087
1869
  return;
3088
1870
  }
3089
1871
  this.isProcessing = true;
1872
+ this.terminalInput.setStreaming(true);
3090
1873
  const requestStartTime = Date.now(); // Alpha Zero 2 timing
3091
- // Claude Code style: Enable scroll region for fixed chat box at bottom
3092
- this.pinnedChatBox.setProcessing(true);
3093
- // Claude Code style: Update status (optional, won't interfere with streaming)
3094
- this.persistentPrompt.updateStatusBar({ message: '⏳ Processing...' });
3095
1874
  this.uiAdapter.startProcessing('Working on your request');
3096
1875
  this.setProcessingStatus();
3097
1876
  try {
@@ -3100,13 +1879,6 @@ export class InteractiveShell {
3100
1879
  // Claude Code style: Show unified streaming header before response
3101
1880
  // This provides visual consistency with the startup Ready bar
3102
1881
  display.showStreamingHeader();
3103
- // NOTE: Do NOT call showWaitingPrompt() here - the PinnedChatBox scroll region
3104
- // handles rendering the prompt at the bottom via renderPersistentInput().
3105
- // Calling showWaitingPrompt() writes to the scroll area instead of the reserved bottom.
3106
- // Claude Code style: Start streaming mode using UnifiedChatBox
3107
- // - User input is captured and queued
3108
- // - After response ends, new prompt appears at bottom
3109
- this.unifiedChatBox.startStreaming();
3110
1882
  // DISABLED streaming - wait for full response instead
3111
1883
  // This keeps cursor at the > prompt during processing
3112
1884
  await agent.send(request, false);
@@ -3125,30 +1897,20 @@ export class InteractiveShell {
3125
1897
  }
3126
1898
  }
3127
1899
  finally {
3128
- // Claude Code style: End streaming mode using UnifiedChatBox
3129
- // This restores readline echo and returns queue info
3130
- const streamResult = this.unifiedChatBox.endStreaming();
3131
- // Process any queued inputs that came in during streaming
3132
- this.processUnifiedChatBoxQueue();
3133
- // Show queue indicator if there were queued inputs
3134
- if (streamResult.queuedCount > 0) {
3135
- this.unifiedChatBox.showQueueIndicator();
3136
- }
3137
1900
  display.stopThinking(false);
3138
1901
  this.isProcessing = false;
3139
- this.pinnedChatBox.setProcessing(false); // Disable scroll region
1902
+ this.terminalInput.setStreaming(false);
3140
1903
  this.uiAdapter.endProcessing('Ready for prompts');
3141
1904
  this.setIdleStatus();
3142
1905
  display.newLine();
3143
- // Clear the processing status
3144
- this.persistentPrompt.updateStatusBar({ message: undefined });
1906
+ this.updateStatusMessage(null);
3145
1907
  // Claude Code style: Show unified status bar before prompt
3146
1908
  // This creates consistent UI between startup and post-streaming
3147
1909
  this.showUnifiedStatusBar();
3148
1910
  // CRITICAL: Ensure readline prompt is active for user input
3149
1911
  // Claude Code style: New prompt naturally appears at bottom
3150
1912
  this.ensureReadlineReady();
3151
- this.rl.prompt();
1913
+ this.terminalInput.render();
3152
1914
  this.scheduleQueueProcessing();
3153
1915
  this.refreshQueueIndicators();
3154
1916
  }
@@ -3180,26 +1942,17 @@ export class InteractiveShell {
3180
1942
  return;
3181
1943
  }
3182
1944
  this.isProcessing = true;
1945
+ this.terminalInput.setStreaming(true);
3183
1946
  const overallStartTime = Date.now();
3184
- // Claude Code style: Enable scroll region for fixed chat box at bottom
3185
- this.pinnedChatBox.setProcessing(true);
3186
1947
  // Initialize the task completion detector
3187
1948
  const completionDetector = getTaskCompletionDetector();
3188
1949
  completionDetector.reset();
3189
- // Claude Code style: Update status
3190
- this.persistentPrompt.updateStatusBar({ message: '🔄 Continuous mode...' });
3191
1950
  display.showSystemMessage(`🔄 Starting continuous execution mode. Press Ctrl+C to stop.`);
3192
1951
  display.showSystemMessage(`📊 Using intelligent task completion detection with AI verification.`);
3193
1952
  this.uiAdapter.startProcessing('Continuous execution mode');
3194
1953
  this.setProcessingStatus();
3195
1954
  // Claude Code style: Show unified streaming header before response
3196
1955
  display.showStreamingHeader();
3197
- // NOTE: Do NOT call showWaitingPrompt() here - the PinnedChatBox scroll region
3198
- // handles rendering the prompt at the bottom via renderPersistentInput().
3199
- // Calling showWaitingPrompt() writes to the scroll area instead of the reserved bottom.
3200
- // Claude Code style: Start streaming mode using UnifiedChatBox
3201
- // - User input is captured and queued
3202
- this.unifiedChatBox.startStreaming();
3203
1956
  let iteration = 0;
3204
1957
  let lastResponse = '';
3205
1958
  let consecutiveNoProgress = 0;
@@ -3225,9 +1978,7 @@ When truly finished with ALL tasks, explicitly state "TASK_FULLY_COMPLETE".`;
3225
1978
  while (iteration < MAX_ITERATIONS) {
3226
1979
  iteration++;
3227
1980
  display.showSystemMessage(`\n📍 Iteration ${iteration}/${MAX_ITERATIONS}`);
3228
- // Update pinned chat box with iteration status instead of spinner
3229
- this.pinnedChatBox.setStatusMessage(`Working on iteration ${iteration}...`);
3230
- this.pinnedChatBox.forceRender();
1981
+ this.updateStatusMessage(`Working on iteration ${iteration}...`);
3231
1982
  try {
3232
1983
  // Send the request and capture the response (streaming disabled)
3233
1984
  const response = await agent.send(currentPrompt, false);
@@ -3361,28 +2112,18 @@ What's the next action?`;
3361
2112
  display.showSystemMessage(`\n🏁 Continuous execution completed: ${iteration} iterations, ${minutes}m ${seconds}s total`);
3362
2113
  // Reset completion detector for next task
3363
2114
  resetTaskCompletionDetector();
3364
- // Claude Code style: End streaming mode using UnifiedChatBox
3365
- const streamResult = this.unifiedChatBox.endStreaming();
3366
- // Process any queued inputs that came in during streaming
3367
- this.processUnifiedChatBoxQueue();
3368
- // Show queue indicator if there were queued inputs
3369
- if (streamResult.queuedCount > 0) {
3370
- this.unifiedChatBox.showQueueIndicator();
3371
- }
3372
2115
  this.isProcessing = false;
3373
- this.pinnedChatBox.setProcessing(false); // Disable scroll region
2116
+ this.terminalInput.setStreaming(false);
2117
+ this.updateStatusMessage(null);
3374
2118
  this.uiAdapter.endProcessing('Ready for prompts');
3375
2119
  this.setIdleStatus();
3376
2120
  display.newLine();
3377
- // Clear the processing status
3378
- this.persistentPrompt.updateStatusBar({ message: undefined });
3379
2121
  // Claude Code style: Show unified status bar before prompt
3380
2122
  // This creates consistent UI between startup and post-streaming
3381
2123
  this.showUnifiedStatusBar();
3382
2124
  // CRITICAL: Ensure readline prompt is active for user input
3383
2125
  // Claude Code style: New prompt naturally appears at bottom
3384
2126
  this.ensureReadlineReady();
3385
- this.rl.prompt();
3386
2127
  this.scheduleQueueProcessing();
3387
2128
  this.refreshQueueIndicators();
3388
2129
  }
@@ -3590,8 +2331,7 @@ What's the next action?`;
3590
2331
  this.autoTestInFlight = true;
3591
2332
  const command = 'npm test -- --runInBand';
3592
2333
  display.showSystemMessage(`🧪 Auto-testing recent changes (${trigger}) with "${command}"...`);
3593
- this.pinnedChatBox.setStatusMessage('Running tests automatically...');
3594
- this.pinnedChatBox.forceRender();
2334
+ this.updateStatusMessage('Running tests automatically...');
3595
2335
  try {
3596
2336
  const { stdout, stderr } = await execAsync(command, {
3597
2337
  cwd: this.workingDir,
@@ -3619,8 +2359,7 @@ What's the next action?`;
3619
2359
  });
3620
2360
  }
3621
2361
  finally {
3622
- this.pinnedChatBox.setStatusMessage(null);
3623
- this.pinnedChatBox.forceRender();
2362
+ this.updateStatusMessage(null);
3624
2363
  this.autoTestInFlight = false;
3625
2364
  }
3626
2365
  }
@@ -3691,7 +2430,7 @@ What's the next action?`;
3691
2430
  display.showNarrative(content.trim());
3692
2431
  }
3693
2432
  // The isProcessing flag already shows "⏳ Processing..." - no need for duplicate status
3694
- this.pinnedChatBox.forceRender();
2433
+ this.terminalInput.render();
3695
2434
  return;
3696
2435
  }
3697
2436
  const cleanup = this.handleContextTelemetry(metadata, enriched);
@@ -3710,10 +2449,6 @@ What's the next action?`;
3710
2449
  onContextRecovery: (attempt, maxAttempts, message) => {
3711
2450
  // Show recovery progress in UI
3712
2451
  display.showSystemMessage(`⚡ Context Recovery (${attempt}/${maxAttempts}): ${message}`);
3713
- // Update persistent prompt to show recovery status
3714
- this.persistentPrompt.updateStatusBar({
3715
- contextUsage: 100, // Show as full during recovery
3716
- });
3717
2452
  },
3718
2453
  onContextPruned: (removedCount, stats) => {
3719
2454
  // Clear squish overlay if active
@@ -3727,30 +2462,28 @@ What's the next action?`;
3727
2462
  // Update context usage in UI
3728
2463
  if (typeof percentage === 'number') {
3729
2464
  this.uiAdapter.updateContextUsage(percentage);
3730
- this.persistentPrompt.updateStatusBar({ contextUsage: percentage });
3731
2465
  }
3732
2466
  // Ensure prompt remains visible at bottom after context messages
3733
- this.pinnedChatBox.forceRender();
2467
+ this.terminalInput.render();
3734
2468
  },
3735
2469
  onContinueAfterRecovery: () => {
3736
2470
  // Update UI to show we're continuing after context recovery
3737
2471
  display.showSystemMessage(`🔄 Continuing after context recovery...`);
3738
- // Update pinned chat box status instead of spinner
3739
- this.pinnedChatBox.setStatusMessage('Retrying with reduced context...');
3740
- this.pinnedChatBox.forceRender();
2472
+ this.updateStatusMessage('Retrying with reduced context...');
2473
+ this.terminalInput.render();
3741
2474
  },
3742
2475
  onAutoContinue: (attempt, maxAttempts, _message) => {
3743
2476
  // Show auto-continue progress in UI
3744
2477
  display.showSystemMessage(`🔄 Auto-continue (${attempt}/${maxAttempts}): Model expressed intent, prompting to act...`);
3745
- this.pinnedChatBox.setStatusMessage('Auto-continuing...');
3746
- this.pinnedChatBox.forceRender();
2478
+ this.updateStatusMessage('Auto-continuing...');
2479
+ this.terminalInput.render();
3747
2480
  },
3748
2481
  onCancelled: () => {
3749
2482
  // Update UI to show operation was cancelled
3750
2483
  display.showWarning('Operation cancelled.');
3751
- this.pinnedChatBox.setStatusMessage(null);
3752
- this.pinnedChatBox.setProcessing(false);
3753
- this.pinnedChatBox.forceRender();
2484
+ this.updateStatusMessage(null);
2485
+ this.terminalInput.setStreaming(false);
2486
+ this.terminalInput.render();
3754
2487
  },
3755
2488
  onVerificationNeeded: () => {
3756
2489
  void this.enforceAutoTests('verification');
@@ -3784,10 +2517,9 @@ What's the next action?`;
3784
2517
  * just like on fresh startup.
3785
2518
  */
3786
2519
  resetChatBoxAfterModelSwap() {
3787
- this.pinnedChatBox.setStatusMessage(null);
3788
- this.pinnedChatBox.setProcessing(false);
3789
- this.pinnedChatBox.show();
3790
- this.pinnedChatBox.forceRender();
2520
+ this.updateStatusMessage(null);
2521
+ this.terminalInput.setStreaming(false);
2522
+ this.terminalInput.render();
3791
2523
  this.ensureReadlineReady();
3792
2524
  }
3793
2525
  buildSystemPrompt() {
@@ -3854,10 +2586,6 @@ What's the next action?`;
3854
2586
  // Always update context usage in the UI
3855
2587
  const percentUsed = Math.round(usageRatio * 100);
3856
2588
  this.uiAdapter.updateContextUsage(percentUsed);
3857
- // Update persistent prompt status bar with context usage
3858
- this.persistentPrompt.updateStatusBar({ contextUsage: percentUsed });
3859
- // Update pinned chat box with context usage
3860
- this.pinnedChatBox.setContextUsage(percentUsed);
3861
2589
  if (usageRatio < CONTEXT_USAGE_THRESHOLD) {
3862
2590
  return null;
3863
2591
  }
@@ -3905,8 +2633,6 @@ What's the next action?`;
3905
2633
  const percentUsed = Math.round((totalTokens / windowTokens) * 100);
3906
2634
  // Update context usage in unified UI
3907
2635
  this.uiAdapter.updateContextUsage(percentUsed);
3908
- // Update persistent prompt status bar with context usage
3909
- this.persistentPrompt.updateStatusBar({ contextUsage: percentUsed });
3910
2636
  display.showSystemMessage([
3911
2637
  `Context usage: ${totalTokens.toLocaleString('en-US')} of ${windowTokens.toLocaleString('en-US')} tokens`,
3912
2638
  `(${percentUsed}% full). Running automatic cleanup...`,
@@ -4143,14 +2869,6 @@ What's the next action?`;
4143
2869
  const entry = this.agentMenu.options.find((option) => option.name === name);
4144
2870
  return entry?.label ?? name;
4145
2871
  }
4146
- updatePersistentPromptFileChanges() {
4147
- const summary = this._fileChangeTracker.getSummary();
4148
- if (summary.files === 0) {
4149
- return;
4150
- }
4151
- const fileChangesText = `${summary.files} file${summary.files === 1 ? '' : 's'} +${summary.additions} -${summary.removals}`;
4152
- this.persistentPrompt.updateStatusBar({ fileChanges: fileChangesText });
4153
- }
4154
2872
  extractThoughtSummary(thought) {
4155
2873
  // Extract first non-empty line
4156
2874
  const lines = thought?.split('\n').filter(line => line.trim()) ?? [];
@@ -4440,32 +3158,6 @@ What's the next action?`;
4440
3158
  ];
4441
3159
  display.showSystemMessage(lines.join('\n'));
4442
3160
  }
4443
- enableBracketedPasteMode() {
4444
- if (!input.isTTY || !output.isTTY) {
4445
- return false;
4446
- }
4447
- try {
4448
- output.write(BRACKETED_PASTE_ENABLE);
4449
- return true;
4450
- }
4451
- catch (error) {
4452
- const message = error instanceof Error ? error.message : String(error);
4453
- display.showWarning(`Unable to enable bracketed paste: ${message}`);
4454
- return false;
4455
- }
4456
- }
4457
- disableBracketedPasteMode() {
4458
- if (!this.bracketedPasteEnabled || !output.isTTY) {
4459
- return;
4460
- }
4461
- try {
4462
- output.write(BRACKETED_PASTE_DISABLE);
4463
- }
4464
- finally {
4465
- this.bracketedPasteEnabled = false;
4466
- this.bracketedPaste.reset();
4467
- }
4468
- }
4469
3161
  /**
4470
3162
  * Set the cached provider status for unified status bar display.
4471
3163
  * Called once at startup after checking providers.
@@ -4473,10 +3165,6 @@ What's the next action?`;
4473
3165
  setProviderStatus(providers) {
4474
3166
  this.cachedProviderStatus = providers;
4475
3167
  }
4476
- // NOTE: showWaitingPrompt() was removed because the PinnedChatBox scroll region
4477
- // now handles rendering the prompt at the bottom via renderPersistentInput().
4478
- // The scroll region approach keeps the cursor in the correct location during
4479
- // streaming while allowing the user to type in the reserved bottom area.
4480
3168
  /**
4481
3169
  * Show the unified status bar (Claude Code style).
4482
3170
  * Displays provider indicators and ready hints before the prompt.