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.
Files changed (45) hide show
  1. package/dist/core/offsecAlphaZero.d.ts +56 -0
  2. package/dist/core/offsecAlphaZero.d.ts.map +1 -0
  3. package/dist/core/offsecAlphaZero.js +395 -0
  4. package/dist/core/offsecAlphaZero.js.map +1 -0
  5. package/dist/core/preferences.d.ts +1 -0
  6. package/dist/core/preferences.d.ts.map +1 -1
  7. package/dist/core/preferences.js +7 -0
  8. package/dist/core/preferences.js.map +1 -1
  9. package/dist/shell/interactiveShell.d.ts +23 -5
  10. package/dist/shell/interactiveShell.d.ts.map +1 -1
  11. package/dist/shell/interactiveShell.js +486 -135
  12. package/dist/shell/interactiveShell.js.map +1 -1
  13. package/dist/shell/keyboardShortcuts.d.ts.map +1 -1
  14. package/dist/shell/keyboardShortcuts.js +11 -8
  15. package/dist/shell/keyboardShortcuts.js.map +1 -1
  16. package/dist/shell/liveStatus.d.ts +1 -0
  17. package/dist/shell/liveStatus.d.ts.map +1 -1
  18. package/dist/shell/liveStatus.js +32 -7
  19. package/dist/shell/liveStatus.js.map +1 -1
  20. package/dist/shell/terminalInput.d.ts +25 -2
  21. package/dist/shell/terminalInput.d.ts.map +1 -1
  22. package/dist/shell/terminalInput.js +270 -60
  23. package/dist/shell/terminalInput.js.map +1 -1
  24. package/dist/shell/terminalInputAdapter.d.ts +14 -0
  25. package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
  26. package/dist/shell/terminalInputAdapter.js +14 -0
  27. package/dist/shell/terminalInputAdapter.js.map +1 -1
  28. package/dist/shell/updateManager.d.ts +8 -1
  29. package/dist/shell/updateManager.d.ts.map +1 -1
  30. package/dist/shell/updateManager.js +4 -2
  31. package/dist/shell/updateManager.js.map +1 -1
  32. package/dist/ui/ShellUIAdapter.d.ts +6 -12
  33. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  34. package/dist/ui/ShellUIAdapter.js +26 -37
  35. package/dist/ui/ShellUIAdapter.js.map +1 -1
  36. package/dist/ui/shortcutsHelp.d.ts.map +1 -1
  37. package/dist/ui/shortcutsHelp.js +22 -21
  38. package/dist/ui/shortcutsHelp.js.map +1 -1
  39. package/dist/ui/unified/index.d.ts +2 -2
  40. package/dist/ui/unified/index.d.ts.map +1 -1
  41. package/dist/ui/unified/index.js.map +1 -1
  42. package/dist/ui/unified/layout.d.ts.map +1 -1
  43. package/dist/ui/unified/layout.js +31 -25
  44. package/dist/ui/unified/layout.js.map +1 -1
  45. 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 = 'alt+v';
122
- autoContinueHotkey = 'alt+c';
123
- thinkingHotkey = '/thinking';
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 = 250; // ms between renders during streaming
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
- return;
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
- this.buffer = selected.command + ' ';
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 Enter)`));
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
- // During streaming, throttle re-renders
733
- if (streamingActive && this.lastStreamingRender > 0) {
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 area
799
- for (let i = 0; i < chatBoxHeight; i++) {
800
- const row = chatBoxStartRow + i;
801
- if (row <= rows) {
802
- this.write(ESC.TO(row, 1));
803
- this.write(ESC.CLEAR_LINE);
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
- // Verification toggle (Alt+V)
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: `⌥V:${verifyStatus}verify`,
1136
+ text: `${verifyHotkey} ${verifyStatus}`,
1087
1137
  tone: this.verificationEnabled ? 'success' : 'muted'
1088
1138
  });
1089
- // Auto-continue toggle (Alt+C)
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: `⌥C:${autoStatus}auto`,
1142
+ text: `${autoHotkey} ${autoStatus}`,
1093
1143
  tone: this.autoContinueEnabled ? 'info' : 'muted'
1094
1144
  });
1095
- // Thinking mode toggle (Alt+T)
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
- toggleParts.push({ text: `⌥T:${shortThinking}`, tone: 'info' });
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
- return part.length === 1 ? part.toUpperCase() : part.toUpperCase();
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 during streaming
1340
- this.scheduleStreamingRender(200);
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
- break;
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
- break;
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
- break;
1675
+ return true;
1541
1676
  case 'u': // Delete to start
1542
1677
  this.deleteToStart();
1543
- break;
1678
+ return true;
1544
1679
  case 'k': // Delete to end
1545
1680
  this.deleteToEnd();
1546
- break;
1681
+ return true;
1547
1682
  case 'w': // Delete word
1548
1683
  this.deleteWord();
1549
- break;
1684
+ return true;
1550
1685
  case 'left': // Word left
1551
1686
  this.moveCursorWordLeft();
1552
- break;
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
- * All major erosolar-cli features accessible via keyboard shortcuts.
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 don't submit yet
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 hotkey)
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.renderChatBoxAtBottom();
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(() => this.render());
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")