erosolar-cli 1.7.393 → 1.7.395
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/core/offsecAlphaZero.d.ts +56 -0
- package/dist/core/offsecAlphaZero.d.ts.map +1 -0
- package/dist/core/offsecAlphaZero.js +395 -0
- package/dist/core/offsecAlphaZero.js.map +1 -0
- package/dist/core/preferences.d.ts +1 -0
- package/dist/core/preferences.d.ts.map +1 -1
- package/dist/core/preferences.js +7 -0
- package/dist/core/preferences.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +23 -5
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +486 -135
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/keyboardShortcuts.d.ts.map +1 -1
- package/dist/shell/keyboardShortcuts.js +11 -8
- package/dist/shell/keyboardShortcuts.js.map +1 -1
- package/dist/shell/liveStatus.d.ts +1 -0
- package/dist/shell/liveStatus.d.ts.map +1 -1
- package/dist/shell/liveStatus.js +32 -7
- package/dist/shell/liveStatus.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +25 -2
- package/dist/shell/terminalInput.d.ts.map +1 -1
- package/dist/shell/terminalInput.js +270 -60
- package/dist/shell/terminalInput.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +14 -0
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +14 -0
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/shell/updateManager.d.ts +8 -1
- package/dist/shell/updateManager.d.ts.map +1 -1
- package/dist/shell/updateManager.js +4 -2
- package/dist/shell/updateManager.js.map +1 -1
- package/dist/ui/ShellUIAdapter.d.ts +6 -12
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +26 -37
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/shortcutsHelp.d.ts.map +1 -1
- package/dist/ui/shortcutsHelp.js +22 -21
- package/dist/ui/shortcutsHelp.js.map +1 -1
- package/dist/ui/unified/index.d.ts +2 -2
- package/dist/ui/unified/index.d.ts.map +1 -1
- 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 +31 -25
- package/dist/ui/unified/layout.js.map +1 -1
- package/package.json +1 -1
|
@@ -118,14 +118,17 @@ export class TerminalInput extends EventEmitter {
|
|
|
118
118
|
editMode = 'display-edits';
|
|
119
119
|
verificationEnabled = true;
|
|
120
120
|
autoContinueEnabled = false;
|
|
121
|
-
verificationHotkey = '
|
|
122
|
-
autoContinueHotkey = '
|
|
123
|
-
thinkingHotkey = '
|
|
121
|
+
verificationHotkey = 'ctrl+shift+v';
|
|
122
|
+
autoContinueHotkey = 'ctrl+shift+c';
|
|
123
|
+
thinkingHotkey = 'ctrl+shift+t';
|
|
124
|
+
alphaZeroModeEnabled = false;
|
|
125
|
+
alphaZeroHotkey = 'ctrl+shift+a';
|
|
126
|
+
alphaZeroLabel = null;
|
|
124
127
|
modelLabel = null;
|
|
125
128
|
providerLabel = null;
|
|
126
129
|
// Streaming render throttle
|
|
127
130
|
lastStreamingRender = 0;
|
|
128
|
-
streamingRenderInterval =
|
|
131
|
+
streamingRenderInterval = 150; // ms between renders during streaming
|
|
129
132
|
streamingRenderTimer = null;
|
|
130
133
|
// Command autocomplete state
|
|
131
134
|
commandSuggestions = [];
|
|
@@ -137,6 +140,8 @@ export class TerminalInput extends EventEmitter {
|
|
|
137
140
|
lastChatBoxStartRow = null;
|
|
138
141
|
lastChatBoxHeight = 0;
|
|
139
142
|
displayInterceptorDispose = null;
|
|
143
|
+
recentActions = [];
|
|
144
|
+
maxRecentActions = 5;
|
|
140
145
|
constructor(writeStream = process.stdout, config = {}) {
|
|
141
146
|
super();
|
|
142
147
|
this.out = writeStream;
|
|
@@ -293,10 +298,20 @@ export class TerminalInput extends EventEmitter {
|
|
|
293
298
|
const normalizedName = key?.name ?? this.getArrowKeyName(key?.sequence ?? str);
|
|
294
299
|
const effectiveKey = normalizedName ? { ...key, name: normalizedName } : key;
|
|
295
300
|
const safeStr = this.isArrowEscapeFragment(str) ? undefined : str;
|
|
301
|
+
// Some terminals emit raw DEL/backspace bytes without a parsed key name.
|
|
302
|
+
// Treat these as backspace to avoid inserting the control character.
|
|
303
|
+
const isRawBackspace = !effectiveKey?.name && safeStr && (safeStr === '\b' || safeStr === '\x7f');
|
|
304
|
+
if (isRawBackspace) {
|
|
305
|
+
this.deleteBackward();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
let handled = false;
|
|
296
309
|
// Handle control keys
|
|
297
310
|
if (effectiveKey?.ctrl) {
|
|
298
|
-
this.handleCtrlKey(effectiveKey);
|
|
299
|
-
|
|
311
|
+
handled = this.handleCtrlKey(effectiveKey);
|
|
312
|
+
if (handled) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
300
315
|
}
|
|
301
316
|
// Handle meta/alt keys
|
|
302
317
|
if (effectiveKey?.meta) {
|
|
@@ -458,7 +473,8 @@ export class TerminalInput extends EventEmitter {
|
|
|
458
473
|
/**
|
|
459
474
|
* Handle selecting a suggestion (Enter key or click)
|
|
460
475
|
*/
|
|
461
|
-
selectSuggestion() {
|
|
476
|
+
selectSuggestion(options = {}) {
|
|
477
|
+
const { submit = false } = options;
|
|
462
478
|
if (!this.showingSuggestions)
|
|
463
479
|
return false;
|
|
464
480
|
const filtered = this.getFilteredCommands();
|
|
@@ -468,10 +484,14 @@ export class TerminalInput extends EventEmitter {
|
|
|
468
484
|
if (!selected)
|
|
469
485
|
return false;
|
|
470
486
|
// Replace buffer with selected command
|
|
471
|
-
|
|
487
|
+
const nextBuffer = submit ? selected.command : `${selected.command} `;
|
|
488
|
+
this.buffer = nextBuffer;
|
|
472
489
|
this.cursor = this.buffer.length;
|
|
473
490
|
this.showingSuggestions = false;
|
|
474
491
|
this.scheduleRender();
|
|
492
|
+
if (submit) {
|
|
493
|
+
this.submit();
|
|
494
|
+
}
|
|
475
495
|
return true;
|
|
476
496
|
}
|
|
477
497
|
/**
|
|
@@ -555,7 +575,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
555
575
|
visibleCount: Math.min(maxDisplay, Math.max(0, filtered.length - startIdx))
|
|
556
576
|
};
|
|
557
577
|
// Header with navigation hint - keep it short
|
|
558
|
-
lines.push(theme.ui.muted(`Commands (↑↓ Tab
|
|
578
|
+
lines.push(theme.ui.muted(`Commands (↑↓ Enter to run · Tab to fill)`));
|
|
559
579
|
// Render visible suggestions
|
|
560
580
|
for (let i = 0; i < maxDisplay; i++) {
|
|
561
581
|
const idx = startIdx + i;
|
|
@@ -677,13 +697,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
677
697
|
const nextVerifyHotkey = options.verificationHotkey ?? this.verificationHotkey;
|
|
678
698
|
const nextAutoHotkey = options.autoContinueHotkey ?? this.autoContinueHotkey;
|
|
679
699
|
const nextThinkingHotkey = options.thinkingHotkey ?? this.thinkingHotkey;
|
|
700
|
+
const nextAlphaHotkey = options.alphaZeroHotkey ?? this.alphaZeroHotkey;
|
|
680
701
|
const nextThinkingLabel = options.thinkingModeLabel === undefined ? this.thinkingModeLabel : (options.thinkingModeLabel || null);
|
|
702
|
+
const nextAlphaZeroEnabled = options.alphaZeroEnabled === undefined ? this.alphaZeroModeEnabled : !!options.alphaZeroEnabled;
|
|
703
|
+
const nextAlphaZeroLabel = options.alphaZeroLabel === undefined ? this.alphaZeroLabel : (options.alphaZeroLabel || null);
|
|
681
704
|
if (this.verificationEnabled === nextVerification &&
|
|
682
705
|
this.autoContinueEnabled === nextAutoContinue &&
|
|
683
706
|
this.verificationHotkey === nextVerifyHotkey &&
|
|
684
707
|
this.autoContinueHotkey === nextAutoHotkey &&
|
|
685
708
|
this.thinkingHotkey === nextThinkingHotkey &&
|
|
686
|
-
this.thinkingModeLabel === nextThinkingLabel
|
|
709
|
+
this.thinkingModeLabel === nextThinkingLabel &&
|
|
710
|
+
this.alphaZeroModeEnabled === nextAlphaZeroEnabled &&
|
|
711
|
+
this.alphaZeroHotkey === nextAlphaHotkey &&
|
|
712
|
+
this.alphaZeroLabel === nextAlphaZeroLabel) {
|
|
687
713
|
return;
|
|
688
714
|
}
|
|
689
715
|
this.verificationEnabled = nextVerification;
|
|
@@ -692,6 +718,9 @@ export class TerminalInput extends EventEmitter {
|
|
|
692
718
|
this.autoContinueHotkey = nextAutoHotkey;
|
|
693
719
|
this.thinkingHotkey = nextThinkingHotkey;
|
|
694
720
|
this.thinkingModeLabel = nextThinkingLabel;
|
|
721
|
+
this.alphaZeroModeEnabled = nextAlphaZeroEnabled;
|
|
722
|
+
this.alphaZeroHotkey = nextAlphaHotkey;
|
|
723
|
+
this.alphaZeroLabel = nextAlphaZeroLabel;
|
|
695
724
|
this.scheduleRender();
|
|
696
725
|
}
|
|
697
726
|
/**
|
|
@@ -729,8 +758,19 @@ export class TerminalInput extends EventEmitter {
|
|
|
729
758
|
if (this.isRendering)
|
|
730
759
|
return;
|
|
731
760
|
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
732
|
-
|
|
733
|
-
|
|
761
|
+
if (this.scrollRegionActive && streamingActive) {
|
|
762
|
+
this.renderStreamingFrame();
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const bufferChanged = this.buffer !== this.lastRenderContent || this.cursor !== this.lastRenderCursor;
|
|
766
|
+
// During streaming, throttle re-renders unless the buffer actually changed
|
|
767
|
+
// (e.g., typing slash commands while streaming should update immediately).
|
|
768
|
+
const shouldThrottle = streamingActive &&
|
|
769
|
+
this.lastStreamingRender > 0 &&
|
|
770
|
+
!this.renderDirty &&
|
|
771
|
+
!bufferChanged &&
|
|
772
|
+
!this.showingSuggestions;
|
|
773
|
+
if (shouldThrottle) {
|
|
734
774
|
const elapsed = Date.now() - this.lastStreamingRender;
|
|
735
775
|
const waitMs = Math.max(0, this.streamingRenderInterval - elapsed);
|
|
736
776
|
if (waitMs > 0) {
|
|
@@ -739,9 +779,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
739
779
|
return;
|
|
740
780
|
}
|
|
741
781
|
}
|
|
742
|
-
const shouldSkip = !this.renderDirty &&
|
|
743
|
-
this.buffer === this.lastRenderContent &&
|
|
744
|
-
this.cursor === this.lastRenderCursor;
|
|
782
|
+
const shouldSkip = !this.renderDirty && !bufferChanged;
|
|
745
783
|
this.renderDirty = false;
|
|
746
784
|
if (shouldSkip) {
|
|
747
785
|
return;
|
|
@@ -761,6 +799,8 @@ export class TerminalInput extends EventEmitter {
|
|
|
761
799
|
const { rows, cols } = this.getSize();
|
|
762
800
|
const maxWidth = Math.max(8, cols - 4);
|
|
763
801
|
const streamingActive = this.mode === 'streaming' || isStreamingMode();
|
|
802
|
+
const prevChatBoxStart = this.lastChatBoxStartRow;
|
|
803
|
+
const prevChatBoxHeight = this.lastChatBoxHeight;
|
|
764
804
|
// Wrap buffer into display lines
|
|
765
805
|
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
766
806
|
const maxVisible = Math.max(1, Math.min(this.config.maxLines, rows - 3));
|
|
@@ -795,13 +835,16 @@ export class TerminalInput extends EventEmitter {
|
|
|
795
835
|
if (this.scrollRegionActive) {
|
|
796
836
|
this.write(ESC.RESET_SCROLL);
|
|
797
837
|
}
|
|
798
|
-
// Clear the chat box
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
838
|
+
// Clear the current and previous chat box footprint to avoid ghost text when the height shrinks
|
|
839
|
+
const prevHeight = prevChatBoxStart !== null ? Math.max(1, prevChatBoxHeight) : chatBoxHeight;
|
|
840
|
+
const prevStart = prevChatBoxStart ?? chatBoxStartRow;
|
|
841
|
+
const prevEnd = prevStart + prevHeight - 1;
|
|
842
|
+
const newEnd = chatBoxStartRow + chatBoxHeight - 1;
|
|
843
|
+
const clearStart = Math.max(1, Math.min(prevStart, chatBoxStartRow));
|
|
844
|
+
const clearEnd = Math.min(rows, Math.max(prevEnd, newEnd));
|
|
845
|
+
for (let row = clearStart; row <= clearEnd; row++) {
|
|
846
|
+
this.write(ESC.TO(row, 1));
|
|
847
|
+
this.write(ESC.CLEAR_LINE);
|
|
805
848
|
}
|
|
806
849
|
let currentRow = chatBoxStartRow;
|
|
807
850
|
// Render scroll/status indicator on the left (Claude Code style)
|
|
@@ -812,6 +855,13 @@ export class TerminalInput extends EventEmitter {
|
|
|
812
855
|
this.write(metaLine);
|
|
813
856
|
currentRow += 1;
|
|
814
857
|
}
|
|
858
|
+
// Recent actions strip (sits above the divider)
|
|
859
|
+
const recentLines = this.buildRecentActionLines(cols - 2);
|
|
860
|
+
for (const recentLine of recentLines) {
|
|
861
|
+
this.write(ESC.TO(currentRow, 1));
|
|
862
|
+
this.write(recentLine);
|
|
863
|
+
currentRow += 1;
|
|
864
|
+
}
|
|
815
865
|
// Separator line with scroll status
|
|
816
866
|
this.write(ESC.TO(currentRow, 1));
|
|
817
867
|
const dividerLabel = scrollIndicator || undefined;
|
|
@@ -1080,24 +1130,33 @@ export class TerminalInput extends EventEmitter {
|
|
|
1080
1130
|
const contextTone = this.contextUsage > 80 ? 'warn' : this.contextUsage > 60 ? 'info' : 'muted';
|
|
1081
1131
|
toggleParts.push({ text: `ctx:${remaining}%`, tone: contextTone });
|
|
1082
1132
|
}
|
|
1083
|
-
|
|
1084
|
-
const verifyStatus = this.verificationEnabled ? '
|
|
1133
|
+
const verifyHotkey = this.formatHotkey(this.verificationHotkey);
|
|
1134
|
+
const verifyStatus = this.verificationEnabled ? 'Verify on' : 'Verify off';
|
|
1085
1135
|
toggleParts.push({
|
|
1086
|
-
text:
|
|
1136
|
+
text: `${verifyHotkey} ${verifyStatus}`,
|
|
1087
1137
|
tone: this.verificationEnabled ? 'success' : 'muted'
|
|
1088
1138
|
});
|
|
1089
|
-
|
|
1090
|
-
const autoStatus = this.autoContinueEnabled ? '
|
|
1139
|
+
const autoHotkey = this.formatHotkey(this.autoContinueHotkey);
|
|
1140
|
+
const autoStatus = this.autoContinueEnabled ? 'Auto-continue on' : 'Auto-continue off';
|
|
1091
1141
|
toggleParts.push({
|
|
1092
|
-
text:
|
|
1142
|
+
text: `${autoHotkey} ${autoStatus}`,
|
|
1093
1143
|
tone: this.autoContinueEnabled ? 'info' : 'muted'
|
|
1094
1144
|
});
|
|
1095
|
-
//
|
|
1145
|
+
// AlphaZero RL mode toggle
|
|
1146
|
+
const alphaHotkey = this.formatHotkey(this.alphaZeroHotkey);
|
|
1147
|
+
const alphaLabel = this.alphaZeroLabel || 'AlphaZero RL';
|
|
1148
|
+
const alphaStatus = this.alphaZeroModeEnabled ? `${alphaLabel} on` : `${alphaLabel} off`;
|
|
1149
|
+
toggleParts.push({
|
|
1150
|
+
text: `${alphaHotkey} ${alphaStatus}`,
|
|
1151
|
+
tone: this.alphaZeroModeEnabled ? 'success' : 'muted',
|
|
1152
|
+
});
|
|
1153
|
+
// Thinking mode toggle
|
|
1096
1154
|
if (this.thinkingModeLabel) {
|
|
1097
1155
|
const shortThinking = this.thinkingModeLabel.length > 10
|
|
1098
1156
|
? this.thinkingModeLabel.slice(0, 8) + '..'
|
|
1099
1157
|
: this.thinkingModeLabel;
|
|
1100
|
-
|
|
1158
|
+
const thinkingHotkey = this.formatHotkey(this.thinkingHotkey);
|
|
1159
|
+
toggleParts.push({ text: `${thinkingHotkey} Thinking ${shortThinking}`, tone: 'info' });
|
|
1101
1160
|
}
|
|
1102
1161
|
// Navigation shortcuts - always show these
|
|
1103
1162
|
toggleParts.push({ text: 'PgUp/Dn:scroll', tone: 'muted' });
|
|
@@ -1156,24 +1215,28 @@ export class TerminalInput extends EventEmitter {
|
|
|
1156
1215
|
return hotkey;
|
|
1157
1216
|
const parts = normalized.split('+').filter(Boolean);
|
|
1158
1217
|
const map = {
|
|
1159
|
-
shift: '
|
|
1160
|
-
sh: '
|
|
1161
|
-
alt: '
|
|
1162
|
-
option: '
|
|
1163
|
-
opt: '
|
|
1164
|
-
ctrl: '
|
|
1165
|
-
control: '
|
|
1166
|
-
cmd: '
|
|
1167
|
-
meta: '
|
|
1218
|
+
shift: 'Shift',
|
|
1219
|
+
sh: 'Shift',
|
|
1220
|
+
alt: 'Alt',
|
|
1221
|
+
option: 'Alt',
|
|
1222
|
+
opt: 'Alt',
|
|
1223
|
+
ctrl: 'Ctrl',
|
|
1224
|
+
control: 'Ctrl',
|
|
1225
|
+
cmd: 'Cmd',
|
|
1226
|
+
meta: 'Cmd',
|
|
1168
1227
|
};
|
|
1169
1228
|
const formatted = parts
|
|
1170
1229
|
.map((part) => {
|
|
1171
1230
|
const symbol = map[part];
|
|
1172
1231
|
if (symbol)
|
|
1173
1232
|
return symbol;
|
|
1174
|
-
|
|
1233
|
+
if (part.startsWith('/'))
|
|
1234
|
+
return part;
|
|
1235
|
+
if (part.length === 1)
|
|
1236
|
+
return part.toUpperCase();
|
|
1237
|
+
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
1175
1238
|
})
|
|
1176
|
-
.join('');
|
|
1239
|
+
.join('+');
|
|
1177
1240
|
return formatted || hotkey;
|
|
1178
1241
|
}
|
|
1179
1242
|
computeContextRemaining() {
|
|
@@ -1213,6 +1276,41 @@ export class TerminalInput extends EventEmitter {
|
|
|
1213
1276
|
const ansiPattern = /\u001B\[[0-?]*[ -/]*[@-~]/g;
|
|
1214
1277
|
return value.replace(ansiPattern, '').length;
|
|
1215
1278
|
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Record a compact recent action for display above the divider.
|
|
1281
|
+
*/
|
|
1282
|
+
recordRecentAction(action) {
|
|
1283
|
+
const clean = this.sanitize(action).replace(/\s+/g, ' ').trim();
|
|
1284
|
+
if (!clean) {
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const maxLen = 80;
|
|
1288
|
+
const text = clean.length > maxLen ? `${clean.slice(0, maxLen - 1)}…` : clean;
|
|
1289
|
+
const last = this.recentActions[this.recentActions.length - 1];
|
|
1290
|
+
if (last && last.text === text) {
|
|
1291
|
+
last.timestamp = Date.now();
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (!this.isTTY()) {
|
|
1295
|
+
this.write(`[action] ${text}\n`);
|
|
1296
|
+
}
|
|
1297
|
+
this.recentActions.push({ text, timestamp: Date.now() });
|
|
1298
|
+
while (this.recentActions.length > this.maxRecentActions) {
|
|
1299
|
+
this.recentActions.shift();
|
|
1300
|
+
}
|
|
1301
|
+
this.scheduleRender();
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Build the single-line recent actions strip.
|
|
1305
|
+
*/
|
|
1306
|
+
buildRecentActionLines(width) {
|
|
1307
|
+
if (this.recentActions.length === 0 || width <= 0) {
|
|
1308
|
+
return [];
|
|
1309
|
+
}
|
|
1310
|
+
const items = this.recentActions.map((entry) => ({ text: entry.text, tone: 'muted' }));
|
|
1311
|
+
const parts = [{ text: 'recent', tone: 'info' }, ...items];
|
|
1312
|
+
return [renderStatusLine(parts, width)];
|
|
1313
|
+
}
|
|
1216
1314
|
/**
|
|
1217
1315
|
* Debug-only snapshot used by tests to assert rendered strings without
|
|
1218
1316
|
* needing a TTY. Not used by production code.
|
|
@@ -1259,6 +1357,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
1259
1357
|
* Sets up terminal scroll region to exclude chat box.
|
|
1260
1358
|
*/
|
|
1261
1359
|
enterStreamingScrollRegion() {
|
|
1360
|
+
if (!this.isTTY()) {
|
|
1361
|
+
this.scrollRegionActive = false;
|
|
1362
|
+
this.setStatusMessage('esc to interrupt');
|
|
1363
|
+
this.forceRender();
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1262
1366
|
const { rows } = this.getSize();
|
|
1263
1367
|
const chatBoxHeight = this.getChatBoxHeight();
|
|
1264
1368
|
const scrollEnd = Math.max(1, rows - chatBoxHeight);
|
|
@@ -1282,6 +1386,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
1282
1386
|
* Clears the old pinned chat box area to prevent duplication.
|
|
1283
1387
|
*/
|
|
1284
1388
|
exitStreamingScrollRegion() {
|
|
1389
|
+
if (!this.isTTY()) {
|
|
1390
|
+
this.scrollRegionActive = false;
|
|
1391
|
+
this.setStatusMessage('Ready for prompts');
|
|
1392
|
+
this.forceRender();
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1285
1395
|
const { rows } = this.getSize();
|
|
1286
1396
|
const chatBoxHeight = this.getChatBoxHeight();
|
|
1287
1397
|
writeLock.lock('exitStreamingScrollRegion');
|
|
@@ -1336,8 +1446,16 @@ export class TerminalInput extends EventEmitter {
|
|
|
1336
1446
|
finally {
|
|
1337
1447
|
writeLock.unlock();
|
|
1338
1448
|
}
|
|
1339
|
-
// Throttle chat box updates
|
|
1340
|
-
|
|
1449
|
+
// Throttle chat box updates only when a re-render is needed.
|
|
1450
|
+
// In streaming mode the chat box stays pinned, so skip redundant redraws.
|
|
1451
|
+
const needsRender = !this.scrollRegionActive || this.renderDirty;
|
|
1452
|
+
if (needsRender) {
|
|
1453
|
+
if (!this.scrollRegionActive) {
|
|
1454
|
+
// Content moved; force a render so the chat box follows.
|
|
1455
|
+
this.renderDirty = true;
|
|
1456
|
+
}
|
|
1457
|
+
this.scheduleStreamingRender(150);
|
|
1458
|
+
}
|
|
1341
1459
|
}
|
|
1342
1460
|
/**
|
|
1343
1461
|
* Enable scroll region (no-op in floating mode).
|
|
@@ -1380,9 +1498,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
1380
1498
|
const filtered = this.getFilteredCommands();
|
|
1381
1499
|
suggestionLines = Math.min(filtered.length, this.maxVisibleSuggestions) + 1; // +1 for header
|
|
1382
1500
|
}
|
|
1501
|
+
// Recent actions strip (single line when present)
|
|
1502
|
+
const recentLines = this.buildRecentActionLines(cols - 2).length;
|
|
1383
1503
|
// Total: meta + divider + input lines + suggestions + controls + buffer
|
|
1384
1504
|
// Leave a small buffer so streamed content still has space above the chat box
|
|
1385
|
-
const totalHeight = metaLines + 1 + inputLines + suggestionLines + controlLineCount + 1;
|
|
1505
|
+
const totalHeight = metaLines + recentLines + 1 + inputLines + suggestionLines + controlLineCount + 1;
|
|
1386
1506
|
const minContentRows = 2;
|
|
1387
1507
|
const maxHeight = Math.max(4, rows - minContentRows);
|
|
1388
1508
|
return Math.min(totalHeight, maxHeight);
|
|
@@ -1417,6 +1537,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
1417
1537
|
return;
|
|
1418
1538
|
// Capture content in scrollback buffer
|
|
1419
1539
|
this.addToScrollback(content);
|
|
1540
|
+
if (!this.isTTY()) {
|
|
1541
|
+
this.write(content);
|
|
1542
|
+
const rowsUsed = this.countRenderedRows(content);
|
|
1543
|
+
this.contentRow += rowsUsed;
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1420
1546
|
writeLock.lock('writeToScrollRegion');
|
|
1421
1547
|
try {
|
|
1422
1548
|
// Position cursor at content row and write
|
|
@@ -1531,33 +1657,76 @@ export class TerminalInput extends EventEmitter {
|
|
|
1531
1657
|
else {
|
|
1532
1658
|
this.emit('interrupt');
|
|
1533
1659
|
}
|
|
1534
|
-
|
|
1660
|
+
return true;
|
|
1535
1661
|
case 'a': // Home
|
|
1662
|
+
if (key.shift) {
|
|
1663
|
+
this.emit('toggleAlphaZero');
|
|
1664
|
+
return true;
|
|
1665
|
+
}
|
|
1536
1666
|
this.moveCursorToLineStart();
|
|
1537
|
-
|
|
1667
|
+
return true;
|
|
1538
1668
|
case 'e': // End
|
|
1669
|
+
if (key.shift) {
|
|
1670
|
+
this.toggleEditMode();
|
|
1671
|
+
this.emit('toggleEditMode');
|
|
1672
|
+
return true;
|
|
1673
|
+
}
|
|
1539
1674
|
this.moveCursorToLineEnd();
|
|
1540
|
-
|
|
1675
|
+
return true;
|
|
1541
1676
|
case 'u': // Delete to start
|
|
1542
1677
|
this.deleteToStart();
|
|
1543
|
-
|
|
1678
|
+
return true;
|
|
1544
1679
|
case 'k': // Delete to end
|
|
1545
1680
|
this.deleteToEnd();
|
|
1546
|
-
|
|
1681
|
+
return true;
|
|
1547
1682
|
case 'w': // Delete word
|
|
1548
1683
|
this.deleteWord();
|
|
1549
|
-
|
|
1684
|
+
return true;
|
|
1550
1685
|
case 'left': // Word left
|
|
1551
1686
|
this.moveCursorWordLeft();
|
|
1552
|
-
|
|
1687
|
+
return true;
|
|
1553
1688
|
case 'right': // Word right
|
|
1554
1689
|
this.moveCursorWordRight();
|
|
1690
|
+
return true;
|
|
1691
|
+
case 'return': // Ctrl+Enter => newline instead of submit
|
|
1692
|
+
this.insertNewline();
|
|
1693
|
+
return true;
|
|
1694
|
+
case 'v':
|
|
1695
|
+
if (key.shift) {
|
|
1696
|
+
this.emit('toggleVerify');
|
|
1697
|
+
return true;
|
|
1698
|
+
}
|
|
1699
|
+
break;
|
|
1700
|
+
case 'c':
|
|
1701
|
+
if (key.shift) {
|
|
1702
|
+
this.emit('toggleAutoContinue');
|
|
1703
|
+
return true;
|
|
1704
|
+
}
|
|
1705
|
+
break;
|
|
1706
|
+
case 't':
|
|
1707
|
+
if (key.shift) {
|
|
1708
|
+
this.emit('toggleThinking');
|
|
1709
|
+
return true;
|
|
1710
|
+
}
|
|
1711
|
+
break;
|
|
1712
|
+
case 'x':
|
|
1713
|
+
if (key.shift) {
|
|
1714
|
+
this.emit('clearContext');
|
|
1715
|
+
return true;
|
|
1716
|
+
}
|
|
1717
|
+
break;
|
|
1718
|
+
case 's':
|
|
1719
|
+
if (key.shift) {
|
|
1720
|
+
this.toggleScrollbackMode();
|
|
1721
|
+
return true;
|
|
1722
|
+
}
|
|
1555
1723
|
break;
|
|
1556
1724
|
}
|
|
1725
|
+
return false;
|
|
1557
1726
|
}
|
|
1558
1727
|
/**
|
|
1559
1728
|
* Handle Alt/Meta key combinations for mode toggles and navigation.
|
|
1560
|
-
*
|
|
1729
|
+
* Ctrl-based shortcuts are primary; Alt/Meta remains as a compatibility fallback.
|
|
1561
1730
|
*/
|
|
1562
1731
|
handleMetaKey(key) {
|
|
1563
1732
|
switch (key.name) {
|
|
@@ -1589,6 +1758,10 @@ export class TerminalInput extends EventEmitter {
|
|
|
1589
1758
|
// Alt+T: Toggle/cycle thinking mode
|
|
1590
1759
|
this.emit('toggleThinking');
|
|
1591
1760
|
break;
|
|
1761
|
+
case 'a':
|
|
1762
|
+
// Alt+A: Toggle AlphaZero RL mode
|
|
1763
|
+
this.emit('toggleAlphaZero');
|
|
1764
|
+
break;
|
|
1592
1765
|
case 'e':
|
|
1593
1766
|
// Alt+E: Toggle edit permission mode (ask/auto)
|
|
1594
1767
|
this.toggleEditMode();
|
|
@@ -1656,12 +1829,12 @@ export class TerminalInput extends EventEmitter {
|
|
|
1656
1829
|
handleSpecialKey(_str, key) {
|
|
1657
1830
|
switch (key.name) {
|
|
1658
1831
|
case 'return':
|
|
1659
|
-
if (key.shift || key.meta) {
|
|
1832
|
+
if (key.shift || key.meta || key.ctrl) {
|
|
1660
1833
|
this.insertNewline();
|
|
1661
1834
|
}
|
|
1662
1835
|
else if (this.showingSuggestions) {
|
|
1663
|
-
// Select suggestion and
|
|
1664
|
-
this.selectSuggestion();
|
|
1836
|
+
// Select highlighted suggestion and submit immediately
|
|
1837
|
+
this.selectSuggestion({ submit: true });
|
|
1665
1838
|
}
|
|
1666
1839
|
else {
|
|
1667
1840
|
this.submit();
|
|
@@ -1748,7 +1921,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
1748
1921
|
// Scrollback disabled in alternate screen mode
|
|
1749
1922
|
return true;
|
|
1750
1923
|
case 'tab':
|
|
1751
|
-
// Tab can select suggestion too
|
|
1924
|
+
// Tab can select suggestion too without submitting
|
|
1752
1925
|
if (this.showingSuggestions && !key.shift) {
|
|
1753
1926
|
this.selectSuggestion();
|
|
1754
1927
|
return true;
|
|
@@ -2184,7 +2357,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
2184
2357
|
this.isInScrollbackMode = false;
|
|
2185
2358
|
}
|
|
2186
2359
|
/**
|
|
2187
|
-
* Toggle scrollback mode on/off (Alt+S
|
|
2360
|
+
* Toggle scrollback mode on/off (Ctrl+Shift+S hotkey; Alt+S legacy)
|
|
2188
2361
|
* DISABLED: Scrollback navigation causes display corruption in alternate screen mode.
|
|
2189
2362
|
*/
|
|
2190
2363
|
toggleScrollbackMode() {
|
|
@@ -2440,13 +2613,38 @@ export class TerminalInput extends EventEmitter {
|
|
|
2440
2613
|
this.streamingRenderTimer = null;
|
|
2441
2614
|
// During streaming, only update chat box (not full render)
|
|
2442
2615
|
if (this.scrollRegionActive) {
|
|
2443
|
-
this.
|
|
2616
|
+
this.renderStreamingFrame();
|
|
2444
2617
|
}
|
|
2445
2618
|
else {
|
|
2446
2619
|
this.render();
|
|
2447
2620
|
}
|
|
2448
2621
|
}, wait);
|
|
2449
2622
|
}
|
|
2623
|
+
/**
|
|
2624
|
+
* Render the pinned chat box during streaming only when we actually have
|
|
2625
|
+
* pending UI changes. Prevents meta/header lines from being echoed into the
|
|
2626
|
+
* streamed log when nothing changed.
|
|
2627
|
+
*/
|
|
2628
|
+
renderStreamingFrame(force = false) {
|
|
2629
|
+
if (!this.canRender())
|
|
2630
|
+
return;
|
|
2631
|
+
if (this.isRendering)
|
|
2632
|
+
return;
|
|
2633
|
+
const shouldSkip = !force &&
|
|
2634
|
+
!this.renderDirty &&
|
|
2635
|
+
this.buffer === this.lastRenderContent &&
|
|
2636
|
+
this.cursor === this.lastRenderCursor;
|
|
2637
|
+
// Clear the dirty flag even when skipping to avoid runaway retries.
|
|
2638
|
+
this.renderDirty = false;
|
|
2639
|
+
if (shouldSkip) {
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
if (writeLock.isLocked()) {
|
|
2643
|
+
writeLock.safeWrite(() => this.renderStreamingFrame(force));
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
this.renderPinnedChatBox();
|
|
2647
|
+
}
|
|
2450
2648
|
resetStreamingRenderThrottle() {
|
|
2451
2649
|
if (this.streamingRenderTimer) {
|
|
2452
2650
|
clearTimeout(this.streamingRenderTimer);
|
|
@@ -2458,7 +2656,14 @@ export class TerminalInput extends EventEmitter {
|
|
|
2458
2656
|
if (!this.canRender())
|
|
2459
2657
|
return;
|
|
2460
2658
|
this.renderDirty = true;
|
|
2461
|
-
queueMicrotask(() =>
|
|
2659
|
+
queueMicrotask(() => {
|
|
2660
|
+
const streamingActive = this.scrollRegionActive || this.mode === 'streaming' || isStreamingMode();
|
|
2661
|
+
if (streamingActive && this.scrollRegionActive) {
|
|
2662
|
+
this.renderStreamingFrame();
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
this.render();
|
|
2666
|
+
});
|
|
2462
2667
|
}
|
|
2463
2668
|
canRender() {
|
|
2464
2669
|
return !this.disposed && this.enabled && this.isTTY();
|
|
@@ -2489,7 +2694,7 @@ export class TerminalInput extends EventEmitter {
|
|
|
2489
2694
|
return text
|
|
2490
2695
|
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, '') // OSC sequences
|
|
2491
2696
|
.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '') // CSI sequences
|
|
2492
|
-
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '') // Control chars except \n and \r
|
|
2697
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '') // Control chars except \n and \r (incl. DEL/C1)
|
|
2493
2698
|
.replace(/\r\n?/g, '\n'); // Normalize line endings
|
|
2494
2699
|
}
|
|
2495
2700
|
getArrowKeyName(sequence) {
|
|
@@ -2523,6 +2728,11 @@ export class TerminalInput extends EventEmitter {
|
|
|
2523
2728
|
isOrphanedEscapeSequence(text, key) {
|
|
2524
2729
|
if (!text || key?.name)
|
|
2525
2730
|
return false;
|
|
2731
|
+
// If sanitizing leaves printable content, treat it as user input.
|
|
2732
|
+
const sanitized = this.sanitize(text);
|
|
2733
|
+
if (sanitized.length > 0) {
|
|
2734
|
+
return false;
|
|
2735
|
+
}
|
|
2526
2736
|
if (text.includes('\x1b'))
|
|
2527
2737
|
return true;
|
|
2528
2738
|
// Common stray fragments when terminals partially echo arrow sequences (e.g., "[D")
|