erosolar-cli 2.1.52 → 2.1.54
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/capabilities/askUserCapability.d.ts.map +1 -1
- package/dist/capabilities/askUserCapability.js +32 -49
- package/dist/capabilities/askUserCapability.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +6 -2
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/systemPrompt.js +1 -1
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/tools/interactionTools.d.ts.map +1 -1
- package/dist/tools/interactionTools.js +6 -23
- package/dist/tools/interactionTools.js.map +1 -1
- package/dist/ui/UnifiedUIRenderer.d.ts +35 -2
- package/dist/ui/UnifiedUIRenderer.d.ts.map +1 -1
- package/dist/ui/UnifiedUIRenderer.js +359 -103
- package/dist/ui/UnifiedUIRenderer.js.map +1 -1
- package/dist/ui/display.d.ts +9 -0
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +26 -0
- package/dist/ui/display.js.map +1 -1
- package/dist/utils/askUserPrompt.d.ts.map +1 -1
- package/dist/utils/askUserPrompt.js +15 -5
- package/dist/utils/askUserPrompt.js.map +1 -1
- package/package.json +1 -1
|
@@ -13,7 +13,7 @@ import { EventEmitter } from 'node:events';
|
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
14
|
import { theme } from './theme.js';
|
|
15
15
|
import { isPlainOutputMode } from './outputMode.js';
|
|
16
|
-
import { renderDivider
|
|
16
|
+
import { renderDivider } from './unified/layout.js';
|
|
17
17
|
const ESC = {
|
|
18
18
|
HIDE_CURSOR: '\x1b[?25l',
|
|
19
19
|
SHOW_CURSOR: '\x1b[?25h',
|
|
@@ -38,6 +38,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
38
38
|
interactive;
|
|
39
39
|
rows = 24;
|
|
40
40
|
cols = 80;
|
|
41
|
+
lastRenderWidth = null;
|
|
41
42
|
eventQueue = [];
|
|
42
43
|
isProcessingQueue = false;
|
|
43
44
|
buffer = '';
|
|
@@ -69,20 +70,32 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
69
70
|
promptHeight = 0;
|
|
70
71
|
lastOverlayHeight = 0;
|
|
71
72
|
lastPromptIndex = 0;
|
|
73
|
+
overlayBottomPadding = 1;
|
|
72
74
|
inlinePanel = [];
|
|
73
75
|
overlayInvalidated = false;
|
|
74
76
|
hasConversationContent = false;
|
|
75
77
|
isPromptActive = false;
|
|
76
78
|
inputRenderOffset = 0;
|
|
79
|
+
plainPasteIdleMs = 24;
|
|
80
|
+
plainPasteWindowMs = 60;
|
|
81
|
+
plainPasteTriggerChars = 24;
|
|
82
|
+
plainPasteNewlineAssist = 3;
|
|
77
83
|
cursorVisibleColumn = 1;
|
|
78
84
|
inBracketedPaste = false;
|
|
79
85
|
pasteBuffer = '';
|
|
86
|
+
inPlainPaste = false;
|
|
87
|
+
plainPasteBuffer = '';
|
|
88
|
+
plainPasteTimer = null;
|
|
89
|
+
pasteBurstWindowStart = 0;
|
|
90
|
+
pasteBurstCharCount = 0;
|
|
91
|
+
plainRecentChunks = [];
|
|
80
92
|
lastRenderedEventKey = null;
|
|
81
93
|
lastOutputEndedWithNewline = true;
|
|
82
94
|
hasRenderedPrompt = false;
|
|
83
95
|
hasEverRenderedOverlay = false; // Track if we've ever rendered to prevent first-render scrollback pollution
|
|
84
96
|
lastOverlay = null;
|
|
85
97
|
allowPromptRender = true;
|
|
98
|
+
inputCapture = null;
|
|
86
99
|
constructor(output = process.stdout, input = process.stdin, options) {
|
|
87
100
|
super();
|
|
88
101
|
this.output = output;
|
|
@@ -131,6 +144,8 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
131
144
|
this.renderPrompt();
|
|
132
145
|
}
|
|
133
146
|
cleanup() {
|
|
147
|
+
this.cancelInputCapture(new Error('Renderer disposed'));
|
|
148
|
+
this.cancelPlainPasteCapture();
|
|
134
149
|
if (!this.interactive) {
|
|
135
150
|
this.rl.close();
|
|
136
151
|
return;
|
|
@@ -174,6 +189,14 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
174
189
|
if (this.handleBracketedPaste(str, key)) {
|
|
175
190
|
return;
|
|
176
191
|
}
|
|
192
|
+
if (this.handlePlainPaste(str, key)) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (this.inputCapture && key.ctrl && (key.name === 'c' || key.name === 'd')) {
|
|
196
|
+
this.cancelInputCapture(new Error('Input capture cancelled'));
|
|
197
|
+
this.clearBuffer();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
177
200
|
if (key.ctrl && key.shift && key.name?.toLowerCase() === 'a') {
|
|
178
201
|
this.emit('toggle-critical-approval');
|
|
179
202
|
return;
|
|
@@ -319,6 +342,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
319
342
|
if (sequence === '\x1b[200~') {
|
|
320
343
|
this.inBracketedPaste = true;
|
|
321
344
|
this.pasteBuffer = '';
|
|
345
|
+
this.cancelPlainPasteCapture();
|
|
322
346
|
return true;
|
|
323
347
|
}
|
|
324
348
|
if (!this.inBracketedPaste) {
|
|
@@ -366,10 +390,141 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
366
390
|
}
|
|
367
391
|
this.inBracketedPaste = false;
|
|
368
392
|
this.pasteBuffer = '';
|
|
393
|
+
this.cancelPlainPasteCapture();
|
|
394
|
+
}
|
|
395
|
+
handlePlainPaste(str, key) {
|
|
396
|
+
// Fallback paste capture when bracketed paste isn't supported
|
|
397
|
+
if (this.inBracketedPaste || key?.ctrl || key?.meta) {
|
|
398
|
+
this.resetPlainPasteBurst();
|
|
399
|
+
this.pruneRecentPlainChunks();
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
const sequence = key?.sequence ?? '';
|
|
403
|
+
const chunk = typeof str === 'string' && str.length > 0 ? str : sequence;
|
|
404
|
+
if (!chunk) {
|
|
405
|
+
this.resetPlainPasteBurst();
|
|
406
|
+
this.pruneRecentPlainChunks();
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
const now = Date.now();
|
|
410
|
+
this.trackPlainPasteBurst(chunk.length, now);
|
|
411
|
+
if (!this.inPlainPaste) {
|
|
412
|
+
this.recordRecentPlainChunk(chunk, now);
|
|
413
|
+
}
|
|
414
|
+
const chunkMultiple = chunk.length > 1;
|
|
415
|
+
const hasNewline = /[\r\n]/.test(chunk);
|
|
416
|
+
const burstActive = this.pasteBurstWindowStart > 0 && now - this.pasteBurstWindowStart <= this.plainPasteWindowMs;
|
|
417
|
+
const burstTrigger = burstActive && this.pasteBurstCharCount >= this.plainPasteTriggerChars;
|
|
418
|
+
const newlineTrigger = hasNewline && (this.inPlainPaste || chunkMultiple || this.pasteBurstCharCount >= this.plainPasteNewlineAssist);
|
|
419
|
+
const looksLikePaste = this.inPlainPaste || chunkMultiple || burstTrigger || newlineTrigger;
|
|
420
|
+
if (!looksLikePaste) {
|
|
421
|
+
this.pruneRecentPlainChunks();
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
let chunkAlreadyCaptured = false;
|
|
425
|
+
if (!this.inPlainPaste) {
|
|
426
|
+
this.inPlainPaste = true;
|
|
427
|
+
const reclaimed = this.reclaimRecentPlainChunks();
|
|
428
|
+
if (reclaimed.length > 0) {
|
|
429
|
+
const removeCount = Math.min(this.buffer.length, reclaimed.length);
|
|
430
|
+
const suffix = this.buffer.slice(-removeCount);
|
|
431
|
+
if (removeCount > 0 && suffix === reclaimed.slice(-removeCount)) {
|
|
432
|
+
this.buffer = this.buffer.slice(0, this.buffer.length - removeCount);
|
|
433
|
+
this.cursor = Math.max(0, this.buffer.length);
|
|
434
|
+
}
|
|
435
|
+
this.plainPasteBuffer = reclaimed;
|
|
436
|
+
chunkAlreadyCaptured = reclaimed.endsWith(chunk);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
this.plainPasteBuffer = '';
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (!chunkAlreadyCaptured) {
|
|
443
|
+
this.plainPasteBuffer += chunk;
|
|
444
|
+
}
|
|
445
|
+
this.schedulePlainPasteCommit();
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
trackPlainPasteBurst(length, now) {
|
|
449
|
+
if (!this.pasteBurstWindowStart || now - this.pasteBurstWindowStart > this.plainPasteWindowMs) {
|
|
450
|
+
this.pasteBurstWindowStart = now;
|
|
451
|
+
this.pasteBurstCharCount = 0;
|
|
452
|
+
}
|
|
453
|
+
this.pasteBurstCharCount += length;
|
|
454
|
+
}
|
|
455
|
+
resetPlainPasteBurst() {
|
|
456
|
+
this.pasteBurstWindowStart = 0;
|
|
457
|
+
this.pasteBurstCharCount = 0;
|
|
458
|
+
}
|
|
459
|
+
cancelPlainPasteCapture() {
|
|
460
|
+
if (this.plainPasteTimer) {
|
|
461
|
+
clearTimeout(this.plainPasteTimer);
|
|
462
|
+
this.plainPasteTimer = null;
|
|
463
|
+
}
|
|
464
|
+
this.inPlainPaste = false;
|
|
465
|
+
this.plainPasteBuffer = '';
|
|
466
|
+
this.plainRecentChunks = [];
|
|
467
|
+
this.resetPlainPasteBurst();
|
|
468
|
+
}
|
|
469
|
+
recordRecentPlainChunk(text, at) {
|
|
470
|
+
const windowStart = at - this.plainPasteWindowMs;
|
|
471
|
+
this.plainRecentChunks.push({ text, at });
|
|
472
|
+
this.plainRecentChunks = this.plainRecentChunks.filter(entry => entry.at >= windowStart);
|
|
473
|
+
}
|
|
474
|
+
pruneRecentPlainChunks() {
|
|
475
|
+
if (!this.plainRecentChunks.length)
|
|
476
|
+
return;
|
|
477
|
+
const now = Date.now();
|
|
478
|
+
const windowStart = now - this.plainPasteWindowMs;
|
|
479
|
+
this.plainRecentChunks = this.plainRecentChunks.filter(entry => entry.at >= windowStart);
|
|
480
|
+
}
|
|
481
|
+
reclaimRecentPlainChunks() {
|
|
482
|
+
if (!this.plainRecentChunks.length)
|
|
483
|
+
return '';
|
|
484
|
+
const combined = this.plainRecentChunks.map(entry => entry.text).join('');
|
|
485
|
+
this.plainRecentChunks = [];
|
|
486
|
+
return combined;
|
|
487
|
+
}
|
|
488
|
+
schedulePlainPasteCommit() {
|
|
489
|
+
if (this.plainPasteTimer) {
|
|
490
|
+
clearTimeout(this.plainPasteTimer);
|
|
491
|
+
}
|
|
492
|
+
this.plainPasteTimer = setTimeout(() => {
|
|
493
|
+
this.finalizePlainPaste();
|
|
494
|
+
}, this.plainPasteIdleMs);
|
|
495
|
+
}
|
|
496
|
+
finalizePlainPaste() {
|
|
497
|
+
if (!this.inPlainPaste)
|
|
498
|
+
return;
|
|
499
|
+
const content = this.plainPasteBuffer.replace(/\r\n?/g, '\n');
|
|
500
|
+
this.inPlainPaste = false;
|
|
501
|
+
this.plainPasteBuffer = '';
|
|
502
|
+
this.plainRecentChunks = [];
|
|
503
|
+
this.resetPlainPasteBurst();
|
|
504
|
+
if (this.plainPasteTimer) {
|
|
505
|
+
clearTimeout(this.plainPasteTimer);
|
|
506
|
+
this.plainPasteTimer = null;
|
|
507
|
+
}
|
|
508
|
+
if (!content)
|
|
509
|
+
return;
|
|
510
|
+
const lines = content.split('\n');
|
|
511
|
+
if (lines.length > 1 || content.length > 200) {
|
|
512
|
+
this.collapsedPaste = { text: content, lines: lines.length, chars: content.length };
|
|
513
|
+
this.buffer = '';
|
|
514
|
+
this.cursor = 0;
|
|
515
|
+
this.updateSuggestions();
|
|
516
|
+
this.renderPrompt();
|
|
517
|
+
this.emitInputChange();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
this.insertText(content);
|
|
369
521
|
}
|
|
370
522
|
insertText(text) {
|
|
371
523
|
if (!text)
|
|
372
524
|
return;
|
|
525
|
+
if (this.inPlainPaste) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
373
528
|
if (this.collapsedPaste) {
|
|
374
529
|
this.expandCollapsedPaste();
|
|
375
530
|
}
|
|
@@ -384,6 +539,24 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
384
539
|
this.expandCollapsedPaste();
|
|
385
540
|
return;
|
|
386
541
|
}
|
|
542
|
+
if (this.inputCapture) {
|
|
543
|
+
const shouldTrim = this.inputCapture.options.trim;
|
|
544
|
+
const normalizedCapture = shouldTrim ? text.trim() : text;
|
|
545
|
+
if (!this.inputCapture.options.allowEmpty && !normalizedCapture) {
|
|
546
|
+
this.renderPrompt();
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const resolver = this.inputCapture;
|
|
550
|
+
this.inputCapture = null;
|
|
551
|
+
this.buffer = '';
|
|
552
|
+
this.cursor = 0;
|
|
553
|
+
this.inputRenderOffset = 0;
|
|
554
|
+
this.resetSuggestions();
|
|
555
|
+
this.renderPrompt();
|
|
556
|
+
this.emitInputChange();
|
|
557
|
+
resolver.resolve(normalizedCapture);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
387
560
|
const normalized = text.trim();
|
|
388
561
|
if (!normalized) {
|
|
389
562
|
this.renderPrompt();
|
|
@@ -785,79 +958,59 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
785
958
|
}
|
|
786
959
|
// Rich mode: inline overlay anchored to current scrollback (no full-screen clear)
|
|
787
960
|
this.updateTerminalSize();
|
|
961
|
+
const maxWidth = this.safeWidth();
|
|
962
|
+
if (this.lastRenderWidth !== null && maxWidth !== this.lastRenderWidth) {
|
|
963
|
+
// Terminal resized; force a clean anchor so the overlay doesn't jitter.
|
|
964
|
+
this.overlayInvalidated = true;
|
|
965
|
+
}
|
|
966
|
+
this.lastRenderWidth = maxWidth;
|
|
788
967
|
const overlay = this.buildOverlayLines();
|
|
789
968
|
if (!overlay.lines.length) {
|
|
790
969
|
return;
|
|
791
970
|
}
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
const prevHeight = this.lastOverlay?.lines.length ?? this.lastOverlayHeight ?? 0;
|
|
796
|
-
const prevPromptIndex = this.lastOverlay?.promptIndex ?? this.lastPromptIndex ?? promptIndex;
|
|
797
|
-
const needsFreshAnchor = this.overlayInvalidated || !this.hasRenderedPrompt;
|
|
798
|
-
// For first render, ensure we start on a fresh line
|
|
799
|
-
if (needsFreshAnchor) {
|
|
800
|
-
if (!this.lastOutputEndedWithNewline) {
|
|
801
|
-
this.write('\n');
|
|
802
|
-
this.lastOutputEndedWithNewline = true;
|
|
803
|
-
}
|
|
804
|
-
// Write blank lines to push the overlay below scrollback
|
|
805
|
-
// This ensures the overlay doesn't mix with scrollback content
|
|
806
|
-
for (let i = 0; i < height; i++) {
|
|
807
|
-
this.write('\n');
|
|
808
|
-
}
|
|
809
|
-
// Move cursor back up to where we want to start the overlay
|
|
810
|
-
if (height > 0) {
|
|
811
|
-
this.write(`\x1b[${height}A`);
|
|
812
|
-
}
|
|
813
|
-
this.write('\r');
|
|
971
|
+
const renderedLines = overlay.lines.map(line => this.truncateLine(line, maxWidth));
|
|
972
|
+
if (!renderedLines.length) {
|
|
973
|
+
return;
|
|
814
974
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
this.write(
|
|
975
|
+
let promptIndex = Math.max(0, Math.min(overlay.promptIndex, renderedLines.length - 1));
|
|
976
|
+
let height = renderedLines.length;
|
|
977
|
+
// Keep at least one free line below the overlay so typing always has breathing room
|
|
978
|
+
const bottomPadding = this.overlayBottomPadding;
|
|
979
|
+
const totalRows = this.rows || 24;
|
|
980
|
+
const availableRows = Math.max(1, totalRows - bottomPadding);
|
|
981
|
+
if (height > availableRows) {
|
|
982
|
+
renderedLines.splice(availableRows);
|
|
983
|
+
height = renderedLines.length;
|
|
984
|
+
promptIndex = Math.max(0, Math.min(promptIndex, height - 1));
|
|
985
|
+
}
|
|
986
|
+
const startRow = Math.max(1, availableRows - height + 1);
|
|
987
|
+
const promptRow = startRow + promptIndex;
|
|
988
|
+
const promptCol = Math.min(Math.max(1, 3 + this.cursor), this.cols || 80);
|
|
989
|
+
// Clear any previous overlay footprint (status, prompt, controls) to avoid leaking into scrollback
|
|
990
|
+
this.clearOverlayRows(height, startRow);
|
|
991
|
+
if (bottomPadding > 0 && startRow + height <= totalRows) {
|
|
992
|
+
this.write(ESC.TO(startRow + height, 1));
|
|
993
|
+
this.write(ESC.CLEAR_LINE);
|
|
833
994
|
}
|
|
834
|
-
//
|
|
995
|
+
// Render overlay lines in place without pushing scrollback
|
|
835
996
|
for (let idx = 0; idx < height; idx++) {
|
|
836
|
-
const
|
|
997
|
+
const row = startRow + idx;
|
|
998
|
+
const line = renderedLines[idx] ?? '';
|
|
999
|
+
this.write(ESC.TO(row, 1));
|
|
837
1000
|
this.write(ESC.CLEAR_LINE);
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
this.write('\n');
|
|
1001
|
+
if (line) {
|
|
1002
|
+
this.write(line);
|
|
841
1003
|
}
|
|
842
1004
|
}
|
|
843
1005
|
// Position cursor at prompt row/col
|
|
844
|
-
|
|
845
|
-
if (fromBottomToPrompt > 0) {
|
|
846
|
-
this.write(`\x1b[${fromBottomToPrompt}A`);
|
|
847
|
-
}
|
|
848
|
-
else if (fromBottomToPrompt < 0) {
|
|
849
|
-
this.write(`\x1b[${-fromBottomToPrompt}B`);
|
|
850
|
-
}
|
|
851
|
-
const promptCol = Math.min(Math.max(1, 3 + this.cursor), this.cols);
|
|
852
|
-
this.write('\r');
|
|
853
|
-
this.write(ESC.TO_COL(promptCol));
|
|
1006
|
+
this.write(ESC.TO(promptRow, promptCol));
|
|
854
1007
|
this.cursorVisibleColumn = promptCol;
|
|
855
1008
|
this.hasRenderedPrompt = true;
|
|
856
1009
|
this.hasEverRenderedOverlay = true; // Mark that we've rendered at least once
|
|
857
1010
|
this.isPromptActive = true;
|
|
858
1011
|
this.lastOverlayHeight = height;
|
|
859
1012
|
this.lastPromptIndex = promptIndex;
|
|
860
|
-
this.lastOverlay = { lines, promptIndex };
|
|
1013
|
+
this.lastOverlay = { lines: renderedLines, promptIndex };
|
|
861
1014
|
this.overlayInvalidated = false;
|
|
862
1015
|
this.lastOutputEndedWithNewline = true;
|
|
863
1016
|
this.promptHeight = height;
|
|
@@ -865,6 +1018,8 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
865
1018
|
buildOverlayLines() {
|
|
866
1019
|
const lines = [];
|
|
867
1020
|
const maxWidth = this.safeWidth();
|
|
1021
|
+
// Top border to frame status/meta block
|
|
1022
|
+
lines.push(this.truncateLine(renderDivider(Math.min(maxWidth, 96), 'status'), maxWidth));
|
|
868
1023
|
const chromeLines = this.buildChromeLines();
|
|
869
1024
|
for (const line of chromeLines) {
|
|
870
1025
|
lines.push(this.truncateLine(line, maxWidth));
|
|
@@ -895,12 +1050,10 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
895
1050
|
return { lines, promptIndex };
|
|
896
1051
|
}
|
|
897
1052
|
buildChromeLines() {
|
|
898
|
-
const parts = [...this.buildStatusParts(), ...this.buildMetaParts()];
|
|
899
|
-
if (parts.length === 0) {
|
|
900
|
-
return [];
|
|
901
|
-
}
|
|
902
1053
|
const maxWidth = this.safeWidth();
|
|
903
|
-
|
|
1054
|
+
const statusLines = this.buildStatusBlock(maxWidth);
|
|
1055
|
+
const metaLines = this.buildMetaBlock(maxWidth);
|
|
1056
|
+
return [...statusLines, ...metaLines];
|
|
904
1057
|
}
|
|
905
1058
|
abbreviatePath(pathValue) {
|
|
906
1059
|
const home = homedir();
|
|
@@ -909,54 +1062,116 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
909
1062
|
}
|
|
910
1063
|
return pathValue;
|
|
911
1064
|
}
|
|
912
|
-
|
|
913
|
-
const
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
const
|
|
1065
|
+
buildStatusBlock(maxWidth) {
|
|
1066
|
+
const statusLabel = this.composeStatusLabel();
|
|
1067
|
+
if (!statusLabel) {
|
|
1068
|
+
return [];
|
|
1069
|
+
}
|
|
1070
|
+
const segments = [];
|
|
1071
|
+
segments.push(`${theme.ui.muted('status')} ${this.applyTone(statusLabel.text, statusLabel.tone)}`);
|
|
918
1072
|
if (this.statusMeta.sessionTime) {
|
|
919
|
-
|
|
1073
|
+
segments.push(`${theme.ui.muted('runtime')} ${theme.ui.muted(this.statusMeta.sessionTime)}`);
|
|
920
1074
|
}
|
|
921
1075
|
if (this.statusMeta.contextPercent !== undefined) {
|
|
922
1076
|
const ctx = this.statusMeta.contextPercent;
|
|
923
|
-
const
|
|
924
|
-
|
|
1077
|
+
const tone = ctx > 90 ? 'error' : ctx > 70 ? 'warn' : 'muted';
|
|
1078
|
+
const color = tone === 'error' ? theme.error : tone === 'warn' ? theme.warning : theme.ui.muted;
|
|
1079
|
+
segments.push(`${theme.ui.muted('ctx')} ${color(`${ctx}%`)}`);
|
|
925
1080
|
}
|
|
926
|
-
return
|
|
1081
|
+
return this.wrapSegments(segments, maxWidth);
|
|
927
1082
|
}
|
|
928
|
-
|
|
929
|
-
const
|
|
1083
|
+
buildMetaBlock(maxWidth) {
|
|
1084
|
+
const segments = [];
|
|
930
1085
|
if (this.statusMeta.profile) {
|
|
931
|
-
|
|
1086
|
+
segments.push(this.formatMetaSegment('profile', this.statusMeta.profile, 'info'));
|
|
932
1087
|
}
|
|
933
1088
|
const model = this.statusMeta.provider && this.statusMeta.model
|
|
934
|
-
? `${this.statusMeta.provider}
|
|
1089
|
+
? `${this.statusMeta.provider} / ${this.statusMeta.model}`
|
|
935
1090
|
: this.statusMeta.model || this.statusMeta.provider;
|
|
936
1091
|
if (model) {
|
|
937
|
-
|
|
1092
|
+
segments.push(this.formatMetaSegment('model', model, 'info'));
|
|
938
1093
|
}
|
|
939
1094
|
const workspace = this.statusMeta.workspace || this.statusMeta.directory;
|
|
940
1095
|
if (workspace) {
|
|
941
|
-
|
|
1096
|
+
segments.push(this.formatMetaSegment('dir', this.abbreviatePath(workspace), 'muted'));
|
|
942
1097
|
}
|
|
943
1098
|
if (this.statusMeta.writes) {
|
|
944
|
-
|
|
1099
|
+
segments.push(this.formatMetaSegment('writes', this.statusMeta.writes, 'muted'));
|
|
945
1100
|
}
|
|
946
1101
|
if (this.statusMeta.toolSummary) {
|
|
947
|
-
|
|
1102
|
+
segments.push(this.formatMetaSegment('tools', this.statusMeta.toolSummary, 'muted'));
|
|
948
1103
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
parts.push({ text: `session ${sessionLabel}`, tone: 'muted' });
|
|
1104
|
+
if (this.statusMeta.sessionLabel) {
|
|
1105
|
+
segments.push(this.formatMetaSegment('session', this.statusMeta.sessionLabel, 'muted'));
|
|
952
1106
|
}
|
|
953
1107
|
if (this.statusMeta.version) {
|
|
954
|
-
|
|
1108
|
+
segments.push(this.formatMetaSegment('build', `v${this.statusMeta.version}`, 'muted'));
|
|
955
1109
|
}
|
|
956
|
-
if (
|
|
1110
|
+
if (segments.length === 0) {
|
|
957
1111
|
return [];
|
|
958
1112
|
}
|
|
959
|
-
return
|
|
1113
|
+
return this.wrapSegments(segments, maxWidth);
|
|
1114
|
+
}
|
|
1115
|
+
composeStatusLabel() {
|
|
1116
|
+
const statuses = [this.statusStreaming, this.statusOverride, this.statusMessage].filter((value, index, arr) => Boolean(value) && arr.indexOf(value) === index);
|
|
1117
|
+
const text = statuses.length > 0 ? statuses.join(' / ') : 'Ready for prompts';
|
|
1118
|
+
if (!text.trim()) {
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
const normalized = text.toLowerCase();
|
|
1122
|
+
const tone = normalized.includes('ready') ? 'success' : 'info';
|
|
1123
|
+
return { text, tone };
|
|
1124
|
+
}
|
|
1125
|
+
formatMetaSegment(label, value, tone) {
|
|
1126
|
+
const colorizer = tone === 'success'
|
|
1127
|
+
? theme.success
|
|
1128
|
+
: tone === 'warn'
|
|
1129
|
+
? theme.warning
|
|
1130
|
+
: tone === 'error'
|
|
1131
|
+
? theme.error
|
|
1132
|
+
: tone === 'muted'
|
|
1133
|
+
? theme.ui.muted
|
|
1134
|
+
: theme.info;
|
|
1135
|
+
return `${theme.ui.muted(label)} ${colorizer(value)}`;
|
|
1136
|
+
}
|
|
1137
|
+
applyTone(text, tone) {
|
|
1138
|
+
switch (tone) {
|
|
1139
|
+
case 'success':
|
|
1140
|
+
return theme.success(text);
|
|
1141
|
+
case 'warn':
|
|
1142
|
+
return theme.warning(text);
|
|
1143
|
+
case 'error':
|
|
1144
|
+
return theme.error(text);
|
|
1145
|
+
case 'info':
|
|
1146
|
+
default:
|
|
1147
|
+
return theme.info(text);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
wrapSegments(segments, maxWidth) {
|
|
1151
|
+
const lines = [];
|
|
1152
|
+
const separator = theme.ui.muted(' | ');
|
|
1153
|
+
let current = '';
|
|
1154
|
+
for (const segment of segments) {
|
|
1155
|
+
const normalized = segment.trim();
|
|
1156
|
+
if (!normalized)
|
|
1157
|
+
continue;
|
|
1158
|
+
if (!current) {
|
|
1159
|
+
current = this.truncateLine(normalized, maxWidth);
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
const candidate = `${current}${separator}${normalized}`;
|
|
1163
|
+
if (this.visibleLength(candidate) <= maxWidth) {
|
|
1164
|
+
current = candidate;
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
lines.push(this.truncateLine(current, maxWidth));
|
|
1168
|
+
current = this.truncateLine(normalized, maxWidth);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
if (current) {
|
|
1172
|
+
lines.push(this.truncateLine(current, maxWidth));
|
|
1173
|
+
}
|
|
1174
|
+
return lines;
|
|
960
1175
|
}
|
|
961
1176
|
buildControlLines() {
|
|
962
1177
|
const lines = [];
|
|
@@ -1084,6 +1299,37 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1084
1299
|
this.renderPrompt();
|
|
1085
1300
|
this.emitInputChange();
|
|
1086
1301
|
}
|
|
1302
|
+
captureInput(options = {}) {
|
|
1303
|
+
if (this.inputCapture) {
|
|
1304
|
+
return Promise.reject(new Error('Input capture already in progress'));
|
|
1305
|
+
}
|
|
1306
|
+
if (options.resetBuffer) {
|
|
1307
|
+
this.buffer = '';
|
|
1308
|
+
this.cursor = 0;
|
|
1309
|
+
this.inputRenderOffset = 0;
|
|
1310
|
+
this.resetSuggestions();
|
|
1311
|
+
this.renderPrompt();
|
|
1312
|
+
this.emitInputChange();
|
|
1313
|
+
}
|
|
1314
|
+
return new Promise((resolve, reject) => {
|
|
1315
|
+
this.inputCapture = {
|
|
1316
|
+
resolve,
|
|
1317
|
+
reject,
|
|
1318
|
+
options: {
|
|
1319
|
+
trim: options.trim !== false,
|
|
1320
|
+
allowEmpty: options.allowEmpty ?? false,
|
|
1321
|
+
},
|
|
1322
|
+
};
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
cancelInputCapture(reason) {
|
|
1326
|
+
if (!this.inputCapture) {
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
const capture = this.inputCapture;
|
|
1330
|
+
this.inputCapture = null;
|
|
1331
|
+
capture.reject?.(reason ?? new Error('Input capture cancelled'));
|
|
1332
|
+
}
|
|
1087
1333
|
// ------------ Helpers ------------
|
|
1088
1334
|
safeWidth() {
|
|
1089
1335
|
const cols = this.output.isTTY ? this.cols || 80 : 80;
|
|
@@ -1139,6 +1385,17 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1139
1385
|
}
|
|
1140
1386
|
return result;
|
|
1141
1387
|
}
|
|
1388
|
+
clearOverlayRows(rows, startRow) {
|
|
1389
|
+
const totalRows = this.rows || 24;
|
|
1390
|
+
const limit = Math.max(0, Math.min(rows, totalRows));
|
|
1391
|
+
for (let idx = 0; idx < limit; idx++) {
|
|
1392
|
+
const row = startRow + idx;
|
|
1393
|
+
if (row < 1 || row > totalRows)
|
|
1394
|
+
continue;
|
|
1395
|
+
this.write(ESC.TO(row, 1));
|
|
1396
|
+
this.write(ESC.CLEAR_LINE);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1142
1399
|
getBuffer() {
|
|
1143
1400
|
return this.buffer;
|
|
1144
1401
|
}
|
|
@@ -1154,6 +1411,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1154
1411
|
this.emitInputChange();
|
|
1155
1412
|
}
|
|
1156
1413
|
clearBuffer() {
|
|
1414
|
+
this.cancelPlainPasteCapture();
|
|
1157
1415
|
this.buffer = '';
|
|
1158
1416
|
this.cursor = 0;
|
|
1159
1417
|
this.inputRenderOffset = 0;
|
|
@@ -1205,28 +1463,26 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1205
1463
|
this.addEvent('prompt', normalized);
|
|
1206
1464
|
}
|
|
1207
1465
|
clearPromptArea() {
|
|
1208
|
-
const
|
|
1209
|
-
if (
|
|
1466
|
+
const height = this.lastOverlay?.lines.length ?? this.promptHeight ?? 0;
|
|
1467
|
+
if (height === 0)
|
|
1210
1468
|
return;
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1469
|
+
this.updateTerminalSize();
|
|
1470
|
+
const totalRows = this.rows || 24;
|
|
1471
|
+
const startRow = Math.max(1, Math.max(1, totalRows - this.overlayBottomPadding) - height + 1);
|
|
1472
|
+
this.clearOverlayRows(height, startRow);
|
|
1473
|
+
// Keep the padding row clean as well
|
|
1474
|
+
const paddingRow = startRow + height;
|
|
1475
|
+
if (this.overlayBottomPadding > 0 && paddingRow <= totalRows) {
|
|
1476
|
+
this.write(ESC.TO(paddingRow, 1));
|
|
1217
1477
|
this.write(ESC.CLEAR_LINE);
|
|
1218
|
-
if (i < rows - 1) {
|
|
1219
|
-
this.write('\n');
|
|
1220
|
-
}
|
|
1221
1478
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
this.
|
|
1226
|
-
this.lastOverlayHeight = rows;
|
|
1227
|
-
this.lastPromptIndex = promptIndex;
|
|
1479
|
+
// Move cursor to the bottom ready for new scrollback output
|
|
1480
|
+
this.write(ESC.TO(totalRows, 1));
|
|
1481
|
+
this.lastOverlayHeight = height;
|
|
1482
|
+
this.lastPromptIndex = this.lastOverlay?.promptIndex ?? this.lastPromptIndex;
|
|
1228
1483
|
this.lastOverlay = null;
|
|
1229
1484
|
this.overlayInvalidated = true;
|
|
1485
|
+
this.promptHeight = 0;
|
|
1230
1486
|
}
|
|
1231
1487
|
updateTerminalSize() {
|
|
1232
1488
|
if (this.output.isTTY) {
|