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.
Files changed (70) hide show
  1. package/dist/core/contextManager.d.ts +4 -0
  2. package/dist/core/contextManager.d.ts.map +1 -1
  3. package/dist/core/contextManager.js +16 -0
  4. package/dist/core/contextManager.js.map +1 -1
  5. package/dist/core/secretStore.d.ts +1 -0
  6. package/dist/core/secretStore.d.ts.map +1 -1
  7. package/dist/core/secretStore.js +3 -0
  8. package/dist/core/secretStore.js.map +1 -1
  9. package/dist/core/toolPreconditions.d.ts.map +1 -1
  10. package/dist/core/toolPreconditions.js +9 -0
  11. package/dist/core/toolPreconditions.js.map +1 -1
  12. package/dist/core/toolRuntime.d.ts +6 -0
  13. package/dist/core/toolRuntime.d.ts.map +1 -1
  14. package/dist/core/toolRuntime.js +7 -0
  15. package/dist/core/toolRuntime.js.map +1 -1
  16. package/dist/runtime/agentHost.d.ts +3 -1
  17. package/dist/runtime/agentHost.d.ts.map +1 -1
  18. package/dist/runtime/agentHost.js +3 -0
  19. package/dist/runtime/agentHost.js.map +1 -1
  20. package/dist/runtime/agentSession.d.ts +2 -1
  21. package/dist/runtime/agentSession.d.ts.map +1 -1
  22. package/dist/runtime/agentSession.js +1 -0
  23. package/dist/runtime/agentSession.js.map +1 -1
  24. package/dist/runtime/universal.d.ts +2 -1
  25. package/dist/runtime/universal.d.ts.map +1 -1
  26. package/dist/runtime/universal.js +1 -0
  27. package/dist/runtime/universal.js.map +1 -1
  28. package/dist/shell/interactiveShell.d.ts +34 -0
  29. package/dist/shell/interactiveShell.d.ts.map +1 -1
  30. package/dist/shell/interactiveShell.js +424 -139
  31. package/dist/shell/interactiveShell.js.map +1 -1
  32. package/dist/shell/shellApp.d.ts.map +1 -1
  33. package/dist/shell/shellApp.js +5 -0
  34. package/dist/shell/shellApp.js.map +1 -1
  35. package/dist/shell/systemPrompt.d.ts.map +1 -1
  36. package/dist/shell/systemPrompt.js +4 -0
  37. package/dist/shell/systemPrompt.js.map +1 -1
  38. package/dist/shell/terminalInput.d.ts +43 -0
  39. package/dist/shell/terminalInput.d.ts.map +1 -1
  40. package/dist/shell/terminalInput.js +273 -28
  41. package/dist/shell/terminalInput.js.map +1 -1
  42. package/dist/shell/terminalInputAdapter.d.ts +9 -0
  43. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  44. package/dist/shell/terminalInputAdapter.js +8 -2
  45. package/dist/shell/terminalInputAdapter.js.map +1 -1
  46. package/dist/tools/fileTools.d.ts.map +1 -1
  47. package/dist/tools/fileTools.js +29 -5
  48. package/dist/tools/fileTools.js.map +1 -1
  49. package/dist/tools/grepTools.d.ts.map +1 -1
  50. package/dist/tools/grepTools.js +22 -4
  51. package/dist/tools/grepTools.js.map +1 -1
  52. package/dist/tools/searchTools.d.ts.map +1 -1
  53. package/dist/tools/searchTools.js +47 -13
  54. package/dist/tools/searchTools.js.map +1 -1
  55. package/dist/tools/webTools.d.ts.map +1 -1
  56. package/dist/tools/webTools.js +36 -9
  57. package/dist/tools/webTools.js.map +1 -1
  58. package/dist/ui/ShellUIAdapter.d.ts +13 -0
  59. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  60. package/dist/ui/ShellUIAdapter.js +40 -3
  61. package/dist/ui/ShellUIAdapter.js.map +1 -1
  62. package/dist/ui/display.d.ts +1 -0
  63. package/dist/ui/display.d.ts.map +1 -1
  64. package/dist/ui/display.js +61 -12
  65. package/dist/ui/display.js.map +1 -1
  66. package/dist/ui/layout.js +8 -7
  67. package/dist/ui/layout.js.map +1 -1
  68. package/dist/ui/unified/layout.js +2 -2
  69. package/dist/ui/unified/layout.js.map +1 -1
  70. 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
- this.handleMouseEvent(button, 0, 0, match[4]);
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, _x, _y, _action) {
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 = col < line.length ? line[col] : ' ';
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 + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
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 = suggestionRow;
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 newlines
1196
- const newlines = (content.match(/\n/g) || []).length;
1197
- this.contentRow += newlines;
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
- // Limit input lines to reasonable max (leave room for content)
1233
- const maxInputLines = Math.min(this.config.maxLines, Math.max(1, Math.floor(rows / 3)));
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
- // Limit total height to leave room for content (max ~50% of terminal when showing suggestions)
1249
- const totalHeight = metaLines + 1 + inputLines + suggestionLines + controlLineCount + 1;
1250
- const maxHeight = this.showingSuggestions
1251
- ? Math.max(12, Math.floor(rows * 0.5))
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
- // No-op: Use streamContent() for cleaner floating chat box behavior
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 newlines
1277
- const newlines = (content.match(/\n/g) || []).length;
1278
- this.contentRow += newlines;
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().trim();
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] !== text) {
1864
- this.history.push(text);
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', text);
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', text);
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
  }