erosolar-cli 1.7.185 → 1.7.187

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.
@@ -317,6 +317,8 @@ export class PinnedChatBox {
317
317
  enableScrollRegion() {
318
318
  if (this.scrollRegionActive || !this.supportsRendering())
319
319
  return;
320
+ // Make sure reserved height matches current content/terminal width
321
+ this.updateReservedLinesForContent();
320
322
  const rows = this.writeStream.rows || 24;
321
323
  const scrollBottom = rows - this.reservedLines;
322
324
  // Step 1: Set scroll region (excludes bottom reserved lines)
@@ -415,8 +417,7 @@ export class PinnedChatBox {
415
417
  return;
416
418
  this.isRendering = true;
417
419
  try {
418
- const rows = this.writeStream.rows || 24;
419
- const cols = Math.max(this.writeStream.columns || 80, 40);
420
+ const { rows, cols, maxInputWidth } = this.getRenderDimensions();
420
421
  // ANSI codes - keep simple, no theme functions
421
422
  const HIDE_CURSOR = '\x1b[?25l';
422
423
  const SHOW_CURSOR = '\x1b[?25h';
@@ -426,38 +427,19 @@ export class PinnedChatBox {
426
427
  // Hide cursor during render
427
428
  this.safeWrite(HIDE_CURSOR);
428
429
  this.safeWrite(RESET);
429
- // Split input into lines for multi-line support
430
- const inputLines = this.inputBuffer.split('\n');
431
- const totalInputLines = inputLines.length;
430
+ // Wrap input into terminal-safe lines so long text doesn't overflow
431
+ const { lines: wrappedLines, cursorLine: cursorLineIndex, cursorCol: cursorColInLine } = this.wrapInputBuffer(maxInputWidth);
432
+ const totalInputLines = Math.max(1, wrappedLines.length);
432
433
  const displayLineCount = Math.min(totalInputLines, this.maxDisplayLines);
434
+ // Keep reserved height in sync with what we need to show
435
+ this.updateReservedLinesForContent(maxInputWidth, totalInputLines);
433
436
  // Find cursor position: which line and column
434
- let cursorLineIndex = 0;
435
- let cursorColInLine = 0;
436
- let charCount = 0;
437
- for (let i = 0; i < inputLines.length; i++) {
438
- const currentLine = inputLines[i] ?? '';
439
- const lineLen = currentLine.length;
440
- if (charCount + lineLen >= this.cursorPosition) {
441
- cursorLineIndex = i;
442
- cursorColInLine = this.cursorPosition - charCount;
443
- break;
444
- }
445
- charCount += lineLen + 1; // +1 for newline
446
- if (i === inputLines.length - 1) {
447
- cursorLineIndex = i;
448
- cursorColInLine = currentLine.length;
449
- }
450
- }
451
- // Calculate display window to keep cursor visible
452
437
  let displayStartLine = 0;
453
438
  if (totalInputLines > displayLineCount) {
454
- if (cursorLineIndex >= displayLineCount - 1) {
455
- displayStartLine = cursorLineIndex - displayLineCount + 2;
456
- }
439
+ displayStartLine = Math.max(0, cursorLineIndex - displayLineCount + 1);
457
440
  displayStartLine = Math.min(displayStartLine, totalInputLines - displayLineCount);
458
- displayStartLine = Math.max(0, displayStartLine);
459
441
  }
460
- const linesToShow = inputLines.slice(displayStartLine, displayStartLine + displayLineCount);
442
+ const linesToShow = wrappedLines.slice(displayStartLine, displayStartLine + displayLineCount);
461
443
  // Move to the start of reserved area
462
444
  const reservedStart = rows - this.reservedLines + 1;
463
445
  this.safeWrite(ANSI.CURSOR_TO_BOTTOM(reservedStart));
@@ -481,7 +463,6 @@ export class PinnedChatBox {
481
463
  this.safeWrite(' ' + hints.join(' • '));
482
464
  this.safeWrite(RESET);
483
465
  }
484
- const maxInputWidth = cols - 5;
485
466
  let finalCursorRow = reservedStart + 1;
486
467
  let finalCursorCol = 3;
487
468
  // Render each input line
@@ -497,21 +478,12 @@ export class PinnedChatBox {
497
478
  this.safeWrite(isFirstLine ? '>' : '│');
498
479
  this.safeWrite(RESET);
499
480
  this.safeWrite(' ');
500
- // Handle long lines - scroll horizontally to keep cursor visible
501
- let displayStart = 0;
502
- let displayLine = line;
503
- if (line.length > maxInputWidth) {
504
- if (isCursorLine && cursorColInLine > maxInputWidth - 5) {
505
- displayStart = cursorColInLine - maxInputWidth + 5;
506
- }
507
- displayLine = line.slice(displayStart, displayStart + maxInputWidth);
508
- }
509
481
  if (isCursorLine) {
510
482
  // Render line with cursor
511
- const displayCursorCol = Math.max(0, cursorColInLine - displayStart);
512
- const beforeCursor = displayLine.slice(0, displayCursorCol);
513
- const atCursor = displayLine[displayCursorCol] ?? ' ';
514
- const afterCursor = displayLine.slice(displayCursorCol + 1);
483
+ const displayCursorCol = Math.max(0, cursorColInLine);
484
+ const beforeCursor = line.slice(0, displayCursorCol);
485
+ const atCursor = line[displayCursorCol] ?? ' ';
486
+ const afterCursor = line.slice(displayCursorCol + 1);
515
487
  this.safeWrite(beforeCursor);
516
488
  this.safeWrite(REVERSE_VIDEO);
517
489
  this.safeWrite(atCursor);
@@ -523,7 +495,7 @@ export class PinnedChatBox {
523
495
  }
524
496
  else {
525
497
  // Render line without cursor
526
- this.safeWrite(displayLine);
498
+ this.safeWrite(line);
527
499
  }
528
500
  }
529
501
  // Position terminal cursor and show it
@@ -594,10 +566,13 @@ export class PinnedChatBox {
594
566
  return withoutAnsi.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
595
567
  }
596
568
  /**
597
- * Sanitize queued command text (trimmed, single line)
569
+ * Sanitize queued command text (trimmed, preserves newlines)
598
570
  */
599
571
  sanitizeCommandText(text) {
600
- return this.sanitizeInlineText(text).trim();
572
+ if (!text)
573
+ return '';
574
+ const clean = this.sanitizeMultilineForDisplay(text);
575
+ return clean.slice(0, this.maxInputLength).trim();
601
576
  }
602
577
  /**
603
578
  * Sanitize status message (single line, bounded length)
@@ -630,6 +605,12 @@ export class PinnedChatBox {
630
605
  this.updateReservedLinesForContent();
631
606
  this.scheduleRender();
632
607
  }
608
+ /**
609
+ * Treat spaces, tabs, and newlines as whitespace for navigation helpers.
610
+ */
611
+ isWhitespace(char) {
612
+ return char === ' ' || char === '\t' || char === '\n';
613
+ }
633
614
  /**
634
615
  * Update cursor position while keeping it in bounds and re-rendering.
635
616
  */
@@ -637,51 +618,6 @@ export class PinnedChatBox {
637
618
  this.cursorPosition = Math.max(0, Math.min(position, this.inputBuffer.length));
638
619
  this.scheduleRender();
639
620
  }
640
- /**
641
- * Convert the current cursor index into line/column info.
642
- */
643
- getCursorLineInfo() {
644
- const lines = this.inputBuffer.split('\n');
645
- let remaining = this.cursorPosition;
646
- for (let i = 0; i < lines.length; i++) {
647
- const line = lines[i] ?? '';
648
- if (remaining <= line.length) {
649
- return { lines, lineIndex: i, column: remaining };
650
- }
651
- remaining -= line.length + 1; // account for newline
652
- }
653
- // Fallback to last line
654
- const lastLine = Math.max(0, lines.length - 1);
655
- return { lines, lineIndex: lastLine, column: (lines[lastLine] ?? '').length };
656
- }
657
- /**
658
- * Convert line/column back to a single cursor index.
659
- */
660
- positionFromLineCol(lines, lineIndex, column) {
661
- let pos = 0;
662
- for (let i = 0; i < lineIndex; i++) {
663
- pos += (lines[i] ?? '').length + 1; // +1 for newline
664
- }
665
- return pos + column;
666
- }
667
- /**
668
- * Move cursor vertically within a multi-line buffer.
669
- * Returns true if a movement occurred.
670
- */
671
- moveCursorVertical(direction) {
672
- const info = this.getCursorLineInfo();
673
- if (info.lines.length <= 1) {
674
- return false;
675
- }
676
- const targetLine = info.lineIndex + direction;
677
- if (targetLine < 0 || targetLine >= info.lines.length) {
678
- return false;
679
- }
680
- const targetCol = Math.min(info.column, (info.lines[targetLine] ?? '').length);
681
- const newPos = this.positionFromLineCol(info.lines, targetLine, targetCol);
682
- this.setCursorPosition(newPos);
683
- return true;
684
- }
685
621
  /**
686
622
  * Insert multi-line content directly into the buffer (manual typing).
687
623
  */
@@ -797,7 +733,8 @@ export class PinnedChatBox {
797
733
  }
798
734
  // Sanitize and truncate command text
799
735
  const truncated = sanitizedText.slice(0, this.maxInputLength);
800
- const preview = truncated.length > 60 ? `${truncated.slice(0, 57)}...` : truncated;
736
+ const previewSource = this.sanitizeInlineText(truncated);
737
+ const preview = previewSource.length > 60 ? `${previewSource.slice(0, 57)}...` : previewSource;
801
738
  const cmd = {
802
739
  id: `cmd-${++this.commandIdCounter}`,
803
740
  text: truncated,
@@ -853,14 +790,25 @@ export class PinnedChatBox {
853
790
  * Handle character input with validation
854
791
  * Detects multiline paste and stores full content while showing summary
855
792
  */
856
- handleInput(char) {
793
+ handleInput(char, options = {}) {
857
794
  if (!this.isEnabled || this.isDisposed)
858
795
  return;
859
796
  if (typeof char !== 'string')
860
797
  return;
861
- // Detect multiline paste (content with newlines)
862
- if (char.includes('\n') || char.includes('\r')) {
863
- this.handleMultilinePaste(char);
798
+ const allowNewlines = options.allowNewlines ?? options.isPaste ?? false;
799
+ const hasNewline = char.includes('\n') || char.includes('\r');
800
+ const shouldHandleAsPaste = options.isPaste || (allowNewlines && hasNewline);
801
+ // Detect paste explicitly or any newline-containing input when allowed
802
+ if (shouldHandleAsPaste) {
803
+ if (options.isPaste) {
804
+ this.handleMultilinePaste(char);
805
+ return;
806
+ }
807
+ this.insertMultilineText(char);
808
+ return;
809
+ }
810
+ // Ignore unexpected newlines unless explicitly allowed
811
+ if (hasNewline) {
864
812
  return;
865
813
  }
866
814
  const sanitized = this.sanitizeInlineText(char);
@@ -874,13 +822,10 @@ export class PinnedChatBox {
874
822
  if (!chunk)
875
823
  return;
876
824
  // Insert character at cursor position
877
- this.inputBuffer =
878
- this.inputBuffer.slice(0, this.cursorPosition) +
879
- chunk +
880
- this.inputBuffer.slice(this.cursorPosition);
881
- this.cursorPosition = Math.min(this.cursorPosition + chunk.length, this.inputBuffer.length);
882
- this.state.currentInput = this.inputBuffer;
883
- this.scheduleRender();
825
+ this.startManualEdit();
826
+ this.applyBufferChange(this.inputBuffer.slice(0, this.cursorPosition) +
827
+ chunk +
828
+ this.inputBuffer.slice(this.cursorPosition), this.cursorPosition + chunk.length);
884
829
  }
885
830
  /**
886
831
  * Handle multiline paste - store full content and display all lines
@@ -928,37 +873,88 @@ export class PinnedChatBox {
928
873
  */
929
874
  clearPastedBlockState() {
930
875
  this.clearPastedBlock();
931
- this.inputBuffer = '';
932
- this.cursorPosition = 0;
933
- this.state.currentInput = '';
934
- this.resetReservedLines();
876
+ this.applyBufferChange('', 0);
935
877
  }
936
878
  /**
937
- * Calculate and update reserved lines based on current input content.
938
- * Ensures multi-line content is fully visible within maxDisplayLines limit.
879
+ * Get terminal dimensions and a safe width for rendering input content.
939
880
  */
940
- updateReservedLinesForContent() {
941
- const lines = this.inputBuffer.split('\n');
942
- const lineCount = lines.length;
943
- // Calculate needed lines: 1 for separator + lineCount for input (clamped to maxDisplayLines)
944
- const inputLines = Math.min(lineCount, this.maxDisplayLines);
945
- this.reservedLines = 1 + inputLines; // 1 separator + input lines
946
- // If we have a scroll region active, update it
947
- if (this.scrollRegionActive) {
948
- const rows = this.writeStream.rows || 24;
949
- const scrollBottom = rows - this.reservedLines;
950
- this.safeWrite(ANSI.SET_SCROLL_REGION(1, scrollBottom));
881
+ getRenderDimensions() {
882
+ const rows = this.writeStream.rows || 24;
883
+ const cols = Math.max(this.writeStream.columns || 80, 40);
884
+ // Leave a small gutter for prefix + padding so we never wrap unexpectedly
885
+ const maxInputWidth = Math.max(10, cols - 5);
886
+ return { rows, cols, maxInputWidth };
887
+ }
888
+ /**
889
+ * Wrap the current input buffer to the provided width and locate the cursor.
890
+ */
891
+ wrapInputBuffer(maxInputWidth) {
892
+ const width = Math.max(1, maxInputWidth);
893
+ const rawLines = this.inputBuffer.split('\n');
894
+ const wrappedLines = [];
895
+ const targetCursor = Math.max(0, Math.min(this.cursorPosition, this.inputBuffer.length));
896
+ let cursorLine = 0;
897
+ let cursorCol = 0;
898
+ let absoluteIndex = 0;
899
+ for (let i = 0; i < rawLines.length; i++) {
900
+ const raw = rawLines[i] ?? '';
901
+ if (raw.length === 0) {
902
+ // Preserve empty lines so vertical space stays accurate
903
+ wrappedLines.push('');
904
+ if (targetCursor === absoluteIndex) {
905
+ cursorLine = wrappedLines.length - 1;
906
+ cursorCol = 0;
907
+ }
908
+ }
909
+ else {
910
+ for (let start = 0; start < raw.length; start += width) {
911
+ const segment = raw.slice(start, start + width);
912
+ const segmentStart = absoluteIndex + start;
913
+ wrappedLines.push(segment);
914
+ if (targetCursor >= segmentStart && targetCursor <= segmentStart + segment.length) {
915
+ cursorLine = wrappedLines.length - 1;
916
+ cursorCol = targetCursor - segmentStart;
917
+ }
918
+ }
919
+ }
920
+ absoluteIndex += raw.length;
921
+ if (i < rawLines.length - 1) {
922
+ // Account for the newline character between raw lines
923
+ if (targetCursor === absoluteIndex) {
924
+ cursorLine = wrappedLines.length;
925
+ cursorCol = 0;
926
+ }
927
+ absoluteIndex += 1;
928
+ }
929
+ }
930
+ if (wrappedLines.length === 0) {
931
+ wrappedLines.push('');
932
+ cursorLine = 0;
933
+ cursorCol = 0;
951
934
  }
935
+ cursorLine = Math.min(cursorLine, wrappedLines.length - 1);
936
+ cursorCol = Math.min(cursorCol, wrappedLines[cursorLine]?.length ?? 0);
937
+ return { lines: wrappedLines, cursorLine, cursorCol };
952
938
  }
953
939
  /**
954
- * Reset reserved lines to base value (for single-line input)
940
+ * Calculate and update reserved lines based on current input content.
941
+ * Ensures multi-line content is fully visible within maxDisplayLines limit.
955
942
  */
956
- resetReservedLines() {
957
- this.reservedLines = this.baseReservedLines;
943
+ updateReservedLinesForContent(maxInputWidth, wrappedLineCount) {
944
+ const { rows, maxInputWidth: derivedWidth } = this.getRenderDimensions();
945
+ const width = Math.max(1, maxInputWidth ?? derivedWidth);
946
+ const totalLines = wrappedLineCount ?? this.wrapInputBuffer(width).lines.length;
947
+ const lineCount = Math.max(1, totalLines);
948
+ // Calculate needed lines: 1 for separator + lineCount for input (clamped to maxDisplayLines)
949
+ const inputLines = Math.min(lineCount, this.maxDisplayLines);
950
+ const calculated = 1 + inputLines; // 1 separator + input lines
951
+ // Ensure we keep at least one row for output scrolling
952
+ const maxAllowed = Math.max(1, rows - 1);
953
+ const desired = Math.max(this.baseReservedLines, calculated);
954
+ this.reservedLines = Math.min(desired, maxAllowed);
958
955
  // If we have a scroll region active, update it
959
956
  if (this.scrollRegionActive) {
960
- const rows = this.writeStream.rows || 24;
961
- const scrollBottom = rows - this.reservedLines;
957
+ const scrollBottom = Math.max(1, rows - this.reservedLines);
962
958
  this.safeWrite(ANSI.SET_SCROLL_REGION(1, scrollBottom));
963
959
  }
964
960
  }
@@ -997,10 +993,9 @@ export class PinnedChatBox {
997
993
  handleCursorLeft() {
998
994
  if (this.isDisposed)
999
995
  return;
1000
- this.cursorPosition = Math.min(this.cursorPosition, this.inputBuffer.length);
1001
- if (this.cursorPosition > 0) {
1002
- this.cursorPosition--;
1003
- this.scheduleRender();
996
+ const bounded = Math.min(this.cursorPosition, this.inputBuffer.length);
997
+ if (bounded > 0) {
998
+ this.setCursorPosition(bounded - 1);
1004
999
  }
1005
1000
  }
1006
1001
  /**
@@ -1009,10 +1004,9 @@ export class PinnedChatBox {
1009
1004
  handleCursorRight() {
1010
1005
  if (this.isDisposed)
1011
1006
  return;
1012
- this.cursorPosition = Math.min(this.cursorPosition, this.inputBuffer.length);
1013
- if (this.cursorPosition < this.inputBuffer.length) {
1014
- this.cursorPosition++;
1015
- this.scheduleRender();
1007
+ const bounded = Math.min(this.cursorPosition, this.inputBuffer.length);
1008
+ if (bounded < this.inputBuffer.length) {
1009
+ this.setCursorPosition(bounded + 1);
1016
1010
  }
1017
1011
  }
1018
1012
  /**
@@ -1028,8 +1022,7 @@ export class PinnedChatBox {
1028
1022
  for (let i = 0; i < lineIndex; i++) {
1029
1023
  newPos += (lines[i] ?? '').length + 1;
1030
1024
  }
1031
- this.cursorPosition = newPos;
1032
- this.scheduleRender();
1025
+ this.setCursorPosition(newPos);
1033
1026
  }
1034
1027
  /**
1035
1028
  * Handle end key (Ctrl+E) - move to end of current line (not end of buffer)
@@ -1046,8 +1039,7 @@ export class PinnedChatBox {
1046
1039
  newPos += (lines[i] ?? '').length + 1;
1047
1040
  }
1048
1041
  newPos += currentLine.length;
1049
- this.cursorPosition = newPos;
1050
- this.scheduleRender();
1042
+ this.setCursorPosition(newPos);
1051
1043
  }
1052
1044
  /**
1053
1045
  * Handle inserting a newline character at cursor position (Shift+Enter or Option+Enter)
@@ -1058,16 +1050,10 @@ export class PinnedChatBox {
1058
1050
  // Respect max input length
1059
1051
  if (this.inputBuffer.length >= this.maxInputLength)
1060
1052
  return;
1061
- // Insert newline at cursor position
1062
- this.inputBuffer =
1063
- this.inputBuffer.slice(0, this.cursorPosition) +
1064
- '\n' +
1065
- this.inputBuffer.slice(this.cursorPosition);
1066
- this.cursorPosition++;
1067
- this.state.currentInput = this.inputBuffer;
1068
- // Update reserved lines for the new content
1069
- this.updateReservedLinesForContent();
1070
- this.scheduleRender();
1053
+ this.startManualEdit();
1054
+ this.applyBufferChange(this.inputBuffer.slice(0, this.cursorPosition) +
1055
+ '\n' +
1056
+ this.inputBuffer.slice(this.cursorPosition), this.cursorPosition + 1);
1071
1057
  }
1072
1058
  /**
1073
1059
  * Get cursor line and column position within multi-line content.
@@ -1111,9 +1097,7 @@ export class PinnedChatBox {
1111
1097
  newPos += (lines[i] ?? '').length + 1;
1112
1098
  }
1113
1099
  newPos += newCol;
1114
- this.cursorPosition = newPos;
1115
- this.state.currentInput = this.inputBuffer;
1116
- this.scheduleRender();
1100
+ this.setCursorPosition(newPos);
1117
1101
  return;
1118
1102
  }
1119
1103
  // On first line - navigate history
@@ -1126,10 +1110,9 @@ export class PinnedChatBox {
1126
1110
  // Move up in history
1127
1111
  if (this.historyIndex < this.inputHistory.length - 1) {
1128
1112
  this.historyIndex++;
1129
- this.inputBuffer = this.inputHistory[this.inputHistory.length - 1 - this.historyIndex] || '';
1130
- this.cursorPosition = this.inputBuffer.length;
1131
- this.state.currentInput = this.inputBuffer;
1132
- this.scheduleRender();
1113
+ const nextBuffer = this.inputHistory[this.inputHistory.length - 1 - this.historyIndex] || '';
1114
+ this.startManualEdit();
1115
+ this.applyBufferChange(nextBuffer, nextBuffer.length);
1133
1116
  }
1134
1117
  }
1135
1118
  /**
@@ -1150,27 +1133,23 @@ export class PinnedChatBox {
1150
1133
  newPos += (lines[i] ?? '').length + 1;
1151
1134
  }
1152
1135
  newPos += newCol;
1153
- this.cursorPosition = newPos;
1154
- this.state.currentInput = this.inputBuffer;
1155
- this.scheduleRender();
1136
+ this.setCursorPosition(newPos);
1156
1137
  return;
1157
1138
  }
1158
1139
  // On last line - navigate history
1159
1140
  if (this.historyIndex > 0) {
1160
1141
  // Move down in history
1161
1142
  this.historyIndex--;
1162
- this.inputBuffer = this.inputHistory[this.inputHistory.length - 1 - this.historyIndex] || '';
1163
- this.cursorPosition = this.inputBuffer.length;
1164
- this.state.currentInput = this.inputBuffer;
1165
- this.scheduleRender();
1143
+ const nextBuffer = this.inputHistory[this.inputHistory.length - 1 - this.historyIndex] || '';
1144
+ this.startManualEdit();
1145
+ this.applyBufferChange(nextBuffer, nextBuffer.length);
1166
1146
  }
1167
1147
  else if (this.historyIndex === 0) {
1168
1148
  // Return to current input
1169
1149
  this.historyIndex = -1;
1170
- this.inputBuffer = this.tempCurrentInput;
1171
- this.cursorPosition = this.inputBuffer.length;
1172
- this.state.currentInput = this.inputBuffer;
1173
- this.scheduleRender();
1150
+ const restored = this.tempCurrentInput;
1151
+ this.startManualEdit();
1152
+ this.applyBufferChange(restored, restored.length);
1174
1153
  }
1175
1154
  }
1176
1155
  /**
@@ -1181,10 +1160,9 @@ export class PinnedChatBox {
1181
1160
  return;
1182
1161
  if (this.cursorPosition === 0)
1183
1162
  return;
1184
- this.inputBuffer = this.inputBuffer.slice(this.cursorPosition);
1185
- this.cursorPosition = 0;
1186
- this.state.currentInput = this.inputBuffer;
1187
- this.scheduleRender();
1163
+ this.startManualEdit();
1164
+ const newBuffer = this.inputBuffer.slice(this.cursorPosition);
1165
+ this.applyBufferChange(newBuffer, 0);
1188
1166
  }
1189
1167
  /**
1190
1168
  * Handle Ctrl+K - delete from cursor to end of line
@@ -1194,9 +1172,9 @@ export class PinnedChatBox {
1194
1172
  return;
1195
1173
  if (this.cursorPosition >= this.inputBuffer.length)
1196
1174
  return;
1197
- this.inputBuffer = this.inputBuffer.slice(0, this.cursorPosition);
1198
- this.state.currentInput = this.inputBuffer;
1199
- this.scheduleRender();
1175
+ this.startManualEdit();
1176
+ const newBuffer = this.inputBuffer.slice(0, this.cursorPosition);
1177
+ this.applyBufferChange(newBuffer, this.cursorPosition);
1200
1178
  }
1201
1179
  /**
1202
1180
  * Handle Ctrl+W - delete word before cursor
@@ -1209,17 +1187,16 @@ export class PinnedChatBox {
1209
1187
  // Find the start of the word (skip trailing spaces, then find word boundary)
1210
1188
  let pos = this.cursorPosition;
1211
1189
  // Skip any spaces before cursor
1212
- while (pos > 0 && this.inputBuffer[pos - 1] === ' ') {
1190
+ while (pos > 0 && this.isWhitespace(this.inputBuffer[pos - 1])) {
1213
1191
  pos--;
1214
1192
  }
1215
1193
  // Find start of word
1216
- while (pos > 0 && this.inputBuffer[pos - 1] !== ' ') {
1194
+ while (pos > 0 && !this.isWhitespace(this.inputBuffer[pos - 1])) {
1217
1195
  pos--;
1218
1196
  }
1219
- this.inputBuffer = this.inputBuffer.slice(0, pos) + this.inputBuffer.slice(this.cursorPosition);
1220
- this.cursorPosition = pos;
1221
- this.state.currentInput = this.inputBuffer;
1222
- this.scheduleRender();
1197
+ const newBuffer = this.inputBuffer.slice(0, pos) + this.inputBuffer.slice(this.cursorPosition);
1198
+ this.startManualEdit();
1199
+ this.applyBufferChange(newBuffer, pos);
1223
1200
  }
1224
1201
  /**
1225
1202
  * Handle Alt+Left - move cursor to previous word
@@ -1231,15 +1208,14 @@ export class PinnedChatBox {
1231
1208
  return;
1232
1209
  let pos = this.cursorPosition;
1233
1210
  // Skip any spaces before cursor
1234
- while (pos > 0 && this.inputBuffer[pos - 1] === ' ') {
1211
+ while (pos > 0 && this.isWhitespace(this.inputBuffer[pos - 1])) {
1235
1212
  pos--;
1236
1213
  }
1237
1214
  // Find start of word
1238
- while (pos > 0 && this.inputBuffer[pos - 1] !== ' ') {
1215
+ while (pos > 0 && !this.isWhitespace(this.inputBuffer[pos - 1])) {
1239
1216
  pos--;
1240
1217
  }
1241
- this.cursorPosition = pos;
1242
- this.scheduleRender();
1218
+ this.setCursorPosition(pos);
1243
1219
  }
1244
1220
  /**
1245
1221
  * Handle Alt+Right - move cursor to next word
@@ -1251,15 +1227,14 @@ export class PinnedChatBox {
1251
1227
  return;
1252
1228
  let pos = this.cursorPosition;
1253
1229
  // Skip current word
1254
- while (pos < this.inputBuffer.length && this.inputBuffer[pos] !== ' ') {
1230
+ while (pos < this.inputBuffer.length && !this.isWhitespace(this.inputBuffer[pos])) {
1255
1231
  pos++;
1256
1232
  }
1257
1233
  // Skip any spaces
1258
- while (pos < this.inputBuffer.length && this.inputBuffer[pos] === ' ') {
1234
+ while (pos < this.inputBuffer.length && this.isWhitespace(this.inputBuffer[pos])) {
1259
1235
  pos++;
1260
1236
  }
1261
- this.cursorPosition = pos;
1262
- this.scheduleRender();
1237
+ this.setCursorPosition(pos);
1263
1238
  }
1264
1239
  /**
1265
1240
  * Add input to history (call after successful submit)
@@ -1326,15 +1301,12 @@ export class PinnedChatBox {
1326
1301
  clearInput() {
1327
1302
  if (this.isDisposed)
1328
1303
  return;
1329
- this.inputBuffer = '';
1330
- this.cursorPosition = 0;
1331
- this.state.currentInput = '';
1332
1304
  // Reset history navigation
1333
1305
  this.historyIndex = -1;
1334
1306
  this.tempCurrentInput = '';
1335
1307
  // Clear paste state
1336
1308
  this.clearPastedBlock();
1337
- this.scheduleRender();
1309
+ this.applyBufferChange('', 0);
1338
1310
  }
1339
1311
  /**
1340
1312
  * Set input text and optionally cursor position (for readline sync)
@@ -1343,13 +1315,14 @@ export class PinnedChatBox {
1343
1315
  setInput(text, cursorPos) {
1344
1316
  if (this.isDisposed)
1345
1317
  return;
1346
- const normalized = this.sanitizeInlineText(text).slice(0, this.maxInputLength);
1318
+ const normalized = this.sanitizeMultilineForDisplay(text).slice(0, this.maxInputLength);
1347
1319
  this.inputBuffer = normalized;
1348
1320
  // Use provided cursor position, or default to end of input
1349
1321
  this.cursorPosition = typeof cursorPos === 'number'
1350
1322
  ? Math.max(0, Math.min(cursorPos, normalized.length))
1351
1323
  : normalized.length;
1352
1324
  this.state.currentInput = normalized;
1325
+ this.updateReservedLinesForContent();
1353
1326
  // Do NOT schedule render here - readline handles display during input
1354
1327
  // render() will only work when isProcessing=true anyway
1355
1328
  }
@@ -1484,10 +1457,16 @@ export class PinnedChatBox {
1484
1457
  handleResize() {
1485
1458
  // Invalidate render state to force re-render with new dimensions
1486
1459
  this.invalidateRenderedState();
1487
- if (this.scrollRegionActive) {
1488
- // Reset and re-enable scroll region with new dimensions
1460
+ const wasActive = this.scrollRegionActive;
1461
+ if (wasActive) {
1462
+ // Reset scroll region before recalculating dimensions
1489
1463
  this.safeWrite(ANSI.RESET_SCROLL_REGION);
1490
1464
  this.scrollRegionActive = false;
1465
+ }
1466
+ // Recompute reserved lines for the new terminal size
1467
+ this.updateReservedLinesForContent();
1468
+ if (wasActive) {
1469
+ // Re-enable scroll region with the updated height
1491
1470
  this.enableScrollRegion();
1492
1471
  }
1493
1472
  this.scheduleRender();