erosolar-cli 1.7.364 → 1.7.365
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 +15 -32
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +19 -32
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +227 -187
- 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 +84 -168
- 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;
|
|
@@ -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
244
|
}
|
|
238
|
-
//
|
|
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
|
-
}
|
|
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,98 @@ 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
|
|
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
|
|
718
|
-
|
|
719
|
-
// Streaming indicator with animated spinner
|
|
708
|
+
const parts = [];
|
|
709
|
+
// Streaming status with stop hint
|
|
720
710
|
if (this.streamingLabel) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
//
|
|
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)
|
|
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)
|
|
753
731
|
if (this.thinkingModeLabel) {
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
}
|
|
759
|
-
//
|
|
760
|
-
|
|
761
|
-
|
|
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
|
-
});
|
|
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' });
|
|
777
740
|
}
|
|
778
|
-
// === STATUS
|
|
779
|
-
// Context
|
|
741
|
+
// === STATUS INFO ===
|
|
742
|
+
// Context remaining
|
|
780
743
|
const contextRemaining = this.computeContextRemaining();
|
|
781
744
|
if (contextRemaining !== null) {
|
|
782
|
-
const tone = contextRemaining <= 10 ? '
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
}
|
|
795
|
-
|
|
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);
|
|
745
|
+
const tone = contextRemaining <= 10 ? 'warn' : contextRemaining <= 30 ? 'info' : 'muted';
|
|
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);
|
|
820
759
|
}
|
|
821
760
|
formatHotkey(hotkey) {
|
|
822
761
|
const normalized = hotkey.trim().toLowerCase();
|
|
823
762
|
if (!normalized)
|
|
824
763
|
return hotkey;
|
|
825
764
|
const parts = normalized.split('+').filter(Boolean);
|
|
765
|
+
// Use readable key names instead of symbols for better terminal compatibility
|
|
826
766
|
const map = {
|
|
827
|
-
shift: '
|
|
828
|
-
sh: '
|
|
829
|
-
alt: '
|
|
830
|
-
option: '
|
|
831
|
-
opt: '
|
|
832
|
-
ctrl: '
|
|
833
|
-
control: '
|
|
834
|
-
cmd: '
|
|
835
|
-
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',
|
|
836
785
|
};
|
|
837
786
|
const formatted = parts
|
|
838
787
|
.map((part) => {
|
|
839
|
-
const
|
|
840
|
-
if (
|
|
841
|
-
return
|
|
842
|
-
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);
|
|
843
792
|
})
|
|
844
|
-
.join('');
|
|
845
|
-
return formatted
|
|
793
|
+
.join('+');
|
|
794
|
+
return `[${formatted}]`;
|
|
846
795
|
}
|
|
847
796
|
computeContextRemaining() {
|
|
848
797
|
if (this.contextUsage === null) {
|
|
@@ -1008,7 +957,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1008
957
|
* Calculate chat box height.
|
|
1009
958
|
*/
|
|
1010
959
|
getChatBoxHeight() {
|
|
1011
|
-
return
|
|
960
|
+
return 5; // Fixed: divider + input + status + buffer
|
|
1012
961
|
}
|
|
1013
962
|
/**
|
|
1014
963
|
* @deprecated Use streamContent() instead
|
|
@@ -1044,26 +993,41 @@ export class TerminalInput extends EventEmitter {
|
|
|
1044
993
|
}
|
|
1045
994
|
}
|
|
1046
995
|
/**
|
|
1047
|
-
* Enter alternate screen buffer.
|
|
1048
|
-
*
|
|
996
|
+
* Enter alternate screen buffer and clear it.
|
|
997
|
+
* This gives us full control over the terminal without affecting user's history.
|
|
1049
998
|
*/
|
|
1050
999
|
enterAlternateScreen() {
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
+
}
|
|
1053
1011
|
}
|
|
1054
1012
|
/**
|
|
1055
1013
|
* Exit alternate screen buffer.
|
|
1056
|
-
*
|
|
1014
|
+
* Restores the user's previous terminal content.
|
|
1057
1015
|
*/
|
|
1058
1016
|
exitAlternateScreen() {
|
|
1059
|
-
|
|
1017
|
+
writeLock.lock('exitAltScreen');
|
|
1018
|
+
try {
|
|
1019
|
+
this.write(ESC.EXIT_ALT_SCREEN);
|
|
1020
|
+
this.alternateScreenActive = false;
|
|
1021
|
+
}
|
|
1022
|
+
finally {
|
|
1023
|
+
writeLock.unlock();
|
|
1024
|
+
}
|
|
1060
1025
|
}
|
|
1061
1026
|
/**
|
|
1062
1027
|
* Check if alternate screen buffer is currently active.
|
|
1063
|
-
* Always returns false - using terminal-native mode.
|
|
1064
1028
|
*/
|
|
1065
1029
|
isAlternateScreenActive() {
|
|
1066
|
-
return
|
|
1030
|
+
return this.alternateScreenActive;
|
|
1067
1031
|
}
|
|
1068
1032
|
/**
|
|
1069
1033
|
* Get a snapshot of the scrollback buffer (for display on exit).
|
|
@@ -1072,17 +1036,14 @@ export class TerminalInput extends EventEmitter {
|
|
|
1072
1036
|
return [...this.scrollbackBuffer];
|
|
1073
1037
|
}
|
|
1074
1038
|
/**
|
|
1075
|
-
* Clear the
|
|
1076
|
-
*
|
|
1077
|
-
* 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.
|
|
1078
1041
|
*/
|
|
1079
1042
|
clearScreen() {
|
|
1080
1043
|
writeLock.lock('clearScreen');
|
|
1081
1044
|
try {
|
|
1082
|
-
// In native mode, scroll past existing content rather than clearing
|
|
1083
|
-
const { rows } = this.getSize();
|
|
1084
|
-
this.write('\n'.repeat(rows));
|
|
1085
1045
|
this.write(ESC.HOME);
|
|
1046
|
+
this.write(ESC.CLEAR_SCREEN);
|
|
1086
1047
|
this.contentRow = 1;
|
|
1087
1048
|
}
|
|
1088
1049
|
finally {
|
|
@@ -1182,8 +1143,8 @@ export class TerminalInput extends EventEmitter {
|
|
|
1182
1143
|
this.insertNewline();
|
|
1183
1144
|
break;
|
|
1184
1145
|
// === MODE TOGGLES ===
|
|
1185
|
-
case '
|
|
1186
|
-
// Alt+
|
|
1146
|
+
case 'd':
|
|
1147
|
+
// Alt+D: Toggle verification/double-check mode (auto-tests after edits)
|
|
1187
1148
|
this.emit('toggleVerify');
|
|
1188
1149
|
break;
|
|
1189
1150
|
case 'c':
|
|
@@ -1250,12 +1211,25 @@ export class TerminalInput extends EventEmitter {
|
|
|
1250
1211
|
return Math.max(5, rows - chatBoxHeight - 2);
|
|
1251
1212
|
}
|
|
1252
1213
|
/**
|
|
1253
|
-
* Build scroll indicator for the divider line.
|
|
1254
|
-
*
|
|
1255
|
-
* 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.
|
|
1256
1216
|
*/
|
|
1257
1217
|
buildScrollIndicator() {
|
|
1258
|
-
|
|
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
|
+
}
|
|
1259
1233
|
return null;
|
|
1260
1234
|
}
|
|
1261
1235
|
handleSpecialKey(_str, key) {
|
|
@@ -1317,11 +1291,10 @@ export class TerminalInput extends EventEmitter {
|
|
|
1317
1291
|
}
|
|
1318
1292
|
return true;
|
|
1319
1293
|
case 'pageup':
|
|
1320
|
-
//
|
|
1321
|
-
// Users should use terminal's native scrollback if available
|
|
1294
|
+
this.scrollUp(20); // Scroll up by 20 lines
|
|
1322
1295
|
return true;
|
|
1323
1296
|
case 'pagedown':
|
|
1324
|
-
//
|
|
1297
|
+
this.scrollDown(20); // Scroll down by 20 lines
|
|
1325
1298
|
return true;
|
|
1326
1299
|
case 'tab':
|
|
1327
1300
|
if (key.shift) {
|
|
@@ -1715,53 +1688,120 @@ export class TerminalInput extends EventEmitter {
|
|
|
1715
1688
|
}
|
|
1716
1689
|
/**
|
|
1717
1690
|
* 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
1691
|
*/
|
|
1721
|
-
scrollUp(
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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
|
+
}
|
|
1725
1703
|
}
|
|
1726
1704
|
/**
|
|
1727
1705
|
* Scroll down by a number of lines (PageDown)
|
|
1728
|
-
* Note: Scrollback disabled - see scrollUp comment
|
|
1729
1706
|
*/
|
|
1730
|
-
scrollDown(
|
|
1731
|
-
|
|
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
|
+
}
|
|
1732
1717
|
}
|
|
1733
1718
|
/**
|
|
1734
1719
|
* 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
1720
|
*/
|
|
1738
1721
|
scrollToTop() {
|
|
1739
|
-
|
|
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();
|
|
1740
1729
|
}
|
|
1741
1730
|
/**
|
|
1742
1731
|
* Jump to the bottom (live mode)
|
|
1743
|
-
* DISABLED: Scrollback navigation causes display corruption.
|
|
1744
1732
|
*/
|
|
1745
1733
|
scrollToBottom() {
|
|
1746
|
-
// Reset scrollback state in case it was somehow enabled
|
|
1747
1734
|
this.scrollbackOffset = 0;
|
|
1748
1735
|
this.isInScrollbackMode = false;
|
|
1736
|
+
this.forceRender();
|
|
1749
1737
|
}
|
|
1750
1738
|
/**
|
|
1751
1739
|
* Toggle scrollback mode on/off (Alt+S hotkey)
|
|
1752
|
-
* DISABLED: Scrollback navigation causes display corruption in alternate screen mode.
|
|
1753
1740
|
*/
|
|
1754
1741
|
toggleScrollbackMode() {
|
|
1755
|
-
|
|
1742
|
+
if (this.isInScrollbackMode) {
|
|
1743
|
+
this.scrollToBottom();
|
|
1744
|
+
}
|
|
1745
|
+
else if (this.scrollbackBuffer.length > 0) {
|
|
1746
|
+
this.scrollUp(20);
|
|
1747
|
+
}
|
|
1756
1748
|
}
|
|
1757
1749
|
/**
|
|
1758
|
-
* Render the scrollback buffer view
|
|
1759
|
-
*
|
|
1760
|
-
*
|
|
1761
|
-
*
|
|
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
|
|
1762
1756
|
*/
|
|
1763
1757
|
renderScrollbackView() {
|
|
1764
|
-
|
|
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();
|
|
1765
1805
|
}
|
|
1766
1806
|
/**
|
|
1767
1807
|
* Build scrollback header with navigation hints
|