erosolar-cli 1.7.383 → 1.7.386
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/contextManager.d.ts +4 -0
- package/dist/core/contextManager.d.ts.map +1 -1
- package/dist/core/contextManager.js +16 -0
- package/dist/core/contextManager.js.map +1 -1
- package/dist/core/secretStore.d.ts +1 -0
- package/dist/core/secretStore.d.ts.map +1 -1
- package/dist/core/secretStore.js +3 -0
- package/dist/core/secretStore.js.map +1 -1
- package/dist/core/toolPreconditions.d.ts.map +1 -1
- package/dist/core/toolPreconditions.js +9 -0
- package/dist/core/toolPreconditions.js.map +1 -1
- package/dist/core/toolRuntime.d.ts +6 -0
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +7 -0
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/runtime/agentHost.d.ts +3 -1
- package/dist/runtime/agentHost.d.ts.map +1 -1
- package/dist/runtime/agentHost.js +3 -0
- package/dist/runtime/agentHost.js.map +1 -1
- package/dist/runtime/agentSession.d.ts +2 -1
- package/dist/runtime/agentSession.d.ts.map +1 -1
- package/dist/runtime/agentSession.js +1 -0
- package/dist/runtime/agentSession.js.map +1 -1
- package/dist/runtime/universal.d.ts +2 -1
- package/dist/runtime/universal.d.ts.map +1 -1
- package/dist/runtime/universal.js +1 -0
- package/dist/runtime/universal.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +34 -0
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +424 -139
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/shellApp.d.ts.map +1 -1
- package/dist/shell/shellApp.js +5 -0
- package/dist/shell/shellApp.js.map +1 -1
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +4 -0
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +43 -0
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +273 -28
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +9 -0
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +8 -2
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/tools/fileTools.d.ts.map +1 -1
- package/dist/tools/fileTools.js +29 -5
- package/dist/tools/fileTools.js.map +1 -1
- package/dist/tools/grepTools.d.ts.map +1 -1
- package/dist/tools/grepTools.js +22 -4
- package/dist/tools/grepTools.js.map +1 -1
- package/dist/tools/searchTools.d.ts.map +1 -1
- package/dist/tools/searchTools.js +47 -13
- package/dist/tools/searchTools.js.map +1 -1
- package/dist/tools/webTools.d.ts.map +1 -1
- package/dist/tools/webTools.js +36 -9
- package/dist/tools/webTools.js.map +1 -1
- package/dist/ui/ShellUIAdapter.d.ts +13 -0
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +40 -3
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/display.d.ts +1 -0
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +61 -12
- package/dist/ui/display.js.map +1 -1
- package/dist/ui/layout.js +8 -7
- package/dist/ui/layout.js.map +1 -1
- package/dist/ui/unified/layout.js +2 -2
- package/dist/ui/unified/layout.js.map +1 -1
- package/package.json +1 -1
|
@@ -67,6 +67,7 @@ const ESC = {
|
|
|
67
67
|
export class TerminalInput extends EventEmitter {
|
|
68
68
|
out;
|
|
69
69
|
config;
|
|
70
|
+
inlinePanel = null;
|
|
70
71
|
// Core state
|
|
71
72
|
buffer = '';
|
|
72
73
|
cursor = 0;
|
|
@@ -132,6 +133,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
132
133
|
showingSuggestions = false;
|
|
133
134
|
selectedSuggestionIndex = 0;
|
|
134
135
|
maxVisibleSuggestions = 10;
|
|
136
|
+
suggestionRenderState = null;
|
|
137
|
+
lastSuggestionStartRow = null;
|
|
138
|
+
lastChatBoxStartRow = null;
|
|
139
|
+
lastChatBoxHeight = 0;
|
|
140
|
+
displayInterceptorDispose = null;
|
|
135
141
|
constructor(writeStream = process.stdout, config = {}) {
|
|
136
142
|
super();
|
|
137
143
|
this.out = writeStream;
|
|
@@ -192,7 +198,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
192
198
|
let match;
|
|
193
199
|
while ((match = mousePattern.exec(data)) !== null) {
|
|
194
200
|
const button = parseInt(match[1], 10);
|
|
195
|
-
|
|
201
|
+
const x = parseInt(match[2], 10);
|
|
202
|
+
const y = parseInt(match[3], 10);
|
|
203
|
+
this.handleMouseEvent(button, x, y, match[4]);
|
|
196
204
|
hadMouseEvents = true;
|
|
197
205
|
}
|
|
198
206
|
if (hadMouseEvents) {
|
|
@@ -240,7 +248,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
240
248
|
/**
|
|
241
249
|
* Handle mouse events (button, x, y coordinates, action)
|
|
242
250
|
*/
|
|
243
|
-
handleMouseEvent(button,
|
|
251
|
+
handleMouseEvent(button, x, y, action) {
|
|
244
252
|
// Mouse wheel events: button 64 = scroll up, button 65 = scroll down
|
|
245
253
|
if (button === 64) {
|
|
246
254
|
// Scroll up (3 lines per wheel tick)
|
|
@@ -252,6 +260,13 @@ export class TerminalInput extends EventEmitter {
|
|
|
252
260
|
this.scrollDown(3);
|
|
253
261
|
return;
|
|
254
262
|
}
|
|
263
|
+
// Left-click selection inside the command suggestions menu
|
|
264
|
+
if (button === 0 && action === 'M') {
|
|
265
|
+
if (this.handleSuggestionClick(y)) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// No other left-click handling needed; fall through to consume silently
|
|
269
|
+
}
|
|
255
270
|
// Left button (0), middle button (1), right button (2)
|
|
256
271
|
// These are captured by SGR mouse tracking but we don't need to handle them
|
|
257
272
|
// Just consume silently - the escape sequence has already been processed
|
|
@@ -292,6 +307,10 @@ export class TerminalInput extends EventEmitter {
|
|
|
292
307
|
if (handled)
|
|
293
308
|
return;
|
|
294
309
|
}
|
|
310
|
+
// Ignore orphaned escape fragments that sometimes leak through (e.g., "[D" from arrow keys)
|
|
311
|
+
if (str && this.isOrphanedEscapeSequence(str, key)) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
295
314
|
// Insert printable characters
|
|
296
315
|
if (str && !key?.ctrl && !key?.meta) {
|
|
297
316
|
this.insertText(str);
|
|
@@ -479,6 +498,26 @@ export class TerminalInput extends EventEmitter {
|
|
|
479
498
|
this.scheduleRender();
|
|
480
499
|
return true;
|
|
481
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* Handle mouse selection inside the suggestions list.
|
|
503
|
+
* Returns true if the click selected a command.
|
|
504
|
+
*/
|
|
505
|
+
handleSuggestionClick(clickRow) {
|
|
506
|
+
if (!this.showingSuggestions || !this.suggestionRenderState)
|
|
507
|
+
return false;
|
|
508
|
+
if (this.lastSuggestionStartRow === null)
|
|
509
|
+
return false;
|
|
510
|
+
// First line is the header; suggestions start on the next row
|
|
511
|
+
const relativeRow = clickRow - this.lastSuggestionStartRow - 1;
|
|
512
|
+
if (relativeRow < 0)
|
|
513
|
+
return false;
|
|
514
|
+
const { startIdx, visibleCount } = this.suggestionRenderState;
|
|
515
|
+
if (relativeRow >= visibleCount)
|
|
516
|
+
return false;
|
|
517
|
+
this.selectedSuggestionIndex = startIdx + relativeRow;
|
|
518
|
+
this.selectSuggestion();
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
482
521
|
/**
|
|
483
522
|
* Cancel suggestion display (Escape)
|
|
484
523
|
*/
|
|
@@ -493,6 +532,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
493
532
|
* Build command suggestion display lines
|
|
494
533
|
*/
|
|
495
534
|
buildSuggestionLines(width) {
|
|
535
|
+
this.suggestionRenderState = null;
|
|
496
536
|
if (!this.showingSuggestions)
|
|
497
537
|
return [];
|
|
498
538
|
const filtered = this.getFilteredCommands();
|
|
@@ -508,6 +548,10 @@ export class TerminalInput extends EventEmitter {
|
|
|
508
548
|
startIdx = Math.max(0, this.selectedSuggestionIndex - padding);
|
|
509
549
|
startIdx = Math.min(startIdx, filtered.length - maxDisplay);
|
|
510
550
|
}
|
|
551
|
+
this.suggestionRenderState = {
|
|
552
|
+
startIdx,
|
|
553
|
+
visibleCount: Math.min(maxDisplay, Math.max(0, filtered.length - startIdx))
|
|
554
|
+
};
|
|
511
555
|
// Header with navigation hint - keep it short
|
|
512
556
|
lines.push(theme.ui.muted(`Commands (↑↓ Tab Enter)`));
|
|
513
557
|
// Render visible suggestions
|
|
@@ -542,6 +586,56 @@ export class TerminalInput extends EventEmitter {
|
|
|
542
586
|
}
|
|
543
587
|
return lines;
|
|
544
588
|
}
|
|
589
|
+
/**
|
|
590
|
+
* Build inline panel lines for settings/menus rendered inside the input area.
|
|
591
|
+
*/
|
|
592
|
+
buildInlinePanelLines(width) {
|
|
593
|
+
if (!this.inlinePanel)
|
|
594
|
+
return [];
|
|
595
|
+
const panel = this.inlinePanel;
|
|
596
|
+
const lines = [];
|
|
597
|
+
const safeWidth = Math.max(10, width - 2);
|
|
598
|
+
const colorize = (() => {
|
|
599
|
+
switch (panel.tone) {
|
|
600
|
+
case 'success':
|
|
601
|
+
return theme.success;
|
|
602
|
+
case 'warning':
|
|
603
|
+
return theme.warning;
|
|
604
|
+
case 'error':
|
|
605
|
+
return theme.error;
|
|
606
|
+
case 'muted':
|
|
607
|
+
return theme.ui.muted;
|
|
608
|
+
default:
|
|
609
|
+
return theme.info;
|
|
610
|
+
}
|
|
611
|
+
})();
|
|
612
|
+
if (panel.title) {
|
|
613
|
+
const wrappedTitle = this.wrapPlainText(panel.title, safeWidth);
|
|
614
|
+
for (const line of wrappedTitle) {
|
|
615
|
+
lines.push(theme.primary(line));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
for (const rawLine of panel.lines) {
|
|
619
|
+
const wrapped = this.wrapPlainText(rawLine, safeWidth);
|
|
620
|
+
for (const wrappedLine of wrapped) {
|
|
621
|
+
lines.push(` ${colorize(wrappedLine)}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return lines;
|
|
625
|
+
}
|
|
626
|
+
countInlinePanelLines(width) {
|
|
627
|
+
if (!this.inlinePanel)
|
|
628
|
+
return 0;
|
|
629
|
+
const safeWidth = Math.max(10, width - 2);
|
|
630
|
+
let count = 0;
|
|
631
|
+
if (this.inlinePanel.title) {
|
|
632
|
+
count += this.wrapPlainText(this.inlinePanel.title, safeWidth).length;
|
|
633
|
+
}
|
|
634
|
+
for (const rawLine of this.inlinePanel.lines) {
|
|
635
|
+
count += this.wrapPlainText(rawLine, safeWidth).length;
|
|
636
|
+
}
|
|
637
|
+
return count;
|
|
638
|
+
}
|
|
545
639
|
/**
|
|
546
640
|
* Update the inline status message shown before the prompt (e.g., streaming heartbeat).
|
|
547
641
|
*/
|
|
@@ -670,6 +764,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
670
764
|
this.providerLabel = nextProvider;
|
|
671
765
|
this.scheduleRender();
|
|
672
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* Show an inline panel (menus/settings) within the pinned input area.
|
|
769
|
+
* This keeps configuration flows out of the scrollback history.
|
|
770
|
+
*/
|
|
771
|
+
setInlinePanel(panel) {
|
|
772
|
+
if (panel && panel.lines.length === 0) {
|
|
773
|
+
this.inlinePanel = null;
|
|
774
|
+
this.scheduleRender();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
this.inlinePanel = panel ? { ...panel, lines: [...panel.lines] } : null;
|
|
778
|
+
this.scheduleRender();
|
|
779
|
+
}
|
|
673
780
|
/**
|
|
674
781
|
* Render the floating input area at contentRow.
|
|
675
782
|
*
|
|
@@ -733,9 +840,16 @@ export class TerminalInput extends EventEmitter {
|
|
|
733
840
|
// ALWAYS pin chat box at absolute bottom
|
|
734
841
|
const chatBoxStartRow = Math.max(1, rows - chatBoxHeight + 1);
|
|
735
842
|
const scrollEnd = chatBoxStartRow - 1;
|
|
843
|
+
this.lastChatBoxStartRow = chatBoxStartRow;
|
|
844
|
+
this.lastChatBoxHeight = chatBoxHeight;
|
|
845
|
+
this.lastSuggestionStartRow = null;
|
|
736
846
|
writeLock.lock('terminalInput.renderPinned');
|
|
737
847
|
this.isRendering = true;
|
|
738
848
|
try {
|
|
849
|
+
// Ensure content stays above the expanded chat/menu area when idle
|
|
850
|
+
if (!streamingActive && !this.scrollRegionActive) {
|
|
851
|
+
this.ensureContentAboveChatBox(chatBoxStartRow, rows);
|
|
852
|
+
}
|
|
739
853
|
this.write(ESC.SAVE);
|
|
740
854
|
this.write(ESC.HIDE);
|
|
741
855
|
// Temporarily reset scroll region to write chat box cleanly
|
|
@@ -773,15 +887,15 @@ export class TerminalInput extends EventEmitter {
|
|
|
773
887
|
const line = visibleLines[i] ?? '';
|
|
774
888
|
const isFirstLine = (startLine + i) === 0;
|
|
775
889
|
const isCursorLine = i === adjustedCursorLine;
|
|
890
|
+
const col = isCursorLine ? Math.max(0, Math.min(cursorCol, line.length)) : 0;
|
|
776
891
|
this.write(ESC.BG_DARK);
|
|
777
892
|
this.write(ESC.DIM);
|
|
778
893
|
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
779
894
|
this.write(ESC.RESET);
|
|
780
895
|
this.write(ESC.BG_DARK);
|
|
781
896
|
if (isCursorLine) {
|
|
782
|
-
const col = Math.min(cursorCol, line.length);
|
|
783
897
|
const before = line.slice(0, col);
|
|
784
|
-
const at =
|
|
898
|
+
const at = line.charAt(col) || ' ';
|
|
785
899
|
const after = col < line.length ? line.slice(col + 1) : '';
|
|
786
900
|
this.write(before);
|
|
787
901
|
this.write(ESC.REVERSE + ESC.BOLD);
|
|
@@ -795,7 +909,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
795
909
|
this.write(line);
|
|
796
910
|
}
|
|
797
911
|
// Pad to edge
|
|
798
|
-
const lineLen = this.config.promptChar.length +
|
|
912
|
+
const lineLen = this.config.promptChar.length +
|
|
913
|
+
line.length +
|
|
914
|
+
(isCursorLine && col >= line.length ? 1 : 0);
|
|
799
915
|
const padding = Math.max(0, cols - lineLen - 1);
|
|
800
916
|
if (padding > 0)
|
|
801
917
|
this.write(' '.repeat(padding));
|
|
@@ -804,16 +920,27 @@ export class TerminalInput extends EventEmitter {
|
|
|
804
920
|
// Command suggestions (shown above controls when "/" is typed)
|
|
805
921
|
const suggestionLines = this.buildSuggestionLines(cols - 2);
|
|
806
922
|
let suggestionRow = currentRow + visibleLines.length;
|
|
923
|
+
if (suggestionLines.length > 0) {
|
|
924
|
+
this.lastSuggestionStartRow = suggestionRow;
|
|
925
|
+
}
|
|
807
926
|
for (const suggestionLine of suggestionLines) {
|
|
808
927
|
this.write(ESC.TO(suggestionRow, 1));
|
|
809
928
|
this.write(' '); // Indent to match input
|
|
810
929
|
this.write(suggestionLine);
|
|
811
930
|
suggestionRow += 1;
|
|
812
931
|
}
|
|
932
|
+
// Inline panel (menus/settings) rendered inside the pinned area
|
|
933
|
+
const panelLines = this.buildInlinePanelLines(cols - 2);
|
|
934
|
+
let panelRow = suggestionRow;
|
|
935
|
+
for (const panelLine of panelLines) {
|
|
936
|
+
this.write(ESC.TO(panelRow, 1));
|
|
937
|
+
this.write(panelLine);
|
|
938
|
+
panelRow += 1;
|
|
939
|
+
}
|
|
813
940
|
// Mode controls lines with all keyboard shortcuts
|
|
814
941
|
// Can be multiple lines during streaming (status + toggles)
|
|
815
942
|
const controlLines = this.buildModeControls(cols);
|
|
816
|
-
let controlRow =
|
|
943
|
+
let controlRow = panelRow;
|
|
817
944
|
for (const controlLine of controlLines) {
|
|
818
945
|
this.write(ESC.TO(controlRow, 1));
|
|
819
946
|
this.write(controlLine);
|
|
@@ -846,6 +973,69 @@ export class TerminalInput extends EventEmitter {
|
|
|
846
973
|
this.isRendering = false;
|
|
847
974
|
}
|
|
848
975
|
}
|
|
976
|
+
/**
|
|
977
|
+
* Keep assistant/user content above the chat box when it expands (e.g., slash menu).
|
|
978
|
+
* Scrolls just enough to free space without moving the cursor.
|
|
979
|
+
*/
|
|
980
|
+
ensureContentAboveChatBox(chatBoxStartRow, rows) {
|
|
981
|
+
const contentMaxRow = Math.max(1, chatBoxStartRow - 1);
|
|
982
|
+
if (this.contentRow < contentMaxRow)
|
|
983
|
+
return;
|
|
984
|
+
const overflow = (this.contentRow - contentMaxRow) + 1;
|
|
985
|
+
const scrollLines = Math.min(overflow, rows);
|
|
986
|
+
this.write(ESC.SAVE);
|
|
987
|
+
this.write(ESC.TO(rows, 1));
|
|
988
|
+
this.write('\n'.repeat(scrollLines));
|
|
989
|
+
this.write(ESC.RESTORE);
|
|
990
|
+
this.contentRow = contentMaxRow;
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Track output written directly by the display (system/assistant messages) so
|
|
994
|
+
* it is captured in scrollback and accounted for when re-rendering the chat box.
|
|
995
|
+
*/
|
|
996
|
+
trackExternalOutput(content) {
|
|
997
|
+
if (!content)
|
|
998
|
+
return;
|
|
999
|
+
// Normalize and capture in scrollback for history
|
|
1000
|
+
const normalized = content.replace(/\r/g, '\n');
|
|
1001
|
+
this.addToScrollback(normalized);
|
|
1002
|
+
// Advance content row based on rendered height (accounts for wrapping)
|
|
1003
|
+
const rowsUsed = this.countRenderedRows(normalized);
|
|
1004
|
+
if (rowsUsed > 0) {
|
|
1005
|
+
this.contentRow += rowsUsed;
|
|
1006
|
+
if (!this.scrollRegionActive) {
|
|
1007
|
+
this.scheduleRender();
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Estimate how many terminal rows the provided content will consume.
|
|
1013
|
+
* Considers ANSI-stripped visible length and terminal width so wrapped lines
|
|
1014
|
+
* correctly advance the tracked cursor position.
|
|
1015
|
+
*/
|
|
1016
|
+
countRenderedRows(content) {
|
|
1017
|
+
if (!content)
|
|
1018
|
+
return 0;
|
|
1019
|
+
const width = Math.max(1, this.getSize().cols);
|
|
1020
|
+
const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
1021
|
+
const lines = normalized.split('\n');
|
|
1022
|
+
let rows = 0;
|
|
1023
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1024
|
+
const line = lines[i] ?? '';
|
|
1025
|
+
const visibleLen = this.visibleLength(line);
|
|
1026
|
+
if (visibleLen > 0) {
|
|
1027
|
+
const wraps = Math.ceil(visibleLen / width);
|
|
1028
|
+
if (wraps > 1) {
|
|
1029
|
+
rows += wraps - 1;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
// Each newline moves the cursor to the start of the next row
|
|
1033
|
+
if (i < lines.length - 1) {
|
|
1034
|
+
rows += 1;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return rows;
|
|
1038
|
+
}
|
|
849
1039
|
/**
|
|
850
1040
|
* Build compact meta line above the divider.
|
|
851
1041
|
* Shows model/provider and key metrics in a single line.
|
|
@@ -1076,6 +1266,28 @@ export class TerminalInput extends EventEmitter {
|
|
|
1076
1266
|
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
1077
1267
|
return value.replace(ansiPattern, '').length;
|
|
1078
1268
|
}
|
|
1269
|
+
wrapPlainText(text, width) {
|
|
1270
|
+
const lines = [];
|
|
1271
|
+
const safeWidth = Math.max(1, width);
|
|
1272
|
+
const words = text.split(/\s+/);
|
|
1273
|
+
let current = '';
|
|
1274
|
+
for (const word of words) {
|
|
1275
|
+
const candidate = current ? `${current} ${word}` : word;
|
|
1276
|
+
if (this.visibleLength(candidate) <= safeWidth) {
|
|
1277
|
+
current = candidate;
|
|
1278
|
+
}
|
|
1279
|
+
else {
|
|
1280
|
+
if (current) {
|
|
1281
|
+
lines.push(current);
|
|
1282
|
+
}
|
|
1283
|
+
current = word;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
if (current) {
|
|
1287
|
+
lines.push(current);
|
|
1288
|
+
}
|
|
1289
|
+
return lines.length ? lines : [''];
|
|
1290
|
+
}
|
|
1079
1291
|
/**
|
|
1080
1292
|
* Debug-only snapshot used by tests to assert rendered strings without
|
|
1081
1293
|
* needing a TTY. Not used by production code.
|
|
@@ -1192,9 +1404,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1192
1404
|
try {
|
|
1193
1405
|
// Write content - scroll region handles scrolling
|
|
1194
1406
|
this.write(content);
|
|
1195
|
-
// Track
|
|
1196
|
-
const
|
|
1197
|
-
this.contentRow +=
|
|
1407
|
+
// Track rendered rows (handles wrapping)
|
|
1408
|
+
const rowsUsed = this.countRenderedRows(content);
|
|
1409
|
+
this.contentRow += rowsUsed;
|
|
1198
1410
|
}
|
|
1199
1411
|
finally {
|
|
1200
1412
|
writeLock.unlock();
|
|
@@ -1229,9 +1441,8 @@ export class TerminalInput extends EventEmitter {
|
|
|
1229
1441
|
// Calculate actual input lines needed (wrapped buffer)
|
|
1230
1442
|
const maxWidth = Math.max(1, cols - 4); // Account for prompt and padding
|
|
1231
1443
|
const { lines: wrappedLines } = this.wrapBuffer(maxWidth);
|
|
1232
|
-
//
|
|
1233
|
-
const
|
|
1234
|
-
const inputLines = Math.min(wrappedLines.length, maxInputLines);
|
|
1444
|
+
// Allow the chat box to grow with the prompt while respecting any explicit limit
|
|
1445
|
+
const inputLines = Math.max(1, Math.min(wrappedLines.length, this.config.maxLines));
|
|
1235
1446
|
// Calculate actual control lines (they wrap as needed)
|
|
1236
1447
|
const controlLines = this.buildModeControls(cols);
|
|
1237
1448
|
const controlLineCount = Math.max(1, controlLines.length);
|
|
@@ -1244,12 +1455,13 @@ export class TerminalInput extends EventEmitter {
|
|
|
1244
1455
|
const filtered = this.getFilteredCommands();
|
|
1245
1456
|
suggestionLines = Math.min(filtered.length, this.maxVisibleSuggestions) + 1; // +1 for header
|
|
1246
1457
|
}
|
|
1458
|
+
// Inline panel (menus/settings)
|
|
1459
|
+
const inlinePanelLines = this.countInlinePanelLines(cols - 2);
|
|
1247
1460
|
// Total: meta + divider + input lines + suggestions + controls + buffer
|
|
1248
|
-
//
|
|
1249
|
-
const totalHeight = metaLines + 1 + inputLines + suggestionLines + controlLineCount + 1;
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
: Math.max(6, Math.floor(rows * 0.4));
|
|
1461
|
+
// Leave a small buffer so streamed content still has space above the chat box
|
|
1462
|
+
const totalHeight = metaLines + 1 + inputLines + suggestionLines + inlinePanelLines + controlLineCount + 1;
|
|
1463
|
+
const minContentRows = 2;
|
|
1464
|
+
const maxHeight = Math.max(4, rows - minContentRows);
|
|
1253
1465
|
return Math.min(totalHeight, maxHeight);
|
|
1254
1466
|
}
|
|
1255
1467
|
/**
|
|
@@ -1257,7 +1469,21 @@ export class TerminalInput extends EventEmitter {
|
|
|
1257
1469
|
* Register with display's output interceptor - kept for backwards compatibility
|
|
1258
1470
|
*/
|
|
1259
1471
|
registerOutputInterceptor(_display) {
|
|
1260
|
-
|
|
1472
|
+
const display = _display;
|
|
1473
|
+
if (!display?.registerOutputInterceptor)
|
|
1474
|
+
return;
|
|
1475
|
+
// Remove prior hook if present
|
|
1476
|
+
if (this.displayInterceptorDispose) {
|
|
1477
|
+
this.displayInterceptorDispose();
|
|
1478
|
+
this.displayInterceptorDispose = null;
|
|
1479
|
+
}
|
|
1480
|
+
this.displayInterceptorDispose = display.registerOutputInterceptor({
|
|
1481
|
+
afterWrite: (content) => {
|
|
1482
|
+
if (!content)
|
|
1483
|
+
return;
|
|
1484
|
+
this.trackExternalOutput(content);
|
|
1485
|
+
},
|
|
1486
|
+
});
|
|
1261
1487
|
}
|
|
1262
1488
|
/**
|
|
1263
1489
|
* Write content above the floating chat box.
|
|
@@ -1273,9 +1499,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
1273
1499
|
// Position cursor at content row and write
|
|
1274
1500
|
this.write(ESC.TO(this.contentRow, 1));
|
|
1275
1501
|
this.write(content);
|
|
1276
|
-
// Track
|
|
1277
|
-
const
|
|
1278
|
-
this.contentRow +=
|
|
1502
|
+
// Track rendered rows (handles wrapping)
|
|
1503
|
+
const rowsUsed = this.countRenderedRows(content);
|
|
1504
|
+
this.contentRow += rowsUsed;
|
|
1279
1505
|
}
|
|
1280
1506
|
finally {
|
|
1281
1507
|
writeLock.unlock();
|
|
@@ -1359,6 +1585,10 @@ export class TerminalInput extends EventEmitter {
|
|
|
1359
1585
|
return;
|
|
1360
1586
|
this.disposed = true;
|
|
1361
1587
|
this.enabled = false;
|
|
1588
|
+
if (this.displayInterceptorDispose) {
|
|
1589
|
+
this.displayInterceptorDispose();
|
|
1590
|
+
this.displayInterceptorDispose = null;
|
|
1591
|
+
}
|
|
1362
1592
|
this.disableScrollRegion();
|
|
1363
1593
|
this.resetStreamingRenderThrottle();
|
|
1364
1594
|
this.disableBracketedPaste();
|
|
@@ -1856,12 +2086,13 @@ export class TerminalInput extends EventEmitter {
|
|
|
1856
2086
|
};
|
|
1857
2087
|
}
|
|
1858
2088
|
submit() {
|
|
1859
|
-
const text = this.assembleText()
|
|
1860
|
-
if (!text)
|
|
2089
|
+
const text = this.assembleText();
|
|
2090
|
+
if (!text.trim())
|
|
1861
2091
|
return;
|
|
2092
|
+
const submission = text;
|
|
1862
2093
|
// Add to history
|
|
1863
|
-
if (this.history[this.history.length - 1] !==
|
|
1864
|
-
this.history.push(
|
|
2094
|
+
if (this.history[this.history.length - 1] !== submission) {
|
|
2095
|
+
this.history.push(submission);
|
|
1865
2096
|
if (this.history.length > this.maxHistory) {
|
|
1866
2097
|
this.history.shift();
|
|
1867
2098
|
}
|
|
@@ -1875,17 +2106,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
1875
2106
|
}
|
|
1876
2107
|
this.queue.push({
|
|
1877
2108
|
id: ++this.queueIdCounter,
|
|
1878
|
-
text,
|
|
2109
|
+
text: submission,
|
|
1879
2110
|
timestamp: Date.now(),
|
|
1880
2111
|
});
|
|
1881
|
-
this.emit('queue',
|
|
2112
|
+
this.emit('queue', submission);
|
|
1882
2113
|
this.clear(); // Clear immediately for queued input
|
|
1883
2114
|
}
|
|
1884
2115
|
else {
|
|
1885
2116
|
// In idle mode, clear the input first, then emit submit.
|
|
1886
2117
|
// The prompt will be logged as a visible message by the caller.
|
|
1887
2118
|
this.clear();
|
|
1888
|
-
this.emit('submit',
|
|
2119
|
+
this.emit('submit', submission);
|
|
1889
2120
|
}
|
|
1890
2121
|
}
|
|
1891
2122
|
finishPaste() {
|
|
@@ -2338,6 +2569,20 @@ export class TerminalInput extends EventEmitter {
|
|
|
2338
2569
|
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '') // Control chars except \n and \r
|
|
2339
2570
|
.replace(/\r\n?/g, '\n'); // Normalize line endings
|
|
2340
2571
|
}
|
|
2572
|
+
isOrphanedEscapeSequence(text, key) {
|
|
2573
|
+
if (!text || key?.name)
|
|
2574
|
+
return false;
|
|
2575
|
+
const looksLikeArrowFragment = text.length <= 5 && (/^[\[O][0-9;]*[A-Za-z]$/.test(text));
|
|
2576
|
+
const sanitized = this.sanitize(text);
|
|
2577
|
+
// If sanitization leaves visible text and this isn't an arrow fragment, treat it as real input
|
|
2578
|
+
if (sanitized.trim().length > 0 && !looksLikeArrowFragment) {
|
|
2579
|
+
return false;
|
|
2580
|
+
}
|
|
2581
|
+
if (text.includes('\x1b'))
|
|
2582
|
+
return true;
|
|
2583
|
+
// Common stray fragments when terminals partially echo arrow sequences (e.g., "[D")
|
|
2584
|
+
return looksLikeArrowFragment;
|
|
2585
|
+
}
|
|
2341
2586
|
isWhitespace(char) {
|
|
2342
2587
|
return char === ' ' || char === '\t' || char === '\n';
|
|
2343
2588
|
}
|