erosolar-cli 1.7.184 → 1.7.185

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.
@@ -246,6 +246,12 @@ export class PinnedChatBox {
246
246
  pastedFullContent = '';
247
247
  /** Cleanup function for output interceptor registration */
248
248
  outputInterceptorCleanup;
249
+ // Render deduplication - prevent duplicate displays
250
+ lastRenderedContent = '';
251
+ lastRenderedCursor = -1;
252
+ lastRenderedRows = 0;
253
+ lastRenderedCols = 0;
254
+ isRendering = false; // Render lock to prevent concurrent renders
249
255
  constructor(writeStream, _promptText = '> ', // Unused - readline handles input display
250
256
  options = {}) {
251
257
  this.writeStream = writeStream;
@@ -315,6 +321,8 @@ export class PinnedChatBox {
315
321
  const scrollBottom = rows - this.reservedLines;
316
322
  // Step 1: Set scroll region (excludes bottom reserved lines)
317
323
  this.safeWrite(ANSI.SET_SCROLL_REGION(1, scrollBottom));
324
+ // Invalidate render state to ensure fresh render in the new scroll region
325
+ this.invalidateRenderedState();
318
326
  // Step 2: Render the prompt in the protected bottom area
319
327
  this.renderPersistentInput();
320
328
  // Mark scroll region as active AFTER rendering
@@ -344,14 +352,52 @@ export class PinnedChatBox {
344
352
  // Move cursor to the end of the terminal
345
353
  this.safeWrite(ANSI.CURSOR_TO_BOTTOM(rows));
346
354
  }
355
+ /**
356
+ * Check if render is needed by comparing with last rendered state.
357
+ * Returns true if content, cursor, or terminal size has changed.
358
+ */
359
+ needsRender() {
360
+ const rows = this.writeStream.rows || 24;
361
+ const cols = this.writeStream.columns || 80;
362
+ // Check if anything changed that requires a re-render
363
+ if (this.inputBuffer !== this.lastRenderedContent)
364
+ return true;
365
+ if (this.cursorPosition !== this.lastRenderedCursor)
366
+ return true;
367
+ if (rows !== this.lastRenderedRows)
368
+ return true;
369
+ if (cols !== this.lastRenderedCols)
370
+ return true;
371
+ return false;
372
+ }
373
+ /**
374
+ * Update last rendered state after successful render.
375
+ */
376
+ updateRenderedState() {
377
+ this.lastRenderedContent = this.inputBuffer;
378
+ this.lastRenderedCursor = this.cursorPosition;
379
+ this.lastRenderedRows = this.writeStream.rows || 24;
380
+ this.lastRenderedCols = this.writeStream.columns || 80;
381
+ }
382
+ /**
383
+ * Reset rendered state to force next render.
384
+ */
385
+ invalidateRenderedState() {
386
+ this.lastRenderedContent = '';
387
+ this.lastRenderedCursor = -1;
388
+ this.lastRenderedRows = 0;
389
+ this.lastRenderedCols = 0;
390
+ }
347
391
  /**
348
392
  * Render the persistent input area at the bottom of the terminal.
349
393
  *
350
394
  * CLEAN CLAUDE CODE STYLE:
351
395
  * - Line 1: Simple separator with optional queue count
352
- * - Line 2: Clean prompt "> " with input and visible block cursor
396
+ * - Line 2+: Clean prompt "> " with input and visible block cursor
353
397
  *
354
398
  * Key for clean rendering:
399
+ * - Render deduplication via content/cursor comparison
400
+ * - Render lock to prevent concurrent renders
355
401
  * - Hide cursor during render (prevents flicker)
356
402
  * - Clear lines completely before writing
357
403
  * - Reset styles explicitly
@@ -361,63 +407,135 @@ export class PinnedChatBox {
361
407
  renderPersistentInput() {
362
408
  if (!this.supportsRendering())
363
409
  return;
364
- const rows = this.writeStream.rows || 24;
365
- const cols = Math.max(this.writeStream.columns || 80, 40);
366
- // ANSI codes - keep simple, no theme functions
367
- const HIDE_CURSOR = '\x1b[?25l';
368
- const SHOW_CURSOR = '\x1b[?25h';
369
- const REVERSE_VIDEO = '\x1b[7m';
370
- const RESET = '\x1b[0m';
371
- const DIM = '\x1b[2m';
372
- // Hide cursor during render
373
- this.safeWrite(HIDE_CURSOR);
374
- this.safeWrite(RESET);
375
- // Line 1: Separator (at row N-1)
376
- this.safeWrite(ANSI.CURSOR_TO_BOTTOM(rows - 1));
377
- this.safeWrite(ANSI.CLEAR_LINE);
378
- const separatorWidth = Math.min(cols - 2, 60);
379
- this.safeWrite(DIM);
380
- this.safeWrite('─'.repeat(separatorWidth));
381
- this.safeWrite(RESET);
382
- // Queue count hint if processing
383
- const queueCount = this.state.queuedCommands.length;
384
- if (this.state.isProcessing && queueCount > 0) {
410
+ // Render lock - prevent concurrent renders that cause flicker
411
+ if (this.isRendering)
412
+ return;
413
+ // Deduplication - skip if nothing changed
414
+ if (!this.needsRender())
415
+ return;
416
+ this.isRendering = true;
417
+ try {
418
+ const rows = this.writeStream.rows || 24;
419
+ const cols = Math.max(this.writeStream.columns || 80, 40);
420
+ // ANSI codes - keep simple, no theme functions
421
+ const HIDE_CURSOR = '\x1b[?25l';
422
+ const SHOW_CURSOR = '\x1b[?25h';
423
+ const REVERSE_VIDEO = '\x1b[7m';
424
+ const RESET = '\x1b[0m';
425
+ const DIM = '\x1b[2m';
426
+ // Hide cursor during render
427
+ this.safeWrite(HIDE_CURSOR);
428
+ this.safeWrite(RESET);
429
+ // Split input into lines for multi-line support
430
+ const inputLines = this.inputBuffer.split('\n');
431
+ const totalInputLines = inputLines.length;
432
+ const displayLineCount = Math.min(totalInputLines, this.maxDisplayLines);
433
+ // 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
+ let displayStartLine = 0;
453
+ if (totalInputLines > displayLineCount) {
454
+ if (cursorLineIndex >= displayLineCount - 1) {
455
+ displayStartLine = cursorLineIndex - displayLineCount + 2;
456
+ }
457
+ displayStartLine = Math.min(displayStartLine, totalInputLines - displayLineCount);
458
+ displayStartLine = Math.max(0, displayStartLine);
459
+ }
460
+ const linesToShow = inputLines.slice(displayStartLine, displayStartLine + displayLineCount);
461
+ // Move to the start of reserved area
462
+ const reservedStart = rows - this.reservedLines + 1;
463
+ this.safeWrite(ANSI.CURSOR_TO_BOTTOM(reservedStart));
464
+ this.safeWrite(ANSI.CLEAR_LINE);
465
+ // Line 1: Separator
466
+ const separatorWidth = Math.min(cols - 2, 60);
385
467
  this.safeWrite(DIM);
386
- this.safeWrite(` ${queueCount} queued`);
468
+ this.safeWrite('─'.repeat(separatorWidth));
387
469
  this.safeWrite(RESET);
388
- }
389
- // Line 2: Prompt (at row N)
390
- this.safeWrite(ANSI.CURSOR_TO_BOTTOM(rows));
391
- this.safeWrite(ANSI.CLEAR_LINE);
392
- const promptPrefix = '> ';
393
- const maxInputWidth = cols - 5;
394
- const input = this.inputBuffer;
395
- const cursorPos = Math.min(this.cursorPosition, input.length);
396
- // Handle long input
397
- let displayStart = 0;
398
- let displayInput = input;
399
- if (input.length > maxInputWidth) {
400
- if (cursorPos > maxInputWidth - 5) {
401
- displayStart = cursorPos - maxInputWidth + 5;
470
+ // Status hints (line count, queue count)
471
+ const queueCount = this.state.queuedCommands.length;
472
+ const hints = [];
473
+ if (totalInputLines > 1) {
474
+ hints.push(`${totalInputLines} lines`);
475
+ }
476
+ if (this.state.isProcessing && queueCount > 0) {
477
+ hints.push(`${queueCount} queued`);
478
+ }
479
+ if (hints.length > 0) {
480
+ this.safeWrite(DIM);
481
+ this.safeWrite(' ' + hints.join(' '));
482
+ this.safeWrite(RESET);
483
+ }
484
+ const maxInputWidth = cols - 5;
485
+ let finalCursorRow = reservedStart + 1;
486
+ let finalCursorCol = 3;
487
+ // Render each input line
488
+ for (let lineIdx = 0; lineIdx < linesToShow.length; lineIdx++) {
489
+ this.safeWrite('\n');
490
+ this.safeWrite(ANSI.CLEAR_LINE);
491
+ const line = linesToShow[lineIdx] ?? '';
492
+ const absoluteLineIdx = displayStartLine + lineIdx;
493
+ const isFirstLine = absoluteLineIdx === 0;
494
+ const isCursorLine = absoluteLineIdx === cursorLineIndex;
495
+ // Prompt prefix: ">" for first line, "│" for continuation lines
496
+ this.safeWrite(DIM);
497
+ this.safeWrite(isFirstLine ? '>' : '│');
498
+ this.safeWrite(RESET);
499
+ 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
+ if (isCursorLine) {
510
+ // 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);
515
+ this.safeWrite(beforeCursor);
516
+ this.safeWrite(REVERSE_VIDEO);
517
+ this.safeWrite(atCursor);
518
+ this.safeWrite(RESET);
519
+ this.safeWrite(afterCursor);
520
+ // Remember cursor position for final positioning
521
+ finalCursorRow = reservedStart + 1 + lineIdx;
522
+ finalCursorCol = 3 + displayCursorCol;
523
+ }
524
+ else {
525
+ // Render line without cursor
526
+ this.safeWrite(displayLine);
527
+ }
402
528
  }
403
- displayInput = input.slice(displayStart, displayStart + maxInputWidth);
529
+ // Position terminal cursor and show it
530
+ this.safeWrite(ANSI.CURSOR_TO_BOTTOM(finalCursorRow));
531
+ this.safeWrite(ANSI.MOVE_TO_COL(finalCursorCol));
532
+ this.safeWrite(SHOW_CURSOR);
533
+ // Update rendered state for deduplication
534
+ this.updateRenderedState();
535
+ }
536
+ finally {
537
+ this.isRendering = false;
404
538
  }
405
- const displayCursorPos = cursorPos - displayStart;
406
- // Write prompt
407
- this.safeWrite(promptPrefix);
408
- // Text before cursor
409
- this.safeWrite(displayInput.slice(0, displayCursorPos));
410
- // Cursor block (reverse video)
411
- const atCursor = displayInput[displayCursorPos] || ' ';
412
- this.safeWrite(REVERSE_VIDEO);
413
- this.safeWrite(atCursor);
414
- this.safeWrite(RESET);
415
- // Text after cursor
416
- this.safeWrite(displayInput.slice(displayCursorPos + 1));
417
- // Position terminal cursor and show it
418
- const terminalCursorCol = promptPrefix.length + displayCursorPos + 1;
419
- this.safeWrite(ANSI.MOVE_TO_COL(terminalCursorCol));
420
- this.safeWrite(SHOW_CURSOR);
421
539
  }
422
540
  /**
423
541
  * Update the persistent input during streaming.
@@ -492,6 +610,95 @@ export class PinnedChatBox {
492
610
  return null;
493
611
  return clean.slice(0, this.maxStatusMessageLength);
494
612
  }
613
+ /**
614
+ * Clear paste-specific state when the user edits the buffer manually.
615
+ * Prevents us from sending stale paste content after edits.
616
+ */
617
+ startManualEdit() {
618
+ if (this.isPastedBlock) {
619
+ this.clearPastedBlock();
620
+ }
621
+ }
622
+ /**
623
+ * Apply a new buffer/cursor position and trigger downstream updates.
624
+ */
625
+ applyBufferChange(buffer, cursorPos) {
626
+ this.inputBuffer = buffer;
627
+ const nextCursor = typeof cursorPos === 'number' ? cursorPos : buffer.length;
628
+ this.cursorPosition = Math.max(0, Math.min(nextCursor, this.inputBuffer.length));
629
+ this.state.currentInput = this.inputBuffer;
630
+ this.updateReservedLinesForContent();
631
+ this.scheduleRender();
632
+ }
633
+ /**
634
+ * Update cursor position while keeping it in bounds and re-rendering.
635
+ */
636
+ setCursorPosition(position) {
637
+ this.cursorPosition = Math.max(0, Math.min(position, this.inputBuffer.length));
638
+ this.scheduleRender();
639
+ }
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
+ /**
686
+ * Insert multi-line content directly into the buffer (manual typing).
687
+ */
688
+ insertMultilineText(text) {
689
+ const sanitized = this.sanitizeMultilineForDisplay(text);
690
+ if (!sanitized)
691
+ return;
692
+ // Respect max input length
693
+ const availableSpace = this.maxInputLength - this.inputBuffer.length;
694
+ if (availableSpace <= 0)
695
+ return;
696
+ const chunk = sanitized.slice(0, availableSpace);
697
+ this.startManualEdit();
698
+ this.applyBufferChange(this.inputBuffer.slice(0, this.cursorPosition) +
699
+ chunk +
700
+ this.inputBuffer.slice(this.cursorPosition), this.cursorPosition + chunk.length);
701
+ }
495
702
  /**
496
703
  * Safely write to the output stream, swallowing non-fatal errors
497
704
  */
@@ -681,15 +888,13 @@ export class PinnedChatBox {
681
888
  handleMultilinePaste(content) {
682
889
  // Keep the original content for submission
683
890
  const displaySafeContent = this.sanitizeMultilineForDisplay(content);
684
- this.pastedFullContent = content;
891
+ if (!displaySafeContent)
892
+ return;
893
+ const truncatedDisplay = displaySafeContent.slice(0, this.maxInputLength);
894
+ const truncatedOriginal = content.slice(0, this.maxInputLength);
895
+ this.pastedFullContent = truncatedOriginal;
685
896
  this.isPastedBlock = true;
686
- // Store the full content for display (not just a summary)
687
- this.inputBuffer = displaySafeContent;
688
- this.cursorPosition = displaySafeContent.length;
689
- this.state.currentInput = displaySafeContent;
690
- // Update reserved lines based on content
691
- this.updateReservedLinesForContent();
692
- this.scheduleRender();
897
+ this.applyBufferChange(truncatedDisplay, truncatedDisplay.length);
693
898
  }
694
899
  /**
695
900
  * Public helper for routing external multi-line pastes directly into the chat box
@@ -766,12 +971,11 @@ export class PinnedChatBox {
766
971
  this.cursorPosition = Math.min(this.cursorPosition, this.inputBuffer.length);
767
972
  if (this.cursorPosition === 0)
768
973
  return;
769
- this.inputBuffer =
770
- this.inputBuffer.slice(0, this.cursorPosition - 1) +
771
- this.inputBuffer.slice(this.cursorPosition);
772
- this.cursorPosition = Math.max(0, this.cursorPosition - 1);
773
- this.state.currentInput = this.inputBuffer;
774
- this.scheduleRender();
974
+ const newBuffer = this.inputBuffer.slice(0, this.cursorPosition - 1) +
975
+ this.inputBuffer.slice(this.cursorPosition);
976
+ const newCursor = Math.max(0, this.cursorPosition - 1);
977
+ this.startManualEdit();
978
+ this.applyBufferChange(newBuffer, newCursor);
775
979
  }
776
980
  /**
777
981
  * Handle delete key
@@ -782,11 +986,10 @@ export class PinnedChatBox {
782
986
  this.cursorPosition = Math.min(this.cursorPosition, this.inputBuffer.length);
783
987
  if (this.cursorPosition >= this.inputBuffer.length)
784
988
  return;
785
- this.inputBuffer =
786
- this.inputBuffer.slice(0, this.cursorPosition) +
787
- this.inputBuffer.slice(this.cursorPosition + 1);
788
- this.state.currentInput = this.inputBuffer;
789
- this.scheduleRender();
989
+ const newBuffer = this.inputBuffer.slice(0, this.cursorPosition) +
990
+ this.inputBuffer.slice(this.cursorPosition + 1);
991
+ this.startManualEdit();
992
+ this.applyBufferChange(newBuffer, this.cursorPosition);
790
993
  }
791
994
  /**
792
995
  * Handle cursor left
@@ -813,29 +1016,107 @@ export class PinnedChatBox {
813
1016
  }
814
1017
  }
815
1018
  /**
816
- * Handle home key
1019
+ * Handle home key - move to start of current line (not start of buffer)
817
1020
  */
818
1021
  handleHome() {
819
1022
  if (this.isDisposed)
820
1023
  return;
821
- this.cursorPosition = 0;
1024
+ // In multi-line mode, move to start of current line
1025
+ const { lineIndex, lines } = this.getCursorLinePosition();
1026
+ // Calculate position at start of current line
1027
+ let newPos = 0;
1028
+ for (let i = 0; i < lineIndex; i++) {
1029
+ newPos += (lines[i] ?? '').length + 1;
1030
+ }
1031
+ this.cursorPosition = newPos;
822
1032
  this.scheduleRender();
823
1033
  }
824
1034
  /**
825
- * Handle end key (Ctrl+E)
1035
+ * Handle end key (Ctrl+E) - move to end of current line (not end of buffer)
826
1036
  */
827
1037
  handleEnd() {
828
1038
  if (this.isDisposed)
829
1039
  return;
830
- this.cursorPosition = this.inputBuffer.length;
1040
+ // In multi-line mode, move to end of current line
1041
+ const { lineIndex, lines } = this.getCursorLinePosition();
1042
+ const currentLine = lines[lineIndex] ?? '';
1043
+ // Calculate position at end of current line
1044
+ let newPos = 0;
1045
+ for (let i = 0; i < lineIndex; i++) {
1046
+ newPos += (lines[i] ?? '').length + 1;
1047
+ }
1048
+ newPos += currentLine.length;
1049
+ this.cursorPosition = newPos;
831
1050
  this.scheduleRender();
832
1051
  }
833
1052
  /**
834
- * Handle up arrow - navigate to previous history entry
1053
+ * Handle inserting a newline character at cursor position (Shift+Enter or Option+Enter)
1054
+ */
1055
+ handleNewline() {
1056
+ if (!this.isEnabled || this.isDisposed)
1057
+ return;
1058
+ // Respect max input length
1059
+ if (this.inputBuffer.length >= this.maxInputLength)
1060
+ 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();
1071
+ }
1072
+ /**
1073
+ * Get cursor line and column position within multi-line content.
1074
+ */
1075
+ getCursorLinePosition() {
1076
+ const lines = this.inputBuffer.split('\n');
1077
+ let charCount = 0;
1078
+ for (let i = 0; i < lines.length; i++) {
1079
+ const lineLen = (lines[i] ?? '').length;
1080
+ if (charCount + lineLen >= this.cursorPosition) {
1081
+ return {
1082
+ lineIndex: i,
1083
+ colInLine: this.cursorPosition - charCount,
1084
+ lines,
1085
+ };
1086
+ }
1087
+ charCount += lineLen + 1; // +1 for newline
1088
+ }
1089
+ // Cursor at end
1090
+ return {
1091
+ lineIndex: lines.length - 1,
1092
+ colInLine: (lines[lines.length - 1] ?? '').length,
1093
+ lines,
1094
+ };
1095
+ }
1096
+ /**
1097
+ * Handle up arrow - move cursor up in multi-line content, or navigate history if on first line
835
1098
  */
836
1099
  handleHistoryUp() {
837
1100
  if (this.isDisposed)
838
1101
  return;
1102
+ // For multi-line content, move cursor up within content first
1103
+ const { lineIndex, colInLine, lines } = this.getCursorLinePosition();
1104
+ if (lineIndex > 0) {
1105
+ // Move cursor to previous line, keeping column position if possible
1106
+ const prevLine = lines[lineIndex - 1] ?? '';
1107
+ const newCol = Math.min(colInLine, prevLine.length);
1108
+ // Calculate new cursor position
1109
+ let newPos = 0;
1110
+ for (let i = 0; i < lineIndex - 1; i++) {
1111
+ newPos += (lines[i] ?? '').length + 1;
1112
+ }
1113
+ newPos += newCol;
1114
+ this.cursorPosition = newPos;
1115
+ this.state.currentInput = this.inputBuffer;
1116
+ this.scheduleRender();
1117
+ return;
1118
+ }
1119
+ // On first line - navigate history
839
1120
  if (this.inputHistory.length === 0)
840
1121
  return;
841
1122
  // If at current input, save it temporarily
@@ -852,11 +1133,29 @@ export class PinnedChatBox {
852
1133
  }
853
1134
  }
854
1135
  /**
855
- * Handle down arrow - navigate to next history entry
1136
+ * Handle down arrow - move cursor down in multi-line content, or navigate history if on last line
856
1137
  */
857
1138
  handleHistoryDown() {
858
1139
  if (this.isDisposed)
859
1140
  return;
1141
+ // For multi-line content, move cursor down within content first
1142
+ const { lineIndex, colInLine, lines } = this.getCursorLinePosition();
1143
+ if (lineIndex < lines.length - 1) {
1144
+ // Move cursor to next line, keeping column position if possible
1145
+ const nextLine = lines[lineIndex + 1] ?? '';
1146
+ const newCol = Math.min(colInLine, nextLine.length);
1147
+ // Calculate new cursor position
1148
+ let newPos = 0;
1149
+ for (let i = 0; i <= lineIndex; i++) {
1150
+ newPos += (lines[i] ?? '').length + 1;
1151
+ }
1152
+ newPos += newCol;
1153
+ this.cursorPosition = newPos;
1154
+ this.state.currentInput = this.inputBuffer;
1155
+ this.scheduleRender();
1156
+ return;
1157
+ }
1158
+ // On last line - navigate history
860
1159
  if (this.historyIndex > 0) {
861
1160
  // Move down in history
862
1161
  this.historyIndex--;
@@ -1183,6 +1482,8 @@ export class PinnedChatBox {
1183
1482
  * Handle terminal resize - update scroll region if active
1184
1483
  */
1185
1484
  handleResize() {
1485
+ // Invalidate render state to force re-render with new dimensions
1486
+ this.invalidateRenderedState();
1186
1487
  if (this.scrollRegionActive) {
1187
1488
  // Reset and re-enable scroll region with new dimensions
1188
1489
  this.safeWrite(ANSI.RESET_SCROLL_REGION);
@@ -1239,13 +1540,14 @@ export class PinnedChatBox {
1239
1540
  return !this.isDisposed && this.isEnabled;
1240
1541
  }
1241
1542
  /**
1242
- * Force immediate render (bypass throttling)
1543
+ * Force immediate render (bypass throttling and deduplication)
1243
1544
  */
1244
1545
  forceRender() {
1245
1546
  if (this.isDisposed)
1246
1547
  return;
1247
1548
  this.lastRenderTime = 0;
1248
1549
  this.renderScheduled = false;
1550
+ this.invalidateRenderedState(); // Force re-render even if content unchanged
1249
1551
  this.render();
1250
1552
  }
1251
1553
  /**
@@ -1264,6 +1566,7 @@ export class PinnedChatBox {
1264
1566
  statusMessage: null,
1265
1567
  isVisible: true,
1266
1568
  };
1569
+ this.invalidateRenderedState();
1267
1570
  this.scheduleRender();
1268
1571
  }
1269
1572
  }