erosolar-cli 1.7.367 → 1.7.369

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 (52) hide show
  1. package/dist/capabilities/learnCapability.d.ts +9 -0
  2. package/dist/capabilities/learnCapability.d.ts.map +1 -1
  3. package/dist/capabilities/learnCapability.js +15 -2
  4. package/dist/capabilities/learnCapability.js.map +1 -1
  5. package/dist/shell/interactiveShell.d.ts +2 -0
  6. package/dist/shell/interactiveShell.d.ts.map +1 -1
  7. package/dist/shell/interactiveShell.js +32 -15
  8. package/dist/shell/interactiveShell.js.map +1 -1
  9. package/dist/shell/terminalInput.d.ts +44 -22
  10. package/dist/shell/terminalInput.d.ts.map +1 -1
  11. package/dist/shell/terminalInput.js +235 -260
  12. package/dist/shell/terminalInput.js.map +1 -1
  13. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  14. package/dist/shell/terminalInputAdapter.js +5 -2
  15. package/dist/shell/terminalInputAdapter.js.map +1 -1
  16. package/dist/subagents/taskRunner.d.ts.map +1 -1
  17. package/dist/subagents/taskRunner.js +7 -25
  18. package/dist/subagents/taskRunner.js.map +1 -1
  19. package/dist/tools/learnTools.js +127 -4
  20. package/dist/tools/learnTools.js.map +1 -1
  21. package/dist/tools/localExplore.d.ts +225 -0
  22. package/dist/tools/localExplore.d.ts.map +1 -0
  23. package/dist/tools/localExplore.js +1295 -0
  24. package/dist/tools/localExplore.js.map +1 -0
  25. package/dist/ui/ShellUIAdapter.d.ts +28 -0
  26. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  27. package/dist/ui/ShellUIAdapter.js +215 -9
  28. package/dist/ui/ShellUIAdapter.js.map +1 -1
  29. package/dist/ui/compactRenderer.d.ts +139 -0
  30. package/dist/ui/compactRenderer.d.ts.map +1 -0
  31. package/dist/ui/compactRenderer.js +398 -0
  32. package/dist/ui/compactRenderer.js.map +1 -0
  33. package/dist/ui/inPlaceUpdater.d.ts +181 -0
  34. package/dist/ui/inPlaceUpdater.d.ts.map +1 -0
  35. package/dist/ui/inPlaceUpdater.js +515 -0
  36. package/dist/ui/inPlaceUpdater.js.map +1 -0
  37. package/dist/ui/theme.d.ts +108 -3
  38. package/dist/ui/theme.d.ts.map +1 -1
  39. package/dist/ui/theme.js +124 -3
  40. package/dist/ui/theme.js.map +1 -1
  41. package/dist/ui/toolDisplay.d.ts +44 -7
  42. package/dist/ui/toolDisplay.d.ts.map +1 -1
  43. package/dist/ui/toolDisplay.js +201 -85
  44. package/dist/ui/toolDisplay.js.map +1 -1
  45. package/dist/ui/unified/index.d.ts +11 -0
  46. package/dist/ui/unified/index.d.ts.map +1 -1
  47. package/dist/ui/unified/index.js +16 -0
  48. package/dist/ui/unified/index.js.map +1 -1
  49. package/dist/ui/unified/layout.d.ts.map +1 -1
  50. package/dist/ui/unified/layout.js +32 -47
  51. package/dist/ui/unified/layout.js.map +1 -1
  52. package/package.json +1 -1
@@ -92,7 +92,6 @@ export class TerminalInput extends EventEmitter {
92
92
  metaTokenLimit = null; // Optional token window
93
93
  metaThinkingMs = null; // Optional thinking duration
94
94
  metaThinkingHasContent = false; // Whether collapsed thinking content exists
95
- toolCount = null; // Total tool count for status display
96
95
  lastRenderContent = '';
97
96
  lastRenderCursor = -1;
98
97
  renderDirty = false;
@@ -118,7 +117,7 @@ export class TerminalInput extends EventEmitter {
118
117
  editMode = 'display-edits';
119
118
  verificationEnabled = true;
120
119
  autoContinueEnabled = false;
121
- verificationHotkey = 'alt+d';
120
+ verificationHotkey = 'alt+v';
122
121
  autoContinueHotkey = 'alt+c';
123
122
  thinkingHotkey = '/thinking';
124
123
  modelLabel = null;
@@ -174,60 +173,63 @@ export class TerminalInput extends EventEmitter {
174
173
  }
175
174
  }
176
175
  /**
177
- * Process raw terminal data (handles bracketed paste sequences, mouse events, and escape sequences)
178
- * Returns true if the data was consumed (paste sequence, mouse event, etc.)
176
+ * Process raw terminal data (handles bracketed paste sequences and mouse events)
177
+ * Returns true if the data was consumed (paste sequence)
179
178
  */
180
179
  processRawData(data) {
181
- // Check for mouse events (SGR mode: \x1b[<button;x;yM or m)
182
- const mouseMatch = data.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
183
- if (mouseMatch) {
184
- const button = parseInt(mouseMatch[1], 10);
185
- const x = parseInt(mouseMatch[2], 10);
186
- const y = parseInt(mouseMatch[3], 10);
187
- const action = mouseMatch[4]; // 'M' = press, 'm' = release
188
- this.handleMouseEvent(button, x, y, action);
189
- // Remove mouse event from data and return remaining
190
- const mouseEventEnd = data.indexOf(action) + 1;
191
- const remaining = data.slice(mouseEventEnd);
192
- return { consumed: true, passthrough: remaining };
193
- }
194
- // Filter out arrow key escape sequences (they're handled by keypress events)
195
- // Arrow keys: \x1b[A (up), \x1b[B (down), \x1b[C (right), \x1b[D (left)
196
- const arrowMatch = data.match(/\x1b\[[ABCD]/);
197
- if (arrowMatch) {
198
- // Arrow keys should be handled by keypress handler, strip them from passthrough
199
- const filtered = data.replace(/\x1b\[[ABCD]/g, '');
200
- return { consumed: filtered.length !== data.length, passthrough: filtered };
180
+ // Handle ALL mouse events in the data (SGR mode: \x1b[<button;x;yM or m)
181
+ // Process repeatedly until no more mouse events
182
+ const mousePattern = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
183
+ let remaining = data;
184
+ let hadMouseEvents = false;
185
+ // Remove all mouse escape sequences from the data
186
+ let match;
187
+ while ((match = mousePattern.exec(data)) !== null) {
188
+ const button = parseInt(match[1], 10);
189
+ this.handleMouseEvent(button, 0, 0, match[4]);
190
+ hadMouseEvents = true;
191
+ }
192
+ if (hadMouseEvents) {
193
+ // Strip all mouse sequences from the data
194
+ remaining = data.replace(/\x1b\[<\d+;\d+;\d+[Mm]/g, '');
195
+ if (!remaining) {
196
+ return { consumed: true, passthrough: '' };
197
+ }
198
+ // If there's remaining content, continue processing it
201
199
  }
202
200
  // Check for paste start
203
- if (data.includes(ESC.PASTE_START)) {
201
+ if (remaining.includes(ESC.PASTE_START)) {
204
202
  this.isPasting = true;
205
203
  this.pasteBuffer = '';
206
204
  // Extract content after paste start
207
- const startIdx = data.indexOf(ESC.PASTE_START) + ESC.PASTE_START.length;
208
- const remaining = data.slice(startIdx);
205
+ const startIdx = remaining.indexOf(ESC.PASTE_START) + ESC.PASTE_START.length;
206
+ const afterStart = remaining.slice(startIdx);
209
207
  // Check if paste end is also in this chunk
210
- if (remaining.includes(ESC.PASTE_END)) {
211
- const endIdx = remaining.indexOf(ESC.PASTE_END);
212
- this.pasteBuffer = remaining.slice(0, endIdx);
208
+ if (afterStart.includes(ESC.PASTE_END)) {
209
+ const endIdx = afterStart.indexOf(ESC.PASTE_END);
210
+ this.pasteBuffer = afterStart.slice(0, endIdx);
213
211
  this.finishPaste();
214
- return { consumed: true, passthrough: remaining.slice(endIdx + ESC.PASTE_END.length) };
212
+ return { consumed: true, passthrough: afterStart.slice(endIdx + ESC.PASTE_END.length) };
215
213
  }
216
- this.pasteBuffer = remaining;
214
+ this.pasteBuffer = afterStart;
217
215
  return { consumed: true, passthrough: '' };
218
216
  }
219
217
  // Accumulating paste
220
218
  if (this.isPasting) {
221
- if (data.includes(ESC.PASTE_END)) {
222
- const endIdx = data.indexOf(ESC.PASTE_END);
223
- this.pasteBuffer += data.slice(0, endIdx);
219
+ if (remaining.includes(ESC.PASTE_END)) {
220
+ const endIdx = remaining.indexOf(ESC.PASTE_END);
221
+ this.pasteBuffer += remaining.slice(0, endIdx);
224
222
  this.finishPaste();
225
- return { consumed: true, passthrough: data.slice(endIdx + ESC.PASTE_END.length) };
223
+ return { consumed: true, passthrough: remaining.slice(endIdx + ESC.PASTE_END.length) };
226
224
  }
227
- this.pasteBuffer += data;
225
+ this.pasteBuffer += remaining;
228
226
  return { consumed: true, passthrough: '' };
229
227
  }
230
- return { consumed: false, passthrough: data };
228
+ // If we processed mouse events but have remaining text, return it as passthrough
229
+ if (hadMouseEvents && remaining) {
230
+ return { consumed: true, passthrough: remaining };
231
+ }
232
+ return { consumed: false, passthrough: remaining };
231
233
  }
232
234
  /**
233
235
  * Handle mouse events (button, x, y coordinates, action)
@@ -237,12 +239,30 @@ export class TerminalInput extends EventEmitter {
237
239
  if (button === 64) {
238
240
  // Scroll up (3 lines per wheel tick)
239
241
  this.scrollUp(3);
242
+ return;
240
243
  }
241
- else if (button === 65) {
244
+ if (button === 65) {
242
245
  // Scroll down (3 lines per wheel tick)
243
246
  this.scrollDown(3);
247
+ return;
248
+ }
249
+ // Left button (0), middle button (1), right button (2)
250
+ // These are captured by SGR mouse tracking but we don't need to handle them
251
+ // Just consume silently - the escape sequence has already been processed
252
+ // The 'M' action is press, 'm' is release
253
+ if (button <= 2) {
254
+ // Silently consume click events to prevent artifacts
255
+ // Left clicks (button=0) in the input area could focus the input
256
+ // but terminal apps typically handle this natively
257
+ return;
258
+ }
259
+ // Button with motion flag (button + 32) - drag events
260
+ if (button >= 32 && button <= 34) {
261
+ // Drag events - consume silently
262
+ return;
244
263
  }
245
- // Ignore other mouse events (clicks, drags, etc.) for now
264
+ // Any other unrecognized button - log for debugging in dev mode
265
+ // but don't output anything to prevent artifacts
246
266
  }
247
267
  /**
248
268
  * Handle a keypress event
@@ -448,14 +468,12 @@ export class TerminalInput extends EventEmitter {
448
468
  const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
449
469
  const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
450
470
  const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
451
- const nextToolCount = options.toolCount === undefined ? this.toolCount : (options.toolCount ?? null);
452
471
  if (this.verificationEnabled === nextVerification &&
453
472
  this.autoContinueEnabled === nextAutoContinue &&
454
473
  this.verificationHotkey === nextVerifyHotkey &&
455
474
  this.autoContinueHotkey === nextAutoHotkey &&
456
475
  this.thinkingHotkey === nextThinkingHotkey &&
457
- this.thinkingModeLabel === nextThinkingLabel &&
458
- this.toolCount === nextToolCount) {
476
+ this.thinkingModeLabel === nextThinkingLabel) {
459
477
  return;
460
478
  }
461
479
  this.verificationEnabled = nextVerification;
@@ -464,7 +482,6 @@ export class TerminalInput extends EventEmitter {
464
482
  this.autoContinueHotkey = nextAutoHotkey;
465
483
  this.thinkingHotkey = nextThinkingHotkey;
466
484
  this.thinkingModeLabel = nextThinkingLabel;
467
- this.toolCount = nextToolCount;
468
485
  this.scheduleRender();
469
486
  }
470
487
  /**
@@ -620,10 +637,15 @@ export class TerminalInput extends EventEmitter {
620
637
  this.write(' '.repeat(padding));
621
638
  this.write(ESC.RESET);
622
639
  }
623
- // Mode controls line with all keyboard shortcuts
624
- const controlRow = currentRow + visibleLines.length;
625
- this.write(ESC.TO(controlRow, 1));
626
- this.write(this.buildModeControls(cols));
640
+ // Mode controls lines with all keyboard shortcuts
641
+ // Can be multiple lines during streaming (status + toggles)
642
+ const controlLines = this.buildModeControls(cols);
643
+ let controlRow = currentRow + visibleLines.length;
644
+ for (const controlLine of controlLines) {
645
+ this.write(ESC.TO(controlRow, 1));
646
+ this.write(controlLine);
647
+ controlRow += 1;
648
+ }
627
649
  // Restore scroll region and cursor
628
650
  if (this.scrollRegionActive) {
629
651
  // Restore scroll region
@@ -700,103 +722,133 @@ export class TerminalInput extends EventEmitter {
700
722
  return [`${leftText}${' '.repeat(spacing)}${rightText}`];
701
723
  }
702
724
  /**
703
- * Build mode controls line with all keyboard shortcuts and toggle states.
704
- * Shows below the chat box input area with fully spelled-out key names.
705
- * Always visible before and during streaming.
725
+ * Build mode controls lines with all keyboard shortcuts.
726
+ * Shows status, all toggles, and contextual information.
727
+ * Enhanced with comprehensive feature status display.
728
+ *
729
+ * Returns multiple lines to ensure all context is visible during streaming:
730
+ * - Line 1 (during streaming): Status/spinner + interrupt shortcut
731
+ * - Line 2: Mode toggles (edit, verify, auto-continue, thinking)
732
+ *
733
+ * Format: [Key] Label:status · [Key] Label:status · context% · model
706
734
  */
707
735
  buildModeControls(cols) {
708
736
  const width = Math.max(8, cols - 2);
709
- const parts = [];
710
- // Streaming status with stop hint (if streaming)
737
+ const streamingActive = this.mode === 'streaming' || this.scrollRegionActive;
738
+ // === LINE 1: STATUS LINE (streaming status + interrupt) ===
739
+ const statusParts = [];
740
+ // Streaming indicator with animated spinner
711
741
  if (this.streamingLabel) {
712
- parts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
713
- parts.push({ text: `Esc:stop`, tone: 'warn' });
742
+ statusParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
743
+ }
744
+ // Override status (warnings, errors)
745
+ if (this.overrideStatusMessage) {
746
+ statusParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
714
747
  }
715
- // === ALL MODE TOGGLES WITH FULL KEY NAMES (always shown) ===
716
- // Edit mode toggle (Shift+Tab)
717
- const editIcon = this.editMode === 'display-edits' ? '' : '';
748
+ // Main status message
749
+ if (this.statusMessage) {
750
+ statusParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
751
+ }
752
+ // Interrupt shortcut (during streaming)
753
+ if (streamingActive) {
754
+ statusParts.push({ text: `Escape:stop`, tone: 'warn' });
755
+ }
756
+ // Queued commands
757
+ if (this.queue.length > 0) {
758
+ const queueIcon = streamingActive ? '⏳' : '▸';
759
+ statusParts.push({ text: `${queueIcon}${this.queue.length} queued`, tone: 'info' });
760
+ }
761
+ // === LINE 2: MODE TOGGLES (always visible, same as before streaming) ===
762
+ const toggleParts = [];
763
+ // Edit mode toggle (Shift+Tab) - Full English key names for clarity
764
+ const editStatus = this.editMode === 'display-edits' ? '✓auto' : '?ask';
718
765
  const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
719
- parts.push({ text: `Shift+Tab:edits${editIcon}`, tone: editTone });
720
- // Verify mode toggle (Alt+D)
721
- const verifyIcon = this.verificationEnabled ? '✓' : '○';
722
- const verifyTone = this.verificationEnabled ? 'success' : 'muted';
723
- parts.push({ text: `Alt+D:verify${verifyIcon}`, tone: verifyTone });
724
- // Auto-continue toggle (Alt+C)
725
- const autoIcon = this.autoContinueEnabled ? '✓' : '○';
726
- const autoTone = this.autoContinueEnabled ? 'success' : 'muted';
727
- parts.push({ text: `Alt+C:auto${autoIcon}`, tone: autoTone });
728
- // Thinking mode toggle (Alt+T)
766
+ toggleParts.push({ text: `Shift+Tab:${editStatus}`, tone: editTone });
767
+ // Verification toggle (Alt+V) - Full English key names
768
+ const verifyStatus = this.verificationEnabled ? '✓on' : '○off';
769
+ toggleParts.push({
770
+ text: `Alt+V:${verifyStatus}`,
771
+ tone: this.verificationEnabled ? 'success' : 'muted'
772
+ });
773
+ // Auto-continue toggle (Alt+C) - Full English key names
774
+ const autoStatus = this.autoContinueEnabled ? '↻on' : '○off';
775
+ toggleParts.push({
776
+ text: `Alt+C:${autoStatus}`,
777
+ tone: this.autoContinueEnabled ? 'info' : 'muted'
778
+ });
779
+ // Thinking mode toggle (if available)
729
780
  if (this.thinkingModeLabel) {
730
- const thinkingShort = this.thinkingModeLabel === 'minimal' ? 'min' :
731
- this.thinkingModeLabel === 'balanced' ? 'bal' :
732
- this.thinkingModeLabel === 'extended' ? 'ext' : this.thinkingModeLabel;
733
- parts.push({ text: `Alt+T:think[${thinkingShort}]`, tone: 'info' });
781
+ const shortThinking = this.thinkingModeLabel.length > 10
782
+ ? this.thinkingModeLabel.slice(0, 8) + '..'
783
+ : this.thinkingModeLabel;
784
+ toggleParts.push({ text: `Alt+T:${shortThinking}`, tone: 'info' });
785
+ }
786
+ // Multi-line indicator
787
+ if (this.buffer.includes('\n')) {
788
+ const lineCount = this.buffer.split('\n').length;
789
+ toggleParts.push({ text: `${lineCount}L`, tone: 'muted' });
790
+ }
791
+ // Paste indicator
792
+ if (this.pastePlaceholders.length > 0) {
793
+ const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
794
+ toggleParts.push({
795
+ text: `📋${latest.lineCount}L`,
796
+ tone: 'info',
797
+ });
734
798
  }
735
- else {
736
- parts.push({ text: `Alt+T:think`, tone: 'muted' });
737
- }
738
- // Clear context (Alt+X)
739
- parts.push({ text: `Alt+X:clear`, tone: 'muted' });
740
- // Scroll hint
741
- parts.push({ text: `PgUp/Dn:scroll`, tone: 'muted' });
742
- // === STATUS INFO ===
743
- // Context remaining percentage
744
- const contextRemaining = this.computeContextRemaining();
745
- if (contextRemaining !== null) {
746
- const tone = contextRemaining <= 10 ? 'warn' : contextRemaining <= 30 ? 'info' : 'muted';
747
- parts.push({ text: `ctx:${contextRemaining}%`, tone });
748
- }
749
- // Token usage
750
- if (this.metaTokensUsed !== null) {
751
- const used = this.formatTokenCount(this.metaTokensUsed);
752
- const limit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
753
- parts.push({ text: `${used}${limit}tk`, tone: 'muted' });
799
+ // Build the output lines
800
+ const lines = [];
801
+ // During streaming: show status line first, then toggles line
802
+ // Not streaming: show combined or just toggles
803
+ if (streamingActive && statusParts.length > 0) {
804
+ lines.push(renderStatusLine(statusParts, width));
805
+ lines.push(renderStatusLine(toggleParts, width));
754
806
  }
755
- // Elapsed time (always show if available)
756
- if (this.metaElapsedSeconds !== null) {
757
- parts.push({ text: `time:${this.formatElapsedLabel(this.metaElapsedSeconds)}`, tone: 'muted' });
807
+ else if (statusParts.length > 0) {
808
+ // Not streaming but have status - combine on one line if possible
809
+ const combined = [...statusParts, ...toggleParts];
810
+ lines.push(renderStatusLine(combined, width));
758
811
  }
759
- // Tool count if available
760
- if (this.toolCount !== null && this.toolCount > 0) {
761
- parts.push({ text: `tools:${this.toolCount}`, tone: 'info' });
812
+ else {
813
+ // No streaming, no status - just show toggles
814
+ lines.push(renderStatusLine(toggleParts, width));
762
815
  }
763
- return renderStatusLine(parts, width);
816
+ return lines;
817
+ }
818
+ /**
819
+ * Render a mini context usage bar (5 chars)
820
+ */
821
+ renderMiniContextBar(percentRemaining) {
822
+ const bars = 5;
823
+ const filled = Math.round((percentRemaining / 100) * bars);
824
+ const empty = bars - filled;
825
+ return '█'.repeat(filled) + '░'.repeat(empty);
764
826
  }
765
827
  formatHotkey(hotkey) {
766
828
  const normalized = hotkey.trim().toLowerCase();
767
829
  if (!normalized)
768
830
  return hotkey;
769
831
  const parts = normalized.split('+').filter(Boolean);
770
- // Use readable key names instead of symbols for better terminal compatibility
771
832
  const map = {
772
- shift: 'Shift',
773
- sh: 'Shift',
774
- alt: 'Alt',
775
- option: 'Alt',
776
- opt: 'Alt',
777
- ctrl: 'Ctrl',
778
- control: 'Ctrl',
779
- cmd: 'Cmd',
780
- meta: 'Cmd',
781
- esc: 'Esc',
782
- escape: 'Esc',
783
- tab: 'Tab',
784
- return: 'Enter',
785
- enter: 'Enter',
786
- pageup: 'PgUp',
787
- pagedown: 'PgDn',
788
- home: 'Home',
789
- end: 'End',
833
+ shift: '',
834
+ sh: '',
835
+ alt: '',
836
+ option: '',
837
+ opt: '',
838
+ ctrl: '',
839
+ control: '',
840
+ cmd: '',
841
+ meta: '',
790
842
  };
791
843
  const formatted = parts
792
844
  .map((part) => {
793
- const label = map[part];
794
- if (label)
795
- return label;
796
- return part.length === 1 ? part.toUpperCase() : part.charAt(0).toUpperCase() + part.slice(1);
845
+ const symbol = map[part];
846
+ if (symbol)
847
+ return symbol;
848
+ return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
797
849
  })
798
- .join('+');
799
- return `[${formatted}]`;
850
+ .join('');
851
+ return formatted || hotkey;
800
852
  }
801
853
  computeContextRemaining() {
802
854
  if (this.contextUsage === null) {
@@ -959,10 +1011,24 @@ export class TerminalInput extends EventEmitter {
959
1011
  // No-op: using pure floating approach
960
1012
  }
961
1013
  /**
962
- * Calculate chat box height.
1014
+ * Calculate chat box height dynamically.
1015
+ * Accounts for:
1016
+ * - Meta lines (model, elapsed, tokens)
1017
+ * - Divider
1018
+ * - Input line(s)
1019
+ * - Control lines (1 or 2 during streaming)
1020
+ * - Buffer
963
1021
  */
964
1022
  getChatBoxHeight() {
965
- return 5; // Fixed: divider + input + status + buffer
1023
+ const streamingActive = this.mode === 'streaming' || this.scrollRegionActive;
1024
+ const hasStreamingStatus = this.streamingLabel || this.statusMessage || this.overrideStatusMessage;
1025
+ // Base: meta (1) + divider (1) + input (1) + buffer (1) = 4
1026
+ // Add control lines: 1 normally, 2 during streaming with status
1027
+ const controlLines = (streamingActive && hasStreamingStatus) ? 2 : 1;
1028
+ // Meta line only if we have model/elapsed info
1029
+ const hasMetaInfo = this.modelLabel || this.metaElapsedSeconds !== null || this.metaTokensUsed !== null;
1030
+ const metaLines = hasMetaInfo ? 1 : 0;
1031
+ return metaLines + 1 + 1 + controlLines + 1; // meta + divider + input + controls + buffer
966
1032
  }
967
1033
  /**
968
1034
  * @deprecated Use streamContent() instead
@@ -998,41 +1064,26 @@ export class TerminalInput extends EventEmitter {
998
1064
  }
999
1065
  }
1000
1066
  /**
1001
- * Enter alternate screen buffer and clear it.
1002
- * This gives us full control over the terminal without affecting user's history.
1067
+ * Enter alternate screen buffer.
1068
+ * DISABLED: Using terminal-native mode for proper scrollback and text selection.
1003
1069
  */
1004
1070
  enterAlternateScreen() {
1005
- writeLock.lock('enterAltScreen');
1006
- try {
1007
- this.write(ESC.ENTER_ALT_SCREEN);
1008
- this.write(ESC.HOME);
1009
- this.write(ESC.CLEAR_SCREEN);
1010
- this.contentRow = 1;
1011
- this.alternateScreenActive = true;
1012
- }
1013
- finally {
1014
- writeLock.unlock();
1015
- }
1071
+ // Disabled - using terminal-native mode
1072
+ this.contentRow = 1;
1016
1073
  }
1017
1074
  /**
1018
1075
  * Exit alternate screen buffer.
1019
- * Restores the user's previous terminal content.
1076
+ * DISABLED: Using terminal-native mode.
1020
1077
  */
1021
1078
  exitAlternateScreen() {
1022
- writeLock.lock('exitAltScreen');
1023
- try {
1024
- this.write(ESC.EXIT_ALT_SCREEN);
1025
- this.alternateScreenActive = false;
1026
- }
1027
- finally {
1028
- writeLock.unlock();
1029
- }
1079
+ // Disabled - using terminal-native mode
1030
1080
  }
1031
1081
  /**
1032
1082
  * Check if alternate screen buffer is currently active.
1083
+ * Always returns false - using terminal-native mode.
1033
1084
  */
1034
1085
  isAlternateScreenActive() {
1035
- return this.alternateScreenActive;
1086
+ return false;
1036
1087
  }
1037
1088
  /**
1038
1089
  * Get a snapshot of the scrollback buffer (for display on exit).
@@ -1041,14 +1092,17 @@ export class TerminalInput extends EventEmitter {
1041
1092
  return [...this.scrollbackBuffer];
1042
1093
  }
1043
1094
  /**
1044
- * Clear the entire terminal screen and reset content position.
1045
- * This removes all content including the launching command.
1095
+ * Clear the visible terminal area and reset content position.
1096
+ * In terminal-native mode, this just adds newlines to scroll past content
1097
+ * rather than clearing history (preserving scrollback).
1046
1098
  */
1047
1099
  clearScreen() {
1048
1100
  writeLock.lock('clearScreen');
1049
1101
  try {
1102
+ // In native mode, scroll past existing content rather than clearing
1103
+ const { rows } = this.getSize();
1104
+ this.write('\n'.repeat(rows));
1050
1105
  this.write(ESC.HOME);
1051
- this.write(ESC.CLEAR_SCREEN);
1052
1106
  this.contentRow = 1;
1053
1107
  }
1054
1108
  finally {
@@ -1148,8 +1202,8 @@ export class TerminalInput extends EventEmitter {
1148
1202
  this.insertNewline();
1149
1203
  break;
1150
1204
  // === MODE TOGGLES ===
1151
- case 'd':
1152
- // Alt+D: Toggle verification/double-check mode (auto-tests after edits)
1205
+ case 'v':
1206
+ // Alt+V: Toggle verification mode (auto-tests after edits)
1153
1207
  this.emit('toggleVerify');
1154
1208
  break;
1155
1209
  case 'c':
@@ -1216,25 +1270,12 @@ export class TerminalInput extends EventEmitter {
1216
1270
  return Math.max(5, rows - chatBoxHeight - 2);
1217
1271
  }
1218
1272
  /**
1219
- * Build scroll indicator for the divider line (Claude Code style).
1220
- * Shows scroll position when in scrollback mode, or history size hint when idle.
1273
+ * Build scroll indicator for the divider line.
1274
+ * Scrollback navigation is disabled in alternate screen mode.
1275
+ * This returns null - no scroll indicator is shown.
1221
1276
  */
1222
1277
  buildScrollIndicator() {
1223
- const bufferSize = this.scrollbackBuffer.length;
1224
- // In scrollback mode - show position
1225
- if (this.isInScrollbackMode && this.scrollbackOffset > 0) {
1226
- const { rows } = this.getSize();
1227
- const chatBoxHeight = this.getChatBoxHeight();
1228
- const viewportHeight = Math.max(1, rows - chatBoxHeight);
1229
- const currentPos = Math.max(0, bufferSize - this.scrollbackOffset - viewportHeight);
1230
- const pct = bufferSize > 0 ? Math.round((currentPos / bufferSize) * 100) : 0;
1231
- return `↑${this.scrollbackOffset} · ${pct}% · PgUp/Dn`;
1232
- }
1233
- // Not in scrollback - show hint if there's history
1234
- if (bufferSize > 20) {
1235
- const sizeLabel = bufferSize >= 1000 ? `${Math.floor(bufferSize / 1000)}k` : `${bufferSize}`;
1236
- return `↕${sizeLabel}L · PgUp`;
1237
- }
1278
+ // Scrollback navigation disabled - no indicator needed
1238
1279
  return null;
1239
1280
  }
1240
1281
  handleSpecialKey(_str, key) {
@@ -1296,10 +1337,11 @@ export class TerminalInput extends EventEmitter {
1296
1337
  }
1297
1338
  return true;
1298
1339
  case 'pageup':
1299
- this.scrollUp(20); // Scroll up by 20 lines
1340
+ // Scrollback disabled in alternate screen mode
1341
+ // Users should use terminal's native scrollback if available
1300
1342
  return true;
1301
1343
  case 'pagedown':
1302
- this.scrollDown(20); // Scroll down by 20 lines
1344
+ // Scrollback disabled in alternate screen mode
1303
1345
  return true;
1304
1346
  case 'tab':
1305
1347
  if (key.shift) {
@@ -1693,120 +1735,53 @@ export class TerminalInput extends EventEmitter {
1693
1735
  }
1694
1736
  /**
1695
1737
  * Scroll up by a number of lines (PageUp)
1738
+ * Note: Scrollback is disabled in alternate screen mode to avoid display corruption.
1739
+ * Users should use their terminal's native scrollback or copy/paste features.
1696
1740
  */
1697
- scrollUp(lines = 10) {
1698
- const { rows } = this.getSize();
1699
- const chatBoxHeight = this.getChatBoxHeight();
1700
- const visibleLines = Math.max(1, rows - chatBoxHeight);
1701
- // Calculate max scroll offset
1702
- const maxOffset = Math.max(0, this.scrollbackBuffer.length - visibleLines);
1703
- this.scrollbackOffset = Math.min(this.scrollbackOffset + lines, maxOffset);
1704
- this.isInScrollbackMode = this.scrollbackOffset > 0;
1705
- if (this.isInScrollbackMode) {
1706
- this.renderScrollbackView();
1707
- }
1741
+ scrollUp(_lines = 10) {
1742
+ // Scrollback disabled - alternate screen buffer doesn't support it well
1743
+ // The scrollback buffer is still maintained for potential future use
1744
+ // Users can select and copy text normally since mouse tracking is off
1708
1745
  }
1709
1746
  /**
1710
1747
  * Scroll down by a number of lines (PageDown)
1748
+ * Note: Scrollback disabled - see scrollUp comment
1711
1749
  */
1712
- scrollDown(lines = 10) {
1713
- this.scrollbackOffset = Math.max(0, this.scrollbackOffset - lines);
1714
- this.isInScrollbackMode = this.scrollbackOffset > 0;
1715
- if (this.isInScrollbackMode) {
1716
- this.renderScrollbackView();
1717
- }
1718
- else {
1719
- // Returned to live mode - force re-render
1720
- this.forceRender();
1721
- }
1750
+ scrollDown(_lines = 10) {
1751
+ // Scrollback disabled
1722
1752
  }
1723
1753
  /**
1724
1754
  * Jump to the top of scrollback buffer
1755
+ * DISABLED: Scrollback navigation causes display corruption in alternate screen mode.
1756
+ * The scrollback buffer is maintained but cannot be rendered properly.
1725
1757
  */
1726
1758
  scrollToTop() {
1727
- const { rows } = this.getSize();
1728
- const chatBoxHeight = this.getChatBoxHeight();
1729
- const visibleLines = Math.max(1, rows - chatBoxHeight);
1730
- const maxOffset = Math.max(0, this.scrollbackBuffer.length - visibleLines);
1731
- this.scrollbackOffset = maxOffset;
1732
- this.isInScrollbackMode = true;
1733
- this.renderScrollbackView();
1759
+ // Disabled - causes display corruption in alternate screen buffer
1734
1760
  }
1735
1761
  /**
1736
1762
  * Jump to the bottom (live mode)
1763
+ * DISABLED: Scrollback navigation causes display corruption.
1737
1764
  */
1738
1765
  scrollToBottom() {
1766
+ // Reset scrollback state in case it was somehow enabled
1739
1767
  this.scrollbackOffset = 0;
1740
1768
  this.isInScrollbackMode = false;
1741
- this.forceRender();
1742
1769
  }
1743
1770
  /**
1744
1771
  * Toggle scrollback mode on/off (Alt+S hotkey)
1772
+ * DISABLED: Scrollback navigation causes display corruption in alternate screen mode.
1745
1773
  */
1746
1774
  toggleScrollbackMode() {
1747
- if (this.isInScrollbackMode) {
1748
- this.scrollToBottom();
1749
- }
1750
- else if (this.scrollbackBuffer.length > 0) {
1751
- this.scrollUp(20);
1752
- }
1775
+ // Disabled - alternate screen buffer doesn't support manual scrollback rendering
1753
1776
  }
1754
1777
  /**
1755
- * Render the scrollback buffer view with enhanced visuals
1756
- * Features:
1757
- * - Visual scroll position indicator
1758
- * - Progress bar showing position in history
1759
- * - Keyboard navigation hints
1760
- * - Animated indicators
1778
+ * Render the scrollback buffer view.
1779
+ * DISABLED: This causes display corruption in alternate screen mode.
1780
+ * The alternate screen buffer has its own rendering model that conflicts with
1781
+ * manual scroll region manipulation.
1761
1782
  */
1762
1783
  renderScrollbackView() {
1763
- const { rows, cols } = this.getSize();
1764
- const chatBoxHeight = this.getChatBoxHeight();
1765
- const contentHeight = Math.max(1, rows - chatBoxHeight);
1766
- writeLock.lock('renderScrollback');
1767
- try {
1768
- this.write(ESC.SAVE);
1769
- this.write(ESC.HIDE);
1770
- // Clear content area
1771
- for (let i = 1; i <= contentHeight; i++) {
1772
- this.write(ESC.TO(i, 1));
1773
- this.write(ESC.CLEAR_LINE);
1774
- }
1775
- // Calculate which lines to show
1776
- const totalLines = this.scrollbackBuffer.length;
1777
- const startIdx = Math.max(0, totalLines - this.scrollbackOffset - contentHeight);
1778
- const endIdx = Math.max(0, totalLines - this.scrollbackOffset);
1779
- const visibleLines = this.scrollbackBuffer.slice(startIdx, endIdx);
1780
- // Build header bar with navigation hints
1781
- const headerInfo = this.buildScrollbackHeader(cols, totalLines, startIdx, endIdx);
1782
- this.write(ESC.TO(1, 1));
1783
- this.write(headerInfo);
1784
- // Render visible lines with line numbers and visual guides
1785
- const lineNumWidth = String(totalLines).length + 1;
1786
- const contentStart = 2; // Start after header
1787
- for (let i = 0; i < Math.min(visibleLines.length, contentHeight - 1); i++) {
1788
- const line = visibleLines[i] ?? '';
1789
- const lineNum = startIdx + i + 1;
1790
- this.write(ESC.TO(contentStart + i, 1));
1791
- // Line number gutter
1792
- const numStr = String(lineNum).padStart(lineNumWidth, ' ');
1793
- this.write(theme.ui.muted(`${numStr} │ `));
1794
- // Content with truncation
1795
- const gutterWidth = lineNumWidth + 4;
1796
- const maxLen = cols - gutterWidth - 2;
1797
- const displayLine = line.length > maxLen ? line.slice(0, maxLen - 3) + '...' : line;
1798
- this.write(displayLine);
1799
- }
1800
- // Add visual scroll track on the right edge
1801
- this.renderScrollTrack(cols, contentHeight, totalLines, startIdx, endIdx);
1802
- this.write(ESC.RESTORE);
1803
- this.write(ESC.SHOW);
1804
- }
1805
- finally {
1806
- writeLock.unlock();
1807
- }
1808
- // Re-render chat box
1809
- this.forceRender();
1784
+ // Disabled - causes display corruption
1810
1785
  }
1811
1786
  /**
1812
1787
  * Build scrollback header with navigation hints