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.
- 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 +228 -185
- 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
|
-
}
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
711
|
+
parts.push({ text: `◐ ${this.streamingLabel}`, tone: 'info' });
|
|
712
|
+
parts.push({ text: `Esc:stop`, tone: 'warn' });
|
|
722
713
|
}
|
|
723
|
-
//
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
740
|
-
//
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
//
|
|
779
|
-
|
|
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 ? '
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
}
|
|
795
|
-
//
|
|
796
|
-
if (this.
|
|
797
|
-
|
|
798
|
-
|
|
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
|
|
840
|
-
if (
|
|
841
|
-
return
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
1052
|
-
|
|
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
|
-
*
|
|
1017
|
+
* Restores the user's previous terminal content.
|
|
1057
1018
|
*/
|
|
1058
1019
|
exitAlternateScreen() {
|
|
1059
|
-
|
|
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
|
|
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
|
|
1076
|
-
*
|
|
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 '
|
|
1186
|
-
// Alt+
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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(
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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(
|
|
1731
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1760
|
-
*
|
|
1761
|
-
*
|
|
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
|
-
|
|
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
|