erosolar-cli 1.7.361 → 1.7.363

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 +13 -32
  4. package/dist/shell/interactiveShell.js.map +1 -1
  5. package/dist/shell/terminalInput.d.ts +19 -26
  6. package/dist/shell/terminalInput.d.ts.map +1 -1
  7. package/dist/shell/terminalInput.js +224 -148
  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 +32 -163
  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;
@@ -439,12 +448,14 @@ export class TerminalInput extends EventEmitter {
439
448
  const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
440
449
  const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
441
450
  const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
451
+ const nextToolCount = options.toolCount === undefined ? this.toolCount : (options.toolCount ?? null);
442
452
  if (this.verificationEnabled === nextVerification &&
443
453
  this.autoContinueEnabled === nextAutoContinue &&
444
454
  this.verificationHotkey === nextVerifyHotkey &&
445
455
  this.autoContinueHotkey === nextAutoHotkey &&
446
456
  this.thinkingHotkey === nextThinkingHotkey &&
447
- this.thinkingModeLabel === nextThinkingLabel) {
457
+ this.thinkingModeLabel === nextThinkingLabel &&
458
+ this.toolCount === nextToolCount) {
448
459
  return;
449
460
  }
450
461
  this.verificationEnabled = nextVerification;
@@ -453,6 +464,7 @@ export class TerminalInput extends EventEmitter {
453
464
  this.autoContinueHotkey = nextAutoHotkey;
454
465
  this.thinkingHotkey = nextThinkingHotkey;
455
466
  this.thinkingModeLabel = nextThinkingLabel;
467
+ this.toolCount = nextToolCount;
456
468
  this.scheduleRender();
457
469
  }
458
470
  /**
@@ -688,125 +700,98 @@ export class TerminalInput extends EventEmitter {
688
700
  return [`${leftText}${' '.repeat(spacing)}${rightText}`];
689
701
  }
690
702
  /**
691
- * Build mode controls line with all keyboard shortcuts.
692
- * Shows status, all toggles, and contextual information.
693
- * Enhanced with comprehensive feature status display.
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.
694
705
  */
695
706
  buildModeControls(cols) {
696
707
  const width = Math.max(8, cols - 2);
697
- const leftParts = [];
698
- const rightParts = [];
699
- // Streaming indicator with animated spinner
708
+ const parts = [];
709
+ // Streaming status with stop hint
700
710
  if (this.streamingLabel) {
701
- leftParts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
702
- }
703
- // Override status (warnings, errors)
704
- if (this.overrideStatusMessage) {
705
- leftParts.push({ text: `⚠ ${this.overrideStatusMessage}`, tone: 'warn' });
706
- }
707
- // Main status message
708
- if (this.statusMessage) {
709
- leftParts.push({ text: this.statusMessage, tone: this.streamingLabel ? 'muted' : 'info' });
710
- }
711
- // Scrollback indicator removed - scrollback is disabled in alternate screen mode
712
- // === KEYBOARD SHORTCUTS ===
713
- // Interrupt shortcut (during streaming)
714
- if (this.mode === 'streaming' || this.scrollRegionActive) {
715
- leftParts.push({ text: `${this.formatHotkey('esc')} stop`, tone: 'warn' });
716
- }
717
- // Edit mode toggle (Shift+Tab)
718
- const editHotkey = this.formatHotkey('shift+tab');
719
- const editIcon = this.editMode === 'display-edits' ? '✓' : '?';
720
- const editLabel = this.editMode === 'display-edits' ? 'edits:auto' : 'edits:ask';
721
- const editTone = this.editMode === 'display-edits' ? 'success' : 'muted';
722
- leftParts.push({ text: `${editHotkey}${editIcon}${editLabel}`, tone: editTone });
723
- // Verification toggle (Alt+V)
724
- const verifyIcon = this.verificationEnabled ? '✓' : '○';
725
- const verifyHotkey = this.formatHotkey(this.verificationHotkey || 'alt+v');
726
- const verifyLabel = this.verificationEnabled ? 'verify' : 'no-verify';
727
- leftParts.push({ text: `${verifyHotkey}${verifyIcon}${verifyLabel}`, tone: this.verificationEnabled ? 'success' : 'muted' });
728
- // Auto-continue toggle (Alt+C)
729
- const autoIcon = this.autoContinueEnabled ? '↻' : '○';
730
- const continueHotkey = this.formatHotkey(this.autoContinueHotkey || 'alt+c');
731
- const continueLabel = this.autoContinueEnabled ? 'auto' : 'manual';
732
- leftParts.push({ text: `${continueHotkey}${autoIcon}${continueLabel}`, tone: this.autoContinueEnabled ? 'info' : 'muted' });
733
- // Thinking mode toggle (if available)
711
+ parts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
712
+ parts.push({ text: `Esc:stop`, tone: 'warn' });
713
+ }
714
+ // === COMPACT STATUS ICONS (like banner style) ===
715
+ // Format: ✓verify · ○auto · ◐bal · ⚒43
716
+ // Verify status - compact icon format
717
+ if (this.verificationEnabled) {
718
+ parts.push({ text: '✓verify', tone: 'success' });
719
+ }
720
+ else {
721
+ parts.push({ text: '○verify', tone: 'muted' });
722
+ }
723
+ // Auto-continue status
724
+ if (this.autoContinueEnabled) {
725
+ parts.push({ text: '✓auto', tone: 'success' });
726
+ }
727
+ else {
728
+ parts.push({ text: '○auto', tone: 'muted' });
729
+ }
730
+ // Thinking mode (show as ◐min/bal/ext)
734
731
  if (this.thinkingModeLabel) {
735
- const thinkingHotkey = this.formatHotkey(this.thinkingHotkey || '/thinking');
736
- rightParts.push({ text: `${thinkingHotkey}◐${this.thinkingModeLabel}`, tone: 'info' });
737
- }
738
- // === CONTEXTUAL INFO ===
739
- // Queued commands
740
- if (this.queue.length > 0) {
741
- const queueIcon = this.mode === 'streaming' ? '⏳' : '▸';
742
- leftParts.push({ text: `${queueIcon}${this.queue.length}queued`, tone: 'info' });
743
- }
744
- // Scrollback buffer hint removed - scrollback navigation is disabled
745
- // Multi-line indicator
746
- if (this.buffer.includes('\n')) {
747
- const lineCount = this.buffer.split('\n').length;
748
- rightParts.push({ text: `${lineCount}L`, tone: 'muted' });
749
- }
750
- // Paste indicator
751
- if (this.pastePlaceholders.length > 0) {
752
- const latest = this.pastePlaceholders[this.pastePlaceholders.length - 1];
753
- rightParts.push({
754
- text: `paste#${latest.id}+${latest.lineCount}L`,
755
- tone: 'info',
756
- });
732
+ const thinkingShort = this.thinkingModeLabel === 'minimal' ? 'min' :
733
+ this.thinkingModeLabel === 'balanced' ? 'bal' :
734
+ this.thinkingModeLabel === 'extended' ? 'ext' : this.thinkingModeLabel;
735
+ parts.push({ text: `◐${thinkingShort}`, tone: 'info' });
736
+ }
737
+ // Tool count (⚒43) - from the toolCount if available
738
+ if (this.toolCount !== null && this.toolCount > 0) {
739
+ parts.push({ text: `⚒${this.toolCount}`, tone: 'info' });
757
740
  }
758
- // Context remaining warning with visual indicator
741
+ // === STATUS INFO ===
742
+ // Context remaining
759
743
  const contextRemaining = this.computeContextRemaining();
760
744
  if (contextRemaining !== null) {
761
745
  const tone = contextRemaining <= 10 ? 'warn' : contextRemaining <= 30 ? 'info' : 'muted';
762
- const icon = contextRemaining <= 10 ? '⚠' : '⊛';
763
- const label = contextRemaining === 0 && this.contextUsage !== null
764
- ? `${icon}compact!`
765
- : `${icon}${contextRemaining}%`;
766
- rightParts.push({ text: label, tone });
767
- }
768
- // Model/provider quick reference (compact)
769
- if (this.modelLabel && !this.streamingLabel) {
770
- const shortModel = this.modelLabel.length > 12 ? this.modelLabel.slice(0, 10) + '..' : this.modelLabel;
771
- rightParts.push({ text: shortModel, tone: 'muted' });
772
- }
773
- // Render: left-aligned shortcuts, right-aligned context info
774
- if (!rightParts.length || width < 60) {
775
- const merged = rightParts.length ? [...leftParts, ...rightParts] : leftParts;
776
- return renderStatusLine(merged, width);
777
- }
778
- const leftWidth = Math.max(12, Math.floor(width * 0.65));
779
- const rightWidth = Math.max(14, width - leftWidth - 1);
780
- const leftText = renderStatusLine(leftParts, leftWidth);
781
- const rightText = renderStatusLine(rightParts, rightWidth);
782
- const spacing = Math.max(1, width - this.visibleLength(leftText) - this.visibleLength(rightText));
783
- return `${leftText}${' '.repeat(spacing)}${rightText}`;
746
+ parts.push({ text: `⊛${contextRemaining}%`, tone });
747
+ }
748
+ // Token usage
749
+ if (this.metaTokensUsed !== null) {
750
+ const used = this.formatTokenCount(this.metaTokensUsed);
751
+ const limit = this.metaTokenLimit ? `/${this.formatTokenCount(this.metaTokenLimit)}` : '';
752
+ parts.push({ text: `${used}${limit}tk`, tone: 'muted' });
753
+ }
754
+ // Elapsed time during streaming
755
+ if (this.metaElapsedSeconds !== null && this.streamingLabel) {
756
+ parts.push({ text: `⏱${this.formatElapsedLabel(this.metaElapsedSeconds)}`, tone: 'muted' });
757
+ }
758
+ return renderStatusLine(parts, width);
784
759
  }
785
760
  formatHotkey(hotkey) {
786
761
  const normalized = hotkey.trim().toLowerCase();
787
762
  if (!normalized)
788
763
  return hotkey;
789
764
  const parts = normalized.split('+').filter(Boolean);
765
+ // Use readable key names instead of symbols for better terminal compatibility
790
766
  const map = {
791
- shift: '',
792
- sh: '',
793
- alt: '',
794
- option: '',
795
- opt: '',
796
- ctrl: '',
797
- control: '',
798
- cmd: '',
799
- meta: '',
767
+ shift: 'Shift',
768
+ sh: 'Shift',
769
+ alt: 'Alt',
770
+ option: 'Alt',
771
+ opt: 'Alt',
772
+ ctrl: 'Ctrl',
773
+ control: 'Ctrl',
774
+ cmd: 'Cmd',
775
+ meta: 'Cmd',
776
+ esc: 'Esc',
777
+ escape: 'Esc',
778
+ tab: 'Tab',
779
+ return: 'Enter',
780
+ enter: 'Enter',
781
+ pageup: 'PgUp',
782
+ pagedown: 'PgDn',
783
+ home: 'Home',
784
+ end: 'End',
800
785
  };
801
786
  const formatted = parts
802
787
  .map((part) => {
803
- const symbol = map[part];
804
- if (symbol)
805
- return symbol;
806
- return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
788
+ const label = map[part];
789
+ if (label)
790
+ return label;
791
+ return part.length === 1 ? part.toUpperCase() : part.charAt(0).toUpperCase() + part.slice(1);
807
792
  })
808
- .join('');
809
- return formatted || hotkey;
793
+ .join('+');
794
+ return `[${formatted}]`;
810
795
  }
811
796
  computeContextRemaining() {
812
797
  if (this.contextUsage === null) {
@@ -972,7 +957,7 @@ export class TerminalInput extends EventEmitter {
972
957
  * Calculate chat box height.
973
958
  */
974
959
  getChatBoxHeight() {
975
- return 6; // Fixed: meta + divider + input + controls + buffer
960
+ return 5; // Fixed: divider + input + status + buffer
976
961
  }
977
962
  /**
978
963
  * @deprecated Use streamContent() instead
@@ -1008,26 +993,41 @@ export class TerminalInput extends EventEmitter {
1008
993
  }
1009
994
  }
1010
995
  /**
1011
- * Enter alternate screen buffer.
1012
- * DISABLED: Using terminal-native mode for proper scrollback and text selection.
996
+ * Enter alternate screen buffer and clear it.
997
+ * This gives us full control over the terminal without affecting user's history.
1013
998
  */
1014
999
  enterAlternateScreen() {
1015
- // Disabled - using terminal-native mode
1016
- this.contentRow = 1;
1000
+ writeLock.lock('enterAltScreen');
1001
+ try {
1002
+ this.write(ESC.ENTER_ALT_SCREEN);
1003
+ this.write(ESC.HOME);
1004
+ this.write(ESC.CLEAR_SCREEN);
1005
+ this.contentRow = 1;
1006
+ this.alternateScreenActive = true;
1007
+ }
1008
+ finally {
1009
+ writeLock.unlock();
1010
+ }
1017
1011
  }
1018
1012
  /**
1019
1013
  * Exit alternate screen buffer.
1020
- * DISABLED: Using terminal-native mode.
1014
+ * Restores the user's previous terminal content.
1021
1015
  */
1022
1016
  exitAlternateScreen() {
1023
- // Disabled - using terminal-native mode
1017
+ writeLock.lock('exitAltScreen');
1018
+ try {
1019
+ this.write(ESC.EXIT_ALT_SCREEN);
1020
+ this.alternateScreenActive = false;
1021
+ }
1022
+ finally {
1023
+ writeLock.unlock();
1024
+ }
1024
1025
  }
1025
1026
  /**
1026
1027
  * Check if alternate screen buffer is currently active.
1027
- * Always returns false - using terminal-native mode.
1028
1028
  */
1029
1029
  isAlternateScreenActive() {
1030
- return false;
1030
+ return this.alternateScreenActive;
1031
1031
  }
1032
1032
  /**
1033
1033
  * Get a snapshot of the scrollback buffer (for display on exit).
@@ -1036,17 +1036,14 @@ export class TerminalInput extends EventEmitter {
1036
1036
  return [...this.scrollbackBuffer];
1037
1037
  }
1038
1038
  /**
1039
- * Clear the visible terminal area and reset content position.
1040
- * In terminal-native mode, this just adds newlines to scroll past content
1041
- * rather than clearing history (preserving scrollback).
1039
+ * Clear the entire terminal screen and reset content position.
1040
+ * This removes all content including the launching command.
1042
1041
  */
1043
1042
  clearScreen() {
1044
1043
  writeLock.lock('clearScreen');
1045
1044
  try {
1046
- // In native mode, scroll past existing content rather than clearing
1047
- const { rows } = this.getSize();
1048
- this.write('\n'.repeat(rows));
1049
1045
  this.write(ESC.HOME);
1046
+ this.write(ESC.CLEAR_SCREEN);
1050
1047
  this.contentRow = 1;
1051
1048
  }
1052
1049
  finally {
@@ -1146,8 +1143,8 @@ export class TerminalInput extends EventEmitter {
1146
1143
  this.insertNewline();
1147
1144
  break;
1148
1145
  // === MODE TOGGLES ===
1149
- case 'v':
1150
- // Alt+V: Toggle verification mode (auto-tests after edits)
1146
+ case 'd':
1147
+ // Alt+D: Toggle verification/double-check mode (auto-tests after edits)
1151
1148
  this.emit('toggleVerify');
1152
1149
  break;
1153
1150
  case 'c':
@@ -1214,12 +1211,25 @@ export class TerminalInput extends EventEmitter {
1214
1211
  return Math.max(5, rows - chatBoxHeight - 2);
1215
1212
  }
1216
1213
  /**
1217
- * Build scroll indicator for the divider line.
1218
- * Scrollback navigation is disabled in alternate screen mode.
1219
- * This returns null - no scroll indicator is shown.
1214
+ * Build scroll indicator for the divider line (Claude Code style).
1215
+ * Shows scroll position when in scrollback mode, or history size hint when idle.
1220
1216
  */
1221
1217
  buildScrollIndicator() {
1222
- // Scrollback navigation disabled - no indicator needed
1218
+ const bufferSize = this.scrollbackBuffer.length;
1219
+ // In scrollback mode - show position
1220
+ if (this.isInScrollbackMode && this.scrollbackOffset > 0) {
1221
+ const { rows } = this.getSize();
1222
+ const chatBoxHeight = this.getChatBoxHeight();
1223
+ const viewportHeight = Math.max(1, rows - chatBoxHeight);
1224
+ const currentPos = Math.max(0, bufferSize - this.scrollbackOffset - viewportHeight);
1225
+ const pct = bufferSize > 0 ? Math.round((currentPos / bufferSize) * 100) : 0;
1226
+ return `↑${this.scrollbackOffset} · ${pct}% · PgUp/Dn`;
1227
+ }
1228
+ // Not in scrollback - show hint if there's history
1229
+ if (bufferSize > 20) {
1230
+ const sizeLabel = bufferSize >= 1000 ? `${Math.floor(bufferSize / 1000)}k` : `${bufferSize}`;
1231
+ return `↕${sizeLabel}L · PgUp`;
1232
+ }
1223
1233
  return null;
1224
1234
  }
1225
1235
  handleSpecialKey(_str, key) {
@@ -1281,11 +1291,10 @@ export class TerminalInput extends EventEmitter {
1281
1291
  }
1282
1292
  return true;
1283
1293
  case 'pageup':
1284
- // Scrollback disabled in alternate screen mode
1285
- // Users should use terminal's native scrollback if available
1294
+ this.scrollUp(20); // Scroll up by 20 lines
1286
1295
  return true;
1287
1296
  case 'pagedown':
1288
- // Scrollback disabled in alternate screen mode
1297
+ this.scrollDown(20); // Scroll down by 20 lines
1289
1298
  return true;
1290
1299
  case 'tab':
1291
1300
  if (key.shift) {
@@ -1679,53 +1688,120 @@ export class TerminalInput extends EventEmitter {
1679
1688
  }
1680
1689
  /**
1681
1690
  * Scroll up by a number of lines (PageUp)
1682
- * Note: Scrollback is disabled in alternate screen mode to avoid display corruption.
1683
- * Users should use their terminal's native scrollback or copy/paste features.
1684
1691
  */
1685
- scrollUp(_lines = 10) {
1686
- // Scrollback disabled - alternate screen buffer doesn't support it well
1687
- // The scrollback buffer is still maintained for potential future use
1688
- // Users can select and copy text normally since mouse tracking is off
1692
+ scrollUp(lines = 10) {
1693
+ const { rows } = this.getSize();
1694
+ const chatBoxHeight = this.getChatBoxHeight();
1695
+ const visibleLines = Math.max(1, rows - chatBoxHeight);
1696
+ // Calculate max scroll offset
1697
+ const maxOffset = Math.max(0, this.scrollbackBuffer.length - visibleLines);
1698
+ this.scrollbackOffset = Math.min(this.scrollbackOffset + lines, maxOffset);
1699
+ this.isInScrollbackMode = this.scrollbackOffset > 0;
1700
+ if (this.isInScrollbackMode) {
1701
+ this.renderScrollbackView();
1702
+ }
1689
1703
  }
1690
1704
  /**
1691
1705
  * Scroll down by a number of lines (PageDown)
1692
- * Note: Scrollback disabled - see scrollUp comment
1693
1706
  */
1694
- scrollDown(_lines = 10) {
1695
- // Scrollback disabled
1707
+ scrollDown(lines = 10) {
1708
+ this.scrollbackOffset = Math.max(0, this.scrollbackOffset - lines);
1709
+ this.isInScrollbackMode = this.scrollbackOffset > 0;
1710
+ if (this.isInScrollbackMode) {
1711
+ this.renderScrollbackView();
1712
+ }
1713
+ else {
1714
+ // Returned to live mode - force re-render
1715
+ this.forceRender();
1716
+ }
1696
1717
  }
1697
1718
  /**
1698
1719
  * Jump to the top of scrollback buffer
1699
- * DISABLED: Scrollback navigation causes display corruption in alternate screen mode.
1700
- * The scrollback buffer is maintained but cannot be rendered properly.
1701
1720
  */
1702
1721
  scrollToTop() {
1703
- // Disabled - causes display corruption in alternate screen buffer
1722
+ const { rows } = this.getSize();
1723
+ const chatBoxHeight = this.getChatBoxHeight();
1724
+ const visibleLines = Math.max(1, rows - chatBoxHeight);
1725
+ const maxOffset = Math.max(0, this.scrollbackBuffer.length - visibleLines);
1726
+ this.scrollbackOffset = maxOffset;
1727
+ this.isInScrollbackMode = true;
1728
+ this.renderScrollbackView();
1704
1729
  }
1705
1730
  /**
1706
1731
  * Jump to the bottom (live mode)
1707
- * DISABLED: Scrollback navigation causes display corruption.
1708
1732
  */
1709
1733
  scrollToBottom() {
1710
- // Reset scrollback state in case it was somehow enabled
1711
1734
  this.scrollbackOffset = 0;
1712
1735
  this.isInScrollbackMode = false;
1736
+ this.forceRender();
1713
1737
  }
1714
1738
  /**
1715
1739
  * Toggle scrollback mode on/off (Alt+S hotkey)
1716
- * DISABLED: Scrollback navigation causes display corruption in alternate screen mode.
1717
1740
  */
1718
1741
  toggleScrollbackMode() {
1719
- // Disabled - alternate screen buffer doesn't support manual scrollback rendering
1742
+ if (this.isInScrollbackMode) {
1743
+ this.scrollToBottom();
1744
+ }
1745
+ else if (this.scrollbackBuffer.length > 0) {
1746
+ this.scrollUp(20);
1747
+ }
1720
1748
  }
1721
1749
  /**
1722
- * Render the scrollback buffer view.
1723
- * DISABLED: This causes display corruption in alternate screen mode.
1724
- * The alternate screen buffer has its own rendering model that conflicts with
1725
- * manual scroll region manipulation.
1750
+ * Render the scrollback buffer view with enhanced visuals
1751
+ * Features:
1752
+ * - Visual scroll position indicator
1753
+ * - Progress bar showing position in history
1754
+ * - Keyboard navigation hints
1755
+ * - Animated indicators
1726
1756
  */
1727
1757
  renderScrollbackView() {
1728
- // Disabled - causes display corruption
1758
+ const { rows, cols } = this.getSize();
1759
+ const chatBoxHeight = this.getChatBoxHeight();
1760
+ const contentHeight = Math.max(1, rows - chatBoxHeight);
1761
+ writeLock.lock('renderScrollback');
1762
+ try {
1763
+ this.write(ESC.SAVE);
1764
+ this.write(ESC.HIDE);
1765
+ // Clear content area
1766
+ for (let i = 1; i <= contentHeight; i++) {
1767
+ this.write(ESC.TO(i, 1));
1768
+ this.write(ESC.CLEAR_LINE);
1769
+ }
1770
+ // Calculate which lines to show
1771
+ const totalLines = this.scrollbackBuffer.length;
1772
+ const startIdx = Math.max(0, totalLines - this.scrollbackOffset - contentHeight);
1773
+ const endIdx = Math.max(0, totalLines - this.scrollbackOffset);
1774
+ const visibleLines = this.scrollbackBuffer.slice(startIdx, endIdx);
1775
+ // Build header bar with navigation hints
1776
+ const headerInfo = this.buildScrollbackHeader(cols, totalLines, startIdx, endIdx);
1777
+ this.write(ESC.TO(1, 1));
1778
+ this.write(headerInfo);
1779
+ // Render visible lines with line numbers and visual guides
1780
+ const lineNumWidth = String(totalLines).length + 1;
1781
+ const contentStart = 2; // Start after header
1782
+ for (let i = 0; i < Math.min(visibleLines.length, contentHeight - 1); i++) {
1783
+ const line = visibleLines[i] ?? '';
1784
+ const lineNum = startIdx + i + 1;
1785
+ this.write(ESC.TO(contentStart + i, 1));
1786
+ // Line number gutter
1787
+ const numStr = String(lineNum).padStart(lineNumWidth, ' ');
1788
+ this.write(theme.ui.muted(`${numStr} │ `));
1789
+ // Content with truncation
1790
+ const gutterWidth = lineNumWidth + 4;
1791
+ const maxLen = cols - gutterWidth - 2;
1792
+ const displayLine = line.length > maxLen ? line.slice(0, maxLen - 3) + '...' : line;
1793
+ this.write(displayLine);
1794
+ }
1795
+ // Add visual scroll track on the right edge
1796
+ this.renderScrollTrack(cols, contentHeight, totalLines, startIdx, endIdx);
1797
+ this.write(ESC.RESTORE);
1798
+ this.write(ESC.SHOW);
1799
+ }
1800
+ finally {
1801
+ writeLock.unlock();
1802
+ }
1803
+ // Re-render chat box
1804
+ this.forceRender();
1729
1805
  }
1730
1806
  /**
1731
1807
  * Build scrollback header with navigation hints