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.
- package/dist/shell/interactiveShell.d.ts +0 -2
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +13 -32
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +19 -26
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +224 -148
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +2 -5
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/subagents/taskRunner.d.ts.map +1 -1
- package/dist/subagents/taskRunner.js +25 -7
- package/dist/subagents/taskRunner.js.map +1 -1
- package/dist/tools/learnTools.js +4 -127
- package/dist/tools/learnTools.js.map +1 -1
- package/dist/ui/ShellUIAdapter.d.ts +0 -27
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +9 -175
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/theme.d.ts +3 -108
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +3 -124
- package/dist/ui/theme.js.map +1 -1
- package/dist/ui/toolDisplay.d.ts +7 -44
- package/dist/ui/toolDisplay.d.ts.map +1 -1
- package/dist/ui/toolDisplay.js +32 -163
- package/dist/ui/toolDisplay.js.map +1 -1
- package/dist/ui/unified/index.d.ts +0 -11
- package/dist/ui/unified/index.d.ts.map +1 -1
- package/dist/ui/unified/index.js +0 -16
- package/dist/ui/unified/index.js.map +1 -1
- package/dist/ui/unified/layout.d.ts.map +1 -1
- package/dist/ui/unified/layout.js +47 -32
- package/dist/ui/unified/layout.js.map +1 -1
- package/package.json +1 -1
- package/dist/ui/compactRenderer.d.ts +0 -139
- package/dist/ui/compactRenderer.d.ts.map +0 -1
- package/dist/ui/compactRenderer.js +0 -398
- package/dist/ui/compactRenderer.js.map +0 -1
- package/dist/ui/inPlaceUpdater.d.ts +0 -181
- package/dist/ui/inPlaceUpdater.d.ts.map +0 -1
- package/dist/ui/inPlaceUpdater.js +0 -515
- 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+
|
|
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
|
|
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
|
|
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
|
|
698
|
-
|
|
699
|
-
// Streaming indicator with animated spinner
|
|
708
|
+
const parts = [];
|
|
709
|
+
// Streaming status with stop hint
|
|
700
710
|
if (this.streamingLabel) {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
//
|
|
714
|
-
if (this.
|
|
715
|
-
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
|
804
|
-
if (
|
|
805
|
-
return
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
1016
|
-
|
|
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
|
-
*
|
|
1014
|
+
* Restores the user's previous terminal content.
|
|
1021
1015
|
*/
|
|
1022
1016
|
exitAlternateScreen() {
|
|
1023
|
-
|
|
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
|
|
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
|
|
1040
|
-
*
|
|
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 '
|
|
1150
|
-
// Alt+
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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(
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
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(
|
|
1695
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1724
|
-
*
|
|
1725
|
-
*
|
|
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
|
-
|
|
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
|