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.
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +22 -8
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/ui/persistentPrompt.d.ts +64 -6
- package/dist/ui/persistentPrompt.d.ts.map +1 -1
- package/dist/ui/persistentPrompt.js +383 -80
- package/dist/ui/persistentPrompt.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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(
|
|
468
|
+
this.safeWrite('─'.repeat(separatorWidth));
|
|
387
469
|
this.safeWrite(RESET);
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
771
|
-
|
|
772
|
-
this.
|
|
773
|
-
this.
|
|
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(
|
|
787
|
-
|
|
788
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 -
|
|
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
|
}
|