erosolar-cli 1.7.285 → 1.7.289

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.
@@ -11,7 +11,6 @@
11
11
  import { EventEmitter } from 'node:events';
12
12
  import { isMultilinePaste, generatePasteSummary } from '../core/multilinePasteHandler.js';
13
13
  import { writeLock } from '../ui/writeLock.js';
14
- import { renderDivider } from '../ui/unified/layout.js';
15
14
  import { UI_COLORS, DEFAULT_UI_CONFIG, } from '../ui/terminalUISchema.js';
16
15
  // ANSI escape codes
17
16
  const ESC = {
@@ -71,7 +70,6 @@ export class TerminalInput extends EventEmitter {
71
70
  overrideStatusMessage = null; // Secondary status (warnings, etc.)
72
71
  streamingLabel = null; // Streaming progress indicator
73
72
  reservedLines = 2;
74
- scrollRegionActive = false;
75
73
  lastRenderContent = '';
76
74
  lastRenderCursor = -1;
77
75
  renderDirty = false;
@@ -288,12 +286,57 @@ export class TerminalInput extends EventEmitter {
288
286
  // Position cursor in input area
289
287
  this.write(ESC.TO(startRow + (this.modelInfo ? 3 : 2), this.config.promptChar.length + 1));
290
288
  }
289
+ /**
290
+ * Render input area at current cursor position (inline, not pinned).
291
+ * Used after streaming ends - renders input below the streamed content.
292
+ */
293
+ renderInlineInputAreaAtCursor() {
294
+ const { cols } = this.getSize();
295
+ const divider = '─'.repeat(cols - 1);
296
+ const { dim: DIM, reset: R } = UI_COLORS;
297
+ // Status bar - shows "Type a message" hint
298
+ process.stdout.write(this.buildStatusBar(cols) + '\n');
299
+ // Model info line (if set)
300
+ if (this.modelInfo) {
301
+ let modelLine = `${DIM}${this.modelInfo}${R}`;
302
+ if (this.contextUsage !== null) {
303
+ const rem = Math.max(0, 100 - this.contextUsage);
304
+ if (rem < 10)
305
+ modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
306
+ else if (rem < 25)
307
+ modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
308
+ else
309
+ modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
310
+ }
311
+ process.stdout.write(modelLine + '\n');
312
+ }
313
+ // Top divider
314
+ process.stdout.write(divider + '\n');
315
+ // Input line with prompt and any buffer content
316
+ const { lines, cursorCol } = this.wrapBuffer(cols - 4);
317
+ const displayLine = lines[0] ?? '';
318
+ process.stdout.write(ESC.BG_DARK + ESC.DIM + this.config.promptChar + ESC.RESET);
319
+ process.stdout.write(ESC.BG_DARK + displayLine);
320
+ const padding = Math.max(0, cols - this.config.promptChar.length - displayLine.length - 1);
321
+ if (padding > 0)
322
+ process.stdout.write(' '.repeat(padding));
323
+ process.stdout.write(ESC.RESET + '\n');
324
+ // Bottom divider
325
+ process.stdout.write(divider + '\n');
326
+ // Mode controls
327
+ process.stdout.write(this.buildModeControls(cols) + '\n');
328
+ // Show cursor
329
+ this.write(ESC.SHOW);
330
+ // Update tracking
331
+ this.lastRenderContent = this.buffer;
332
+ this.lastRenderCursor = this.cursor;
333
+ }
291
334
  /**
292
335
  * Set the input mode
293
336
  *
294
- * Streaming mode disables scroll region and lets content flow naturally.
295
- * The input area will be re-rendered after streaming ends at wherever
296
- * the cursor is (below the streamed content).
337
+ * Streaming mode: NO scroll region, NO bottom-pinned input.
338
+ * Content flows naturally after the initial launch layout.
339
+ * After streaming ends, input area renders inline at current position.
297
340
  */
298
341
  setMode(mode) {
299
342
  const prevMode = this.mode;
@@ -301,322 +344,34 @@ export class TerminalInput extends EventEmitter {
301
344
  if (mode === 'streaming' && prevMode !== 'streaming') {
302
345
  // Track streaming start time for elapsed display
303
346
  this.streamingStartTime = Date.now();
304
- const { rows } = this.getSize();
305
347
  // Ensure unified UI is initialized (if not already done on launch)
306
348
  if (!this.unifiedUIInitialized) {
307
349
  this.initializeUnifiedUI();
308
350
  }
309
- // Set up scroll region to reserve bottom for persistent input area
310
- this.pinnedTopRows = 0;
311
- this.reservedLines = 6; // status + model + divider + input + divider + controls
312
- // Ensure scroll region is enabled (may have been initialized already)
313
- if (!this.scrollRegionActive) {
314
- const contentBottomRow = Math.max(1, rows - this.reservedLines);
315
- this.write(ESC.TO(contentBottomRow, 1));
316
- this.enableScrollRegion();
317
- }
318
- // Render bottom input area
319
- this.renderBottomInputArea();
320
- // Start timer to update bottom input area (updates elapsed time)
321
- this.streamingRenderTimer = setInterval(() => {
322
- if (this.mode === 'streaming') {
323
- this.updateStreamingStatus();
324
- this.renderBottomInputArea();
325
- }
326
- }, 1000);
351
+ // NO scroll region - let content flow naturally
352
+ // NO bottom-pinned input area
353
+ // Don't clear anything - content flows from current cursor position
354
+ // The cursor should already be positioned after the user's prompt
327
355
  this.renderDirty = true;
328
356
  }
329
357
  else if (mode !== 'streaming' && prevMode === 'streaming') {
330
- // Stop streaming render timer
358
+ // Stop streaming render timer (if any)
331
359
  if (this.streamingRenderTimer) {
332
360
  clearInterval(this.streamingRenderTimer);
333
361
  this.streamingRenderTimer = null;
334
362
  }
335
363
  // Reset streaming time
336
364
  this.streamingStartTime = null;
337
- // Keep scroll region active for consistent bottom-pinned UI
338
- // (scroll region reserves bottom for input area in all modes)
339
365
  // Reset flow mode tracking
340
366
  this.flowModeRenderedLines = 0;
341
- // Render using unified bottom input area (same layout as streaming)
367
+ // Add spacing after streamed content
368
+ this.write('\n\n');
369
+ // Render input area inline at current position (below streamed content)
342
370
  writeLock.withLock(() => {
343
- this.renderBottomInputArea();
371
+ this.renderInlineInputAreaAtCursor();
344
372
  }, 'terminalInput.streamingEnd');
345
373
  }
346
374
  }
347
- /**
348
- * Update streaming status label (called by timer)
349
- */
350
- updateStreamingStatus() {
351
- if (this.mode !== 'streaming' || !this.streamingStartTime)
352
- return;
353
- // Calculate elapsed time
354
- const elapsed = Date.now() - this.streamingStartTime;
355
- const seconds = Math.floor(elapsed / 1000);
356
- const minutes = Math.floor(seconds / 60);
357
- const secs = seconds % 60;
358
- // Format elapsed time
359
- let elapsedStr;
360
- if (minutes > 0) {
361
- elapsedStr = `${minutes}m ${secs}s`;
362
- }
363
- else {
364
- elapsedStr = `${secs}s`;
365
- }
366
- // Update streaming label
367
- this.streamingLabel = `Streaming ${elapsedStr}`;
368
- }
369
- /**
370
- * Render input area - unified for streaming and normal modes.
371
- *
372
- * In streaming mode: renders at absolute bottom, uses cursor save/restore
373
- * In normal mode: renders right after the banner (pinnedTopRows + 1)
374
- */
375
- renderPinnedInputArea() {
376
- const { rows, cols } = this.getSize();
377
- const maxWidth = Math.max(8, cols - 4);
378
- const divider = renderDivider(cols - 2);
379
- const isStreaming = this.mode === 'streaming';
380
- // Wrap buffer into display lines (multi-line support)
381
- const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
382
- const availableForContent = Math.max(1, rows - 5); // Reserve 5 lines for UI
383
- const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
384
- const displayLines = Math.min(lines.length, maxVisible);
385
- // Calculate display window (keep cursor visible)
386
- let startLine = 0;
387
- if (lines.length > displayLines) {
388
- startLine = Math.max(0, cursorLine - displayLines + 1);
389
- startLine = Math.min(startLine, lines.length - displayLines);
390
- }
391
- const visibleLines = lines.slice(startLine, startLine + displayLines);
392
- const adjustedCursorLine = cursorLine - startLine;
393
- // Calculate total height: status + (model info if set) + topDiv + input lines + bottomDiv + controls
394
- const hasModelInfo = !!this.modelInfo;
395
- const totalHeight = 4 + visibleLines.length + (hasModelInfo ? 1 : 0);
396
- // Save cursor position during streaming (so content flow resumes correctly)
397
- if (isStreaming) {
398
- this.write(ESC.SAVE);
399
- }
400
- this.write(ESC.HIDE);
401
- this.write(ESC.RESET);
402
- // Calculate start row based on mode:
403
- // - Streaming: absolute bottom (rows - totalHeight + 1)
404
- // - Normal: right after content (contentEndRow + 1)
405
- let currentRow;
406
- if (isStreaming) {
407
- currentRow = Math.max(1, rows - totalHeight + 1);
408
- }
409
- else {
410
- // In normal mode, render right after content
411
- // Use contentEndRow if set, otherwise use pinnedTopRows
412
- const contentRow = this.contentEndRow > 0 ? this.contentEndRow : this.pinnedTopRows;
413
- currentRow = Math.max(1, contentRow + 1);
414
- }
415
- let finalRow = currentRow;
416
- let finalCol = 3;
417
- // Clear from current position to end of screen to remove any "ghost" content
418
- this.write(ESC.TO(currentRow, 1));
419
- this.write(ESC.CLEAR_TO_END);
420
- // Status bar
421
- this.write(ESC.TO(currentRow, 1));
422
- this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
423
- currentRow++;
424
- // Model info line (if set) - displayed below status, above input
425
- if (hasModelInfo) {
426
- const { dim: DIM, reset: R } = UI_COLORS;
427
- this.write(ESC.TO(currentRow, 1));
428
- // Build model info with context usage
429
- let modelLine = `${DIM}${this.modelInfo}${R}`;
430
- if (this.contextUsage !== null) {
431
- const rem = Math.max(0, 100 - this.contextUsage);
432
- if (rem < 10)
433
- modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
434
- else if (rem < 25)
435
- modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
436
- else
437
- modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
438
- }
439
- this.write(modelLine);
440
- currentRow++;
441
- }
442
- // Top divider
443
- this.write(ESC.TO(currentRow, 1));
444
- this.write(divider);
445
- currentRow++;
446
- // Input lines with background styling
447
- for (let i = 0; i < visibleLines.length; i++) {
448
- this.write(ESC.TO(currentRow, 1));
449
- const line = visibleLines[i] ?? '';
450
- const absoluteLineIdx = startLine + i;
451
- const isFirstLine = absoluteLineIdx === 0;
452
- const isCursorLine = i === adjustedCursorLine;
453
- // Background
454
- this.write(ESC.BG_DARK);
455
- // Prompt prefix
456
- this.write(ESC.DIM);
457
- this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
458
- this.write(ESC.RESET);
459
- this.write(ESC.BG_DARK);
460
- if (isCursorLine) {
461
- const col = Math.min(cursorCol, line.length);
462
- const before = line.slice(0, col);
463
- const at = col < line.length ? line[col] : ' ';
464
- const after = col < line.length ? line.slice(col + 1) : '';
465
- this.write(before);
466
- this.write(ESC.REVERSE + ESC.BOLD);
467
- this.write(at);
468
- this.write(ESC.RESET + ESC.BG_DARK);
469
- this.write(after);
470
- finalRow = currentRow;
471
- finalCol = this.config.promptChar.length + col + 1;
472
- }
473
- else {
474
- this.write(line);
475
- }
476
- // Pad to edge
477
- const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
478
- const padding = Math.max(0, cols - lineLen - 1);
479
- if (padding > 0)
480
- this.write(' '.repeat(padding));
481
- this.write(ESC.RESET);
482
- currentRow++;
483
- }
484
- // Bottom divider
485
- this.write(ESC.TO(currentRow, 1));
486
- this.write(divider);
487
- currentRow++;
488
- // Mode controls line
489
- this.write(ESC.TO(currentRow, 1));
490
- this.write(this.buildModeControls(cols));
491
- // Restore cursor position during streaming, or show cursor in normal mode
492
- if (isStreaming) {
493
- this.write(ESC.RESTORE);
494
- }
495
- else {
496
- // Position cursor in input area
497
- this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
498
- this.write(ESC.SHOW);
499
- }
500
- // Update reserved lines for scroll region calculations
501
- this.updateReservedLines(totalHeight);
502
- }
503
- /**
504
- * Render input area during streaming (alias for unified method)
505
- */
506
- renderStreamingInputArea() {
507
- this.renderPinnedInputArea();
508
- }
509
- /**
510
- * Render bottom input area - UNIFIED for all modes.
511
- * Uses cursor save/restore to update bottom without affecting content flow.
512
- *
513
- * Layout (same for idle/streaming/ready):
514
- * - Status bar (streaming timer or "Type a message")
515
- * - Model info line (provider · model · ctx)
516
- * - Divider
517
- * - Input area
518
- * - Divider
519
- * - Mode controls
520
- */
521
- renderBottomInputArea() {
522
- const { rows, cols } = this.getSize();
523
- const maxWidth = Math.max(8, cols - 4);
524
- const divider = renderDivider(cols - 2);
525
- const { dim: DIM, reset: R } = UI_COLORS;
526
- const isStreaming = this.mode === 'streaming';
527
- // Wrap buffer into display lines
528
- const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
529
- // Allow multi-line in non-streaming, single line during streaming
530
- const maxDisplayLines = isStreaming ? 1 : 3;
531
- const displayLines = Math.min(lines.length, maxDisplayLines);
532
- const visibleLines = lines.slice(0, displayLines);
533
- // Calculate total height for bottom area
534
- const hasModelInfo = !!this.modelInfo;
535
- const totalHeight = 4 + displayLines + (hasModelInfo ? 1 : 0);
536
- // Ensure scroll region is always enabled (unified behavior)
537
- if (!this.scrollRegionActive || this.reservedLines !== totalHeight) {
538
- this.reservedLines = totalHeight;
539
- this.enableScrollRegion();
540
- }
541
- const startRow = Math.max(1, rows - totalHeight + 1);
542
- // Save cursor, hide it
543
- this.write(ESC.SAVE);
544
- this.write(ESC.HIDE);
545
- let currentRow = startRow;
546
- // Clear the bottom reserved area
547
- for (let r = startRow; r <= rows; r++) {
548
- this.write(ESC.TO(r, 1));
549
- this.write(ESC.CLEAR_LINE);
550
- }
551
- // Status bar - UNIFIED: same format for all modes
552
- this.write(ESC.TO(currentRow, 1));
553
- this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
554
- currentRow++;
555
- // Model info line (if set)
556
- if (hasModelInfo) {
557
- this.write(ESC.TO(currentRow, 1));
558
- let modelLine = `${DIM}${this.modelInfo}${R}`;
559
- if (this.contextUsage !== null) {
560
- const rem = Math.max(0, 100 - this.contextUsage);
561
- if (rem < 10)
562
- modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
563
- else if (rem < 25)
564
- modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
565
- else
566
- modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
567
- }
568
- this.write(modelLine);
569
- currentRow++;
570
- }
571
- // Top divider
572
- this.write(ESC.TO(currentRow, 1));
573
- this.write(divider);
574
- currentRow++;
575
- // Input lines with background styling
576
- for (let i = 0; i < visibleLines.length; i++) {
577
- this.write(ESC.TO(currentRow, 1));
578
- const line = visibleLines[i] ?? '';
579
- const isFirstLine = i === 0;
580
- this.write(ESC.BG_DARK);
581
- this.write(ESC.DIM);
582
- this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
583
- this.write(ESC.RESET);
584
- this.write(ESC.BG_DARK);
585
- this.write(line);
586
- // Pad to edge
587
- const lineLen = this.config.promptChar.length + line.length;
588
- const padding = Math.max(0, cols - lineLen - 1);
589
- if (padding > 0)
590
- this.write(' '.repeat(padding));
591
- this.write(ESC.RESET);
592
- currentRow++;
593
- }
594
- // Bottom divider
595
- this.write(ESC.TO(currentRow, 1));
596
- this.write(divider);
597
- currentRow++;
598
- // Mode controls
599
- this.write(ESC.TO(currentRow, 1));
600
- this.write(this.buildModeControls(cols));
601
- // Cursor positioning depends on mode:
602
- // - Streaming: restore to content area (where streaming output continues)
603
- // - Normal: position in input area for typing
604
- if (isStreaming) {
605
- this.write(ESC.RESTORE);
606
- }
607
- else {
608
- // Position cursor in input area
609
- // Input line is at: startRow + (hasModelInfo ? 2 : 1) + cursorLine
610
- const inputStartRow = startRow + (hasModelInfo ? 2 : 1) + 1; // +1 for status bar, +1 for divider
611
- const targetRow = inputStartRow + Math.min(cursorLine, displayLines - 1);
612
- const targetCol = this.config.promptChar.length + cursorCol + 1;
613
- this.write(ESC.TO(targetRow, Math.min(targetCol, cols)));
614
- }
615
- this.write(ESC.SHOW);
616
- // Track last render state
617
- this.lastRenderContent = this.buffer;
618
- this.lastRenderCursor = this.cursor;
619
- }
620
375
  /**
621
376
  * Enable or disable flow mode.
622
377
  * In flow mode, the input renders immediately after content (wherever cursor is).
@@ -739,16 +494,11 @@ export class TerminalInput extends EventEmitter {
739
494
  return this.thinkingEnabled;
740
495
  }
741
496
  /**
742
- * Keep the top N rows pinned outside the scroll region (used for the launch banner).
497
+ * Keep the top N rows pinned (used for the launch banner tracking).
498
+ * Note: No longer uses scroll regions - inline rendering only.
743
499
  */
744
500
  setPinnedHeaderLines(count) {
745
- // Set pinned header rows (banner area that scroll region excludes)
746
- if (this.pinnedTopRows !== count) {
747
- this.pinnedTopRows = count;
748
- if (this.scrollRegionActive) {
749
- this.applyScrollRegion();
750
- }
751
- }
501
+ this.pinnedTopRows = count;
752
502
  }
753
503
  /**
754
504
  * Anchor prompt rendering near a specific row (inline layout). Pass null to
@@ -817,14 +567,17 @@ export class TerminalInput extends EventEmitter {
817
567
  }
818
568
  /**
819
569
  * Clear the buffer
570
+ * @param skipRender - If true, don't trigger a re-render (used during submit flow)
820
571
  */
821
- clear() {
572
+ clear(skipRender = false) {
822
573
  this.buffer = '';
823
574
  this.cursor = 0;
824
575
  this.historyIndex = -1;
825
576
  this.tempInput = '';
826
577
  this.pastePlaceholders = [];
827
- this.scheduleRender();
578
+ if (!skipRender) {
579
+ this.scheduleRender();
580
+ }
828
581
  }
829
582
  /**
830
583
  * Get queued inputs
@@ -936,18 +689,21 @@ export class TerminalInput extends EventEmitter {
936
689
  this.scheduleRender();
937
690
  }
938
691
  /**
939
- * Render the input area - UNIFIED for all modes
692
+ * Render the input area
940
693
  *
941
- * Uses the same bottom-pinned layout with scroll regions for:
942
- * - Idle mode: Shows "Type a message" hint
943
- * - Streaming mode: Shows "● Streaming Xs" timer
944
- * - Ready mode: Shows status info
694
+ * - Idle mode: Renders inline input area
695
+ * - Streaming mode: NO render (content flows naturally, no UI updates)
696
+ * - Ready mode: Renders inline input area
945
697
  */
946
698
  render() {
947
699
  if (!this.canRender())
948
700
  return;
949
701
  if (this.isRendering)
950
702
  return;
703
+ // During streaming, do NOT render - let content flow naturally
704
+ if (this.mode === 'streaming') {
705
+ return;
706
+ }
951
707
  const shouldSkip = !this.renderDirty &&
952
708
  this.buffer === this.lastRenderContent &&
953
709
  this.cursor === this.lastRenderCursor;
@@ -964,176 +720,14 @@ export class TerminalInput extends EventEmitter {
964
720
  this.isRendering = true;
965
721
  writeLock.lock('terminalInput.render');
966
722
  try {
967
- // UNIFIED: Use the same bottom input area for all modes
968
- this.renderBottomInputArea();
723
+ // Render inline input area at current position
724
+ this.renderInlineInputAreaAtCursor();
969
725
  }
970
726
  finally {
971
727
  writeLock.unlock();
972
728
  this.isRendering = false;
973
729
  }
974
730
  }
975
- /**
976
- * Render in flow mode - delegates to bottom-pinned for stability.
977
- *
978
- * Flow mode attempted inline rendering but caused duplicate renders
979
- * due to unreliable cursor position tracking. Bottom-pinned is reliable.
980
- */
981
- renderFlowMode() {
982
- // Use stable bottom-pinned approach
983
- this.renderBottomPinned();
984
- }
985
- /**
986
- * Render in bottom-pinned mode - Claude Code style with suggestions
987
- *
988
- * Works for both normal and streaming modes:
989
- * - During streaming: saves/restores cursor position
990
- * - Status bar shows streaming info or "Type a message"
991
- *
992
- * Layout when suggestions visible:
993
- * - Top divider
994
- * - Input line(s)
995
- * - Bottom divider
996
- * - Suggestions (command list)
997
- *
998
- * Layout when suggestions hidden:
999
- * - Status bar (Ready/Streaming)
1000
- * - Top divider
1001
- * - Input line(s)
1002
- * - Bottom divider
1003
- * - Mode controls
1004
- */
1005
- renderBottomPinned() {
1006
- const { rows, cols } = this.getSize();
1007
- const maxWidth = Math.max(8, cols - 4);
1008
- const isStreaming = this.mode === 'streaming';
1009
- // Use unified pinned input area (works for both streaming and normal)
1010
- // Only use complex rendering when suggestions are visible
1011
- const hasSuggestions = !isStreaming && this.showSuggestions && this.filteredSuggestions.length > 0;
1012
- if (!hasSuggestions) {
1013
- this.renderPinnedInputArea();
1014
- return;
1015
- }
1016
- // Wrap buffer into display lines
1017
- const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
1018
- const availableForContent = Math.max(1, rows - 3);
1019
- const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
1020
- const displayLines = Math.min(lines.length, maxVisible);
1021
- // Calculate display window (keep cursor visible)
1022
- let startLine = 0;
1023
- if (lines.length > displayLines) {
1024
- startLine = Math.max(0, cursorLine - displayLines + 1);
1025
- startLine = Math.min(startLine, lines.length - displayLines);
1026
- }
1027
- const visibleLines = lines.slice(startLine, startLine + displayLines);
1028
- const adjustedCursorLine = cursorLine - startLine;
1029
- // Calculate suggestion display (not during streaming)
1030
- const suggestionsToShow = (!isStreaming && this.showSuggestions)
1031
- ? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
1032
- : [];
1033
- const suggestionLines = suggestionsToShow.length;
1034
- this.write(ESC.HIDE);
1035
- this.write(ESC.RESET);
1036
- const divider = renderDivider(cols - 2);
1037
- // Calculate positions from absolute bottom
1038
- let currentRow;
1039
- if (suggestionLines > 0) {
1040
- // With suggestions: input area + dividers + suggestions
1041
- // Layout: [topDiv] [input] [bottomDiv] [suggestions...]
1042
- const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
1043
- currentRow = Math.max(1, rows - totalHeight + 1);
1044
- this.updateReservedLines(totalHeight);
1045
- // Clear from current position to end of screen to remove any "ghost" content
1046
- this.write(ESC.TO(currentRow, 1));
1047
- this.write(ESC.CLEAR_TO_END);
1048
- // Top divider
1049
- this.write(ESC.TO(currentRow, 1));
1050
- this.write(divider);
1051
- currentRow++;
1052
- // Input lines
1053
- let finalRow = currentRow;
1054
- let finalCol = 3;
1055
- for (let i = 0; i < visibleLines.length; i++) {
1056
- this.write(ESC.TO(currentRow, 1));
1057
- const line = visibleLines[i] ?? '';
1058
- const absoluteLineIdx = startLine + i;
1059
- const isFirstLine = absoluteLineIdx === 0;
1060
- const isCursorLine = i === adjustedCursorLine;
1061
- this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
1062
- if (isCursorLine) {
1063
- const col = Math.min(cursorCol, line.length);
1064
- this.write(line.slice(0, col));
1065
- this.write(ESC.REVERSE);
1066
- this.write(col < line.length ? line[col] : ' ');
1067
- this.write(ESC.RESET);
1068
- this.write(line.slice(col + 1));
1069
- finalRow = currentRow;
1070
- finalCol = this.config.promptChar.length + col + 1;
1071
- }
1072
- else {
1073
- this.write(line);
1074
- }
1075
- currentRow++;
1076
- }
1077
- // Bottom divider
1078
- this.write(ESC.TO(currentRow, 1));
1079
- this.write(divider);
1080
- currentRow++;
1081
- // Suggestions (Claude Code style)
1082
- for (let i = 0; i < suggestionsToShow.length; i++) {
1083
- this.write(ESC.TO(currentRow, 1));
1084
- const suggestion = suggestionsToShow[i];
1085
- const isSelected = i === this.selectedSuggestionIndex;
1086
- // Indent and highlight selected
1087
- this.write(' ');
1088
- if (isSelected) {
1089
- this.write(ESC.REVERSE);
1090
- this.write(ESC.BOLD);
1091
- }
1092
- this.write(suggestion.command);
1093
- if (isSelected) {
1094
- this.write(ESC.RESET);
1095
- }
1096
- // Description (dimmed)
1097
- const descSpace = cols - suggestion.command.length - 8;
1098
- if (descSpace > 10 && suggestion.description) {
1099
- const desc = suggestion.description.slice(0, descSpace);
1100
- this.write(ESC.RESET);
1101
- this.write(ESC.DIM);
1102
- this.write(' ');
1103
- this.write(desc);
1104
- this.write(ESC.RESET);
1105
- }
1106
- currentRow++;
1107
- }
1108
- // Position cursor in input area
1109
- this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
1110
- }
1111
- this.write(ESC.SHOW);
1112
- // Update state
1113
- this.lastRenderContent = this.buffer;
1114
- this.lastRenderCursor = this.cursor;
1115
- }
1116
- /**
1117
- * Build status bar for streaming mode (shows elapsed time, queue count).
1118
- */
1119
- buildStreamingStatusBar(cols) {
1120
- const { green: GREEN, cyan: CYAN, dim: DIM, reset: R } = UI_COLORS;
1121
- // Streaming status with elapsed time
1122
- let elapsed = '0s';
1123
- if (this.streamingStartTime) {
1124
- const secs = Math.floor((Date.now() - this.streamingStartTime) / 1000);
1125
- const mins = Math.floor(secs / 60);
1126
- elapsed = mins > 0 ? `${mins}m ${secs % 60}s` : `${secs}s`;
1127
- }
1128
- let status = `${GREEN}● Streaming${R} ${elapsed}`;
1129
- // Queue indicator
1130
- if (this.queue.length > 0) {
1131
- status += ` ${DIM}·${R} ${CYAN}${this.queue.length} queued${R}`;
1132
- }
1133
- // Hint for typing
1134
- status += ` ${DIM}· type to queue message${R}`;
1135
- return status;
1136
- }
1137
731
  /**
1138
732
  * Build status bar showing streaming/ready status and key info.
1139
733
  * This is the TOP line above the input area - minimal Claude Code style.
@@ -1294,7 +888,8 @@ export class TerminalInput extends EventEmitter {
1294
888
  }
1295
889
  this.disposed = true;
1296
890
  this.enabled = false;
1297
- this.disableScrollRegion();
891
+ // Reset scroll region if it was set
892
+ this.write(ESC.RESET_SCROLL);
1298
893
  this.disableBracketedPaste();
1299
894
  this.buffer = '';
1300
895
  this.queue = [];
@@ -1685,12 +1280,13 @@ export class TerminalInput extends EventEmitter {
1685
1280
  timestamp: Date.now(),
1686
1281
  });
1687
1282
  this.emit('queue', text);
1688
- this.clear(); // Clear immediately for queued input
1283
+ this.clear(); // Clear immediately for queued input, re-render to update queue display
1689
1284
  }
1690
1285
  else {
1691
- // In idle mode, clear the input first, then emit submit.
1692
- // The prompt will be logged as a visible message by the caller.
1693
- this.clear();
1286
+ // In idle mode, clear the input WITHOUT rendering.
1287
+ // The caller will display the user message and start streaming.
1288
+ // We'll render the input area again after streaming ends.
1289
+ this.clear(true); // Skip render - streaming will handle display
1694
1290
  this.emit('submit', text);
1695
1291
  }
1696
1292
  }
@@ -1720,40 +1316,6 @@ export class TerminalInput extends EventEmitter {
1720
1316
  this.scheduleRender();
1721
1317
  }
1722
1318
  // ===========================================================================
1723
- // SCROLL REGION
1724
- // ===========================================================================
1725
- enableScrollRegion() {
1726
- if (this.scrollRegionActive || !this.isTTY())
1727
- return;
1728
- this.applyScrollRegion();
1729
- this.scrollRegionActive = true;
1730
- }
1731
- disableScrollRegion() {
1732
- if (!this.scrollRegionActive)
1733
- return;
1734
- this.write(ESC.RESET_SCROLL);
1735
- this.scrollRegionActive = false;
1736
- }
1737
- applyScrollRegion() {
1738
- const { rows } = this.getSize();
1739
- const scrollTop = Math.max(1, this.pinnedTopRows + 1);
1740
- const scrollBottom = Math.max(scrollTop, rows - this.reservedLines);
1741
- this.write(ESC.SET_SCROLL(scrollTop, scrollBottom));
1742
- }
1743
- updateReservedLines(contentLines) {
1744
- const { rows } = this.getSize();
1745
- const baseLines = 2; // separator + control bar
1746
- const needed = baseLines + contentLines;
1747
- const maxAllowed = Math.max(baseLines, rows - 1 - this.pinnedTopRows);
1748
- const newReserved = Math.min(Math.max(baseLines, needed), maxAllowed);
1749
- if (newReserved !== this.reservedLines) {
1750
- this.reservedLines = newReserved;
1751
- if (this.scrollRegionActive) {
1752
- this.applyScrollRegion();
1753
- }
1754
- }
1755
- }
1756
- // ===========================================================================
1757
1319
  // BUFFER WRAPPING
1758
1320
  // ===========================================================================
1759
1321
  wrapBuffer(maxWidth) {