erosolar-cli 1.7.190 → 1.7.191

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,88 @@ 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
+ '',
304
+ `Email ${highlightedEmail} with any bugs or feedback`,
305
+ 'GitHub: https://github.com/ErosolarAI/erosolar-by-bo',
306
+ 'npm: https://www.npmjs.com/package/erosolar-cli',
307
+ ].join('\n');
308
+ display.showInfo(infoMessage);
309
+ exit(0);
310
+ }
311
+ /**
312
+ * Update status bar message
313
+ */
314
+ updateStatusMessage(_message) {
315
+ this.terminalInput.render();
317
316
  }
318
317
  async handleToolSettingsInput(input) {
319
318
  const pending = this.pendingInteraction;
@@ -323,26 +322,26 @@ export class InteractiveShell {
323
322
  const trimmed = input.trim();
324
323
  if (!trimmed) {
325
324
  display.showWarning('Enter a number, "save", "defaults", or "cancel".');
326
- this.rl.prompt();
325
+ this.terminalInput.render();
327
326
  return;
328
327
  }
329
328
  const normalized = trimmed.toLowerCase();
330
329
  if (normalized === 'cancel') {
331
330
  this.pendingInteraction = null;
332
331
  display.showInfo('Tool selection cancelled.');
333
- this.rl.prompt();
332
+ this.terminalInput.render();
334
333
  return;
335
334
  }
336
335
  if (normalized === 'defaults') {
337
336
  pending.selection = buildEnabledToolSet(null);
338
337
  this.renderToolMenu(pending);
339
- this.rl.prompt();
338
+ this.terminalInput.render();
340
339
  return;
341
340
  }
342
341
  if (normalized === 'save') {
343
342
  await this.persistToolSelection(pending);
344
343
  this.pendingInteraction = null;
345
- this.rl.prompt();
344
+ this.terminalInput.render();
346
345
  return;
347
346
  }
348
347
  const choice = Number.parseInt(trimmed, 10);
@@ -360,11 +359,11 @@ export class InteractiveShell {
360
359
  }
361
360
  this.renderToolMenu(pending);
362
361
  }
363
- this.rl.prompt();
362
+ this.terminalInput.render();
364
363
  return;
365
364
  }
366
365
  display.showWarning('Enter a number, "save", "defaults", or "cancel".');
367
- this.rl.prompt();
366
+ this.terminalInput.render();
368
367
  }
369
368
  async persistToolSelection(interaction) {
370
369
  if (setsEqual(interaction.selection, interaction.initialSelection)) {
@@ -391,36 +390,36 @@ export class InteractiveShell {
391
390
  if (!this.agentMenu) {
392
391
  this.pendingInteraction = null;
393
392
  display.showWarning('Agent selection is unavailable in this CLI.');
394
- this.rl.prompt();
393
+ this.terminalInput.render();
395
394
  return;
396
395
  }
397
396
  const trimmed = input.trim();
398
397
  if (!trimmed) {
399
398
  display.showWarning('Enter a number or type "cancel".');
400
- this.rl.prompt();
399
+ this.terminalInput.render();
401
400
  return;
402
401
  }
403
402
  if (trimmed.toLowerCase() === 'cancel') {
404
403
  this.pendingInteraction = null;
405
404
  display.showInfo('Agent selection cancelled.');
406
- this.rl.prompt();
405
+ this.terminalInput.render();
407
406
  return;
408
407
  }
409
408
  const choice = Number.parseInt(trimmed, 10);
410
409
  if (!Number.isFinite(choice)) {
411
410
  display.showWarning('Please enter a valid number.');
412
- this.rl.prompt();
411
+ this.terminalInput.render();
413
412
  return;
414
413
  }
415
414
  const option = pending.options[choice - 1];
416
415
  if (!option) {
417
416
  display.showWarning('That option is not available.');
418
- this.rl.prompt();
417
+ this.terminalInput.render();
419
418
  return;
420
419
  }
421
420
  await this.persistAgentSelection(option.name);
422
421
  this.pendingInteraction = null;
423
- this.rl.prompt();
422
+ this.terminalInput.render();
424
423
  }
425
424
  async persistAgentSelection(profileName) {
426
425
  if (!this.agentMenu) {
@@ -442,380 +441,12 @@ export class InteractiveShell {
442
441
  display.showInfo(`${this.agentMenuLabel(profileName)} will load the next time you start the CLI. Restart to switch now.`);
443
442
  }
444
443
  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
444
  // Handle terminal resize
568
445
  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);
446
+ this.terminalInput.handleResize();
602
447
  });
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);
448
+ // Show initial input UI
449
+ this.terminalInput.render();
819
450
  }
820
451
  setupStatusTracking() {
821
452
  this.statusSubscription = this.statusTracker.subscribe((_state) => {
@@ -873,14 +504,10 @@ export class InteractiveShell {
873
504
  if (!shouldShow && this.slashPreviewVisible) {
874
505
  this.slashPreviewVisible = false;
875
506
  this.uiAdapter.hideSlashCommandPreview();
876
- // Show persistent prompt again after hiding overlay
877
- if (!this.isProcessing) {
878
- this.persistentPrompt.show();
879
- }
880
507
  }
881
508
  }
882
509
  shouldShowSlashCommandPreview() {
883
- const line = this.rl.line ?? '';
510
+ const line = this.currentInput ?? '';
884
511
  if (!line.trim()) {
885
512
  return false;
886
513
  }
@@ -889,642 +516,28 @@ export class InteractiveShell {
889
516
  }
890
517
  showSlashCommandPreview() {
891
518
  // Filter commands based on current input
892
- const line = this.rl.line ?? '';
519
+ const line = this.currentInput ?? '';
893
520
  const trimmed = line.trimStart();
894
521
  // Filter commands that match the current input
895
522
  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
523
  // Show in the unified UI with dynamic overlay
899
524
  this.uiAdapter.showSlashCommandPreview(filtered);
900
525
  // Don't reprompt - this causes flickering
901
526
  }
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
527
  /**
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.
528
+ * Ensure the terminal input is ready for interactive input.
929
529
  */
930
530
  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
- }
531
+ this.terminalInput.render();
1508
532
  }
1509
533
  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
534
  if (this.isProcessing) {
1523
535
  this.setProcessingStatus();
1524
536
  }
1525
537
  else {
1526
538
  this.setIdleStatus();
1527
539
  }
540
+ this.terminalInput.render();
1528
541
  }
1529
542
  enqueueFollowUpAction(action) {
1530
543
  this.followUpQueue.push(action);
@@ -1533,23 +546,17 @@ export class InteractiveShell {
1533
546
  const preview = normalized.length > previewLimit ? `${normalized.slice(0, previewLimit - 3)}...` : normalized;
1534
547
  const position = this.followUpQueue.length === 1 ? 'to run next' : `#${this.followUpQueue.length} in queue`;
1535
548
  const label = action.type === 'continuous' ? 'continuous command' : 'follow-up';
1536
- const queueCount = this.followUpQueue.length;
1537
549
  // Show immediate acknowledgment with checkmark
1538
- const queueLabel = queueCount === 1 ? '1 queued' : `${queueCount} queued`;
1539
550
  if (preview) {
1540
551
  display.showInfo(`✓ ${theme.info(label)} ${position}: ${theme.ui.muted(preview)}`);
1541
552
  }
1542
553
  else {
1543
554
  display.showInfo(`✓ ${theme.info(label)} queued ${position}.`);
1544
555
  }
1545
- // Update status bar to show queue count
1546
- this.persistentPrompt.updateStatusBar({
1547
- message: `⏳ Processing... (${queueLabel})`
1548
- });
1549
556
  this.refreshQueueIndicators();
1550
557
  this.scheduleQueueProcessing();
1551
558
  // Re-show the prompt so user can continue typing more follow-ups
1552
- this.rl.prompt();
559
+ this.terminalInput.render();
1553
560
  }
1554
561
  scheduleQueueProcessing() {
1555
562
  if (!this.followUpQueue.length) {
@@ -1561,71 +568,8 @@ export class InteractiveShell {
1561
568
  });
1562
569
  }
1563
570
  /**
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.
571
+ * Process queued follow-up actions.
1603
572
  */
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
573
  async processQueuedActions() {
1630
574
  if (this.isDrainingQueue || this.isProcessing || !this.followUpQueue.length) {
1631
575
  return;
@@ -1659,85 +603,22 @@ export class InteractiveShell {
1659
603
  if (await this.handlePendingInteraction(trimmed)) {
1660
604
  return;
1661
605
  }
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
606
  if (!trimmed) {
1726
- this.rl.prompt();
1727
607
  return;
1728
608
  }
1729
- if (trimmed.toLowerCase() === 'exit' || trimmed.toLowerCase() === 'quit') {
1730
- this.rl.close();
609
+ const lower = trimmed.toLowerCase();
610
+ if (lower === 'exit' || lower === 'quit') {
611
+ this.shutdown();
1731
612
  return;
1732
613
  }
1733
- if (trimmed.toLowerCase() === 'clear') {
614
+ if (lower === 'clear') {
1734
615
  display.clear();
1735
- this.rl.prompt();
616
+ this.terminalInput.render();
1736
617
  return;
1737
618
  }
1738
- if (trimmed.toLowerCase() === 'help') {
619
+ if (lower === 'help') {
1739
620
  this.showHelp();
1740
- this.rl.prompt();
621
+ this.terminalInput.render();
1741
622
  return;
1742
623
  }
1743
624
  if (trimmed.startsWith('/')) {
@@ -1747,12 +628,12 @@ export class InteractiveShell {
1747
628
  // Check for continuous/infinite loop commands
1748
629
  if (this.isContinuousCommand(trimmed)) {
1749
630
  await this.processContinuousRequest(trimmed);
1750
- this.rl.prompt();
631
+ this.terminalInput.render();
1751
632
  return;
1752
633
  }
1753
634
  // Direct execution for all inputs, including multi-line pastes
1754
635
  await this.processRequest(trimmed);
1755
- this.rl.prompt();
636
+ this.terminalInput.render();
1756
637
  }
1757
638
  /**
1758
639
  * Check if the command is a continuous/infinite loop command
@@ -1804,12 +685,6 @@ export class InteractiveShell {
1804
685
  case 'agent-selection':
1805
686
  await this.handleAgentSelectionInput(input);
1806
687
  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
688
  default:
1814
689
  return false;
1815
690
  }
@@ -1818,7 +693,7 @@ export class InteractiveShell {
1818
693
  const [command] = input.split(/\s+/);
1819
694
  if (!command) {
1820
695
  display.showWarning('Enter a slash command.');
1821
- this.rl.prompt();
696
+ this.terminalInput.render();
1822
697
  return;
1823
698
  }
1824
699
  switch (command) {
@@ -1893,7 +768,7 @@ export class InteractiveShell {
1893
768
  }
1894
769
  break;
1895
770
  }
1896
- this.rl.prompt();
771
+ this.terminalInput.render();
1897
772
  }
1898
773
  async tryCustomSlashCommand(command, fullInput) {
1899
774
  const custom = this.customCommandMap.get(command);
@@ -1927,100 +802,6 @@ export class InteractiveShell {
1927
802
  ];
1928
803
  display.showSystemMessage(info.join('\n'));
1929
804
  }
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
805
  runDoctor() {
2025
806
  const lines = [];
2026
807
  lines.push(theme.bold('Environment diagnostics'));
@@ -2914,29 +1695,29 @@ export class InteractiveShell {
2914
1695
  const trimmed = input.trim();
2915
1696
  if (!trimmed) {
2916
1697
  display.showWarning('Enter a number or type cancel.');
2917
- this.rl.prompt();
1698
+ this.terminalInput.render();
2918
1699
  return;
2919
1700
  }
2920
1701
  if (trimmed.toLowerCase() === 'cancel') {
2921
1702
  this.pendingInteraction = null;
2922
1703
  display.showInfo('Model selection cancelled.');
2923
- this.rl.prompt();
1704
+ this.terminalInput.render();
2924
1705
  return;
2925
1706
  }
2926
1707
  const choice = Number.parseInt(trimmed, 10);
2927
1708
  if (!Number.isFinite(choice)) {
2928
1709
  display.showWarning('Please enter a valid number.');
2929
- this.rl.prompt();
1710
+ this.terminalInput.render();
2930
1711
  return;
2931
1712
  }
2932
1713
  const option = pending.options[choice - 1];
2933
1714
  if (!option) {
2934
1715
  display.showWarning('That option is not available.');
2935
- this.rl.prompt();
1716
+ this.terminalInput.render();
2936
1717
  return;
2937
1718
  }
2938
1719
  this.showProviderModels(option);
2939
- this.rl.prompt();
1720
+ this.terminalInput.render();
2940
1721
  }
2941
1722
  async handleModelSelection(input) {
2942
1723
  const pending = this.pendingInteraction;
@@ -2946,35 +1727,35 @@ export class InteractiveShell {
2946
1727
  const trimmed = input.trim();
2947
1728
  if (!trimmed) {
2948
1729
  display.showWarning('Enter a number, type "back", or type "cancel".');
2949
- this.rl.prompt();
1730
+ this.terminalInput.render();
2950
1731
  return;
2951
1732
  }
2952
1733
  if (trimmed.toLowerCase() === 'back') {
2953
1734
  this.showModelMenu();
2954
- this.rl.prompt();
1735
+ this.terminalInput.render();
2955
1736
  return;
2956
1737
  }
2957
1738
  if (trimmed.toLowerCase() === 'cancel') {
2958
1739
  this.pendingInteraction = null;
2959
1740
  display.showInfo('Model selection cancelled.');
2960
- this.rl.prompt();
1741
+ this.terminalInput.render();
2961
1742
  return;
2962
1743
  }
2963
1744
  const choice = Number.parseInt(trimmed, 10);
2964
1745
  if (!Number.isFinite(choice)) {
2965
1746
  display.showWarning('Please enter a valid number.');
2966
- this.rl.prompt();
1747
+ this.terminalInput.render();
2967
1748
  return;
2968
1749
  }
2969
1750
  const preset = pending.options[choice - 1];
2970
1751
  if (!preset) {
2971
1752
  display.showWarning('That option is not available.');
2972
- this.rl.prompt();
1753
+ this.terminalInput.render();
2973
1754
  return;
2974
1755
  }
2975
1756
  this.pendingInteraction = null;
2976
1757
  await this.applyModelPreset(preset);
2977
- this.rl.prompt();
1758
+ this.terminalInput.render();
2978
1759
  }
2979
1760
  async applyModelPreset(preset) {
2980
1761
  try {
@@ -3007,30 +1788,30 @@ export class InteractiveShell {
3007
1788
  const trimmed = input.trim();
3008
1789
  if (!trimmed) {
3009
1790
  display.showWarning('Enter a number or type cancel.');
3010
- this.rl.prompt();
1791
+ this.terminalInput.render();
3011
1792
  return;
3012
1793
  }
3013
1794
  if (trimmed.toLowerCase() === 'cancel') {
3014
1795
  this.pendingInteraction = null;
3015
1796
  display.showInfo('Secret management cancelled.');
3016
- this.rl.prompt();
1797
+ this.terminalInput.render();
3017
1798
  return;
3018
1799
  }
3019
1800
  const choice = Number.parseInt(trimmed, 10);
3020
1801
  if (!Number.isFinite(choice)) {
3021
1802
  display.showWarning('Please enter a valid number.');
3022
- this.rl.prompt();
1803
+ this.terminalInput.render();
3023
1804
  return;
3024
1805
  }
3025
1806
  const secret = pending.options[choice - 1];
3026
1807
  if (!secret) {
3027
1808
  display.showWarning('That option is not available.');
3028
- this.rl.prompt();
1809
+ this.terminalInput.render();
3029
1810
  return;
3030
1811
  }
3031
1812
  display.showSystemMessage(`Enter a new value for ${secret.label} or type "cancel".`);
3032
1813
  this.pendingInteraction = { type: 'secret-input', secret };
3033
- this.rl.prompt();
1814
+ this.terminalInput.render();
3034
1815
  }
3035
1816
  async handleSecretInput(input) {
3036
1817
  const pending = this.pendingInteraction;
@@ -3040,14 +1821,14 @@ export class InteractiveShell {
3040
1821
  const trimmed = input.trim();
3041
1822
  if (!trimmed) {
3042
1823
  display.showWarning('Enter a value or type cancel.');
3043
- this.rl.prompt();
1824
+ this.terminalInput.render();
3044
1825
  return;
3045
1826
  }
3046
1827
  if (trimmed.toLowerCase() === 'cancel') {
3047
1828
  this.pendingInteraction = null;
3048
1829
  this.pendingSecretRetry = null;
3049
1830
  display.showInfo('Secret unchanged.');
3050
- this.rl.prompt();
1831
+ this.terminalInput.render();
3051
1832
  return;
3052
1833
  }
3053
1834
  try {
@@ -3071,7 +1852,7 @@ export class InteractiveShell {
3071
1852
  this.pendingInteraction = null;
3072
1853
  this.pendingSecretRetry = null;
3073
1854
  }
3074
- this.rl.prompt();
1855
+ this.terminalInput.render();
3075
1856
  }
3076
1857
  async processRequest(request) {
3077
1858
  if (this.isProcessing) {
@@ -3087,11 +1868,8 @@ export class InteractiveShell {
3087
1868
  return;
3088
1869
  }
3089
1870
  this.isProcessing = true;
1871
+ this.terminalInput.setStreaming(true);
3090
1872
  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
1873
  this.uiAdapter.startProcessing('Working on your request');
3096
1874
  this.setProcessingStatus();
3097
1875
  try {
@@ -3100,13 +1878,6 @@ export class InteractiveShell {
3100
1878
  // Claude Code style: Show unified streaming header before response
3101
1879
  // This provides visual consistency with the startup Ready bar
3102
1880
  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
1881
  // DISABLED streaming - wait for full response instead
3111
1882
  // This keeps cursor at the > prompt during processing
3112
1883
  await agent.send(request, false);
@@ -3125,30 +1896,20 @@ export class InteractiveShell {
3125
1896
  }
3126
1897
  }
3127
1898
  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
1899
  display.stopThinking(false);
3138
1900
  this.isProcessing = false;
3139
- this.pinnedChatBox.setProcessing(false); // Disable scroll region
1901
+ this.terminalInput.setStreaming(false);
3140
1902
  this.uiAdapter.endProcessing('Ready for prompts');
3141
1903
  this.setIdleStatus();
3142
1904
  display.newLine();
3143
- // Clear the processing status
3144
- this.persistentPrompt.updateStatusBar({ message: undefined });
1905
+ this.updateStatusMessage(null);
3145
1906
  // Claude Code style: Show unified status bar before prompt
3146
1907
  // This creates consistent UI between startup and post-streaming
3147
1908
  this.showUnifiedStatusBar();
3148
1909
  // CRITICAL: Ensure readline prompt is active for user input
3149
1910
  // Claude Code style: New prompt naturally appears at bottom
3150
1911
  this.ensureReadlineReady();
3151
- this.rl.prompt();
1912
+ this.terminalInput.render();
3152
1913
  this.scheduleQueueProcessing();
3153
1914
  this.refreshQueueIndicators();
3154
1915
  }
@@ -3180,26 +1941,17 @@ export class InteractiveShell {
3180
1941
  return;
3181
1942
  }
3182
1943
  this.isProcessing = true;
1944
+ this.terminalInput.setStreaming(true);
3183
1945
  const overallStartTime = Date.now();
3184
- // Claude Code style: Enable scroll region for fixed chat box at bottom
3185
- this.pinnedChatBox.setProcessing(true);
3186
1946
  // Initialize the task completion detector
3187
1947
  const completionDetector = getTaskCompletionDetector();
3188
1948
  completionDetector.reset();
3189
- // Claude Code style: Update status
3190
- this.persistentPrompt.updateStatusBar({ message: '🔄 Continuous mode...' });
3191
1949
  display.showSystemMessage(`🔄 Starting continuous execution mode. Press Ctrl+C to stop.`);
3192
1950
  display.showSystemMessage(`📊 Using intelligent task completion detection with AI verification.`);
3193
1951
  this.uiAdapter.startProcessing('Continuous execution mode');
3194
1952
  this.setProcessingStatus();
3195
1953
  // Claude Code style: Show unified streaming header before response
3196
1954
  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
1955
  let iteration = 0;
3204
1956
  let lastResponse = '';
3205
1957
  let consecutiveNoProgress = 0;
@@ -3225,9 +1977,7 @@ When truly finished with ALL tasks, explicitly state "TASK_FULLY_COMPLETE".`;
3225
1977
  while (iteration < MAX_ITERATIONS) {
3226
1978
  iteration++;
3227
1979
  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();
1980
+ this.updateStatusMessage(`Working on iteration ${iteration}...`);
3231
1981
  try {
3232
1982
  // Send the request and capture the response (streaming disabled)
3233
1983
  const response = await agent.send(currentPrompt, false);
@@ -3361,28 +2111,18 @@ What's the next action?`;
3361
2111
  display.showSystemMessage(`\n🏁 Continuous execution completed: ${iteration} iterations, ${minutes}m ${seconds}s total`);
3362
2112
  // Reset completion detector for next task
3363
2113
  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
2114
  this.isProcessing = false;
3373
- this.pinnedChatBox.setProcessing(false); // Disable scroll region
2115
+ this.terminalInput.setStreaming(false);
2116
+ this.updateStatusMessage(null);
3374
2117
  this.uiAdapter.endProcessing('Ready for prompts');
3375
2118
  this.setIdleStatus();
3376
2119
  display.newLine();
3377
- // Clear the processing status
3378
- this.persistentPrompt.updateStatusBar({ message: undefined });
3379
2120
  // Claude Code style: Show unified status bar before prompt
3380
2121
  // This creates consistent UI between startup and post-streaming
3381
2122
  this.showUnifiedStatusBar();
3382
2123
  // CRITICAL: Ensure readline prompt is active for user input
3383
2124
  // Claude Code style: New prompt naturally appears at bottom
3384
2125
  this.ensureReadlineReady();
3385
- this.rl.prompt();
3386
2126
  this.scheduleQueueProcessing();
3387
2127
  this.refreshQueueIndicators();
3388
2128
  }
@@ -3590,8 +2330,7 @@ What's the next action?`;
3590
2330
  this.autoTestInFlight = true;
3591
2331
  const command = 'npm test -- --runInBand';
3592
2332
  display.showSystemMessage(`🧪 Auto-testing recent changes (${trigger}) with "${command}"...`);
3593
- this.pinnedChatBox.setStatusMessage('Running tests automatically...');
3594
- this.pinnedChatBox.forceRender();
2333
+ this.updateStatusMessage('Running tests automatically...');
3595
2334
  try {
3596
2335
  const { stdout, stderr } = await execAsync(command, {
3597
2336
  cwd: this.workingDir,
@@ -3619,8 +2358,7 @@ What's the next action?`;
3619
2358
  });
3620
2359
  }
3621
2360
  finally {
3622
- this.pinnedChatBox.setStatusMessage(null);
3623
- this.pinnedChatBox.forceRender();
2361
+ this.updateStatusMessage(null);
3624
2362
  this.autoTestInFlight = false;
3625
2363
  }
3626
2364
  }
@@ -3691,7 +2429,7 @@ What's the next action?`;
3691
2429
  display.showNarrative(content.trim());
3692
2430
  }
3693
2431
  // The isProcessing flag already shows "⏳ Processing..." - no need for duplicate status
3694
- this.pinnedChatBox.forceRender();
2432
+ this.terminalInput.render();
3695
2433
  return;
3696
2434
  }
3697
2435
  const cleanup = this.handleContextTelemetry(metadata, enriched);
@@ -3710,10 +2448,6 @@ What's the next action?`;
3710
2448
  onContextRecovery: (attempt, maxAttempts, message) => {
3711
2449
  // Show recovery progress in UI
3712
2450
  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
2451
  },
3718
2452
  onContextPruned: (removedCount, stats) => {
3719
2453
  // Clear squish overlay if active
@@ -3727,30 +2461,28 @@ What's the next action?`;
3727
2461
  // Update context usage in UI
3728
2462
  if (typeof percentage === 'number') {
3729
2463
  this.uiAdapter.updateContextUsage(percentage);
3730
- this.persistentPrompt.updateStatusBar({ contextUsage: percentage });
3731
2464
  }
3732
2465
  // Ensure prompt remains visible at bottom after context messages
3733
- this.pinnedChatBox.forceRender();
2466
+ this.terminalInput.render();
3734
2467
  },
3735
2468
  onContinueAfterRecovery: () => {
3736
2469
  // Update UI to show we're continuing after context recovery
3737
2470
  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();
2471
+ this.updateStatusMessage('Retrying with reduced context...');
2472
+ this.terminalInput.render();
3741
2473
  },
3742
2474
  onAutoContinue: (attempt, maxAttempts, _message) => {
3743
2475
  // Show auto-continue progress in UI
3744
2476
  display.showSystemMessage(`🔄 Auto-continue (${attempt}/${maxAttempts}): Model expressed intent, prompting to act...`);
3745
- this.pinnedChatBox.setStatusMessage('Auto-continuing...');
3746
- this.pinnedChatBox.forceRender();
2477
+ this.updateStatusMessage('Auto-continuing...');
2478
+ this.terminalInput.render();
3747
2479
  },
3748
2480
  onCancelled: () => {
3749
2481
  // Update UI to show operation was cancelled
3750
2482
  display.showWarning('Operation cancelled.');
3751
- this.pinnedChatBox.setStatusMessage(null);
3752
- this.pinnedChatBox.setProcessing(false);
3753
- this.pinnedChatBox.forceRender();
2483
+ this.updateStatusMessage(null);
2484
+ this.terminalInput.setStreaming(false);
2485
+ this.terminalInput.render();
3754
2486
  },
3755
2487
  onVerificationNeeded: () => {
3756
2488
  void this.enforceAutoTests('verification');
@@ -3784,10 +2516,9 @@ What's the next action?`;
3784
2516
  * just like on fresh startup.
3785
2517
  */
3786
2518
  resetChatBoxAfterModelSwap() {
3787
- this.pinnedChatBox.setStatusMessage(null);
3788
- this.pinnedChatBox.setProcessing(false);
3789
- this.pinnedChatBox.show();
3790
- this.pinnedChatBox.forceRender();
2519
+ this.updateStatusMessage(null);
2520
+ this.terminalInput.setStreaming(false);
2521
+ this.terminalInput.render();
3791
2522
  this.ensureReadlineReady();
3792
2523
  }
3793
2524
  buildSystemPrompt() {
@@ -3854,10 +2585,6 @@ What's the next action?`;
3854
2585
  // Always update context usage in the UI
3855
2586
  const percentUsed = Math.round(usageRatio * 100);
3856
2587
  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
2588
  if (usageRatio < CONTEXT_USAGE_THRESHOLD) {
3862
2589
  return null;
3863
2590
  }
@@ -3905,8 +2632,6 @@ What's the next action?`;
3905
2632
  const percentUsed = Math.round((totalTokens / windowTokens) * 100);
3906
2633
  // Update context usage in unified UI
3907
2634
  this.uiAdapter.updateContextUsage(percentUsed);
3908
- // Update persistent prompt status bar with context usage
3909
- this.persistentPrompt.updateStatusBar({ contextUsage: percentUsed });
3910
2635
  display.showSystemMessage([
3911
2636
  `Context usage: ${totalTokens.toLocaleString('en-US')} of ${windowTokens.toLocaleString('en-US')} tokens`,
3912
2637
  `(${percentUsed}% full). Running automatic cleanup...`,
@@ -4143,14 +2868,6 @@ What's the next action?`;
4143
2868
  const entry = this.agentMenu.options.find((option) => option.name === name);
4144
2869
  return entry?.label ?? name;
4145
2870
  }
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
2871
  extractThoughtSummary(thought) {
4155
2872
  // Extract first non-empty line
4156
2873
  const lines = thought?.split('\n').filter(line => line.trim()) ?? [];
@@ -4440,32 +3157,6 @@ What's the next action?`;
4440
3157
  ];
4441
3158
  display.showSystemMessage(lines.join('\n'));
4442
3159
  }
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
3160
  /**
4470
3161
  * Set the cached provider status for unified status bar display.
4471
3162
  * Called once at startup after checking providers.
@@ -4473,10 +3164,6 @@ What's the next action?`;
4473
3164
  setProviderStatus(providers) {
4474
3165
  this.cachedProviderStatus = providers;
4475
3166
  }
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
3167
  /**
4481
3168
  * Show the unified status bar (Claude Code style).
4482
3169
  * Displays provider indicators and ready hints before the prompt.