erosolar-cli 1.7.185 → 1.7.188
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.
- package/dist/core/toolPreconditions.d.ts.map +1 -1
- package/dist/core/toolPreconditions.js +24 -10
- package/dist/core/toolPreconditions.js.map +1 -1
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +14 -0
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/mcp/config.d.ts.map +1 -1
- package/dist/mcp/config.js +56 -7
- package/dist/mcp/config.js.map +1 -1
- package/dist/mcp/sseClient.d.ts +34 -0
- package/dist/mcp/sseClient.d.ts.map +1 -0
- package/dist/mcp/sseClient.js +299 -0
- package/dist/mcp/sseClient.js.map +1 -0
- package/dist/mcp/stdioClient.d.ts +2 -2
- package/dist/mcp/stdioClient.d.ts.map +1 -1
- package/dist/mcp/stdioClient.js.map +1 -1
- package/dist/mcp/toolBridge.d.ts +1 -0
- package/dist/mcp/toolBridge.d.ts.map +1 -1
- package/dist/mcp/toolBridge.js +13 -2
- package/dist/mcp/toolBridge.js.map +1 -1
- package/dist/mcp/types.d.ts +13 -5
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +6 -4
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/ui/persistentPrompt.d.ts +27 -24
- package/dist/ui/persistentPrompt.d.ts.map +1 -1
- package/dist/ui/persistentPrompt.js +242 -258
- package/dist/ui/persistentPrompt.js.map +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
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,44 +427,25 @@ export class PinnedChatBox {
|
|
|
426
427
|
// Hide cursor during render
|
|
427
428
|
this.safeWrite(HIDE_CURSOR);
|
|
428
429
|
this.safeWrite(RESET);
|
|
429
|
-
//
|
|
430
|
-
const
|
|
431
|
-
const totalInputLines =
|
|
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
|
-
|
|
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 =
|
|
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));
|
|
464
446
|
this.safeWrite(ANSI.CLEAR_LINE);
|
|
465
447
|
// Line 1: Separator
|
|
466
|
-
const separatorWidth = Math.min(cols - 2, 60);
|
|
448
|
+
const separatorWidth = Math.max(1, Math.min(cols - 2, 60));
|
|
467
449
|
this.safeWrite(DIM);
|
|
468
450
|
this.safeWrite('─'.repeat(separatorWidth));
|
|
469
451
|
this.safeWrite(RESET);
|
|
@@ -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
|
|
512
|
-
const beforeCursor =
|
|
513
|
-
const atCursor =
|
|
514
|
-
const afterCursor =
|
|
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,12 +495,12 @@ export class PinnedChatBox {
|
|
|
523
495
|
}
|
|
524
496
|
else {
|
|
525
497
|
// Render line without cursor
|
|
526
|
-
this.safeWrite(
|
|
498
|
+
this.safeWrite(line);
|
|
527
499
|
}
|
|
528
500
|
}
|
|
529
501
|
// Position terminal cursor and show it
|
|
530
502
|
this.safeWrite(ANSI.CURSOR_TO_BOTTOM(finalCursorRow));
|
|
531
|
-
this.safeWrite(ANSI.MOVE_TO_COL(finalCursorCol));
|
|
503
|
+
this.safeWrite(ANSI.MOVE_TO_COL(Math.max(1, Math.min(finalCursorCol, cols))));
|
|
532
504
|
this.safeWrite(SHOW_CURSOR);
|
|
533
505
|
// Update rendered state for deduplication
|
|
534
506
|
this.updateRenderedState();
|
|
@@ -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,
|
|
569
|
+
* Sanitize queued command text (trimmed, preserves newlines)
|
|
598
570
|
*/
|
|
599
571
|
sanitizeCommandText(text) {
|
|
600
|
-
|
|
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
|
|
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
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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.
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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,134 @@ export class PinnedChatBox {
|
|
|
928
873
|
*/
|
|
929
874
|
clearPastedBlockState() {
|
|
930
875
|
this.clearPastedBlock();
|
|
931
|
-
this.
|
|
932
|
-
this.cursorPosition = 0;
|
|
933
|
-
this.state.currentInput = '';
|
|
934
|
-
this.resetReservedLines();
|
|
876
|
+
this.applyBufferChange('', 0);
|
|
935
877
|
}
|
|
936
878
|
/**
|
|
937
|
-
*
|
|
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
|
-
|
|
941
|
-
const
|
|
942
|
-
const
|
|
943
|
-
//
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
881
|
+
getRenderDimensions() {
|
|
882
|
+
const rows = this.writeStream.rows || 24;
|
|
883
|
+
const cols = Math.max(10, this.writeStream.columns ?? 80);
|
|
884
|
+
// Leave a small gutter for prefix + padding so we never wrap unexpectedly,
|
|
885
|
+
// but respect the actual terminal width instead of forcing a wide minimum.
|
|
886
|
+
const maxInputWidth = Math.max(8, cols - 5);
|
|
887
|
+
return { rows, cols, maxInputWidth };
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Wrap the current input buffer to the provided width and locate the cursor.
|
|
891
|
+
*
|
|
892
|
+
* ROBUST CURSOR TRACKING:
|
|
893
|
+
* - Uses cursorFound flag to ensure cursor is explicitly located
|
|
894
|
+
* - Falls back to direct calculation if normal tracking fails
|
|
895
|
+
* - Handles edge cases: empty input, cursor at end, cursor at newlines
|
|
896
|
+
*/
|
|
897
|
+
wrapInputBuffer(maxInputWidth) {
|
|
898
|
+
const width = Math.max(1, maxInputWidth);
|
|
899
|
+
const wrappedLines = [];
|
|
900
|
+
const lineStarts = [];
|
|
901
|
+
const targetCursor = Math.max(0, Math.min(this.cursorPosition, this.inputBuffer.length));
|
|
902
|
+
// Handle empty buffer case explicitly
|
|
903
|
+
if (this.inputBuffer.length === 0) {
|
|
904
|
+
return { lines: [''], cursorLine: 0, cursorCol: 0, lineStarts: [0] };
|
|
905
|
+
}
|
|
906
|
+
let cursorLine = 0;
|
|
907
|
+
let cursorCol = 0;
|
|
908
|
+
let cursorFound = false;
|
|
909
|
+
let absoluteIndex = 0;
|
|
910
|
+
const rawLines = this.inputBuffer.split('\n');
|
|
911
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
912
|
+
const raw = rawLines[i] ?? '';
|
|
913
|
+
if (raw.length === 0) {
|
|
914
|
+
// Preserve empty lines (from newlines)
|
|
915
|
+
wrappedLines.push('');
|
|
916
|
+
lineStarts.push(absoluteIndex);
|
|
917
|
+
// Check if cursor is at this empty line position
|
|
918
|
+
if (!cursorFound && targetCursor === absoluteIndex) {
|
|
919
|
+
cursorLine = wrappedLines.length - 1;
|
|
920
|
+
cursorCol = 0;
|
|
921
|
+
cursorFound = true;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
// Wrap long lines at width boundary
|
|
926
|
+
for (let start = 0; start < raw.length; start += width) {
|
|
927
|
+
const segment = raw.slice(start, start + width);
|
|
928
|
+
const segmentStart = absoluteIndex + start;
|
|
929
|
+
const segmentEnd = segmentStart + segment.length;
|
|
930
|
+
wrappedLines.push(segment);
|
|
931
|
+
lineStarts.push(segmentStart);
|
|
932
|
+
// Check if cursor falls within this segment (inclusive on both ends)
|
|
933
|
+
if (!cursorFound && targetCursor >= segmentStart && targetCursor <= segmentEnd) {
|
|
934
|
+
cursorLine = wrappedLines.length - 1;
|
|
935
|
+
cursorCol = targetCursor - segmentStart;
|
|
936
|
+
cursorFound = true;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
absoluteIndex += raw.length;
|
|
941
|
+
// Account for newline character between raw lines
|
|
942
|
+
if (i < rawLines.length - 1) {
|
|
943
|
+
// Check if cursor is at the newline position (start of next line)
|
|
944
|
+
if (!cursorFound && targetCursor === absoluteIndex) {
|
|
945
|
+
cursorLine = wrappedLines.length; // Will be the next line index
|
|
946
|
+
cursorCol = 0;
|
|
947
|
+
cursorFound = true;
|
|
948
|
+
}
|
|
949
|
+
absoluteIndex += 1; // Account for '\n'
|
|
950
|
+
}
|
|
951
951
|
}
|
|
952
|
+
// Fallback: if cursor wasn't found, calculate directly from position
|
|
953
|
+
if (!cursorFound && wrappedLines.length > 0) {
|
|
954
|
+
// Find which wrapped line contains the cursor by checking lineStarts
|
|
955
|
+
for (let i = lineStarts.length - 1; i >= 0; i--) {
|
|
956
|
+
const lineStart = lineStarts[i] ?? 0;
|
|
957
|
+
if (targetCursor >= lineStart) {
|
|
958
|
+
cursorLine = i;
|
|
959
|
+
cursorCol = targetCursor - lineStart;
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
// Safety: ensure wrappedLines has at least one entry
|
|
965
|
+
if (wrappedLines.length === 0) {
|
|
966
|
+
wrappedLines.push('');
|
|
967
|
+
lineStarts.push(0);
|
|
968
|
+
cursorLine = 0;
|
|
969
|
+
cursorCol = 0;
|
|
970
|
+
}
|
|
971
|
+
// Safety bounds checks
|
|
972
|
+
cursorLine = Math.max(0, Math.min(cursorLine, wrappedLines.length - 1));
|
|
973
|
+
const lineLen = wrappedLines[cursorLine]?.length ?? 0;
|
|
974
|
+
cursorCol = Math.max(0, Math.min(cursorCol, lineLen));
|
|
975
|
+
return { lines: wrappedLines, cursorLine, cursorCol, lineStarts };
|
|
952
976
|
}
|
|
953
977
|
/**
|
|
954
|
-
*
|
|
978
|
+
* Convert a wrapped line/column back to an absolute cursor index.
|
|
955
979
|
*/
|
|
956
|
-
|
|
957
|
-
|
|
980
|
+
getWrappedCursorIndex(lineStarts, lineIndex, column) {
|
|
981
|
+
const start = lineStarts[lineIndex] ?? 0;
|
|
982
|
+
const safeColumn = Math.max(0, column);
|
|
983
|
+
return Math.max(0, Math.min(start + safeColumn, this.inputBuffer.length));
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Calculate and update reserved lines based on current input content.
|
|
987
|
+
* Ensures multi-line content is fully visible within maxDisplayLines limit.
|
|
988
|
+
*/
|
|
989
|
+
updateReservedLinesForContent(maxInputWidth, wrappedLineCount) {
|
|
990
|
+
const { rows, maxInputWidth: derivedWidth } = this.getRenderDimensions();
|
|
991
|
+
const width = Math.max(1, maxInputWidth ?? derivedWidth);
|
|
992
|
+
const totalLines = wrappedLineCount ?? this.wrapInputBuffer(width).lines.length;
|
|
993
|
+
const lineCount = Math.max(1, totalLines);
|
|
994
|
+
// Calculate needed lines: 1 for separator + lineCount for input (clamped to maxDisplayLines)
|
|
995
|
+
const inputLines = Math.min(lineCount, this.maxDisplayLines);
|
|
996
|
+
const calculated = 1 + inputLines; // 1 separator + input lines
|
|
997
|
+
// Ensure we keep at least one row for output scrolling
|
|
998
|
+
const maxAllowed = Math.max(1, rows - 1);
|
|
999
|
+
const desired = Math.max(this.baseReservedLines, calculated);
|
|
1000
|
+
this.reservedLines = Math.min(desired, maxAllowed);
|
|
958
1001
|
// If we have a scroll region active, update it
|
|
959
1002
|
if (this.scrollRegionActive) {
|
|
960
|
-
const
|
|
961
|
-
const scrollBottom = rows - this.reservedLines;
|
|
1003
|
+
const scrollBottom = Math.max(1, rows - this.reservedLines);
|
|
962
1004
|
this.safeWrite(ANSI.SET_SCROLL_REGION(1, scrollBottom));
|
|
963
1005
|
}
|
|
964
1006
|
}
|
|
@@ -997,10 +1039,9 @@ export class PinnedChatBox {
|
|
|
997
1039
|
handleCursorLeft() {
|
|
998
1040
|
if (this.isDisposed)
|
|
999
1041
|
return;
|
|
1000
|
-
|
|
1001
|
-
if (
|
|
1002
|
-
this.
|
|
1003
|
-
this.scheduleRender();
|
|
1042
|
+
const bounded = Math.min(this.cursorPosition, this.inputBuffer.length);
|
|
1043
|
+
if (bounded > 0) {
|
|
1044
|
+
this.setCursorPosition(bounded - 1);
|
|
1004
1045
|
}
|
|
1005
1046
|
}
|
|
1006
1047
|
/**
|
|
@@ -1009,10 +1050,9 @@ export class PinnedChatBox {
|
|
|
1009
1050
|
handleCursorRight() {
|
|
1010
1051
|
if (this.isDisposed)
|
|
1011
1052
|
return;
|
|
1012
|
-
|
|
1013
|
-
if (
|
|
1014
|
-
this.
|
|
1015
|
-
this.scheduleRender();
|
|
1053
|
+
const bounded = Math.min(this.cursorPosition, this.inputBuffer.length);
|
|
1054
|
+
if (bounded < this.inputBuffer.length) {
|
|
1055
|
+
this.setCursorPosition(bounded + 1);
|
|
1016
1056
|
}
|
|
1017
1057
|
}
|
|
1018
1058
|
/**
|
|
@@ -1021,15 +1061,11 @@ export class PinnedChatBox {
|
|
|
1021
1061
|
handleHome() {
|
|
1022
1062
|
if (this.isDisposed)
|
|
1023
1063
|
return;
|
|
1024
|
-
// In multi-line mode, move to start of current line
|
|
1025
|
-
const {
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
newPos += (lines[i] ?? '').length + 1;
|
|
1030
|
-
}
|
|
1031
|
-
this.cursorPosition = newPos;
|
|
1032
|
-
this.scheduleRender();
|
|
1064
|
+
// In multi-line mode, move to start of the current wrapped line
|
|
1065
|
+
const { maxInputWidth } = this.getRenderDimensions();
|
|
1066
|
+
const { lineStarts, cursorLine } = this.wrapInputBuffer(maxInputWidth);
|
|
1067
|
+
const newPos = this.getWrappedCursorIndex(lineStarts, cursorLine, 0);
|
|
1068
|
+
this.setCursorPosition(newPos);
|
|
1033
1069
|
}
|
|
1034
1070
|
/**
|
|
1035
1071
|
* Handle end key (Ctrl+E) - move to end of current line (not end of buffer)
|
|
@@ -1037,17 +1073,12 @@ export class PinnedChatBox {
|
|
|
1037
1073
|
handleEnd() {
|
|
1038
1074
|
if (this.isDisposed)
|
|
1039
1075
|
return;
|
|
1040
|
-
// In multi-line mode, move to end of current line
|
|
1041
|
-
const {
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
newPos += (lines[i] ?? '').length + 1;
|
|
1047
|
-
}
|
|
1048
|
-
newPos += currentLine.length;
|
|
1049
|
-
this.cursorPosition = newPos;
|
|
1050
|
-
this.scheduleRender();
|
|
1076
|
+
// In multi-line mode, move to end of the current wrapped line
|
|
1077
|
+
const { maxInputWidth } = this.getRenderDimensions();
|
|
1078
|
+
const { lines: wrappedLines, lineStarts, cursorLine } = this.wrapInputBuffer(maxInputWidth);
|
|
1079
|
+
const currentLine = wrappedLines[cursorLine] ?? '';
|
|
1080
|
+
const newPos = this.getWrappedCursorIndex(lineStarts, cursorLine, currentLine.length);
|
|
1081
|
+
this.setCursorPosition(newPos);
|
|
1051
1082
|
}
|
|
1052
1083
|
/**
|
|
1053
1084
|
* Handle inserting a newline character at cursor position (Shift+Enter or Option+Enter)
|
|
@@ -1058,40 +1089,10 @@ export class PinnedChatBox {
|
|
|
1058
1089
|
// Respect max input length
|
|
1059
1090
|
if (this.inputBuffer.length >= this.maxInputLength)
|
|
1060
1091
|
return;
|
|
1061
|
-
|
|
1062
|
-
this.inputBuffer
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
-
};
|
|
1092
|
+
this.startManualEdit();
|
|
1093
|
+
this.applyBufferChange(this.inputBuffer.slice(0, this.cursorPosition) +
|
|
1094
|
+
'\n' +
|
|
1095
|
+
this.inputBuffer.slice(this.cursorPosition), this.cursorPosition + 1);
|
|
1095
1096
|
}
|
|
1096
1097
|
/**
|
|
1097
1098
|
* Handle up arrow - move cursor up in multi-line content, or navigate history if on first line
|
|
@@ -1099,21 +1100,14 @@ export class PinnedChatBox {
|
|
|
1099
1100
|
handleHistoryUp() {
|
|
1100
1101
|
if (this.isDisposed)
|
|
1101
1102
|
return;
|
|
1102
|
-
// For multi-line content, move cursor up within content first
|
|
1103
|
-
const {
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
const prevLine =
|
|
1107
|
-
const newCol = Math.min(
|
|
1108
|
-
|
|
1109
|
-
|
|
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();
|
|
1103
|
+
// For multi-line content (including wrapped lines), move cursor up within content first
|
|
1104
|
+
const { maxInputWidth } = this.getRenderDimensions();
|
|
1105
|
+
const { lines: wrappedLines, lineStarts, cursorLine, cursorCol } = this.wrapInputBuffer(maxInputWidth);
|
|
1106
|
+
if (cursorLine > 0) {
|
|
1107
|
+
const prevLine = wrappedLines[cursorLine - 1] ?? '';
|
|
1108
|
+
const newCol = Math.min(cursorCol, prevLine.length);
|
|
1109
|
+
const newPos = this.getWrappedCursorIndex(lineStarts, cursorLine - 1, newCol);
|
|
1110
|
+
this.setCursorPosition(newPos);
|
|
1117
1111
|
return;
|
|
1118
1112
|
}
|
|
1119
1113
|
// On first line - navigate history
|
|
@@ -1126,10 +1120,9 @@ export class PinnedChatBox {
|
|
|
1126
1120
|
// Move up in history
|
|
1127
1121
|
if (this.historyIndex < this.inputHistory.length - 1) {
|
|
1128
1122
|
this.historyIndex++;
|
|
1129
|
-
|
|
1130
|
-
this.
|
|
1131
|
-
this.
|
|
1132
|
-
this.scheduleRender();
|
|
1123
|
+
const nextBuffer = this.inputHistory[this.inputHistory.length - 1 - this.historyIndex] || '';
|
|
1124
|
+
this.startManualEdit();
|
|
1125
|
+
this.applyBufferChange(nextBuffer, nextBuffer.length);
|
|
1133
1126
|
}
|
|
1134
1127
|
}
|
|
1135
1128
|
/**
|
|
@@ -1138,39 +1131,30 @@ export class PinnedChatBox {
|
|
|
1138
1131
|
handleHistoryDown() {
|
|
1139
1132
|
if (this.isDisposed)
|
|
1140
1133
|
return;
|
|
1141
|
-
// For multi-line content, move cursor down within content first
|
|
1142
|
-
const {
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
const nextLine =
|
|
1146
|
-
const newCol = Math.min(
|
|
1147
|
-
|
|
1148
|
-
|
|
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();
|
|
1134
|
+
// For multi-line content (including wrapped lines), move cursor down within content first
|
|
1135
|
+
const { maxInputWidth } = this.getRenderDimensions();
|
|
1136
|
+
const { lines: wrappedLines, lineStarts, cursorLine, cursorCol } = this.wrapInputBuffer(maxInputWidth);
|
|
1137
|
+
if (cursorLine < wrappedLines.length - 1) {
|
|
1138
|
+
const nextLine = wrappedLines[cursorLine + 1] ?? '';
|
|
1139
|
+
const newCol = Math.min(cursorCol, nextLine.length);
|
|
1140
|
+
const newPos = this.getWrappedCursorIndex(lineStarts, cursorLine + 1, newCol);
|
|
1141
|
+
this.setCursorPosition(newPos);
|
|
1156
1142
|
return;
|
|
1157
1143
|
}
|
|
1158
1144
|
// On last line - navigate history
|
|
1159
1145
|
if (this.historyIndex > 0) {
|
|
1160
1146
|
// Move down in history
|
|
1161
1147
|
this.historyIndex--;
|
|
1162
|
-
|
|
1163
|
-
this.
|
|
1164
|
-
this.
|
|
1165
|
-
this.scheduleRender();
|
|
1148
|
+
const nextBuffer = this.inputHistory[this.inputHistory.length - 1 - this.historyIndex] || '';
|
|
1149
|
+
this.startManualEdit();
|
|
1150
|
+
this.applyBufferChange(nextBuffer, nextBuffer.length);
|
|
1166
1151
|
}
|
|
1167
1152
|
else if (this.historyIndex === 0) {
|
|
1168
1153
|
// Return to current input
|
|
1169
1154
|
this.historyIndex = -1;
|
|
1170
|
-
|
|
1171
|
-
this.
|
|
1172
|
-
this.
|
|
1173
|
-
this.scheduleRender();
|
|
1155
|
+
const restored = this.tempCurrentInput;
|
|
1156
|
+
this.startManualEdit();
|
|
1157
|
+
this.applyBufferChange(restored, restored.length);
|
|
1174
1158
|
}
|
|
1175
1159
|
}
|
|
1176
1160
|
/**
|
|
@@ -1181,10 +1165,9 @@ export class PinnedChatBox {
|
|
|
1181
1165
|
return;
|
|
1182
1166
|
if (this.cursorPosition === 0)
|
|
1183
1167
|
return;
|
|
1184
|
-
this.
|
|
1185
|
-
this.cursorPosition
|
|
1186
|
-
this.
|
|
1187
|
-
this.scheduleRender();
|
|
1168
|
+
this.startManualEdit();
|
|
1169
|
+
const newBuffer = this.inputBuffer.slice(this.cursorPosition);
|
|
1170
|
+
this.applyBufferChange(newBuffer, 0);
|
|
1188
1171
|
}
|
|
1189
1172
|
/**
|
|
1190
1173
|
* Handle Ctrl+K - delete from cursor to end of line
|
|
@@ -1194,9 +1177,9 @@ export class PinnedChatBox {
|
|
|
1194
1177
|
return;
|
|
1195
1178
|
if (this.cursorPosition >= this.inputBuffer.length)
|
|
1196
1179
|
return;
|
|
1197
|
-
this.
|
|
1198
|
-
this.
|
|
1199
|
-
this.
|
|
1180
|
+
this.startManualEdit();
|
|
1181
|
+
const newBuffer = this.inputBuffer.slice(0, this.cursorPosition);
|
|
1182
|
+
this.applyBufferChange(newBuffer, this.cursorPosition);
|
|
1200
1183
|
}
|
|
1201
1184
|
/**
|
|
1202
1185
|
* Handle Ctrl+W - delete word before cursor
|
|
@@ -1209,17 +1192,16 @@ export class PinnedChatBox {
|
|
|
1209
1192
|
// Find the start of the word (skip trailing spaces, then find word boundary)
|
|
1210
1193
|
let pos = this.cursorPosition;
|
|
1211
1194
|
// Skip any spaces before cursor
|
|
1212
|
-
while (pos > 0 && this.inputBuffer[pos - 1]
|
|
1195
|
+
while (pos > 0 && this.isWhitespace(this.inputBuffer[pos - 1])) {
|
|
1213
1196
|
pos--;
|
|
1214
1197
|
}
|
|
1215
1198
|
// Find start of word
|
|
1216
|
-
while (pos > 0 && this.inputBuffer[pos - 1]
|
|
1199
|
+
while (pos > 0 && !this.isWhitespace(this.inputBuffer[pos - 1])) {
|
|
1217
1200
|
pos--;
|
|
1218
1201
|
}
|
|
1219
|
-
|
|
1220
|
-
this.
|
|
1221
|
-
this.
|
|
1222
|
-
this.scheduleRender();
|
|
1202
|
+
const newBuffer = this.inputBuffer.slice(0, pos) + this.inputBuffer.slice(this.cursorPosition);
|
|
1203
|
+
this.startManualEdit();
|
|
1204
|
+
this.applyBufferChange(newBuffer, pos);
|
|
1223
1205
|
}
|
|
1224
1206
|
/**
|
|
1225
1207
|
* Handle Alt+Left - move cursor to previous word
|
|
@@ -1231,15 +1213,14 @@ export class PinnedChatBox {
|
|
|
1231
1213
|
return;
|
|
1232
1214
|
let pos = this.cursorPosition;
|
|
1233
1215
|
// Skip any spaces before cursor
|
|
1234
|
-
while (pos > 0 && this.inputBuffer[pos - 1]
|
|
1216
|
+
while (pos > 0 && this.isWhitespace(this.inputBuffer[pos - 1])) {
|
|
1235
1217
|
pos--;
|
|
1236
1218
|
}
|
|
1237
1219
|
// Find start of word
|
|
1238
|
-
while (pos > 0 && this.inputBuffer[pos - 1]
|
|
1220
|
+
while (pos > 0 && !this.isWhitespace(this.inputBuffer[pos - 1])) {
|
|
1239
1221
|
pos--;
|
|
1240
1222
|
}
|
|
1241
|
-
this.
|
|
1242
|
-
this.scheduleRender();
|
|
1223
|
+
this.setCursorPosition(pos);
|
|
1243
1224
|
}
|
|
1244
1225
|
/**
|
|
1245
1226
|
* Handle Alt+Right - move cursor to next word
|
|
@@ -1251,15 +1232,14 @@ export class PinnedChatBox {
|
|
|
1251
1232
|
return;
|
|
1252
1233
|
let pos = this.cursorPosition;
|
|
1253
1234
|
// Skip current word
|
|
1254
|
-
while (pos < this.inputBuffer.length && this.inputBuffer[pos]
|
|
1235
|
+
while (pos < this.inputBuffer.length && !this.isWhitespace(this.inputBuffer[pos])) {
|
|
1255
1236
|
pos++;
|
|
1256
1237
|
}
|
|
1257
1238
|
// Skip any spaces
|
|
1258
|
-
while (pos < this.inputBuffer.length && this.inputBuffer[pos]
|
|
1239
|
+
while (pos < this.inputBuffer.length && this.isWhitespace(this.inputBuffer[pos])) {
|
|
1259
1240
|
pos++;
|
|
1260
1241
|
}
|
|
1261
|
-
this.
|
|
1262
|
-
this.scheduleRender();
|
|
1242
|
+
this.setCursorPosition(pos);
|
|
1263
1243
|
}
|
|
1264
1244
|
/**
|
|
1265
1245
|
* Add input to history (call after successful submit)
|
|
@@ -1326,15 +1306,12 @@ export class PinnedChatBox {
|
|
|
1326
1306
|
clearInput() {
|
|
1327
1307
|
if (this.isDisposed)
|
|
1328
1308
|
return;
|
|
1329
|
-
this.inputBuffer = '';
|
|
1330
|
-
this.cursorPosition = 0;
|
|
1331
|
-
this.state.currentInput = '';
|
|
1332
1309
|
// Reset history navigation
|
|
1333
1310
|
this.historyIndex = -1;
|
|
1334
1311
|
this.tempCurrentInput = '';
|
|
1335
1312
|
// Clear paste state
|
|
1336
1313
|
this.clearPastedBlock();
|
|
1337
|
-
this.
|
|
1314
|
+
this.applyBufferChange('', 0);
|
|
1338
1315
|
}
|
|
1339
1316
|
/**
|
|
1340
1317
|
* Set input text and optionally cursor position (for readline sync)
|
|
@@ -1343,13 +1320,14 @@ export class PinnedChatBox {
|
|
|
1343
1320
|
setInput(text, cursorPos) {
|
|
1344
1321
|
if (this.isDisposed)
|
|
1345
1322
|
return;
|
|
1346
|
-
const normalized = this.
|
|
1323
|
+
const normalized = this.sanitizeMultilineForDisplay(text).slice(0, this.maxInputLength);
|
|
1347
1324
|
this.inputBuffer = normalized;
|
|
1348
1325
|
// Use provided cursor position, or default to end of input
|
|
1349
1326
|
this.cursorPosition = typeof cursorPos === 'number'
|
|
1350
1327
|
? Math.max(0, Math.min(cursorPos, normalized.length))
|
|
1351
1328
|
: normalized.length;
|
|
1352
1329
|
this.state.currentInput = normalized;
|
|
1330
|
+
this.updateReservedLinesForContent();
|
|
1353
1331
|
// Do NOT schedule render here - readline handles display during input
|
|
1354
1332
|
// render() will only work when isProcessing=true anyway
|
|
1355
1333
|
}
|
|
@@ -1484,10 +1462,16 @@ export class PinnedChatBox {
|
|
|
1484
1462
|
handleResize() {
|
|
1485
1463
|
// Invalidate render state to force re-render with new dimensions
|
|
1486
1464
|
this.invalidateRenderedState();
|
|
1487
|
-
|
|
1488
|
-
|
|
1465
|
+
const wasActive = this.scrollRegionActive;
|
|
1466
|
+
if (wasActive) {
|
|
1467
|
+
// Reset scroll region before recalculating dimensions
|
|
1489
1468
|
this.safeWrite(ANSI.RESET_SCROLL_REGION);
|
|
1490
1469
|
this.scrollRegionActive = false;
|
|
1470
|
+
}
|
|
1471
|
+
// Recompute reserved lines for the new terminal size
|
|
1472
|
+
this.updateReservedLinesForContent();
|
|
1473
|
+
if (wasActive) {
|
|
1474
|
+
// Re-enable scroll region with the updated height
|
|
1491
1475
|
this.enableScrollRegion();
|
|
1492
1476
|
}
|
|
1493
1477
|
this.scheduleRender();
|