erosolar-cli 1.7.364 → 1.7.366

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 (44) hide show
  1. package/dist/shell/interactiveShell.d.ts +0 -2
  2. package/dist/shell/interactiveShell.d.ts.map +1 -1
  3. package/dist/shell/interactiveShell.js +15 -32
  4. package/dist/shell/interactiveShell.js.map +1 -1
  5. package/dist/shell/terminalInput.d.ts +19 -32
  6. package/dist/shell/terminalInput.d.ts.map +1 -1
  7. package/dist/shell/terminalInput.js +228 -185
  8. package/dist/shell/terminalInput.js.map +1 -1
  9. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  10. package/dist/shell/terminalInputAdapter.js +2 -5
  11. package/dist/shell/terminalInputAdapter.js.map +1 -1
  12. package/dist/subagents/taskRunner.d.ts.map +1 -1
  13. package/dist/subagents/taskRunner.js +25 -7
  14. package/dist/subagents/taskRunner.js.map +1 -1
  15. package/dist/tools/learnTools.js +4 -127
  16. package/dist/tools/learnTools.js.map +1 -1
  17. package/dist/ui/ShellUIAdapter.d.ts +0 -27
  18. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  19. package/dist/ui/ShellUIAdapter.js +9 -175
  20. package/dist/ui/ShellUIAdapter.js.map +1 -1
  21. package/dist/ui/theme.d.ts +3 -108
  22. package/dist/ui/theme.d.ts.map +1 -1
  23. package/dist/ui/theme.js +3 -124
  24. package/dist/ui/theme.js.map +1 -1
  25. package/dist/ui/toolDisplay.d.ts +7 -44
  26. package/dist/ui/toolDisplay.d.ts.map +1 -1
  27. package/dist/ui/toolDisplay.js +84 -168
  28. package/dist/ui/toolDisplay.js.map +1 -1
  29. package/dist/ui/unified/index.d.ts +0 -11
  30. package/dist/ui/unified/index.d.ts.map +1 -1
  31. package/dist/ui/unified/index.js +0 -16
  32. package/dist/ui/unified/index.js.map +1 -1
  33. package/dist/ui/unified/layout.d.ts.map +1 -1
  34. package/dist/ui/unified/layout.js +47 -32
  35. package/dist/ui/unified/layout.js.map +1 -1
  36. package/package.json +1 -1
  37. package/dist/ui/compactRenderer.d.ts +0 -139
  38. package/dist/ui/compactRenderer.d.ts.map +0 -1
  39. package/dist/ui/compactRenderer.js +0 -398
  40. package/dist/ui/compactRenderer.js.map +0 -1
  41. package/dist/ui/inPlaceUpdater.d.ts +0 -181
  42. package/dist/ui/inPlaceUpdater.d.ts.map +0 -1
  43. package/dist/ui/inPlaceUpdater.js +0 -515
  44. package/dist/ui/inPlaceUpdater.js.map +0 -1
@@ -92,6 +92,7 @@ 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
95
96
  lastRenderContent = '';
96
97
  lastRenderCursor = -1;
97
98
  renderDirty = false;
@@ -117,7 +118,7 @@ export class TerminalInput extends EventEmitter {
117
118
  editMode = 'display-edits';
118
119
  verificationEnabled = true;
119
120
  autoContinueEnabled = false;
120
- verificationHotkey = 'alt+v';
121
+ verificationHotkey = 'alt+d';
121
122
  autoContinueHotkey = 'alt+c';
122
123
  thinkingHotkey = '/thinking';
123
124
  modelLabel = null;
@@ -173,8 +174,8 @@ export class TerminalInput extends EventEmitter {
173
174
  }
174
175
  }
175
176
  /**
176
- * Process raw terminal data (handles bracketed paste sequences and mouse events)
177
- * Returns true if the data was consumed (paste sequence)
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.)
178
179
  */
179
180
  processRawData(data) {
180
181
  // Check for mouse events (SGR mode: \x1b[<button;x;yM or m)
@@ -190,6 +191,14 @@ export class TerminalInput extends EventEmitter {
190
191
  const remaining = data.slice(mouseEventEnd);
191
192
  return { consumed: true, passthrough: remaining };
192
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 };
201
+ }
193
202
  // Check for paste start
194
203
  if (data.includes(ESC.PASTE_START)) {
195
204
  this.isPasting = true;
@@ -228,30 +237,12 @@ export class TerminalInput extends EventEmitter {
228
237
  if (button === 64) {
229
238
  // Scroll up (3 lines per wheel tick)
230
239
  this.scrollUp(3);
231
- return;
232
240
  }
233
- if (button === 65) {
241
+ else if (button === 65) {
234
242
  // Scroll down (3 lines per wheel tick)
235
243
  this.scrollDown(3);
236
- return;
237
- }
238
- // Left button (0), middle button (1), right button (2)
239
- // These are captured by SGR mouse tracking but we don't need to handle them
240
- // Just consume silently - the escape sequence has already been processed
241
- // The 'M' action is press, 'm' is release
242
- if (button <= 2) {
243
- // Silently consume click events to prevent artifacts
244
- // Left clicks (button=0) in the input area could focus the input
245
- // but terminal apps typically handle this natively
246
- return;
247
244
  }
248
- // Button with motion flag (button + 32) - drag events
249
- if (button >= 32 && button <= 34) {
250
- // Drag events - consume silently
251
- return;
252
- }
253
- // Any other unrecognized button - log for debugging in dev mode
254
- // but don't output anything to prevent artifacts
245
+ // Ignore other mouse events (clicks, drags, etc.) for now
255
246
  }
256
247
  /**
257
248
  * Handle a keypress event
@@ -457,12 +448,14 @@ export class TerminalInput extends EventEmitter {
457
448
  const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
458
449
  const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
459
450
  const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
451
+ const nextToolCount = options.toolCount === undefined ? this.toolCount : (options.toolCount ?? null);
460
452
  if (this.verificationEnabled === nextVerification &&
461
453
  this.autoContinueEnabled === nextAutoContinue &&
462
454
  this.verificationHotkey === nextVerifyHotkey &&
463
455
  this.autoContinueHotkey === nextAutoHotkey &&
464
456
  this.thinkingHotkey === nextThinkingHotkey &&
465
- this.thinkingModeLabel === nextThinkingLabel) {
457
+ this.thinkingModeLabel === nextThinkingLabel &&
458
+ this.toolCount === nextToolCount) {
466
459
  return;
467
460
  }
468
461
  this.verificationEnabled = nextVerification;
@@ -471,6 +464,7 @@ export class TerminalInput extends EventEmitter {
471
464
  this.autoContinueHotkey = nextAutoHotkey;
472
465
  this.thinkingHotkey = nextThinkingHotkey;
473
466
  this.thinkingModeLabel = nextThinkingLabel;
467
+ this.toolCount = nextToolCount;
474
468
  this.scheduleRender();
475
469
  }
476
470
  /**
@@ -706,143 +700,101 @@ export class TerminalInput extends EventEmitter {
706
700
  return [`${leftText}${' '.repeat(spacing)}${rightText}`];
707
701
  }
708
702
  /**
709
- * Build mode controls line with all keyboard shortcuts.
710
- * Shows status, all toggles, and contextual information.
711
- * Enhanced with comprehensive feature status display.
712
- *
713
- * Format: [Key] Label:status · [Key] Label:status · context% · model
703
+ * Build mode controls line with all keyboard shortcuts and toggle states.
704
+ * Shows below the chat box input area with spelled-out key names.
714
705
  */
715
706
  buildModeControls(cols) {
716
707
  const width = Math.max(8, cols - 2);
717
- const leftParts = [];
718
- const rightParts = [];
719
- // Streaming indicator with animated spinner
708
+ const parts = [];
709
+ // Streaming status with stop hint
720
710
  if (this.streamingLabel) {
721
- leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
711
+ parts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
712
+ parts.push({ text: `Esc:stop`, tone: 'warn' });
722
713
  }
723
- // Override status (warnings, errors)
724
- if (this.overrideStatusMessage) {
725
- leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
726
- }
727
- // Main status message
728
- if (this.statusMessage) {
729
- leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
730
- }
731
- // === KEYBOARD SHORTCUTS (Compact format: Key·Status) ===
732
- // Interrupt shortcut (during streaming)
733
- if (this.mode === 'streaming' || this.scrollRegionActive) {
734
- leftParts.push({ text: `Esc·Stop`, tone: 'warn' });
735
- }
736
- // Edit mode toggle (Shift+Tab) - Compact display
737
- const editStatus = this.editMode === 'display-edits' ? '✓' : '?';
714
+ // === MODE TOGGLES WITH HOTKEYS ===
715
+ // Format: ⌥D:verify✓ · ⌥C:auto○ · ⌥T:think:bal · ⇧Tab:edits
716
+ // Edit mode toggle (Shift+Tab)
717
+ const editIcon = this.editMode === 'display-edits' ? '✓' : '?';
738
718
  const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
739
- leftParts.push({ text: `⇧Tab·${editStatus}edits`, tone: editTone });
740
- // Verification toggle (Alt+V) - Compact display
741
- const verifyStatus = this.verificationEnabled ? '✓' : '○';
742
- leftParts.push({
743
- text: `⌥V·${verifyStatus}verify`,
744
- tone: this.verificationEnabled ? 'success' : 'muted'
745
- });
746
- // Auto-continue toggle (Alt+C) - Compact display
747
- const autoStatus = this.autoContinueEnabled ? '↻' : '○';
748
- leftParts.push({
749
- text: `⌥C·${autoStatus}cont`,
750
- tone: this.autoContinueEnabled ? 'info' : 'muted'
751
- });
752
- // Thinking mode toggle (if available)
719
+ parts.push({ text: `⇧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: `⌥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: `⌥C:auto${autoIcon}`, tone: autoTone });
728
+ // Thinking mode toggle (Alt+T)
753
729
  if (this.thinkingModeLabel) {
754
- const shortThinking = this.thinkingModeLabel.length > 8
755
- ? this.thinkingModeLabel.slice(0, 6) + '..'
756
- : this.thinkingModeLabel;
757
- rightParts.push({ text: `⌥T·${shortThinking}`, tone: 'info' });
758
- }
759
- // === CONTEXTUAL INFO ===
760
- // Queued commands
761
- if (this.queue.length > 0) {
762
- const queueIcon = this.mode === 'streaming' ? '⏳' : '▸';
763
- leftParts.push({ text: `${queueIcon}${this.queue.length} queued`, tone: 'info' });
764
- }
765
- // Multi-line indicator
766
- if (this.buffer.includes('\n')) {
767
- const lineCount = this.buffer.split('\n').length;
768
- rightParts.push({ text: `${lineCount}L`, tone: 'muted' });
769
- }
770
- // Paste indicator
771
- if (this.pastePlaceholders.length > 0) {
772
- const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
773
- rightParts.push({
774
- text: `📋${latest.lineCount}L`,
775
- tone: 'info',
776
- });
730
+ const thinkingShort = this.thinkingModeLabel === 'minimal' ? 'min' :
731
+ this.thinkingModeLabel === 'balanced' ? 'bal' :
732
+ this.thinkingModeLabel === 'extended' ? 'ext' : this.thinkingModeLabel;
733
+ parts.push({ text: `⌥T:${thinkingShort}`, tone: 'info' });
734
+ }
735
+ else {
736
+ parts.push({ text: `⌥T:think`, tone: 'muted' });
777
737
  }
778
- // === STATUS INDICATORS (Right side) ===
779
- // Context usage with visual bar indicator
738
+ // Clear context (Alt+X)
739
+ parts.push({ text: `⌥X:clear`, tone: 'muted' });
740
+ // === STATUS INFO ===
741
+ // Context remaining percentage
780
742
  const contextRemaining = this.computeContextRemaining();
781
743
  if (contextRemaining !== null) {
782
- const tone = contextRemaining <= 10 ? 'error' : contextRemaining <= 30 ? 'warn' : contextRemaining <= 60 ? 'info' : 'success';
783
- const bar = this.renderMiniContextBar(contextRemaining);
784
- const usedPct = this.contextUsage !== null ? this.contextUsage : (100 - contextRemaining);
785
- const label = contextRemaining === 0
786
- ? `⚠compact!`
787
- : `${bar} ${usedPct}% used`;
788
- rightParts.push({ text: label, tone });
789
- }
790
- else if (this.contextUsage !== null) {
791
- // Fallback: just show raw context usage percentage
792
- const tone = this.contextUsage >= 90 ? 'error' : this.contextUsage >= 70 ? 'warn' : 'muted';
793
- rightParts.push({ text: `⊛${this.contextUsage}%`, tone });
794
- }
795
- // Model/provider quick reference (compact)
796
- if (this.modelLabel && !this.streamingLabel) {
797
- const shortModel = this.modelLabel.length > 15 ? this.modelLabel.slice(0, 13) + '..' : this.modelLabel;
798
- rightParts.push({ text: `⚙${shortModel}`, tone: 'muted' });
799
- }
800
- // Render: left-aligned shortcuts, right-aligned context info
801
- if (!rightParts.length || width < 60) {
802
- const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
803
- return renderStatusLine(merged, width);
804
- }
805
- const leftWidth = Math.max(12, Math.floor(width * 0.6));
806
- const rightWidth = Math.max(14, width - leftWidth - 1);
807
- const leftText = renderStatusLine(leftParts, leftWidth);
808
- const rightText = renderStatusLine(rightParts, rightWidth);
809
- const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
810
- return `${leftText}${' '.repeat(spacing)}${rightText}`;
811
- }
812
- /**
813
- * Render a mini context usage bar (5 chars)
814
- */
815
- renderMiniContextBar(percentRemaining) {
816
- const bars = 5;
817
- const filled = Math.round((percentRemaining / 100) * bars);
818
- const empty = bars - filled;
819
- return '█'.repeat(filled) + '░'.repeat(empty);
744
+ const tone = contextRemaining <= 10 ? 'warn' : contextRemaining <= 30 ? 'info' : 'muted';
745
+ parts.push({ text: `ctx:${contextRemaining}%`, tone });
746
+ }
747
+ // Token usage
748
+ if (this.metaTokensUsed !== null) {
749
+ const used = this.formatTokenCount(this.metaTokensUsed);
750
+ const limit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
751
+ parts.push({ text: `${used}${limit}tk`, tone: 'muted' });
752
+ }
753
+ // Elapsed time (always show if available, not just during streaming)
754
+ if (this.metaElapsedSeconds !== null) {
755
+ parts.push({ text: `⏱${this.formatElapsedLabel(this.metaElapsedSeconds)}`, tone: 'muted' });
756
+ }
757
+ // Tool count if available
758
+ if (this.toolCount !== null && this.toolCount > 0) {
759
+ parts.push({ text: `⚒${this.toolCount}`, tone: 'info' });
760
+ }
761
+ return renderStatusLine(parts, width);
820
762
  }
821
763
  formatHotkey(hotkey) {
822
764
  const normalized = hotkey.trim().toLowerCase();
823
765
  if (!normalized)
824
766
  return hotkey;
825
767
  const parts = normalized.split('+').filter(Boolean);
768
+ // Use readable key names instead of symbols for better terminal compatibility
826
769
  const map = {
827
- shift: '',
828
- sh: '',
829
- alt: '',
830
- option: '',
831
- opt: '',
832
- ctrl: '',
833
- control: '',
834
- cmd: '',
835
- meta: '',
770
+ shift: 'Shift',
771
+ sh: 'Shift',
772
+ alt: 'Alt',
773
+ option: 'Alt',
774
+ opt: 'Alt',
775
+ ctrl: 'Ctrl',
776
+ control: 'Ctrl',
777
+ cmd: 'Cmd',
778
+ meta: 'Cmd',
779
+ esc: 'Esc',
780
+ escape: 'Esc',
781
+ tab: 'Tab',
782
+ return: 'Enter',
783
+ enter: 'Enter',
784
+ pageup: 'PgUp',
785
+ pagedown: 'PgDn',
786
+ home: 'Home',
787
+ end: 'End',
836
788
  };
837
789
  const formatted = parts
838
790
  .map((part) => {
839
- const symbol = map[part];
840
- if (symbol)
841
- return symbol;
842
- return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
791
+ const label = map[part];
792
+ if (label)
793
+ return label;
794
+ return part.length === 1 ? part.toUpperCase() : part.charAt(0).toUpperCase() + part.slice(1);
843
795
  })
844
- .join('');
845
- return formatted || hotkey;
796
+ .join('+');
797
+ return `[${formatted}]`;
846
798
  }
847
799
  computeContextRemaining() {
848
800
  if (this.contextUsage === null) {
@@ -1008,7 +960,7 @@ export class TerminalInput extends EventEmitter {
1008
960
  * Calculate chat box height.
1009
961
  */
1010
962
  getChatBoxHeight() {
1011
- return 6; // Fixed: meta + divider + input + controls + buffer
963
+ return 5; // Fixed: divider + input + status + buffer
1012
964
  }
1013
965
  /**
1014
966
  * @deprecated Use streamContent() instead
@@ -1044,26 +996,41 @@ export class TerminalInput extends EventEmitter {
1044
996
  }
1045
997
  }
1046
998
  /**
1047
- * Enter alternate screen buffer.
1048
- * DISABLED: Using terminal-native mode for proper scrollback and text selection.
999
+ * Enter alternate screen buffer and clear it.
1000
+ * This gives us full control over the terminal without affecting user's history.
1049
1001
  */
1050
1002
  enterAlternateScreen() {
1051
- // Disabled - using terminal-native mode
1052
- this.contentRow = 1;
1003
+ writeLock.lock('enterAltScreen');
1004
+ try {
1005
+ this.write(ESC.ENTER_ALT_SCREEN);
1006
+ this.write(ESC.HOME);
1007
+ this.write(ESC.CLEAR_SCREEN);
1008
+ this.contentRow = 1;
1009
+ this.alternateScreenActive = true;
1010
+ }
1011
+ finally {
1012
+ writeLock.unlock();
1013
+ }
1053
1014
  }
1054
1015
  /**
1055
1016
  * Exit alternate screen buffer.
1056
- * DISABLED: Using terminal-native mode.
1017
+ * Restores the user's previous terminal content.
1057
1018
  */
1058
1019
  exitAlternateScreen() {
1059
- // Disabled - using terminal-native mode
1020
+ writeLock.lock('exitAltScreen');
1021
+ try {
1022
+ this.write(ESC.EXIT_ALT_SCREEN);
1023
+ this.alternateScreenActive = false;
1024
+ }
1025
+ finally {
1026
+ writeLock.unlock();
1027
+ }
1060
1028
  }
1061
1029
  /**
1062
1030
  * Check if alternate screen buffer is currently active.
1063
- * Always returns false - using terminal-native mode.
1064
1031
  */
1065
1032
  isAlternateScreenActive() {
1066
- return false;
1033
+ return this.alternateScreenActive;
1067
1034
  }
1068
1035
  /**
1069
1036
  * Get a snapshot of the scrollback buffer (for display on exit).
@@ -1072,17 +1039,14 @@ export class TerminalInput extends EventEmitter {
1072
1039
  return [...this.scrollbackBuffer];
1073
1040
  }
1074
1041
  /**
1075
- * Clear the visible terminal area and reset content position.
1076
- * In terminal-native mode, this just adds newlines to scroll past content
1077
- * rather than clearing history (preserving scrollback).
1042
+ * Clear the entire terminal screen and reset content position.
1043
+ * This removes all content including the launching command.
1078
1044
  */
1079
1045
  clearScreen() {
1080
1046
  writeLock.lock('clearScreen');
1081
1047
  try {
1082
- // In native mode, scroll past existing content rather than clearing
1083
- const { rows } = this.getSize();
1084
- this.write('\n'.repeat(rows));
1085
1048
  this.write(ESC.HOME);
1049
+ this.write(ESC.CLEAR_SCREEN);
1086
1050
  this.contentRow = 1;
1087
1051
  }
1088
1052
  finally {
@@ -1182,8 +1146,8 @@ export class TerminalInput extends EventEmitter {
1182
1146
  this.insertNewline();
1183
1147
  break;
1184
1148
  // === MODE TOGGLES ===
1185
- case 'v':
1186
- // Alt+V: Toggle verification mode (auto-tests after edits)
1149
+ case 'd':
1150
+ // Alt+D: Toggle verification/double-check mode (auto-tests after edits)
1187
1151
  this.emit('toggleVerify');
1188
1152
  break;
1189
1153
  case 'c':
@@ -1250,12 +1214,25 @@ export class TerminalInput extends EventEmitter {
1250
1214
  return Math.max(5, rows - chatBoxHeight - 2);
1251
1215
  }
1252
1216
  /**
1253
- * Build scroll indicator for the divider line.
1254
- * Scrollback navigation is disabled in alternate screen mode.
1255
- * This returns null - no scroll indicator is shown.
1217
+ * Build scroll indicator for the divider line (Claude Code style).
1218
+ * Shows scroll position when in scrollback mode, or history size hint when idle.
1256
1219
  */
1257
1220
  buildScrollIndicator() {
1258
- // Scrollback navigation disabled - no indicator needed
1221
+ const bufferSize = this.scrollbackBuffer.length;
1222
+ // In scrollback mode - show position
1223
+ if (this.isInScrollbackMode && this.scrollbackOffset > 0) {
1224
+ const { rows } = this.getSize();
1225
+ const chatBoxHeight = this.getChatBoxHeight();
1226
+ const viewportHeight = Math.max(1, rows - chatBoxHeight);
1227
+ const currentPos = Math.max(0, bufferSize - this.scrollbackOffset - viewportHeight);
1228
+ const pct = bufferSize > 0 ? Math.round((currentPos / bufferSize) * 100) : 0;
1229
+ return `↑${this.scrollbackOffset} · ${pct}% · PgUp/Dn`;
1230
+ }
1231
+ // Not in scrollback - show hint if there's history
1232
+ if (bufferSize > 20) {
1233
+ const sizeLabel = bufferSize >= 1000 ? `${Math.floor(bufferSize / 1000)}k` : `${bufferSize}`;
1234
+ return `↕${sizeLabel}L · PgUp`;
1235
+ }
1259
1236
  return null;
1260
1237
  }
1261
1238
  handleSpecialKey(_str, key) {
@@ -1317,11 +1294,10 @@ export class TerminalInput extends EventEmitter {
1317
1294
  }
1318
1295
  return true;
1319
1296
  case 'pageup':
1320
- // Scrollback disabled in alternate screen mode
1321
- // Users should use terminal's native scrollback if available
1297
+ this.scrollUp(20); // Scroll up by 20 lines
1322
1298
  return true;
1323
1299
  case 'pagedown':
1324
- // Scrollback disabled in alternate screen mode
1300
+ this.scrollDown(20); // Scroll down by 20 lines
1325
1301
  return true;
1326
1302
  case 'tab':
1327
1303
  if (key.shift) {
@@ -1715,53 +1691,120 @@ export class TerminalInput extends EventEmitter {
1715
1691
  }
1716
1692
  /**
1717
1693
  * Scroll up by a number of lines (PageUp)
1718
- * Note: Scrollback is disabled in alternate screen mode to avoid display corruption.
1719
- * Users should use their terminal's native scrollback or copy/paste features.
1720
1694
  */
1721
- scrollUp(_lines = 10) {
1722
- // Scrollback disabled - alternate screen buffer doesn't support it well
1723
- // The scrollback buffer is still maintained for potential future use
1724
- // Users can select and copy text normally since mouse tracking is off
1695
+ scrollUp(lines = 10) {
1696
+ const { rows } = this.getSize();
1697
+ const chatBoxHeight = this.getChatBoxHeight();
1698
+ const visibleLines = Math.max(1, rows - chatBoxHeight);
1699
+ // Calculate max scroll offset
1700
+ const maxOffset = Math.max(0, this.scrollbackBuffer.length - visibleLines);
1701
+ this.scrollbackOffset = Math.min(this.scrollbackOffset + lines, maxOffset);
1702
+ this.isInScrollbackMode = this.scrollbackOffset > 0;
1703
+ if (this.isInScrollbackMode) {
1704
+ this.renderScrollbackView();
1705
+ }
1725
1706
  }
1726
1707
  /**
1727
1708
  * Scroll down by a number of lines (PageDown)
1728
- * Note: Scrollback disabled - see scrollUp comment
1729
1709
  */
1730
- scrollDown(_lines = 10) {
1731
- // Scrollback disabled
1710
+ scrollDown(lines = 10) {
1711
+ this.scrollbackOffset = Math.max(0, this.scrollbackOffset - lines);
1712
+ this.isInScrollbackMode = this.scrollbackOffset > 0;
1713
+ if (this.isInScrollbackMode) {
1714
+ this.renderScrollbackView();
1715
+ }
1716
+ else {
1717
+ // Returned to live mode - force re-render
1718
+ this.forceRender();
1719
+ }
1732
1720
  }
1733
1721
  /**
1734
1722
  * Jump to the top of scrollback buffer
1735
- * DISABLED: Scrollback navigation causes display corruption in alternate screen mode.
1736
- * The scrollback buffer is maintained but cannot be rendered properly.
1737
1723
  */
1738
1724
  scrollToTop() {
1739
- // Disabled - causes display corruption in alternate screen buffer
1725
+ const { rows } = this.getSize();
1726
+ const chatBoxHeight = this.getChatBoxHeight();
1727
+ const visibleLines = Math.max(1, rows - chatBoxHeight);
1728
+ const maxOffset = Math.max(0, this.scrollbackBuffer.length - visibleLines);
1729
+ this.scrollbackOffset = maxOffset;
1730
+ this.isInScrollbackMode = true;
1731
+ this.renderScrollbackView();
1740
1732
  }
1741
1733
  /**
1742
1734
  * Jump to the bottom (live mode)
1743
- * DISABLED: Scrollback navigation causes display corruption.
1744
1735
  */
1745
1736
  scrollToBottom() {
1746
- // Reset scrollback state in case it was somehow enabled
1747
1737
  this.scrollbackOffset = 0;
1748
1738
  this.isInScrollbackMode = false;
1739
+ this.forceRender();
1749
1740
  }
1750
1741
  /**
1751
1742
  * Toggle scrollback mode on/off (Alt+S hotkey)
1752
- * DISABLED: Scrollback navigation causes display corruption in alternate screen mode.
1753
1743
  */
1754
1744
  toggleScrollbackMode() {
1755
- // Disabled - alternate screen buffer doesn't support manual scrollback rendering
1745
+ if (this.isInScrollbackMode) {
1746
+ this.scrollToBottom();
1747
+ }
1748
+ else if (this.scrollbackBuffer.length > 0) {
1749
+ this.scrollUp(20);
1750
+ }
1756
1751
  }
1757
1752
  /**
1758
- * Render the scrollback buffer view.
1759
- * DISABLED: This causes display corruption in alternate screen mode.
1760
- * The alternate screen buffer has its own rendering model that conflicts with
1761
- * manual scroll region manipulation.
1753
+ * Render the scrollback buffer view with enhanced visuals
1754
+ * Features:
1755
+ * - Visual scroll position indicator
1756
+ * - Progress bar showing position in history
1757
+ * - Keyboard navigation hints
1758
+ * - Animated indicators
1762
1759
  */
1763
1760
  renderScrollbackView() {
1764
- // Disabled - causes display corruption
1761
+ const { rows, cols } = this.getSize();
1762
+ const chatBoxHeight = this.getChatBoxHeight();
1763
+ const contentHeight = Math.max(1, rows - chatBoxHeight);
1764
+ writeLock.lock('renderScrollback');
1765
+ try {
1766
+ this.write(ESC.SAVE);
1767
+ this.write(ESC.HIDE);
1768
+ // Clear content area
1769
+ for (let i = 1; i <= contentHeight; i++) {
1770
+ this.write(ESC.TO(i, 1));
1771
+ this.write(ESC.CLEAR_LINE);
1772
+ }
1773
+ // Calculate which lines to show
1774
+ const totalLines = this.scrollbackBuffer.length;
1775
+ const startIdx = Math.max(0, totalLines - this.scrollbackOffset - contentHeight);
1776
+ const endIdx = Math.max(0, totalLines - this.scrollbackOffset);
1777
+ const visibleLines = this.scrollbackBuffer.slice(startIdx, endIdx);
1778
+ // Build header bar with navigation hints
1779
+ const headerInfo = this.buildScrollbackHeader(cols, totalLines, startIdx, endIdx);
1780
+ this.write(ESC.TO(1, 1));
1781
+ this.write(headerInfo);
1782
+ // Render visible lines with line numbers and visual guides
1783
+ const lineNumWidth = String(totalLines).length + 1;
1784
+ const contentStart = 2; // Start after header
1785
+ for (let i = 0; i < Math.min(visibleLines.length, contentHeight - 1); i++) {
1786
+ const line = visibleLines[i] ?? '';
1787
+ const lineNum = startIdx + i + 1;
1788
+ this.write(ESC.TO(contentStart + i, 1));
1789
+ // Line number gutter
1790
+ const numStr = String(lineNum).padStart(lineNumWidth, ' ');
1791
+ this.write(theme.ui.muted(`${numStr} │ `));
1792
+ // Content with truncation
1793
+ const gutterWidth = lineNumWidth + 4;
1794
+ const maxLen = cols - gutterWidth - 2;
1795
+ const displayLine = line.length > maxLen ? line.slice(0, maxLen - 3) + '...' : line;
1796
+ this.write(displayLine);
1797
+ }
1798
+ // Add visual scroll track on the right edge
1799
+ this.renderScrollTrack(cols, contentHeight, totalLines, startIdx, endIdx);
1800
+ this.write(ESC.RESTORE);
1801
+ this.write(ESC.SHOW);
1802
+ }
1803
+ finally {
1804
+ writeLock.unlock();
1805
+ }
1806
+ // Re-render chat box
1807
+ this.forceRender();
1765
1808
  }
1766
1809
  /**
1767
1810
  * Build scrollback header with navigation hints