erosolar-cli 1.7.285 → 1.7.289
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/terminalInput.d.ts +16 -75
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +82 -520
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/ui/display.d.ts +2 -2
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +4 -32
- package/dist/ui/display.js.map +1 -1
- package/package.json +1 -1
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
import { EventEmitter } from 'node:events';
|
|
12
12
|
import { isMultilinePaste, generatePasteSummary } from '../core/multilinePasteHandler.js';
|
|
13
13
|
import { writeLock } from '../ui/writeLock.js';
|
|
14
|
-
import { renderDivider } from '../ui/unified/layout.js';
|
|
15
14
|
import { UI_COLORS, DEFAULT_UI_CONFIG, } from '../ui/terminalUISchema.js';
|
|
16
15
|
// ANSI escape codes
|
|
17
16
|
const ESC = {
|
|
@@ -71,7 +70,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
71
70
|
overrideStatusMessage = null; // Secondary status (warnings, etc.)
|
|
72
71
|
streamingLabel = null; // Streaming progress indicator
|
|
73
72
|
reservedLines = 2;
|
|
74
|
-
scrollRegionActive = false;
|
|
75
73
|
lastRenderContent = '';
|
|
76
74
|
lastRenderCursor = -1;
|
|
77
75
|
renderDirty = false;
|
|
@@ -288,12 +286,57 @@ export class TerminalInput extends EventEmitter {
|
|
|
288
286
|
// Position cursor in input area
|
|
289
287
|
this.write(ESC.TO(startRow + (this.modelInfo ? 3 : 2), this.config.promptChar.length + 1));
|
|
290
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* Render input area at current cursor position (inline, not pinned).
|
|
291
|
+
* Used after streaming ends - renders input below the streamed content.
|
|
292
|
+
*/
|
|
293
|
+
renderInlineInputAreaAtCursor() {
|
|
294
|
+
const { cols } = this.getSize();
|
|
295
|
+
const divider = '─'.repeat(cols - 1);
|
|
296
|
+
const { dim: DIM, reset: R } = UI_COLORS;
|
|
297
|
+
// Status bar - shows "Type a message" hint
|
|
298
|
+
process.stdout.write(this.buildStatusBar(cols) + '\n');
|
|
299
|
+
// Model info line (if set)
|
|
300
|
+
if (this.modelInfo) {
|
|
301
|
+
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
302
|
+
if (this.contextUsage !== null) {
|
|
303
|
+
const rem = Math.max(0, 100 - this.contextUsage);
|
|
304
|
+
if (rem < 10)
|
|
305
|
+
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
306
|
+
else if (rem < 25)
|
|
307
|
+
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
308
|
+
else
|
|
309
|
+
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
310
|
+
}
|
|
311
|
+
process.stdout.write(modelLine + '\n');
|
|
312
|
+
}
|
|
313
|
+
// Top divider
|
|
314
|
+
process.stdout.write(divider + '\n');
|
|
315
|
+
// Input line with prompt and any buffer content
|
|
316
|
+
const { lines, cursorCol } = this.wrapBuffer(cols - 4);
|
|
317
|
+
const displayLine = lines[0] ?? '';
|
|
318
|
+
process.stdout.write(ESC.BG_DARK + ESC.DIM + this.config.promptChar + ESC.RESET);
|
|
319
|
+
process.stdout.write(ESC.BG_DARK + displayLine);
|
|
320
|
+
const padding = Math.max(0, cols - this.config.promptChar.length - displayLine.length - 1);
|
|
321
|
+
if (padding > 0)
|
|
322
|
+
process.stdout.write(' '.repeat(padding));
|
|
323
|
+
process.stdout.write(ESC.RESET + '\n');
|
|
324
|
+
// Bottom divider
|
|
325
|
+
process.stdout.write(divider + '\n');
|
|
326
|
+
// Mode controls
|
|
327
|
+
process.stdout.write(this.buildModeControls(cols) + '\n');
|
|
328
|
+
// Show cursor
|
|
329
|
+
this.write(ESC.SHOW);
|
|
330
|
+
// Update tracking
|
|
331
|
+
this.lastRenderContent = this.buffer;
|
|
332
|
+
this.lastRenderCursor = this.cursor;
|
|
333
|
+
}
|
|
291
334
|
/**
|
|
292
335
|
* Set the input mode
|
|
293
336
|
*
|
|
294
|
-
* Streaming mode
|
|
295
|
-
*
|
|
296
|
-
*
|
|
337
|
+
* Streaming mode: NO scroll region, NO bottom-pinned input.
|
|
338
|
+
* Content flows naturally after the initial launch layout.
|
|
339
|
+
* After streaming ends, input area renders inline at current position.
|
|
297
340
|
*/
|
|
298
341
|
setMode(mode) {
|
|
299
342
|
const prevMode = this.mode;
|
|
@@ -301,322 +344,34 @@ export class TerminalInput extends EventEmitter {
|
|
|
301
344
|
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
302
345
|
// Track streaming start time for elapsed display
|
|
303
346
|
this.streamingStartTime = Date.now();
|
|
304
|
-
const { rows } = this.getSize();
|
|
305
347
|
// Ensure unified UI is initialized (if not already done on launch)
|
|
306
348
|
if (!this.unifiedUIInitialized) {
|
|
307
349
|
this.initializeUnifiedUI();
|
|
308
350
|
}
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
//
|
|
313
|
-
if (!this.scrollRegionActive) {
|
|
314
|
-
const contentBottomRow = Math.max(1, rows - this.reservedLines);
|
|
315
|
-
this.write(ESC.TO(contentBottomRow, 1));
|
|
316
|
-
this.enableScrollRegion();
|
|
317
|
-
}
|
|
318
|
-
// Render bottom input area
|
|
319
|
-
this.renderBottomInputArea();
|
|
320
|
-
// Start timer to update bottom input area (updates elapsed time)
|
|
321
|
-
this.streamingRenderTimer = setInterval(() => {
|
|
322
|
-
if (this.mode === 'streaming') {
|
|
323
|
-
this.updateStreamingStatus();
|
|
324
|
-
this.renderBottomInputArea();
|
|
325
|
-
}
|
|
326
|
-
}, 1000);
|
|
351
|
+
// NO scroll region - let content flow naturally
|
|
352
|
+
// NO bottom-pinned input area
|
|
353
|
+
// Don't clear anything - content flows from current cursor position
|
|
354
|
+
// The cursor should already be positioned after the user's prompt
|
|
327
355
|
this.renderDirty = true;
|
|
328
356
|
}
|
|
329
357
|
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
330
|
-
// Stop streaming render timer
|
|
358
|
+
// Stop streaming render timer (if any)
|
|
331
359
|
if (this.streamingRenderTimer) {
|
|
332
360
|
clearInterval(this.streamingRenderTimer);
|
|
333
361
|
this.streamingRenderTimer = null;
|
|
334
362
|
}
|
|
335
363
|
// Reset streaming time
|
|
336
364
|
this.streamingStartTime = null;
|
|
337
|
-
// Keep scroll region active for consistent bottom-pinned UI
|
|
338
|
-
// (scroll region reserves bottom for input area in all modes)
|
|
339
365
|
// Reset flow mode tracking
|
|
340
366
|
this.flowModeRenderedLines = 0;
|
|
341
|
-
//
|
|
367
|
+
// Add spacing after streamed content
|
|
368
|
+
this.write('\n\n');
|
|
369
|
+
// Render input area inline at current position (below streamed content)
|
|
342
370
|
writeLock.withLock(() => {
|
|
343
|
-
this.
|
|
371
|
+
this.renderInlineInputAreaAtCursor();
|
|
344
372
|
}, 'terminalInput.streamingEnd');
|
|
345
373
|
}
|
|
346
374
|
}
|
|
347
|
-
/**
|
|
348
|
-
* Update streaming status label (called by timer)
|
|
349
|
-
*/
|
|
350
|
-
updateStreamingStatus() {
|
|
351
|
-
if (this.mode !== 'streaming' || !this.streamingStartTime)
|
|
352
|
-
return;
|
|
353
|
-
// Calculate elapsed time
|
|
354
|
-
const elapsed = Date.now() - this.streamingStartTime;
|
|
355
|
-
const seconds = Math.floor(elapsed / 1000);
|
|
356
|
-
const minutes = Math.floor(seconds / 60);
|
|
357
|
-
const secs = seconds % 60;
|
|
358
|
-
// Format elapsed time
|
|
359
|
-
let elapsedStr;
|
|
360
|
-
if (minutes > 0) {
|
|
361
|
-
elapsedStr = `${minutes}m ${secs}s`;
|
|
362
|
-
}
|
|
363
|
-
else {
|
|
364
|
-
elapsedStr = `${secs}s`;
|
|
365
|
-
}
|
|
366
|
-
// Update streaming label
|
|
367
|
-
this.streamingLabel = `Streaming ${elapsedStr}`;
|
|
368
|
-
}
|
|
369
|
-
/**
|
|
370
|
-
* Render input area - unified for streaming and normal modes.
|
|
371
|
-
*
|
|
372
|
-
* In streaming mode: renders at absolute bottom, uses cursor save/restore
|
|
373
|
-
* In normal mode: renders right after the banner (pinnedTopRows + 1)
|
|
374
|
-
*/
|
|
375
|
-
renderPinnedInputArea() {
|
|
376
|
-
const { rows, cols } = this.getSize();
|
|
377
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
378
|
-
const divider = renderDivider(cols - 2);
|
|
379
|
-
const isStreaming = this.mode === 'streaming';
|
|
380
|
-
// Wrap buffer into display lines (multi-line support)
|
|
381
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
382
|
-
const availableForContent = Math.max(1, rows - 5); // Reserve 5 lines for UI
|
|
383
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
384
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
385
|
-
// Calculate display window (keep cursor visible)
|
|
386
|
-
let startLine = 0;
|
|
387
|
-
if (lines.length > displayLines) {
|
|
388
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
389
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
390
|
-
}
|
|
391
|
-
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
392
|
-
const adjustedCursorLine = cursorLine - startLine;
|
|
393
|
-
// Calculate total height: status + (model info if set) + topDiv + input lines + bottomDiv + controls
|
|
394
|
-
const hasModelInfo = !!this.modelInfo;
|
|
395
|
-
const totalHeight = 4 + visibleLines.length + (hasModelInfo ? 1 : 0);
|
|
396
|
-
// Save cursor position during streaming (so content flow resumes correctly)
|
|
397
|
-
if (isStreaming) {
|
|
398
|
-
this.write(ESC.SAVE);
|
|
399
|
-
}
|
|
400
|
-
this.write(ESC.HIDE);
|
|
401
|
-
this.write(ESC.RESET);
|
|
402
|
-
// Calculate start row based on mode:
|
|
403
|
-
// - Streaming: absolute bottom (rows - totalHeight + 1)
|
|
404
|
-
// - Normal: right after content (contentEndRow + 1)
|
|
405
|
-
let currentRow;
|
|
406
|
-
if (isStreaming) {
|
|
407
|
-
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
408
|
-
}
|
|
409
|
-
else {
|
|
410
|
-
// In normal mode, render right after content
|
|
411
|
-
// Use contentEndRow if set, otherwise use pinnedTopRows
|
|
412
|
-
const contentRow = this.contentEndRow > 0 ? this.contentEndRow : this.pinnedTopRows;
|
|
413
|
-
currentRow = Math.max(1, contentRow + 1);
|
|
414
|
-
}
|
|
415
|
-
let finalRow = currentRow;
|
|
416
|
-
let finalCol = 3;
|
|
417
|
-
// Clear from current position to end of screen to remove any "ghost" content
|
|
418
|
-
this.write(ESC.TO(currentRow, 1));
|
|
419
|
-
this.write(ESC.CLEAR_TO_END);
|
|
420
|
-
// Status bar
|
|
421
|
-
this.write(ESC.TO(currentRow, 1));
|
|
422
|
-
this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
|
|
423
|
-
currentRow++;
|
|
424
|
-
// Model info line (if set) - displayed below status, above input
|
|
425
|
-
if (hasModelInfo) {
|
|
426
|
-
const { dim: DIM, reset: R } = UI_COLORS;
|
|
427
|
-
this.write(ESC.TO(currentRow, 1));
|
|
428
|
-
// Build model info with context usage
|
|
429
|
-
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
430
|
-
if (this.contextUsage !== null) {
|
|
431
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
432
|
-
if (rem < 10)
|
|
433
|
-
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
434
|
-
else if (rem < 25)
|
|
435
|
-
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
436
|
-
else
|
|
437
|
-
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
438
|
-
}
|
|
439
|
-
this.write(modelLine);
|
|
440
|
-
currentRow++;
|
|
441
|
-
}
|
|
442
|
-
// Top divider
|
|
443
|
-
this.write(ESC.TO(currentRow, 1));
|
|
444
|
-
this.write(divider);
|
|
445
|
-
currentRow++;
|
|
446
|
-
// Input lines with background styling
|
|
447
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
448
|
-
this.write(ESC.TO(currentRow, 1));
|
|
449
|
-
const line = visibleLines[i] ?? '';
|
|
450
|
-
const absoluteLineIdx = startLine + i;
|
|
451
|
-
const isFirstLine = absoluteLineIdx === 0;
|
|
452
|
-
const isCursorLine = i === adjustedCursorLine;
|
|
453
|
-
// Background
|
|
454
|
-
this.write(ESC.BG_DARK);
|
|
455
|
-
// Prompt prefix
|
|
456
|
-
this.write(ESC.DIM);
|
|
457
|
-
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
458
|
-
this.write(ESC.RESET);
|
|
459
|
-
this.write(ESC.BG_DARK);
|
|
460
|
-
if (isCursorLine) {
|
|
461
|
-
const col = Math.min(cursorCol, line.length);
|
|
462
|
-
const before = line.slice(0, col);
|
|
463
|
-
const at = col < line.length ? line[col] : ' ';
|
|
464
|
-
const after = col < line.length ? line.slice(col + 1) : '';
|
|
465
|
-
this.write(before);
|
|
466
|
-
this.write(ESC.REVERSE + ESC.BOLD);
|
|
467
|
-
this.write(at);
|
|
468
|
-
this.write(ESC.RESET + ESC.BG_DARK);
|
|
469
|
-
this.write(after);
|
|
470
|
-
finalRow = currentRow;
|
|
471
|
-
finalCol = this.config.promptChar.length + col + 1;
|
|
472
|
-
}
|
|
473
|
-
else {
|
|
474
|
-
this.write(line);
|
|
475
|
-
}
|
|
476
|
-
// Pad to edge
|
|
477
|
-
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
478
|
-
const padding = Math.max(0, cols - lineLen - 1);
|
|
479
|
-
if (padding > 0)
|
|
480
|
-
this.write(' '.repeat(padding));
|
|
481
|
-
this.write(ESC.RESET);
|
|
482
|
-
currentRow++;
|
|
483
|
-
}
|
|
484
|
-
// Bottom divider
|
|
485
|
-
this.write(ESC.TO(currentRow, 1));
|
|
486
|
-
this.write(divider);
|
|
487
|
-
currentRow++;
|
|
488
|
-
// Mode controls line
|
|
489
|
-
this.write(ESC.TO(currentRow, 1));
|
|
490
|
-
this.write(this.buildModeControls(cols));
|
|
491
|
-
// Restore cursor position during streaming, or show cursor in normal mode
|
|
492
|
-
if (isStreaming) {
|
|
493
|
-
this.write(ESC.RESTORE);
|
|
494
|
-
}
|
|
495
|
-
else {
|
|
496
|
-
// Position cursor in input area
|
|
497
|
-
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
498
|
-
this.write(ESC.SHOW);
|
|
499
|
-
}
|
|
500
|
-
// Update reserved lines for scroll region calculations
|
|
501
|
-
this.updateReservedLines(totalHeight);
|
|
502
|
-
}
|
|
503
|
-
/**
|
|
504
|
-
* Render input area during streaming (alias for unified method)
|
|
505
|
-
*/
|
|
506
|
-
renderStreamingInputArea() {
|
|
507
|
-
this.renderPinnedInputArea();
|
|
508
|
-
}
|
|
509
|
-
/**
|
|
510
|
-
* Render bottom input area - UNIFIED for all modes.
|
|
511
|
-
* Uses cursor save/restore to update bottom without affecting content flow.
|
|
512
|
-
*
|
|
513
|
-
* Layout (same for idle/streaming/ready):
|
|
514
|
-
* - Status bar (streaming timer or "Type a message")
|
|
515
|
-
* - Model info line (provider · model · ctx)
|
|
516
|
-
* - Divider
|
|
517
|
-
* - Input area
|
|
518
|
-
* - Divider
|
|
519
|
-
* - Mode controls
|
|
520
|
-
*/
|
|
521
|
-
renderBottomInputArea() {
|
|
522
|
-
const { rows, cols } = this.getSize();
|
|
523
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
524
|
-
const divider = renderDivider(cols - 2);
|
|
525
|
-
const { dim: DIM, reset: R } = UI_COLORS;
|
|
526
|
-
const isStreaming = this.mode === 'streaming';
|
|
527
|
-
// Wrap buffer into display lines
|
|
528
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
529
|
-
// Allow multi-line in non-streaming, single line during streaming
|
|
530
|
-
const maxDisplayLines = isStreaming ? 1 : 3;
|
|
531
|
-
const displayLines = Math.min(lines.length, maxDisplayLines);
|
|
532
|
-
const visibleLines = lines.slice(0, displayLines);
|
|
533
|
-
// Calculate total height for bottom area
|
|
534
|
-
const hasModelInfo = !!this.modelInfo;
|
|
535
|
-
const totalHeight = 4 + displayLines + (hasModelInfo ? 1 : 0);
|
|
536
|
-
// Ensure scroll region is always enabled (unified behavior)
|
|
537
|
-
if (!this.scrollRegionActive || this.reservedLines !== totalHeight) {
|
|
538
|
-
this.reservedLines = totalHeight;
|
|
539
|
-
this.enableScrollRegion();
|
|
540
|
-
}
|
|
541
|
-
const startRow = Math.max(1, rows - totalHeight + 1);
|
|
542
|
-
// Save cursor, hide it
|
|
543
|
-
this.write(ESC.SAVE);
|
|
544
|
-
this.write(ESC.HIDE);
|
|
545
|
-
let currentRow = startRow;
|
|
546
|
-
// Clear the bottom reserved area
|
|
547
|
-
for (let r = startRow; r <= rows; r++) {
|
|
548
|
-
this.write(ESC.TO(r, 1));
|
|
549
|
-
this.write(ESC.CLEAR_LINE);
|
|
550
|
-
}
|
|
551
|
-
// Status bar - UNIFIED: same format for all modes
|
|
552
|
-
this.write(ESC.TO(currentRow, 1));
|
|
553
|
-
this.write(isStreaming ? this.buildStreamingStatusBar(cols) : this.buildStatusBar(cols));
|
|
554
|
-
currentRow++;
|
|
555
|
-
// Model info line (if set)
|
|
556
|
-
if (hasModelInfo) {
|
|
557
|
-
this.write(ESC.TO(currentRow, 1));
|
|
558
|
-
let modelLine = `${DIM}${this.modelInfo}${R}`;
|
|
559
|
-
if (this.contextUsage !== null) {
|
|
560
|
-
const rem = Math.max(0, 100 - this.contextUsage);
|
|
561
|
-
if (rem < 10)
|
|
562
|
-
modelLine += ` ${UI_COLORS.red}⚠ ctx: ${rem}%${R}`;
|
|
563
|
-
else if (rem < 25)
|
|
564
|
-
modelLine += ` ${UI_COLORS.yellow}! ctx: ${rem}%${R}`;
|
|
565
|
-
else
|
|
566
|
-
modelLine += ` ${DIM}· ctx: ${rem}%${R}`;
|
|
567
|
-
}
|
|
568
|
-
this.write(modelLine);
|
|
569
|
-
currentRow++;
|
|
570
|
-
}
|
|
571
|
-
// Top divider
|
|
572
|
-
this.write(ESC.TO(currentRow, 1));
|
|
573
|
-
this.write(divider);
|
|
574
|
-
currentRow++;
|
|
575
|
-
// Input lines with background styling
|
|
576
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
577
|
-
this.write(ESC.TO(currentRow, 1));
|
|
578
|
-
const line = visibleLines[i] ?? '';
|
|
579
|
-
const isFirstLine = i === 0;
|
|
580
|
-
this.write(ESC.BG_DARK);
|
|
581
|
-
this.write(ESC.DIM);
|
|
582
|
-
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
583
|
-
this.write(ESC.RESET);
|
|
584
|
-
this.write(ESC.BG_DARK);
|
|
585
|
-
this.write(line);
|
|
586
|
-
// Pad to edge
|
|
587
|
-
const lineLen = this.config.promptChar.length + line.length;
|
|
588
|
-
const padding = Math.max(0, cols - lineLen - 1);
|
|
589
|
-
if (padding > 0)
|
|
590
|
-
this.write(' '.repeat(padding));
|
|
591
|
-
this.write(ESC.RESET);
|
|
592
|
-
currentRow++;
|
|
593
|
-
}
|
|
594
|
-
// Bottom divider
|
|
595
|
-
this.write(ESC.TO(currentRow, 1));
|
|
596
|
-
this.write(divider);
|
|
597
|
-
currentRow++;
|
|
598
|
-
// Mode controls
|
|
599
|
-
this.write(ESC.TO(currentRow, 1));
|
|
600
|
-
this.write(this.buildModeControls(cols));
|
|
601
|
-
// Cursor positioning depends on mode:
|
|
602
|
-
// - Streaming: restore to content area (where streaming output continues)
|
|
603
|
-
// - Normal: position in input area for typing
|
|
604
|
-
if (isStreaming) {
|
|
605
|
-
this.write(ESC.RESTORE);
|
|
606
|
-
}
|
|
607
|
-
else {
|
|
608
|
-
// Position cursor in input area
|
|
609
|
-
// Input line is at: startRow + (hasModelInfo ? 2 : 1) + cursorLine
|
|
610
|
-
const inputStartRow = startRow + (hasModelInfo ? 2 : 1) + 1; // +1 for status bar, +1 for divider
|
|
611
|
-
const targetRow = inputStartRow + Math.min(cursorLine, displayLines - 1);
|
|
612
|
-
const targetCol = this.config.promptChar.length + cursorCol + 1;
|
|
613
|
-
this.write(ESC.TO(targetRow, Math.min(targetCol, cols)));
|
|
614
|
-
}
|
|
615
|
-
this.write(ESC.SHOW);
|
|
616
|
-
// Track last render state
|
|
617
|
-
this.lastRenderContent = this.buffer;
|
|
618
|
-
this.lastRenderCursor = this.cursor;
|
|
619
|
-
}
|
|
620
375
|
/**
|
|
621
376
|
* Enable or disable flow mode.
|
|
622
377
|
* In flow mode, the input renders immediately after content (wherever cursor is).
|
|
@@ -739,16 +494,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
739
494
|
return this.thinkingEnabled;
|
|
740
495
|
}
|
|
741
496
|
/**
|
|
742
|
-
* Keep the top N rows pinned
|
|
497
|
+
* Keep the top N rows pinned (used for the launch banner tracking).
|
|
498
|
+
* Note: No longer uses scroll regions - inline rendering only.
|
|
743
499
|
*/
|
|
744
500
|
setPinnedHeaderLines(count) {
|
|
745
|
-
|
|
746
|
-
if (this.pinnedTopRows !== count) {
|
|
747
|
-
this.pinnedTopRows = count;
|
|
748
|
-
if (this.scrollRegionActive) {
|
|
749
|
-
this.applyScrollRegion();
|
|
750
|
-
}
|
|
751
|
-
}
|
|
501
|
+
this.pinnedTopRows = count;
|
|
752
502
|
}
|
|
753
503
|
/**
|
|
754
504
|
* Anchor prompt rendering near a specific row (inline layout). Pass null to
|
|
@@ -817,14 +567,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
817
567
|
}
|
|
818
568
|
/**
|
|
819
569
|
* Clear the buffer
|
|
570
|
+
* @param skipRender - If true, don't trigger a re-render (used during submit flow)
|
|
820
571
|
*/
|
|
821
|
-
clear() {
|
|
572
|
+
clear(skipRender = false) {
|
|
822
573
|
this.buffer = '';
|
|
823
574
|
this.cursor = 0;
|
|
824
575
|
this.historyIndex = -1;
|
|
825
576
|
this.tempInput = '';
|
|
826
577
|
this.pastePlaceholders = [];
|
|
827
|
-
|
|
578
|
+
if (!skipRender) {
|
|
579
|
+
this.scheduleRender();
|
|
580
|
+
}
|
|
828
581
|
}
|
|
829
582
|
/**
|
|
830
583
|
* Get queued inputs
|
|
@@ -936,18 +689,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
936
689
|
this.scheduleRender();
|
|
937
690
|
}
|
|
938
691
|
/**
|
|
939
|
-
* Render the input area
|
|
692
|
+
* Render the input area
|
|
940
693
|
*
|
|
941
|
-
*
|
|
942
|
-
* -
|
|
943
|
-
* -
|
|
944
|
-
* - Ready mode: Shows status info
|
|
694
|
+
* - Idle mode: Renders inline input area
|
|
695
|
+
* - Streaming mode: NO render (content flows naturally, no UI updates)
|
|
696
|
+
* - Ready mode: Renders inline input area
|
|
945
697
|
*/
|
|
946
698
|
render() {
|
|
947
699
|
if (!this.canRender())
|
|
948
700
|
return;
|
|
949
701
|
if (this.isRendering)
|
|
950
702
|
return;
|
|
703
|
+
// During streaming, do NOT render - let content flow naturally
|
|
704
|
+
if (this.mode === 'streaming') {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
951
707
|
const shouldSkip = !this.renderDirty &&
|
|
952
708
|
this.buffer === this.lastRenderContent &&
|
|
953
709
|
this.cursor === this.lastRenderCursor;
|
|
@@ -964,176 +720,14 @@ export class TerminalInput extends EventEmitter {
|
|
|
964
720
|
this.isRendering = true;
|
|
965
721
|
writeLock.lock('terminalInput.render');
|
|
966
722
|
try {
|
|
967
|
-
//
|
|
968
|
-
this.
|
|
723
|
+
// Render inline input area at current position
|
|
724
|
+
this.renderInlineInputAreaAtCursor();
|
|
969
725
|
}
|
|
970
726
|
finally {
|
|
971
727
|
writeLock.unlock();
|
|
972
728
|
this.isRendering = false;
|
|
973
729
|
}
|
|
974
730
|
}
|
|
975
|
-
/**
|
|
976
|
-
* Render in flow mode - delegates to bottom-pinned for stability.
|
|
977
|
-
*
|
|
978
|
-
* Flow mode attempted inline rendering but caused duplicate renders
|
|
979
|
-
* due to unreliable cursor position tracking. Bottom-pinned is reliable.
|
|
980
|
-
*/
|
|
981
|
-
renderFlowMode() {
|
|
982
|
-
// Use stable bottom-pinned approach
|
|
983
|
-
this.renderBottomPinned();
|
|
984
|
-
}
|
|
985
|
-
/**
|
|
986
|
-
* Render in bottom-pinned mode - Claude Code style with suggestions
|
|
987
|
-
*
|
|
988
|
-
* Works for both normal and streaming modes:
|
|
989
|
-
* - During streaming: saves/restores cursor position
|
|
990
|
-
* - Status bar shows streaming info or "Type a message"
|
|
991
|
-
*
|
|
992
|
-
* Layout when suggestions visible:
|
|
993
|
-
* - Top divider
|
|
994
|
-
* - Input line(s)
|
|
995
|
-
* - Bottom divider
|
|
996
|
-
* - Suggestions (command list)
|
|
997
|
-
*
|
|
998
|
-
* Layout when suggestions hidden:
|
|
999
|
-
* - Status bar (Ready/Streaming)
|
|
1000
|
-
* - Top divider
|
|
1001
|
-
* - Input line(s)
|
|
1002
|
-
* - Bottom divider
|
|
1003
|
-
* - Mode controls
|
|
1004
|
-
*/
|
|
1005
|
-
renderBottomPinned() {
|
|
1006
|
-
const { rows, cols } = this.getSize();
|
|
1007
|
-
const maxWidth = Math.max(8, cols - 4);
|
|
1008
|
-
const isStreaming = this.mode === 'streaming';
|
|
1009
|
-
// Use unified pinned input area (works for both streaming and normal)
|
|
1010
|
-
// Only use complex rendering when suggestions are visible
|
|
1011
|
-
const hasSuggestions = !isStreaming && this.showSuggestions && this.filteredSuggestions.length > 0;
|
|
1012
|
-
if (!hasSuggestions) {
|
|
1013
|
-
this.renderPinnedInputArea();
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
// Wrap buffer into display lines
|
|
1017
|
-
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
1018
|
-
const availableForContent = Math.max(1, rows - 3);
|
|
1019
|
-
const maxVisible = Math.max(1, Math.min(this.config.maxLines, availableForContent));
|
|
1020
|
-
const displayLines = Math.min(lines.length, maxVisible);
|
|
1021
|
-
// Calculate display window (keep cursor visible)
|
|
1022
|
-
let startLine = 0;
|
|
1023
|
-
if (lines.length > displayLines) {
|
|
1024
|
-
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
1025
|
-
startLine = Math.min(startLine, lines.length - displayLines);
|
|
1026
|
-
}
|
|
1027
|
-
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
1028
|
-
const adjustedCursorLine = cursorLine - startLine;
|
|
1029
|
-
// Calculate suggestion display (not during streaming)
|
|
1030
|
-
const suggestionsToShow = (!isStreaming && this.showSuggestions)
|
|
1031
|
-
? this.filteredSuggestions.slice(0, this.maxVisibleSuggestions)
|
|
1032
|
-
: [];
|
|
1033
|
-
const suggestionLines = suggestionsToShow.length;
|
|
1034
|
-
this.write(ESC.HIDE);
|
|
1035
|
-
this.write(ESC.RESET);
|
|
1036
|
-
const divider = renderDivider(cols - 2);
|
|
1037
|
-
// Calculate positions from absolute bottom
|
|
1038
|
-
let currentRow;
|
|
1039
|
-
if (suggestionLines > 0) {
|
|
1040
|
-
// With suggestions: input area + dividers + suggestions
|
|
1041
|
-
// Layout: [topDiv] [input] [bottomDiv] [suggestions...]
|
|
1042
|
-
const totalHeight = 2 + visibleLines.length + suggestionLines; // 2 dividers
|
|
1043
|
-
currentRow = Math.max(1, rows - totalHeight + 1);
|
|
1044
|
-
this.updateReservedLines(totalHeight);
|
|
1045
|
-
// Clear from current position to end of screen to remove any "ghost" content
|
|
1046
|
-
this.write(ESC.TO(currentRow, 1));
|
|
1047
|
-
this.write(ESC.CLEAR_TO_END);
|
|
1048
|
-
// Top divider
|
|
1049
|
-
this.write(ESC.TO(currentRow, 1));
|
|
1050
|
-
this.write(divider);
|
|
1051
|
-
currentRow++;
|
|
1052
|
-
// Input lines
|
|
1053
|
-
let finalRow = currentRow;
|
|
1054
|
-
let finalCol = 3;
|
|
1055
|
-
for (let i = 0; i < visibleLines.length; i++) {
|
|
1056
|
-
this.write(ESC.TO(currentRow, 1));
|
|
1057
|
-
const line = visibleLines[i] ?? '';
|
|
1058
|
-
const absoluteLineIdx = startLine + i;
|
|
1059
|
-
const isFirstLine = absoluteLineIdx === 0;
|
|
1060
|
-
const isCursorLine = i === adjustedCursorLine;
|
|
1061
|
-
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
1062
|
-
if (isCursorLine) {
|
|
1063
|
-
const col = Math.min(cursorCol, line.length);
|
|
1064
|
-
this.write(line.slice(0, col));
|
|
1065
|
-
this.write(ESC.REVERSE);
|
|
1066
|
-
this.write(col < line.length ? line[col] : ' ');
|
|
1067
|
-
this.write(ESC.RESET);
|
|
1068
|
-
this.write(line.slice(col + 1));
|
|
1069
|
-
finalRow = currentRow;
|
|
1070
|
-
finalCol = this.config.promptChar.length + col + 1;
|
|
1071
|
-
}
|
|
1072
|
-
else {
|
|
1073
|
-
this.write(line);
|
|
1074
|
-
}
|
|
1075
|
-
currentRow++;
|
|
1076
|
-
}
|
|
1077
|
-
// Bottom divider
|
|
1078
|
-
this.write(ESC.TO(currentRow, 1));
|
|
1079
|
-
this.write(divider);
|
|
1080
|
-
currentRow++;
|
|
1081
|
-
// Suggestions (Claude Code style)
|
|
1082
|
-
for (let i = 0; i < suggestionsToShow.length; i++) {
|
|
1083
|
-
this.write(ESC.TO(currentRow, 1));
|
|
1084
|
-
const suggestion = suggestionsToShow[i];
|
|
1085
|
-
const isSelected = i === this.selectedSuggestionIndex;
|
|
1086
|
-
// Indent and highlight selected
|
|
1087
|
-
this.write(' ');
|
|
1088
|
-
if (isSelected) {
|
|
1089
|
-
this.write(ESC.REVERSE);
|
|
1090
|
-
this.write(ESC.BOLD);
|
|
1091
|
-
}
|
|
1092
|
-
this.write(suggestion.command);
|
|
1093
|
-
if (isSelected) {
|
|
1094
|
-
this.write(ESC.RESET);
|
|
1095
|
-
}
|
|
1096
|
-
// Description (dimmed)
|
|
1097
|
-
const descSpace = cols - suggestion.command.length - 8;
|
|
1098
|
-
if (descSpace > 10 && suggestion.description) {
|
|
1099
|
-
const desc = suggestion.description.slice(0, descSpace);
|
|
1100
|
-
this.write(ESC.RESET);
|
|
1101
|
-
this.write(ESC.DIM);
|
|
1102
|
-
this.write(' ');
|
|
1103
|
-
this.write(desc);
|
|
1104
|
-
this.write(ESC.RESET);
|
|
1105
|
-
}
|
|
1106
|
-
currentRow++;
|
|
1107
|
-
}
|
|
1108
|
-
// Position cursor in input area
|
|
1109
|
-
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
1110
|
-
}
|
|
1111
|
-
this.write(ESC.SHOW);
|
|
1112
|
-
// Update state
|
|
1113
|
-
this.lastRenderContent = this.buffer;
|
|
1114
|
-
this.lastRenderCursor = this.cursor;
|
|
1115
|
-
}
|
|
1116
|
-
/**
|
|
1117
|
-
* Build status bar for streaming mode (shows elapsed time, queue count).
|
|
1118
|
-
*/
|
|
1119
|
-
buildStreamingStatusBar(cols) {
|
|
1120
|
-
const { green: GREEN, cyan: CYAN, dim: DIM, reset: R } = UI_COLORS;
|
|
1121
|
-
// Streaming status with elapsed time
|
|
1122
|
-
let elapsed = '0s';
|
|
1123
|
-
if (this.streamingStartTime) {
|
|
1124
|
-
const secs = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
1125
|
-
const mins = Math.floor(secs / 60);
|
|
1126
|
-
elapsed = mins > 0 ? `${mins}m ${secs % 60}s` : `${secs}s`;
|
|
1127
|
-
}
|
|
1128
|
-
let status = `${GREEN}● Streaming${R} ${elapsed}`;
|
|
1129
|
-
// Queue indicator
|
|
1130
|
-
if (this.queue.length > 0) {
|
|
1131
|
-
status += ` ${DIM}·${R} ${CYAN}${this.queue.length} queued${R}`;
|
|
1132
|
-
}
|
|
1133
|
-
// Hint for typing
|
|
1134
|
-
status += ` ${DIM}· type to queue message${R}`;
|
|
1135
|
-
return status;
|
|
1136
|
-
}
|
|
1137
731
|
/**
|
|
1138
732
|
* Build status bar showing streaming/ready status and key info.
|
|
1139
733
|
* This is the TOP line above the input area - minimal Claude Code style.
|
|
@@ -1294,7 +888,8 @@ export class TerminalInput extends EventEmitter {
|
|
|
1294
888
|
}
|
|
1295
889
|
this.disposed = true;
|
|
1296
890
|
this.enabled = false;
|
|
1297
|
-
|
|
891
|
+
// Reset scroll region if it was set
|
|
892
|
+
this.write(ESC.RESET_SCROLL);
|
|
1298
893
|
this.disableBracketedPaste();
|
|
1299
894
|
this.buffer = '';
|
|
1300
895
|
this.queue = [];
|
|
@@ -1685,12 +1280,13 @@ export class TerminalInput extends EventEmitter {
|
|
|
1685
1280
|
timestamp: Date.now(),
|
|
1686
1281
|
});
|
|
1687
1282
|
this.emit('queue', text);
|
|
1688
|
-
this.clear(); // Clear immediately for queued input
|
|
1283
|
+
this.clear(); // Clear immediately for queued input, re-render to update queue display
|
|
1689
1284
|
}
|
|
1690
1285
|
else {
|
|
1691
|
-
// In idle mode, clear the input
|
|
1692
|
-
// The
|
|
1693
|
-
|
|
1286
|
+
// In idle mode, clear the input WITHOUT rendering.
|
|
1287
|
+
// The caller will display the user message and start streaming.
|
|
1288
|
+
// We'll render the input area again after streaming ends.
|
|
1289
|
+
this.clear(true); // Skip render - streaming will handle display
|
|
1694
1290
|
this.emit('submit', text);
|
|
1695
1291
|
}
|
|
1696
1292
|
}
|
|
@@ -1720,40 +1316,6 @@ export class TerminalInput extends EventEmitter {
|
|
|
1720
1316
|
this.scheduleRender();
|
|
1721
1317
|
}
|
|
1722
1318
|
// ===========================================================================
|
|
1723
|
-
// SCROLL REGION
|
|
1724
|
-
// ===========================================================================
|
|
1725
|
-
enableScrollRegion() {
|
|
1726
|
-
if (this.scrollRegionActive || !this.isTTY())
|
|
1727
|
-
return;
|
|
1728
|
-
this.applyScrollRegion();
|
|
1729
|
-
this.scrollRegionActive = true;
|
|
1730
|
-
}
|
|
1731
|
-
disableScrollRegion() {
|
|
1732
|
-
if (!this.scrollRegionActive)
|
|
1733
|
-
return;
|
|
1734
|
-
this.write(ESC.RESET_SCROLL);
|
|
1735
|
-
this.scrollRegionActive = false;
|
|
1736
|
-
}
|
|
1737
|
-
applyScrollRegion() {
|
|
1738
|
-
const { rows } = this.getSize();
|
|
1739
|
-
const scrollTop = Math.max(1, this.pinnedTopRows + 1);
|
|
1740
|
-
const scrollBottom = Math.max(scrollTop, rows - this.reservedLines);
|
|
1741
|
-
this.write(ESC.SET_SCROLL(scrollTop, scrollBottom));
|
|
1742
|
-
}
|
|
1743
|
-
updateReservedLines(contentLines) {
|
|
1744
|
-
const { rows } = this.getSize();
|
|
1745
|
-
const baseLines = 2; // separator + control bar
|
|
1746
|
-
const needed = baseLines + contentLines;
|
|
1747
|
-
const maxAllowed = Math.max(baseLines, rows - 1 - this.pinnedTopRows);
|
|
1748
|
-
const newReserved = Math.min(Math.max(baseLines, needed), maxAllowed);
|
|
1749
|
-
if (newReserved !== this.reservedLines) {
|
|
1750
|
-
this.reservedLines = newReserved;
|
|
1751
|
-
if (this.scrollRegionActive) {
|
|
1752
|
-
this.applyScrollRegion();
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
// ===========================================================================
|
|
1757
1319
|
// BUFFER WRAPPING
|
|
1758
1320
|
// ===========================================================================
|
|
1759
1321
|
wrapBuffer(maxWidth) {
|