erosolar-cli 1.7.184 → 1.7.187
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +27 -11
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/ui/persistentPrompt.d.ts +68 -12
- package/dist/ui/persistentPrompt.d.ts.map +1 -1
- package/dist/ui/persistentPrompt.js +448 -166
- 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;
|
|
@@ -311,10 +317,14 @@ export class PinnedChatBox {
|
|
|
311
317
|
enableScrollRegion() {
|
|
312
318
|
if (this.scrollRegionActive || !this.supportsRendering())
|
|
313
319
|
return;
|
|
320
|
+
// Make sure reserved height matches current content/terminal width
|
|
321
|
+
this.updateReservedLinesForContent();
|
|
314
322
|
const rows = this.writeStream.rows || 24;
|
|
315
323
|
const scrollBottom = rows - this.reservedLines;
|
|
316
324
|
// Step 1: Set scroll region (excludes bottom reserved lines)
|
|
317
325
|
this.safeWrite(ANSI.SET_SCROLL_REGION(1, scrollBottom));
|
|
326
|
+
// Invalidate render state to ensure fresh render in the new scroll region
|
|
327
|
+
this.invalidateRenderedState();
|
|
318
328
|
// Step 2: Render the prompt in the protected bottom area
|
|
319
329
|
this.renderPersistentInput();
|
|
320
330
|
// Mark scroll region as active AFTER rendering
|
|
@@ -344,14 +354,52 @@ export class PinnedChatBox {
|
|
|
344
354
|
// Move cursor to the end of the terminal
|
|
345
355
|
this.safeWrite(ANSI.CURSOR_TO_BOTTOM(rows));
|
|
346
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Check if render is needed by comparing with last rendered state.
|
|
359
|
+
* Returns true if content, cursor, or terminal size has changed.
|
|
360
|
+
*/
|
|
361
|
+
needsRender() {
|
|
362
|
+
const rows = this.writeStream.rows || 24;
|
|
363
|
+
const cols = this.writeStream.columns || 80;
|
|
364
|
+
// Check if anything changed that requires a re-render
|
|
365
|
+
if (this.inputBuffer !== this.lastRenderedContent)
|
|
366
|
+
return true;
|
|
367
|
+
if (this.cursorPosition !== this.lastRenderedCursor)
|
|
368
|
+
return true;
|
|
369
|
+
if (rows !== this.lastRenderedRows)
|
|
370
|
+
return true;
|
|
371
|
+
if (cols !== this.lastRenderedCols)
|
|
372
|
+
return true;
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Update last rendered state after successful render.
|
|
377
|
+
*/
|
|
378
|
+
updateRenderedState() {
|
|
379
|
+
this.lastRenderedContent = this.inputBuffer;
|
|
380
|
+
this.lastRenderedCursor = this.cursorPosition;
|
|
381
|
+
this.lastRenderedRows = this.writeStream.rows || 24;
|
|
382
|
+
this.lastRenderedCols = this.writeStream.columns || 80;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Reset rendered state to force next render.
|
|
386
|
+
*/
|
|
387
|
+
invalidateRenderedState() {
|
|
388
|
+
this.lastRenderedContent = '';
|
|
389
|
+
this.lastRenderedCursor = -1;
|
|
390
|
+
this.lastRenderedRows = 0;
|
|
391
|
+
this.lastRenderedCols = 0;
|
|
392
|
+
}
|
|
347
393
|
/**
|
|
348
394
|
* Render the persistent input area at the bottom of the terminal.
|
|
349
395
|
*
|
|
350
396
|
* CLEAN CLAUDE CODE STYLE:
|
|
351
397
|
* - Line 1: Simple separator with optional queue count
|
|
352
|
-
* - Line 2
|
|
398
|
+
* - Line 2+: Clean prompt "> " with input and visible block cursor
|
|
353
399
|
*
|
|
354
400
|
* Key for clean rendering:
|
|
401
|
+
* - Render deduplication via content/cursor comparison
|
|
402
|
+
* - Render lock to prevent concurrent renders
|
|
355
403
|
* - Hide cursor during render (prevents flicker)
|
|
356
404
|
* - Clear lines completely before writing
|
|
357
405
|
* - Reset styles explicitly
|
|
@@ -361,63 +409,105 @@ export class PinnedChatBox {
|
|
|
361
409
|
renderPersistentInput() {
|
|
362
410
|
if (!this.supportsRendering())
|
|
363
411
|
return;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
412
|
+
// Render lock - prevent concurrent renders that cause flicker
|
|
413
|
+
if (this.isRendering)
|
|
414
|
+
return;
|
|
415
|
+
// Deduplication - skip if nothing changed
|
|
416
|
+
if (!this.needsRender())
|
|
417
|
+
return;
|
|
418
|
+
this.isRendering = true;
|
|
419
|
+
try {
|
|
420
|
+
const { rows, cols, maxInputWidth } = this.getRenderDimensions();
|
|
421
|
+
// ANSI codes - keep simple, no theme functions
|
|
422
|
+
const HIDE_CURSOR = '\x1b[?25l';
|
|
423
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
424
|
+
const REVERSE_VIDEO = '\x1b[7m';
|
|
425
|
+
const RESET = '\x1b[0m';
|
|
426
|
+
const DIM = '\x1b[2m';
|
|
427
|
+
// Hide cursor during render
|
|
428
|
+
this.safeWrite(HIDE_CURSOR);
|
|
429
|
+
this.safeWrite(RESET);
|
|
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);
|
|
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);
|
|
436
|
+
// Find cursor position: which line and column
|
|
437
|
+
let displayStartLine = 0;
|
|
438
|
+
if (totalInputLines > displayLineCount) {
|
|
439
|
+
displayStartLine = Math.max(0, cursorLineIndex - displayLineCount + 1);
|
|
440
|
+
displayStartLine = Math.min(displayStartLine, totalInputLines - displayLineCount);
|
|
441
|
+
}
|
|
442
|
+
const linesToShow = wrappedLines.slice(displayStartLine, displayStartLine + displayLineCount);
|
|
443
|
+
// Move to the start of reserved area
|
|
444
|
+
const reservedStart = rows - this.reservedLines + 1;
|
|
445
|
+
this.safeWrite(ANSI.CURSOR_TO_BOTTOM(reservedStart));
|
|
446
|
+
this.safeWrite(ANSI.CLEAR_LINE);
|
|
447
|
+
// Line 1: Separator
|
|
448
|
+
const separatorWidth = Math.min(cols - 2, 60);
|
|
385
449
|
this.safeWrite(DIM);
|
|
386
|
-
this.safeWrite(
|
|
450
|
+
this.safeWrite('─'.repeat(separatorWidth));
|
|
387
451
|
this.safeWrite(RESET);
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
displayStart = cursorPos - maxInputWidth + 5;
|
|
452
|
+
// Status hints (line count, queue count)
|
|
453
|
+
const queueCount = this.state.queuedCommands.length;
|
|
454
|
+
const hints = [];
|
|
455
|
+
if (totalInputLines > 1) {
|
|
456
|
+
hints.push(`${totalInputLines} lines`);
|
|
457
|
+
}
|
|
458
|
+
if (this.state.isProcessing && queueCount > 0) {
|
|
459
|
+
hints.push(`${queueCount} queued`);
|
|
460
|
+
}
|
|
461
|
+
if (hints.length > 0) {
|
|
462
|
+
this.safeWrite(DIM);
|
|
463
|
+
this.safeWrite(' ' + hints.join(' • '));
|
|
464
|
+
this.safeWrite(RESET);
|
|
402
465
|
}
|
|
403
|
-
|
|
466
|
+
let finalCursorRow = reservedStart + 1;
|
|
467
|
+
let finalCursorCol = 3;
|
|
468
|
+
// Render each input line
|
|
469
|
+
for (let lineIdx = 0; lineIdx < linesToShow.length; lineIdx++) {
|
|
470
|
+
this.safeWrite('\n');
|
|
471
|
+
this.safeWrite(ANSI.CLEAR_LINE);
|
|
472
|
+
const line = linesToShow[lineIdx] ?? '';
|
|
473
|
+
const absoluteLineIdx = displayStartLine + lineIdx;
|
|
474
|
+
const isFirstLine = absoluteLineIdx === 0;
|
|
475
|
+
const isCursorLine = absoluteLineIdx === cursorLineIndex;
|
|
476
|
+
// Prompt prefix: ">" for first line, "│" for continuation lines
|
|
477
|
+
this.safeWrite(DIM);
|
|
478
|
+
this.safeWrite(isFirstLine ? '>' : '│');
|
|
479
|
+
this.safeWrite(RESET);
|
|
480
|
+
this.safeWrite(' ');
|
|
481
|
+
if (isCursorLine) {
|
|
482
|
+
// Render line with cursor
|
|
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);
|
|
487
|
+
this.safeWrite(beforeCursor);
|
|
488
|
+
this.safeWrite(REVERSE_VIDEO);
|
|
489
|
+
this.safeWrite(atCursor);
|
|
490
|
+
this.safeWrite(RESET);
|
|
491
|
+
this.safeWrite(afterCursor);
|
|
492
|
+
// Remember cursor position for final positioning
|
|
493
|
+
finalCursorRow = reservedStart + 1 + lineIdx;
|
|
494
|
+
finalCursorCol = 3 + displayCursorCol;
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
// Render line without cursor
|
|
498
|
+
this.safeWrite(line);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// Position terminal cursor and show it
|
|
502
|
+
this.safeWrite(ANSI.CURSOR_TO_BOTTOM(finalCursorRow));
|
|
503
|
+
this.safeWrite(ANSI.MOVE_TO_COL(finalCursorCol));
|
|
504
|
+
this.safeWrite(SHOW_CURSOR);
|
|
505
|
+
// Update rendered state for deduplication
|
|
506
|
+
this.updateRenderedState();
|
|
507
|
+
}
|
|
508
|
+
finally {
|
|
509
|
+
this.isRendering = false;
|
|
404
510
|
}
|
|
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
511
|
}
|
|
422
512
|
/**
|
|
423
513
|
* Update the persistent input during streaming.
|
|
@@ -476,10 +566,13 @@ export class PinnedChatBox {
|
|
|
476
566
|
return withoutAnsi.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
|
|
477
567
|
}
|
|
478
568
|
/**
|
|
479
|
-
* Sanitize queued command text (trimmed,
|
|
569
|
+
* Sanitize queued command text (trimmed, preserves newlines)
|
|
480
570
|
*/
|
|
481
571
|
sanitizeCommandText(text) {
|
|
482
|
-
|
|
572
|
+
if (!text)
|
|
573
|
+
return '';
|
|
574
|
+
const clean = this.sanitizeMultilineForDisplay(text);
|
|
575
|
+
return clean.slice(0, this.maxInputLength).trim();
|
|
483
576
|
}
|
|
484
577
|
/**
|
|
485
578
|
* Sanitize status message (single line, bounded length)
|
|
@@ -492,6 +585,56 @@ export class PinnedChatBox {
|
|
|
492
585
|
return null;
|
|
493
586
|
return clean.slice(0, this.maxStatusMessageLength);
|
|
494
587
|
}
|
|
588
|
+
/**
|
|
589
|
+
* Clear paste-specific state when the user edits the buffer manually.
|
|
590
|
+
* Prevents us from sending stale paste content after edits.
|
|
591
|
+
*/
|
|
592
|
+
startManualEdit() {
|
|
593
|
+
if (this.isPastedBlock) {
|
|
594
|
+
this.clearPastedBlock();
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Apply a new buffer/cursor position and trigger downstream updates.
|
|
599
|
+
*/
|
|
600
|
+
applyBufferChange(buffer, cursorPos) {
|
|
601
|
+
this.inputBuffer = buffer;
|
|
602
|
+
const nextCursor = typeof cursorPos === 'number' ? cursorPos : buffer.length;
|
|
603
|
+
this.cursorPosition = Math.max(0, Math.min(nextCursor, this.inputBuffer.length));
|
|
604
|
+
this.state.currentInput = this.inputBuffer;
|
|
605
|
+
this.updateReservedLinesForContent();
|
|
606
|
+
this.scheduleRender();
|
|
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
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Update cursor position while keeping it in bounds and re-rendering.
|
|
616
|
+
*/
|
|
617
|
+
setCursorPosition(position) {
|
|
618
|
+
this.cursorPosition = Math.max(0, Math.min(position, this.inputBuffer.length));
|
|
619
|
+
this.scheduleRender();
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Insert multi-line content directly into the buffer (manual typing).
|
|
623
|
+
*/
|
|
624
|
+
insertMultilineText(text) {
|
|
625
|
+
const sanitized = this.sanitizeMultilineForDisplay(text);
|
|
626
|
+
if (!sanitized)
|
|
627
|
+
return;
|
|
628
|
+
// Respect max input length
|
|
629
|
+
const availableSpace = this.maxInputLength - this.inputBuffer.length;
|
|
630
|
+
if (availableSpace <= 0)
|
|
631
|
+
return;
|
|
632
|
+
const chunk = sanitized.slice(0, availableSpace);
|
|
633
|
+
this.startManualEdit();
|
|
634
|
+
this.applyBufferChange(this.inputBuffer.slice(0, this.cursorPosition) +
|
|
635
|
+
chunk +
|
|
636
|
+
this.inputBuffer.slice(this.cursorPosition), this.cursorPosition + chunk.length);
|
|
637
|
+
}
|
|
495
638
|
/**
|
|
496
639
|
* Safely write to the output stream, swallowing non-fatal errors
|
|
497
640
|
*/
|
|
@@ -590,7 +733,8 @@ export class PinnedChatBox {
|
|
|
590
733
|
}
|
|
591
734
|
// Sanitize and truncate command text
|
|
592
735
|
const truncated = sanitizedText.slice(0, this.maxInputLength);
|
|
593
|
-
const
|
|
736
|
+
const previewSource = this.sanitizeInlineText(truncated);
|
|
737
|
+
const preview = previewSource.length > 60 ? `${previewSource.slice(0, 57)}...` : previewSource;
|
|
594
738
|
const cmd = {
|
|
595
739
|
id: `cmd-${++this.commandIdCounter}`,
|
|
596
740
|
text: truncated,
|
|
@@ -646,14 +790,25 @@ export class PinnedChatBox {
|
|
|
646
790
|
* Handle character input with validation
|
|
647
791
|
* Detects multiline paste and stores full content while showing summary
|
|
648
792
|
*/
|
|
649
|
-
handleInput(char) {
|
|
793
|
+
handleInput(char, options = {}) {
|
|
650
794
|
if (!this.isEnabled || this.isDisposed)
|
|
651
795
|
return;
|
|
652
796
|
if (typeof char !== 'string')
|
|
653
797
|
return;
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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) {
|
|
657
812
|
return;
|
|
658
813
|
}
|
|
659
814
|
const sanitized = this.sanitizeInlineText(char);
|
|
@@ -667,13 +822,10 @@ export class PinnedChatBox {
|
|
|
667
822
|
if (!chunk)
|
|
668
823
|
return;
|
|
669
824
|
// Insert character at cursor position
|
|
670
|
-
this.
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
this.cursorPosition = Math.min(this.cursorPosition + chunk.length, this.inputBuffer.length);
|
|
675
|
-
this.state.currentInput = this.inputBuffer;
|
|
676
|
-
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);
|
|
677
829
|
}
|
|
678
830
|
/**
|
|
679
831
|
* Handle multiline paste - store full content and display all lines
|
|
@@ -681,15 +833,13 @@ export class PinnedChatBox {
|
|
|
681
833
|
handleMultilinePaste(content) {
|
|
682
834
|
// Keep the original content for submission
|
|
683
835
|
const displaySafeContent = this.sanitizeMultilineForDisplay(content);
|
|
684
|
-
|
|
836
|
+
if (!displaySafeContent)
|
|
837
|
+
return;
|
|
838
|
+
const truncatedDisplay = displaySafeContent.slice(0, this.maxInputLength);
|
|
839
|
+
const truncatedOriginal = content.slice(0, this.maxInputLength);
|
|
840
|
+
this.pastedFullContent = truncatedOriginal;
|
|
685
841
|
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();
|
|
842
|
+
this.applyBufferChange(truncatedDisplay, truncatedDisplay.length);
|
|
693
843
|
}
|
|
694
844
|
/**
|
|
695
845
|
* Public helper for routing external multi-line pastes directly into the chat box
|
|
@@ -723,37 +873,88 @@ export class PinnedChatBox {
|
|
|
723
873
|
*/
|
|
724
874
|
clearPastedBlockState() {
|
|
725
875
|
this.clearPastedBlock();
|
|
726
|
-
this.
|
|
727
|
-
this.cursorPosition = 0;
|
|
728
|
-
this.state.currentInput = '';
|
|
729
|
-
this.resetReservedLines();
|
|
876
|
+
this.applyBufferChange('', 0);
|
|
730
877
|
}
|
|
731
878
|
/**
|
|
732
|
-
*
|
|
733
|
-
* Ensures multi-line content is fully visible within maxDisplayLines limit.
|
|
879
|
+
* Get terminal dimensions and a safe width for rendering input content.
|
|
734
880
|
*/
|
|
735
|
-
|
|
736
|
-
const
|
|
737
|
-
const
|
|
738
|
-
//
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
881
|
+
getRenderDimensions() {
|
|
882
|
+
const rows = this.writeStream.rows || 24;
|
|
883
|
+
const cols = Math.max(this.writeStream.columns || 80, 40);
|
|
884
|
+
// Leave a small gutter for prefix + padding so we never wrap unexpectedly
|
|
885
|
+
const maxInputWidth = Math.max(10, cols - 5);
|
|
886
|
+
return { rows, cols, maxInputWidth };
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Wrap the current input buffer to the provided width and locate the cursor.
|
|
890
|
+
*/
|
|
891
|
+
wrapInputBuffer(maxInputWidth) {
|
|
892
|
+
const width = Math.max(1, maxInputWidth);
|
|
893
|
+
const rawLines = this.inputBuffer.split('\n');
|
|
894
|
+
const wrappedLines = [];
|
|
895
|
+
const targetCursor = Math.max(0, Math.min(this.cursorPosition, this.inputBuffer.length));
|
|
896
|
+
let cursorLine = 0;
|
|
897
|
+
let cursorCol = 0;
|
|
898
|
+
let absoluteIndex = 0;
|
|
899
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
900
|
+
const raw = rawLines[i] ?? '';
|
|
901
|
+
if (raw.length === 0) {
|
|
902
|
+
// Preserve empty lines so vertical space stays accurate
|
|
903
|
+
wrappedLines.push('');
|
|
904
|
+
if (targetCursor === absoluteIndex) {
|
|
905
|
+
cursorLine = wrappedLines.length - 1;
|
|
906
|
+
cursorCol = 0;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
for (let start = 0; start < raw.length; start += width) {
|
|
911
|
+
const segment = raw.slice(start, start + width);
|
|
912
|
+
const segmentStart = absoluteIndex + start;
|
|
913
|
+
wrappedLines.push(segment);
|
|
914
|
+
if (targetCursor >= segmentStart && targetCursor <= segmentStart + segment.length) {
|
|
915
|
+
cursorLine = wrappedLines.length - 1;
|
|
916
|
+
cursorCol = targetCursor - segmentStart;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
absoluteIndex += raw.length;
|
|
921
|
+
if (i < rawLines.length - 1) {
|
|
922
|
+
// Account for the newline character between raw lines
|
|
923
|
+
if (targetCursor === absoluteIndex) {
|
|
924
|
+
cursorLine = wrappedLines.length;
|
|
925
|
+
cursorCol = 0;
|
|
926
|
+
}
|
|
927
|
+
absoluteIndex += 1;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (wrappedLines.length === 0) {
|
|
931
|
+
wrappedLines.push('');
|
|
932
|
+
cursorLine = 0;
|
|
933
|
+
cursorCol = 0;
|
|
746
934
|
}
|
|
935
|
+
cursorLine = Math.min(cursorLine, wrappedLines.length - 1);
|
|
936
|
+
cursorCol = Math.min(cursorCol, wrappedLines[cursorLine]?.length ?? 0);
|
|
937
|
+
return { lines: wrappedLines, cursorLine, cursorCol };
|
|
747
938
|
}
|
|
748
939
|
/**
|
|
749
|
-
*
|
|
940
|
+
* Calculate and update reserved lines based on current input content.
|
|
941
|
+
* Ensures multi-line content is fully visible within maxDisplayLines limit.
|
|
750
942
|
*/
|
|
751
|
-
|
|
752
|
-
|
|
943
|
+
updateReservedLinesForContent(maxInputWidth, wrappedLineCount) {
|
|
944
|
+
const { rows, maxInputWidth: derivedWidth } = this.getRenderDimensions();
|
|
945
|
+
const width = Math.max(1, maxInputWidth ?? derivedWidth);
|
|
946
|
+
const totalLines = wrappedLineCount ?? this.wrapInputBuffer(width).lines.length;
|
|
947
|
+
const lineCount = Math.max(1, totalLines);
|
|
948
|
+
// Calculate needed lines: 1 for separator + lineCount for input (clamped to maxDisplayLines)
|
|
949
|
+
const inputLines = Math.min(lineCount, this.maxDisplayLines);
|
|
950
|
+
const calculated = 1 + inputLines; // 1 separator + input lines
|
|
951
|
+
// Ensure we keep at least one row for output scrolling
|
|
952
|
+
const maxAllowed = Math.max(1, rows - 1);
|
|
953
|
+
const desired = Math.max(this.baseReservedLines, calculated);
|
|
954
|
+
this.reservedLines = Math.min(desired, maxAllowed);
|
|
753
955
|
// If we have a scroll region active, update it
|
|
754
956
|
if (this.scrollRegionActive) {
|
|
755
|
-
const
|
|
756
|
-
const scrollBottom = rows - this.reservedLines;
|
|
957
|
+
const scrollBottom = Math.max(1, rows - this.reservedLines);
|
|
757
958
|
this.safeWrite(ANSI.SET_SCROLL_REGION(1, scrollBottom));
|
|
758
959
|
}
|
|
759
960
|
}
|
|
@@ -766,12 +967,11 @@ export class PinnedChatBox {
|
|
|
766
967
|
this.cursorPosition = Math.min(this.cursorPosition, this.inputBuffer.length);
|
|
767
968
|
if (this.cursorPosition === 0)
|
|
768
969
|
return;
|
|
769
|
-
this.inputBuffer
|
|
770
|
-
this.inputBuffer.slice(
|
|
771
|
-
|
|
772
|
-
this.
|
|
773
|
-
this.
|
|
774
|
-
this.scheduleRender();
|
|
970
|
+
const newBuffer = this.inputBuffer.slice(0, this.cursorPosition - 1) +
|
|
971
|
+
this.inputBuffer.slice(this.cursorPosition);
|
|
972
|
+
const newCursor = Math.max(0, this.cursorPosition - 1);
|
|
973
|
+
this.startManualEdit();
|
|
974
|
+
this.applyBufferChange(newBuffer, newCursor);
|
|
775
975
|
}
|
|
776
976
|
/**
|
|
777
977
|
* Handle delete key
|
|
@@ -782,11 +982,10 @@ export class PinnedChatBox {
|
|
|
782
982
|
this.cursorPosition = Math.min(this.cursorPosition, this.inputBuffer.length);
|
|
783
983
|
if (this.cursorPosition >= this.inputBuffer.length)
|
|
784
984
|
return;
|
|
785
|
-
this.inputBuffer
|
|
786
|
-
this.inputBuffer.slice(
|
|
787
|
-
|
|
788
|
-
this.
|
|
789
|
-
this.scheduleRender();
|
|
985
|
+
const newBuffer = this.inputBuffer.slice(0, this.cursorPosition) +
|
|
986
|
+
this.inputBuffer.slice(this.cursorPosition + 1);
|
|
987
|
+
this.startManualEdit();
|
|
988
|
+
this.applyBufferChange(newBuffer, this.cursorPosition);
|
|
790
989
|
}
|
|
791
990
|
/**
|
|
792
991
|
* Handle cursor left
|
|
@@ -794,10 +993,9 @@ export class PinnedChatBox {
|
|
|
794
993
|
handleCursorLeft() {
|
|
795
994
|
if (this.isDisposed)
|
|
796
995
|
return;
|
|
797
|
-
|
|
798
|
-
if (
|
|
799
|
-
this.
|
|
800
|
-
this.scheduleRender();
|
|
996
|
+
const bounded = Math.min(this.cursorPosition, this.inputBuffer.length);
|
|
997
|
+
if (bounded > 0) {
|
|
998
|
+
this.setCursorPosition(bounded - 1);
|
|
801
999
|
}
|
|
802
1000
|
}
|
|
803
1001
|
/**
|
|
@@ -806,36 +1004,103 @@ export class PinnedChatBox {
|
|
|
806
1004
|
handleCursorRight() {
|
|
807
1005
|
if (this.isDisposed)
|
|
808
1006
|
return;
|
|
809
|
-
|
|
810
|
-
if (
|
|
811
|
-
this.
|
|
812
|
-
this.scheduleRender();
|
|
1007
|
+
const bounded = Math.min(this.cursorPosition, this.inputBuffer.length);
|
|
1008
|
+
if (bounded < this.inputBuffer.length) {
|
|
1009
|
+
this.setCursorPosition(bounded + 1);
|
|
813
1010
|
}
|
|
814
1011
|
}
|
|
815
1012
|
/**
|
|
816
|
-
* Handle home key
|
|
1013
|
+
* Handle home key - move to start of current line (not start of buffer)
|
|
817
1014
|
*/
|
|
818
1015
|
handleHome() {
|
|
819
1016
|
if (this.isDisposed)
|
|
820
1017
|
return;
|
|
821
|
-
|
|
822
|
-
this.
|
|
1018
|
+
// In multi-line mode, move to start of current line
|
|
1019
|
+
const { lineIndex, lines } = this.getCursorLinePosition();
|
|
1020
|
+
// Calculate position at start of current line
|
|
1021
|
+
let newPos = 0;
|
|
1022
|
+
for (let i = 0; i < lineIndex; i++) {
|
|
1023
|
+
newPos += (lines[i] ?? '').length + 1;
|
|
1024
|
+
}
|
|
1025
|
+
this.setCursorPosition(newPos);
|
|
823
1026
|
}
|
|
824
1027
|
/**
|
|
825
|
-
* Handle end key (Ctrl+E)
|
|
1028
|
+
* Handle end key (Ctrl+E) - move to end of current line (not end of buffer)
|
|
826
1029
|
*/
|
|
827
1030
|
handleEnd() {
|
|
828
1031
|
if (this.isDisposed)
|
|
829
1032
|
return;
|
|
830
|
-
|
|
831
|
-
this.
|
|
1033
|
+
// In multi-line mode, move to end of current line
|
|
1034
|
+
const { lineIndex, lines } = this.getCursorLinePosition();
|
|
1035
|
+
const currentLine = lines[lineIndex] ?? '';
|
|
1036
|
+
// Calculate position at end of current line
|
|
1037
|
+
let newPos = 0;
|
|
1038
|
+
for (let i = 0; i < lineIndex; i++) {
|
|
1039
|
+
newPos += (lines[i] ?? '').length + 1;
|
|
1040
|
+
}
|
|
1041
|
+
newPos += currentLine.length;
|
|
1042
|
+
this.setCursorPosition(newPos);
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Handle inserting a newline character at cursor position (Shift+Enter or Option+Enter)
|
|
1046
|
+
*/
|
|
1047
|
+
handleNewline() {
|
|
1048
|
+
if (!this.isEnabled || this.isDisposed)
|
|
1049
|
+
return;
|
|
1050
|
+
// Respect max input length
|
|
1051
|
+
if (this.inputBuffer.length >= this.maxInputLength)
|
|
1052
|
+
return;
|
|
1053
|
+
this.startManualEdit();
|
|
1054
|
+
this.applyBufferChange(this.inputBuffer.slice(0, this.cursorPosition) +
|
|
1055
|
+
'\n' +
|
|
1056
|
+
this.inputBuffer.slice(this.cursorPosition), this.cursorPosition + 1);
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Get cursor line and column position within multi-line content.
|
|
1060
|
+
*/
|
|
1061
|
+
getCursorLinePosition() {
|
|
1062
|
+
const lines = this.inputBuffer.split('\n');
|
|
1063
|
+
let charCount = 0;
|
|
1064
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1065
|
+
const lineLen = (lines[i] ?? '').length;
|
|
1066
|
+
if (charCount + lineLen >= this.cursorPosition) {
|
|
1067
|
+
return {
|
|
1068
|
+
lineIndex: i,
|
|
1069
|
+
colInLine: this.cursorPosition - charCount,
|
|
1070
|
+
lines,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
charCount += lineLen + 1; // +1 for newline
|
|
1074
|
+
}
|
|
1075
|
+
// Cursor at end
|
|
1076
|
+
return {
|
|
1077
|
+
lineIndex: lines.length - 1,
|
|
1078
|
+
colInLine: (lines[lines.length - 1] ?? '').length,
|
|
1079
|
+
lines,
|
|
1080
|
+
};
|
|
832
1081
|
}
|
|
833
1082
|
/**
|
|
834
|
-
* Handle up arrow -
|
|
1083
|
+
* Handle up arrow - move cursor up in multi-line content, or navigate history if on first line
|
|
835
1084
|
*/
|
|
836
1085
|
handleHistoryUp() {
|
|
837
1086
|
if (this.isDisposed)
|
|
838
1087
|
return;
|
|
1088
|
+
// For multi-line content, move cursor up within content first
|
|
1089
|
+
const { lineIndex, colInLine, lines } = this.getCursorLinePosition();
|
|
1090
|
+
if (lineIndex > 0) {
|
|
1091
|
+
// Move cursor to previous line, keeping column position if possible
|
|
1092
|
+
const prevLine = lines[lineIndex - 1] ?? '';
|
|
1093
|
+
const newCol = Math.min(colInLine, prevLine.length);
|
|
1094
|
+
// Calculate new cursor position
|
|
1095
|
+
let newPos = 0;
|
|
1096
|
+
for (let i = 0; i < lineIndex - 1; i++) {
|
|
1097
|
+
newPos += (lines[i] ?? '').length + 1;
|
|
1098
|
+
}
|
|
1099
|
+
newPos += newCol;
|
|
1100
|
+
this.setCursorPosition(newPos);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
// On first line - navigate history
|
|
839
1104
|
if (this.inputHistory.length === 0)
|
|
840
1105
|
return;
|
|
841
1106
|
// If at current input, save it temporarily
|
|
@@ -845,33 +1110,46 @@ export class PinnedChatBox {
|
|
|
845
1110
|
// Move up in history
|
|
846
1111
|
if (this.historyIndex < this.inputHistory.length - 1) {
|
|
847
1112
|
this.historyIndex++;
|
|
848
|
-
|
|
849
|
-
this.
|
|
850
|
-
this.
|
|
851
|
-
this.scheduleRender();
|
|
1113
|
+
const nextBuffer = this.inputHistory[this.inputHistory.length - 1 - this.historyIndex] || '';
|
|
1114
|
+
this.startManualEdit();
|
|
1115
|
+
this.applyBufferChange(nextBuffer, nextBuffer.length);
|
|
852
1116
|
}
|
|
853
1117
|
}
|
|
854
1118
|
/**
|
|
855
|
-
* Handle down arrow -
|
|
1119
|
+
* Handle down arrow - move cursor down in multi-line content, or navigate history if on last line
|
|
856
1120
|
*/
|
|
857
1121
|
handleHistoryDown() {
|
|
858
1122
|
if (this.isDisposed)
|
|
859
1123
|
return;
|
|
1124
|
+
// For multi-line content, move cursor down within content first
|
|
1125
|
+
const { lineIndex, colInLine, lines } = this.getCursorLinePosition();
|
|
1126
|
+
if (lineIndex < lines.length - 1) {
|
|
1127
|
+
// Move cursor to next line, keeping column position if possible
|
|
1128
|
+
const nextLine = lines[lineIndex + 1] ?? '';
|
|
1129
|
+
const newCol = Math.min(colInLine, nextLine.length);
|
|
1130
|
+
// Calculate new cursor position
|
|
1131
|
+
let newPos = 0;
|
|
1132
|
+
for (let i = 0; i <= lineIndex; i++) {
|
|
1133
|
+
newPos += (lines[i] ?? '').length + 1;
|
|
1134
|
+
}
|
|
1135
|
+
newPos += newCol;
|
|
1136
|
+
this.setCursorPosition(newPos);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
// On last line - navigate history
|
|
860
1140
|
if (this.historyIndex > 0) {
|
|
861
1141
|
// Move down in history
|
|
862
1142
|
this.historyIndex--;
|
|
863
|
-
|
|
864
|
-
this.
|
|
865
|
-
this.
|
|
866
|
-
this.scheduleRender();
|
|
1143
|
+
const nextBuffer = this.inputHistory[this.inputHistory.length - 1 - this.historyIndex] || '';
|
|
1144
|
+
this.startManualEdit();
|
|
1145
|
+
this.applyBufferChange(nextBuffer, nextBuffer.length);
|
|
867
1146
|
}
|
|
868
1147
|
else if (this.historyIndex === 0) {
|
|
869
1148
|
// Return to current input
|
|
870
1149
|
this.historyIndex = -1;
|
|
871
|
-
|
|
872
|
-
this.
|
|
873
|
-
this.
|
|
874
|
-
this.scheduleRender();
|
|
1150
|
+
const restored = this.tempCurrentInput;
|
|
1151
|
+
this.startManualEdit();
|
|
1152
|
+
this.applyBufferChange(restored, restored.length);
|
|
875
1153
|
}
|
|
876
1154
|
}
|
|
877
1155
|
/**
|
|
@@ -882,10 +1160,9 @@ export class PinnedChatBox {
|
|
|
882
1160
|
return;
|
|
883
1161
|
if (this.cursorPosition === 0)
|
|
884
1162
|
return;
|
|
885
|
-
this.
|
|
886
|
-
this.cursorPosition
|
|
887
|
-
this.
|
|
888
|
-
this.scheduleRender();
|
|
1163
|
+
this.startManualEdit();
|
|
1164
|
+
const newBuffer = this.inputBuffer.slice(this.cursorPosition);
|
|
1165
|
+
this.applyBufferChange(newBuffer, 0);
|
|
889
1166
|
}
|
|
890
1167
|
/**
|
|
891
1168
|
* Handle Ctrl+K - delete from cursor to end of line
|
|
@@ -895,9 +1172,9 @@ export class PinnedChatBox {
|
|
|
895
1172
|
return;
|
|
896
1173
|
if (this.cursorPosition >= this.inputBuffer.length)
|
|
897
1174
|
return;
|
|
898
|
-
this.
|
|
899
|
-
this.
|
|
900
|
-
this.
|
|
1175
|
+
this.startManualEdit();
|
|
1176
|
+
const newBuffer = this.inputBuffer.slice(0, this.cursorPosition);
|
|
1177
|
+
this.applyBufferChange(newBuffer, this.cursorPosition);
|
|
901
1178
|
}
|
|
902
1179
|
/**
|
|
903
1180
|
* Handle Ctrl+W - delete word before cursor
|
|
@@ -910,17 +1187,16 @@ export class PinnedChatBox {
|
|
|
910
1187
|
// Find the start of the word (skip trailing spaces, then find word boundary)
|
|
911
1188
|
let pos = this.cursorPosition;
|
|
912
1189
|
// Skip any spaces before cursor
|
|
913
|
-
while (pos > 0 && this.inputBuffer[pos - 1]
|
|
1190
|
+
while (pos > 0 && this.isWhitespace(this.inputBuffer[pos - 1])) {
|
|
914
1191
|
pos--;
|
|
915
1192
|
}
|
|
916
1193
|
// Find start of word
|
|
917
|
-
while (pos > 0 && this.inputBuffer[pos - 1]
|
|
1194
|
+
while (pos > 0 && !this.isWhitespace(this.inputBuffer[pos - 1])) {
|
|
918
1195
|
pos--;
|
|
919
1196
|
}
|
|
920
|
-
|
|
921
|
-
this.
|
|
922
|
-
this.
|
|
923
|
-
this.scheduleRender();
|
|
1197
|
+
const newBuffer = this.inputBuffer.slice(0, pos) + this.inputBuffer.slice(this.cursorPosition);
|
|
1198
|
+
this.startManualEdit();
|
|
1199
|
+
this.applyBufferChange(newBuffer, pos);
|
|
924
1200
|
}
|
|
925
1201
|
/**
|
|
926
1202
|
* Handle Alt+Left - move cursor to previous word
|
|
@@ -932,15 +1208,14 @@ export class PinnedChatBox {
|
|
|
932
1208
|
return;
|
|
933
1209
|
let pos = this.cursorPosition;
|
|
934
1210
|
// Skip any spaces before cursor
|
|
935
|
-
while (pos > 0 && this.inputBuffer[pos - 1]
|
|
1211
|
+
while (pos > 0 && this.isWhitespace(this.inputBuffer[pos - 1])) {
|
|
936
1212
|
pos--;
|
|
937
1213
|
}
|
|
938
1214
|
// Find start of word
|
|
939
|
-
while (pos > 0 && this.inputBuffer[pos - 1]
|
|
1215
|
+
while (pos > 0 && !this.isWhitespace(this.inputBuffer[pos - 1])) {
|
|
940
1216
|
pos--;
|
|
941
1217
|
}
|
|
942
|
-
this.
|
|
943
|
-
this.scheduleRender();
|
|
1218
|
+
this.setCursorPosition(pos);
|
|
944
1219
|
}
|
|
945
1220
|
/**
|
|
946
1221
|
* Handle Alt+Right - move cursor to next word
|
|
@@ -952,15 +1227,14 @@ export class PinnedChatBox {
|
|
|
952
1227
|
return;
|
|
953
1228
|
let pos = this.cursorPosition;
|
|
954
1229
|
// Skip current word
|
|
955
|
-
while (pos < this.inputBuffer.length && this.inputBuffer[pos]
|
|
1230
|
+
while (pos < this.inputBuffer.length && !this.isWhitespace(this.inputBuffer[pos])) {
|
|
956
1231
|
pos++;
|
|
957
1232
|
}
|
|
958
1233
|
// Skip any spaces
|
|
959
|
-
while (pos < this.inputBuffer.length && this.inputBuffer[pos]
|
|
1234
|
+
while (pos < this.inputBuffer.length && this.isWhitespace(this.inputBuffer[pos])) {
|
|
960
1235
|
pos++;
|
|
961
1236
|
}
|
|
962
|
-
this.
|
|
963
|
-
this.scheduleRender();
|
|
1237
|
+
this.setCursorPosition(pos);
|
|
964
1238
|
}
|
|
965
1239
|
/**
|
|
966
1240
|
* Add input to history (call after successful submit)
|
|
@@ -1027,15 +1301,12 @@ export class PinnedChatBox {
|
|
|
1027
1301
|
clearInput() {
|
|
1028
1302
|
if (this.isDisposed)
|
|
1029
1303
|
return;
|
|
1030
|
-
this.inputBuffer = '';
|
|
1031
|
-
this.cursorPosition = 0;
|
|
1032
|
-
this.state.currentInput = '';
|
|
1033
1304
|
// Reset history navigation
|
|
1034
1305
|
this.historyIndex = -1;
|
|
1035
1306
|
this.tempCurrentInput = '';
|
|
1036
1307
|
// Clear paste state
|
|
1037
1308
|
this.clearPastedBlock();
|
|
1038
|
-
this.
|
|
1309
|
+
this.applyBufferChange('', 0);
|
|
1039
1310
|
}
|
|
1040
1311
|
/**
|
|
1041
1312
|
* Set input text and optionally cursor position (for readline sync)
|
|
@@ -1044,13 +1315,14 @@ export class PinnedChatBox {
|
|
|
1044
1315
|
setInput(text, cursorPos) {
|
|
1045
1316
|
if (this.isDisposed)
|
|
1046
1317
|
return;
|
|
1047
|
-
const normalized = this.
|
|
1318
|
+
const normalized = this.sanitizeMultilineForDisplay(text).slice(0, this.maxInputLength);
|
|
1048
1319
|
this.inputBuffer = normalized;
|
|
1049
1320
|
// Use provided cursor position, or default to end of input
|
|
1050
1321
|
this.cursorPosition = typeof cursorPos === 'number'
|
|
1051
1322
|
? Math.max(0, Math.min(cursorPos, normalized.length))
|
|
1052
1323
|
: normalized.length;
|
|
1053
1324
|
this.state.currentInput = normalized;
|
|
1325
|
+
this.updateReservedLinesForContent();
|
|
1054
1326
|
// Do NOT schedule render here - readline handles display during input
|
|
1055
1327
|
// render() will only work when isProcessing=true anyway
|
|
1056
1328
|
}
|
|
@@ -1183,10 +1455,18 @@ export class PinnedChatBox {
|
|
|
1183
1455
|
* Handle terminal resize - update scroll region if active
|
|
1184
1456
|
*/
|
|
1185
1457
|
handleResize() {
|
|
1186
|
-
|
|
1187
|
-
|
|
1458
|
+
// Invalidate render state to force re-render with new dimensions
|
|
1459
|
+
this.invalidateRenderedState();
|
|
1460
|
+
const wasActive = this.scrollRegionActive;
|
|
1461
|
+
if (wasActive) {
|
|
1462
|
+
// Reset scroll region before recalculating dimensions
|
|
1188
1463
|
this.safeWrite(ANSI.RESET_SCROLL_REGION);
|
|
1189
1464
|
this.scrollRegionActive = false;
|
|
1465
|
+
}
|
|
1466
|
+
// Recompute reserved lines for the new terminal size
|
|
1467
|
+
this.updateReservedLinesForContent();
|
|
1468
|
+
if (wasActive) {
|
|
1469
|
+
// Re-enable scroll region with the updated height
|
|
1190
1470
|
this.enableScrollRegion();
|
|
1191
1471
|
}
|
|
1192
1472
|
this.scheduleRender();
|
|
@@ -1239,13 +1519,14 @@ export class PinnedChatBox {
|
|
|
1239
1519
|
return !this.isDisposed && this.isEnabled;
|
|
1240
1520
|
}
|
|
1241
1521
|
/**
|
|
1242
|
-
* Force immediate render (bypass throttling)
|
|
1522
|
+
* Force immediate render (bypass throttling and deduplication)
|
|
1243
1523
|
*/
|
|
1244
1524
|
forceRender() {
|
|
1245
1525
|
if (this.isDisposed)
|
|
1246
1526
|
return;
|
|
1247
1527
|
this.lastRenderTime = 0;
|
|
1248
1528
|
this.renderScheduled = false;
|
|
1529
|
+
this.invalidateRenderedState(); // Force re-render even if content unchanged
|
|
1249
1530
|
this.render();
|
|
1250
1531
|
}
|
|
1251
1532
|
/**
|
|
@@ -1264,6 +1545,7 @@ export class PinnedChatBox {
|
|
|
1264
1545
|
statusMessage: null,
|
|
1265
1546
|
isVisible: true,
|
|
1266
1547
|
};
|
|
1548
|
+
this.invalidateRenderedState();
|
|
1267
1549
|
this.scheduleRender();
|
|
1268
1550
|
}
|
|
1269
1551
|
}
|