erosolar-cli 1.7.183 → 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 +80 -18
- package/dist/ui/persistentPrompt.d.ts.map +1 -1
- package/dist/ui/persistentPrompt.js +417 -144
- package/dist/ui/persistentPrompt.js.map +1 -1
- package/package.json +1 -1
|
@@ -215,10 +215,12 @@ const ANSI = {
|
|
|
215
215
|
export class PinnedChatBox {
|
|
216
216
|
writeStream;
|
|
217
217
|
state;
|
|
218
|
-
|
|
218
|
+
baseReservedLines = 2; // Base lines reserved (separator + at least 1 input line)
|
|
219
|
+
reservedLines = 2; // Actual reserved lines (dynamically adjusted for multi-line)
|
|
219
220
|
_lastRenderedHeight = 0;
|
|
220
221
|
inputBuffer = '';
|
|
221
222
|
cursorPosition = 0;
|
|
223
|
+
maxDisplayLines = 15; // Maximum lines to show in the input area
|
|
222
224
|
commandIdCounter = 0;
|
|
223
225
|
onCommandQueued;
|
|
224
226
|
onInputSubmit;
|
|
@@ -232,7 +234,6 @@ export class PinnedChatBox {
|
|
|
232
234
|
ansiPattern = /[\u001B\u009B][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
|
|
233
235
|
oscPattern = /\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g;
|
|
234
236
|
maxStatusMessageLength = 200;
|
|
235
|
-
maxPastePreviewLength = 60;
|
|
236
237
|
// Scroll region management for persistent bottom input
|
|
237
238
|
scrollRegionActive = false;
|
|
238
239
|
// Input history for up/down navigation
|
|
@@ -245,6 +246,12 @@ export class PinnedChatBox {
|
|
|
245
246
|
pastedFullContent = '';
|
|
246
247
|
/** Cleanup function for output interceptor registration */
|
|
247
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
|
|
248
255
|
constructor(writeStream, _promptText = '> ', // Unused - readline handles input display
|
|
249
256
|
options = {}) {
|
|
250
257
|
this.writeStream = writeStream;
|
|
@@ -314,6 +321,8 @@ export class PinnedChatBox {
|
|
|
314
321
|
const scrollBottom = rows - this.reservedLines;
|
|
315
322
|
// Step 1: Set scroll region (excludes bottom reserved lines)
|
|
316
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();
|
|
317
326
|
// Step 2: Render the prompt in the protected bottom area
|
|
318
327
|
this.renderPersistentInput();
|
|
319
328
|
// Mark scroll region as active AFTER rendering
|
|
@@ -343,78 +352,190 @@ export class PinnedChatBox {
|
|
|
343
352
|
// Move cursor to the end of the terminal
|
|
344
353
|
this.safeWrite(ANSI.CURSOR_TO_BOTTOM(rows));
|
|
345
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
|
+
}
|
|
346
391
|
/**
|
|
347
392
|
* Render the persistent input area at the bottom of the terminal.
|
|
348
393
|
*
|
|
349
394
|
* CLEAN CLAUDE CODE STYLE:
|
|
350
|
-
* - Line 1: Simple separator with optional
|
|
351
|
-
* - Line 2
|
|
352
|
-
*
|
|
395
|
+
* - Line 1: Simple separator with optional queue count
|
|
396
|
+
* - Line 2+: Clean prompt "> " with input and visible block cursor
|
|
397
|
+
*
|
|
398
|
+
* Key for clean rendering:
|
|
399
|
+
* - Render deduplication via content/cursor comparison
|
|
400
|
+
* - Render lock to prevent concurrent renders
|
|
401
|
+
* - Hide cursor during render (prevents flicker)
|
|
402
|
+
* - Clear lines completely before writing
|
|
403
|
+
* - Reset styles explicitly
|
|
404
|
+
* - No theme functions (avoid extra ANSI codes)
|
|
405
|
+
* - Show cursor at correct position at end
|
|
353
406
|
*/
|
|
354
407
|
renderPersistentInput() {
|
|
355
408
|
if (!this.supportsRendering())
|
|
356
409
|
return;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
this.
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
+
}
|
|
379
450
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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);
|
|
467
|
+
this.safeWrite(DIM);
|
|
468
|
+
this.safeWrite('─'.repeat(separatorWidth));
|
|
469
|
+
this.safeWrite(RESET);
|
|
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
|
+
}
|
|
396
528
|
}
|
|
397
|
-
|
|
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;
|
|
398
538
|
}
|
|
399
|
-
// Calculate cursor position in displayed text
|
|
400
|
-
const displayCursorPos = cursorPos - displayStart;
|
|
401
|
-
// Render: prompt + text before cursor + CURSOR + text after cursor
|
|
402
|
-
this.safeWrite(promptPrefix);
|
|
403
|
-
const beforeCursor = displayInput.slice(0, displayCursorPos);
|
|
404
|
-
const atCursor = displayInput[displayCursorPos] || ' '; // Space if at end
|
|
405
|
-
const afterCursor = displayInput.slice(displayCursorPos + 1);
|
|
406
|
-
// Text before cursor
|
|
407
|
-
this.safeWrite(beforeCursor);
|
|
408
|
-
// Cursor character (reverse video block)
|
|
409
|
-
this.safeWrite(REVERSE_VIDEO);
|
|
410
|
-
this.safeWrite(atCursor);
|
|
411
|
-
this.safeWrite(RESET);
|
|
412
|
-
// Text after cursor
|
|
413
|
-
this.safeWrite(afterCursor);
|
|
414
|
-
// Position the terminal cursor at our visual cursor position
|
|
415
|
-
// (This ensures the terminal's blinking cursor is also at the right spot)
|
|
416
|
-
const terminalCursorCol = promptPrefix.length + displayCursorPos + 1;
|
|
417
|
-
this.safeWrite(ANSI.MOVE_TO_COL(terminalCursorCol));
|
|
418
539
|
}
|
|
419
540
|
/**
|
|
420
541
|
* Update the persistent input during streaming.
|
|
@@ -490,30 +611,93 @@ export class PinnedChatBox {
|
|
|
490
611
|
return clean.slice(0, this.maxStatusMessageLength);
|
|
491
612
|
}
|
|
492
613
|
/**
|
|
493
|
-
*
|
|
614
|
+
* Clear paste-specific state when the user edits the buffer manually.
|
|
615
|
+
* Prevents us from sending stale paste content after edits.
|
|
494
616
|
*/
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
return '(empty)';
|
|
617
|
+
startManualEdit() {
|
|
618
|
+
if (this.isPastedBlock) {
|
|
619
|
+
this.clearPastedBlock();
|
|
499
620
|
}
|
|
500
|
-
|
|
501
|
-
|
|
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
|
|
502
652
|
}
|
|
503
|
-
|
|
653
|
+
// Fallback to last line
|
|
654
|
+
const lastLine = Math.max(0, lines.length - 1);
|
|
655
|
+
return { lines, lineIndex: lastLine, column: (lines[lastLine] ?? '').length };
|
|
504
656
|
}
|
|
505
657
|
/**
|
|
506
|
-
*
|
|
658
|
+
* Convert line/column back to a single cursor index.
|
|
507
659
|
*/
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
|
511
664
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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;
|
|
515
675
|
}
|
|
516
|
-
|
|
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);
|
|
517
701
|
}
|
|
518
702
|
/**
|
|
519
703
|
* Safely write to the output stream, swallowing non-fatal errors
|
|
@@ -699,20 +883,18 @@ export class PinnedChatBox {
|
|
|
699
883
|
this.scheduleRender();
|
|
700
884
|
}
|
|
701
885
|
/**
|
|
702
|
-
* Handle multiline paste - store full content
|
|
886
|
+
* Handle multiline paste - store full content and display all lines
|
|
703
887
|
*/
|
|
704
888
|
handleMultilinePaste(content) {
|
|
705
|
-
// Keep the original content for submission
|
|
889
|
+
// Keep the original content for submission
|
|
706
890
|
const displaySafeContent = this.sanitizeMultilineForDisplay(content);
|
|
707
|
-
|
|
708
|
-
|
|
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;
|
|
709
896
|
this.isPastedBlock = true;
|
|
710
|
-
|
|
711
|
-
const summary = this.generatePasteSummary(displaySafeContent, lineCount);
|
|
712
|
-
this.inputBuffer = summary;
|
|
713
|
-
this.cursorPosition = summary.length;
|
|
714
|
-
this.state.currentInput = summary;
|
|
715
|
-
this.scheduleRender();
|
|
897
|
+
this.applyBufferChange(truncatedDisplay, truncatedDisplay.length);
|
|
716
898
|
}
|
|
717
899
|
/**
|
|
718
900
|
* Public helper for routing external multi-line pastes directly into the chat box
|
|
@@ -722,43 +904,6 @@ export class PinnedChatBox {
|
|
|
722
904
|
return;
|
|
723
905
|
this.handleMultilinePaste(content);
|
|
724
906
|
}
|
|
725
|
-
/**
|
|
726
|
-
* Generate a short summary of pasted content for display
|
|
727
|
-
*/
|
|
728
|
-
generatePasteSummary(content, lineCount) {
|
|
729
|
-
const lines = content.split('\n');
|
|
730
|
-
const firstLine = lines[0]?.trim() || '';
|
|
731
|
-
const charCount = content.length;
|
|
732
|
-
// Detect content type and get appropriate icon/label
|
|
733
|
-
let typeLabel = 'Paste';
|
|
734
|
-
let typeIcon = '📋';
|
|
735
|
-
if (firstLine.match(/^(import|export|const|let|var|function|class|def |async |from |interface |type )/)) {
|
|
736
|
-
typeIcon = '📝';
|
|
737
|
-
typeLabel = 'Code';
|
|
738
|
-
}
|
|
739
|
-
else if (firstLine.match(/^[{[\]]/)) {
|
|
740
|
-
typeIcon = '📊';
|
|
741
|
-
typeLabel = 'JSON';
|
|
742
|
-
}
|
|
743
|
-
else if (firstLine.match(/^<[!?]?[a-zA-Z]/)) {
|
|
744
|
-
typeIcon = '📄';
|
|
745
|
-
typeLabel = 'HTML';
|
|
746
|
-
}
|
|
747
|
-
else if (firstLine.match(/^#|^\/\/|^\/\*|^\*|^--/)) {
|
|
748
|
-
typeIcon = '💬';
|
|
749
|
-
typeLabel = 'Text';
|
|
750
|
-
}
|
|
751
|
-
else if (firstLine.match(/^```|^~~~|^\s{4}/)) {
|
|
752
|
-
typeIcon = '📖';
|
|
753
|
-
typeLabel = 'Markdown';
|
|
754
|
-
}
|
|
755
|
-
// Create a compact preview
|
|
756
|
-
const preview = this.buildPastePreview(firstLine);
|
|
757
|
-
// Format size info compactly
|
|
758
|
-
const sizeInfo = this.formatCharCount(charCount);
|
|
759
|
-
const lineInfo = `${lineCount || 1}L`;
|
|
760
|
-
return `${typeIcon} [${typeLabel}: ${lineInfo}/${sizeInfo}c] ${preview}`;
|
|
761
|
-
}
|
|
762
907
|
/**
|
|
763
908
|
* Check if current input is a pasted block
|
|
764
909
|
*/
|
|
@@ -786,6 +931,36 @@ export class PinnedChatBox {
|
|
|
786
931
|
this.inputBuffer = '';
|
|
787
932
|
this.cursorPosition = 0;
|
|
788
933
|
this.state.currentInput = '';
|
|
934
|
+
this.resetReservedLines();
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Calculate and update reserved lines based on current input content.
|
|
938
|
+
* Ensures multi-line content is fully visible within maxDisplayLines limit.
|
|
939
|
+
*/
|
|
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));
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Reset reserved lines to base value (for single-line input)
|
|
955
|
+
*/
|
|
956
|
+
resetReservedLines() {
|
|
957
|
+
this.reservedLines = this.baseReservedLines;
|
|
958
|
+
// If we have a scroll region active, update it
|
|
959
|
+
if (this.scrollRegionActive) {
|
|
960
|
+
const rows = this.writeStream.rows || 24;
|
|
961
|
+
const scrollBottom = rows - this.reservedLines;
|
|
962
|
+
this.safeWrite(ANSI.SET_SCROLL_REGION(1, scrollBottom));
|
|
963
|
+
}
|
|
789
964
|
}
|
|
790
965
|
/**
|
|
791
966
|
* Handle backspace
|
|
@@ -796,12 +971,11 @@ export class PinnedChatBox {
|
|
|
796
971
|
this.cursorPosition = Math.min(this.cursorPosition, this.inputBuffer.length);
|
|
797
972
|
if (this.cursorPosition === 0)
|
|
798
973
|
return;
|
|
799
|
-
this.inputBuffer
|
|
800
|
-
this.inputBuffer.slice(
|
|
801
|
-
|
|
802
|
-
this.
|
|
803
|
-
this.
|
|
804
|
-
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);
|
|
805
979
|
}
|
|
806
980
|
/**
|
|
807
981
|
* Handle delete key
|
|
@@ -812,11 +986,10 @@ export class PinnedChatBox {
|
|
|
812
986
|
this.cursorPosition = Math.min(this.cursorPosition, this.inputBuffer.length);
|
|
813
987
|
if (this.cursorPosition >= this.inputBuffer.length)
|
|
814
988
|
return;
|
|
815
|
-
this.inputBuffer
|
|
816
|
-
this.inputBuffer.slice(
|
|
817
|
-
|
|
818
|
-
this.
|
|
819
|
-
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);
|
|
820
993
|
}
|
|
821
994
|
/**
|
|
822
995
|
* Handle cursor left
|
|
@@ -843,29 +1016,107 @@ export class PinnedChatBox {
|
|
|
843
1016
|
}
|
|
844
1017
|
}
|
|
845
1018
|
/**
|
|
846
|
-
* Handle home key
|
|
1019
|
+
* Handle home key - move to start of current line (not start of buffer)
|
|
847
1020
|
*/
|
|
848
1021
|
handleHome() {
|
|
849
1022
|
if (this.isDisposed)
|
|
850
1023
|
return;
|
|
851
|
-
|
|
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;
|
|
852
1032
|
this.scheduleRender();
|
|
853
1033
|
}
|
|
854
1034
|
/**
|
|
855
|
-
* Handle end key (Ctrl+E)
|
|
1035
|
+
* Handle end key (Ctrl+E) - move to end of current line (not end of buffer)
|
|
856
1036
|
*/
|
|
857
1037
|
handleEnd() {
|
|
858
1038
|
if (this.isDisposed)
|
|
859
1039
|
return;
|
|
860
|
-
|
|
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;
|
|
861
1050
|
this.scheduleRender();
|
|
862
1051
|
}
|
|
863
1052
|
/**
|
|
864
|
-
* 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
|
|
865
1098
|
*/
|
|
866
1099
|
handleHistoryUp() {
|
|
867
1100
|
if (this.isDisposed)
|
|
868
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
|
|
869
1120
|
if (this.inputHistory.length === 0)
|
|
870
1121
|
return;
|
|
871
1122
|
// If at current input, save it temporarily
|
|
@@ -882,11 +1133,29 @@ export class PinnedChatBox {
|
|
|
882
1133
|
}
|
|
883
1134
|
}
|
|
884
1135
|
/**
|
|
885
|
-
* Handle down arrow -
|
|
1136
|
+
* Handle down arrow - move cursor down in multi-line content, or navigate history if on last line
|
|
886
1137
|
*/
|
|
887
1138
|
handleHistoryDown() {
|
|
888
1139
|
if (this.isDisposed)
|
|
889
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
|
|
890
1159
|
if (this.historyIndex > 0) {
|
|
891
1160
|
// Move down in history
|
|
892
1161
|
this.historyIndex--;
|
|
@@ -1213,6 +1482,8 @@ export class PinnedChatBox {
|
|
|
1213
1482
|
* Handle terminal resize - update scroll region if active
|
|
1214
1483
|
*/
|
|
1215
1484
|
handleResize() {
|
|
1485
|
+
// Invalidate render state to force re-render with new dimensions
|
|
1486
|
+
this.invalidateRenderedState();
|
|
1216
1487
|
if (this.scrollRegionActive) {
|
|
1217
1488
|
// Reset and re-enable scroll region with new dimensions
|
|
1218
1489
|
this.safeWrite(ANSI.RESET_SCROLL_REGION);
|
|
@@ -1269,13 +1540,14 @@ export class PinnedChatBox {
|
|
|
1269
1540
|
return !this.isDisposed && this.isEnabled;
|
|
1270
1541
|
}
|
|
1271
1542
|
/**
|
|
1272
|
-
* Force immediate render (bypass throttling)
|
|
1543
|
+
* Force immediate render (bypass throttling and deduplication)
|
|
1273
1544
|
*/
|
|
1274
1545
|
forceRender() {
|
|
1275
1546
|
if (this.isDisposed)
|
|
1276
1547
|
return;
|
|
1277
1548
|
this.lastRenderTime = 0;
|
|
1278
1549
|
this.renderScheduled = false;
|
|
1550
|
+
this.invalidateRenderedState(); // Force re-render even if content unchanged
|
|
1279
1551
|
this.render();
|
|
1280
1552
|
}
|
|
1281
1553
|
/**
|
|
@@ -1294,6 +1566,7 @@ export class PinnedChatBox {
|
|
|
1294
1566
|
statusMessage: null,
|
|
1295
1567
|
isVisible: true,
|
|
1296
1568
|
};
|
|
1569
|
+
this.invalidateRenderedState();
|
|
1297
1570
|
this.scheduleRender();
|
|
1298
1571
|
}
|
|
1299
1572
|
}
|