erosolar-cli 2.1.173 → 2.1.175

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 (81) hide show
  1. package/agents/erosolar-code.rules.json +5 -0
  2. package/dist/capabilities/askUserCapability.js +1 -1
  3. package/dist/capabilities/askUserCapability.js.map +1 -1
  4. package/dist/headless/evalMode.d.ts.map +1 -1
  5. package/dist/headless/evalMode.js +0 -6
  6. package/dist/headless/evalMode.js.map +1 -1
  7. package/dist/headless/headlessApp.d.ts.map +1 -1
  8. package/dist/headless/headlessApp.js +0 -6
  9. package/dist/headless/headlessApp.js.map +1 -1
  10. package/dist/mcp/sseClient.d.ts +1 -4
  11. package/dist/mcp/sseClient.d.ts.map +1 -1
  12. package/dist/mcp/sseClient.js +2 -36
  13. package/dist/mcp/sseClient.js.map +1 -1
  14. package/dist/mcp/stdioClient.d.ts +1 -4
  15. package/dist/mcp/stdioClient.d.ts.map +1 -1
  16. package/dist/mcp/stdioClient.js +1 -41
  17. package/dist/mcp/stdioClient.js.map +1 -1
  18. package/dist/mcp/toolBridge.d.ts +0 -3
  19. package/dist/mcp/toolBridge.d.ts.map +1 -1
  20. package/dist/mcp/toolBridge.js +2 -2
  21. package/dist/mcp/toolBridge.js.map +1 -1
  22. package/dist/mcp/types.d.ts +0 -18
  23. package/dist/mcp/types.d.ts.map +1 -1
  24. package/dist/shell/interactiveShell.d.ts +24 -14
  25. package/dist/shell/interactiveShell.d.ts.map +1 -1
  26. package/dist/shell/interactiveShell.js +167 -79
  27. package/dist/shell/interactiveShell.js.map +1 -1
  28. package/dist/shell/shellApp.d.ts.map +1 -1
  29. package/dist/shell/shellApp.js +8 -9
  30. package/dist/shell/shellApp.js.map +1 -1
  31. package/dist/ui/PromptController.d.ts +0 -6
  32. package/dist/ui/PromptController.d.ts.map +1 -1
  33. package/dist/ui/PromptController.js +0 -3
  34. package/dist/ui/PromptController.js.map +1 -1
  35. package/dist/ui/ShellUIAdapter.d.ts +6 -0
  36. package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
  37. package/dist/ui/ShellUIAdapter.js +40 -12
  38. package/dist/ui/ShellUIAdapter.js.map +1 -1
  39. package/dist/ui/UnifiedUIController.d.ts +2 -1
  40. package/dist/ui/UnifiedUIController.d.ts.map +1 -1
  41. package/dist/ui/UnifiedUIController.js +1 -0
  42. package/dist/ui/UnifiedUIController.js.map +1 -1
  43. package/dist/ui/UnifiedUIRenderer.d.ts +53 -6
  44. package/dist/ui/UnifiedUIRenderer.d.ts.map +1 -1
  45. package/dist/ui/UnifiedUIRenderer.js +571 -72
  46. package/dist/ui/UnifiedUIRenderer.js.map +1 -1
  47. package/dist/ui/display.d.ts +4 -2
  48. package/dist/ui/display.d.ts.map +1 -1
  49. package/dist/ui/display.js +7 -8
  50. package/dist/ui/display.js.map +1 -1
  51. package/dist/ui/orchestration/StatusOrchestrator.d.ts +1 -1
  52. package/dist/ui/orchestration/StatusOrchestrator.js +1 -1
  53. package/dist/ui/orchestration/UIUpdateCoordinator.d.ts +2 -2
  54. package/dist/ui/orchestration/UIUpdateCoordinator.d.ts.map +1 -1
  55. package/dist/ui/orchestration/UIUpdateCoordinator.js +1 -1
  56. package/dist/ui/orchestration/UIUpdateCoordinator.js.map +1 -1
  57. package/dist/ui/unified/index.d.ts +2 -0
  58. package/dist/ui/unified/index.d.ts.map +1 -1
  59. package/dist/ui/unified/index.js +4 -0
  60. package/dist/ui/unified/index.js.map +1 -1
  61. package/package.json +2 -2
  62. package/dist/codex/capabilities/codexCoreCapability.d.ts +0 -6
  63. package/dist/codex/capabilities/codexCoreCapability.d.ts.map +0 -1
  64. package/dist/codex/capabilities/codexCoreCapability.js +0 -516
  65. package/dist/codex/capabilities/codexCoreCapability.js.map +0 -1
  66. package/dist/codex/fs.d.ts +0 -4
  67. package/dist/codex/fs.d.ts.map +0 -1
  68. package/dist/codex/fs.js +0 -25
  69. package/dist/codex/fs.js.map +0 -1
  70. package/dist/codex/persistence/planStore.d.ts +0 -4
  71. package/dist/codex/persistence/planStore.d.ts.map +0 -1
  72. package/dist/codex/persistence/planStore.js +0 -59
  73. package/dist/codex/persistence/planStore.js.map +0 -1
  74. package/dist/codex/pluginAllowlist.d.ts +0 -4
  75. package/dist/codex/pluginAllowlist.d.ts.map +0 -1
  76. package/dist/codex/pluginAllowlist.js +0 -14
  77. package/dist/codex/pluginAllowlist.js.map +0 -1
  78. package/dist/codex/types.d.ts +0 -21
  79. package/dist/codex/types.d.ts.map +0 -1
  80. package/dist/codex/types.js +0 -62
  81. package/dist/codex/types.js.map +0 -1
@@ -10,15 +10,28 @@
10
10
  */
11
11
  import * as readline from 'node:readline';
12
12
  import { EventEmitter } from 'node:events';
13
+ import { homedir } from 'node:os';
13
14
  import { theme, spinnerFrames } from './theme.js';
14
15
  import { isPlainOutputMode } from './outputMode.js';
16
+ import { ContextMeter, disposeAnimations } from './animatedStatus.js';
15
17
  const ESC = {
18
+ HIDE_CURSOR: '\x1b[?25l',
16
19
  SHOW_CURSOR: '\x1b[?25h',
20
+ CLEAR_SCREEN: '\x1b[2J',
17
21
  CLEAR_LINE: '\x1b[2K',
22
+ HOME: '\x1b[H',
18
23
  ENABLE_BRACKETED_PASTE: '\x1b[?2004h',
19
24
  DISABLE_BRACKETED_PASTE: '\x1b[?2004l',
25
+ TO: (row, col) => `\x1b[${row};${col}H`,
26
+ TO_COL: (col) => `\x1b[${col}G`,
27
+ ERASE_DOWN: '\x1b[J',
20
28
  REVERSE: '\x1b[7m',
21
29
  RESET: '\x1b[0m',
30
+ // Scroll region control - CRITICAL for fixed bottom overlay
31
+ SET_SCROLL_REGION: (top, bottom) => `\x1b[${top};${bottom}r`,
32
+ RESET_SCROLL_REGION: '\x1b[r',
33
+ SAVE_CURSOR: '\x1b[s',
34
+ RESTORE_CURSOR: '\x1b[u',
22
35
  };
23
36
  const COLLAPSE_HINT = theme.ui.muted('…'); // subtle ellipsis, no key hint
24
37
  const NEWLINE_PLACEHOLDER = '↵';
@@ -28,7 +41,9 @@ export class UnifiedUIRenderer extends EventEmitter {
28
41
  rl;
29
42
  plainMode;
30
43
  interactive;
44
+ rows = 24;
31
45
  cols = 80;
46
+ lastRenderWidth = null;
32
47
  eventQueue = [];
33
48
  isProcessingQueue = false;
34
49
  buffer = '';
@@ -38,6 +53,7 @@ export class UnifiedUIRenderer extends EventEmitter {
38
53
  suggestions = [];
39
54
  suggestionIndex = -1;
40
55
  availableCommands = [];
56
+ hotkeysInToggleLine = new Set();
41
57
  collapsedPaste = null;
42
58
  mode = 'idle';
43
59
  streamingStartTime = null;
@@ -45,26 +61,52 @@ export class UnifiedUIRenderer extends EventEmitter {
45
61
  statusOverride = null;
46
62
  statusStreaming = null;
47
63
  // Animated UI components
64
+ streamingSpinner = null;
65
+ thinkingIndicator = null;
66
+ contextMeter;
48
67
  spinnerFrame = 0;
49
68
  spinnerInterval = null;
50
- // Activity/status tracking
69
+ // Compacting status animation
70
+ compactingStatusMessage = '';
71
+ compactingStatusFrame = 0;
72
+ compactingStatusInterval = null;
73
+ compactingSpinnerFrames = ['✻', '✼', '✻', '✺'];
74
+ // Animated activity line (e.g., "✳ Ruminating… (esc to interrupt · 34s · ↑1.2k)")
51
75
  activityMessage = null;
76
+ activityPhraseIndex = 0;
77
+ activityStarFrame = 0;
78
+ activityStarFrames = ['✳', '✴', '✵', '✶', '✷', '✸'];
79
+ // Token count during streaming
52
80
  streamingTokens = 0;
81
+ // Fun phrases to cycle through when no specific activity is provided
82
+ funActivityPhrases = [
83
+ 'Moseying', 'Ruminating', 'Pondering', 'Cogitating', 'Mulling',
84
+ 'Contemplating', 'Deliberating', 'Noodling', 'Percolating', 'Stewing',
85
+ 'Brewing', 'Simmering', 'Churning', 'Puzzling', 'Meandering',
86
+ 'Wandering', 'Musing', 'Daydreaming', 'Woolgathering', 'Chewing',
87
+ ];
53
88
  statusMeta = {};
54
89
  toggleState = {
55
90
  verificationEnabled: false,
56
91
  criticalApprovalMode: 'auto',
57
92
  };
58
93
  // ------------ Helpers ------------
94
+ formatHotkey(combo) {
95
+ if (!combo?.trim())
96
+ return null;
97
+ return combo.trim().toUpperCase();
98
+ }
59
99
  lastPromptEvent = null;
60
100
  promptHeight = 0;
101
+ lastOverlayHeight = 0;
102
+ inlinePanel = [];
103
+ hasConversationContent = false;
61
104
  isPromptActive = false;
62
105
  inputRenderOffset = 0;
63
106
  plainPasteIdleMs = 24;
64
107
  plainPasteWindowMs = 60;
65
108
  plainPasteTriggerChars = 24;
66
109
  cursorVisibleColumn = 1;
67
- cursorVisibleRow = 0;
68
110
  inBracketedPaste = false;
69
111
  pasteBuffer = '';
70
112
  inPlainPaste = false;
@@ -75,6 +117,10 @@ export class UnifiedUIRenderer extends EventEmitter {
75
117
  plainRecentChunks = [];
76
118
  lastRenderedEventKey = null;
77
119
  lastOutputEndedWithNewline = true;
120
+ hasRenderedPrompt = false;
121
+ hasEverRenderedOverlay = false; // Track if we've ever rendered for inline clearing
122
+ lastOverlay = null;
123
+ allowPromptRender = true;
78
124
  inputCapture = null;
79
125
  constructor(output = process.stdout, input = process.stdin, options) {
80
126
  super();
@@ -82,6 +128,8 @@ export class UnifiedUIRenderer extends EventEmitter {
82
128
  this.input = input;
83
129
  this.interactive = Boolean(this.output.isTTY && this.input.isTTY && !process.env['CI']);
84
130
  this.plainMode = isPlainOutputMode() || !this.interactive;
131
+ // Initialize animated components
132
+ this.contextMeter = new ContextMeter();
85
133
  this.rl = readline.createInterface({
86
134
  input: this.input,
87
135
  output: this.output,
@@ -92,7 +140,7 @@ export class UnifiedUIRenderer extends EventEmitter {
92
140
  this.rl.setPrompt('');
93
141
  this.updateTerminalSize();
94
142
  this.output.on('resize', () => {
95
- if (this.interactive) {
143
+ if (!this.plainMode) {
96
144
  this.updateTerminalSize();
97
145
  this.renderPrompt();
98
146
  }
@@ -103,30 +151,59 @@ export class UnifiedUIRenderer extends EventEmitter {
103
151
  if (!this.interactive) {
104
152
  return;
105
153
  }
106
- this.write(ESC.ENABLE_BRACKETED_PASTE);
154
+ if (!this.plainMode) {
155
+ // If an overlay was already rendered before initialization (e.g., banner emitted early),
156
+ // clear it so initialize() doesn't stack a second control bar in scrollback.
157
+ if (this.hasRenderedPrompt || this.lastOverlay) {
158
+ this.clearPromptArea();
159
+ }
160
+ this.write(ESC.ENABLE_BRACKETED_PASTE);
161
+ this.updateTerminalSize();
162
+ this.hasRenderedPrompt = false;
163
+ this.lastOutputEndedWithNewline = true;
164
+ this.write(ESC.SHOW_CURSOR);
165
+ return;
166
+ }
167
+ // Plain mode: minimal setup, still render a simple prompt line
107
168
  this.updateTerminalSize();
169
+ this.hasRenderedPrompt = false;
108
170
  this.lastOutputEndedWithNewline = true;
109
- this.write(ESC.SHOW_CURSOR);
110
171
  this.renderPrompt();
111
172
  }
112
173
  cleanup() {
113
174
  this.cancelInputCapture(new Error('Renderer disposed'));
114
175
  this.cancelPlainPasteCapture();
176
+ // Stop any running animations
115
177
  if (this.spinnerInterval) {
116
178
  clearInterval(this.spinnerInterval);
117
179
  this.spinnerInterval = null;
118
180
  }
181
+ if (this.streamingSpinner) {
182
+ this.streamingSpinner.stop();
183
+ this.streamingSpinner = null;
184
+ }
185
+ if (this.thinkingIndicator) {
186
+ this.thinkingIndicator.stop();
187
+ this.thinkingIndicator = null;
188
+ }
189
+ this.contextMeter.dispose();
190
+ disposeAnimations();
119
191
  if (!this.interactive) {
120
192
  this.rl.close();
121
193
  return;
122
194
  }
123
- this.write(ESC.DISABLE_BRACKETED_PASTE);
124
- this.write(ESC.SHOW_CURSOR);
125
- this.write('\n');
195
+ if (!this.plainMode) {
196
+ // Clear the prompt area so it doesn't remain in scrollback history
197
+ this.clearPromptArea();
198
+ this.write(ESC.DISABLE_BRACKETED_PASTE);
199
+ this.write(ESC.SHOW_CURSOR);
200
+ this.write('\n');
201
+ }
126
202
  if (this.input.isTTY) {
127
203
  this.input.setRawMode(false);
128
204
  }
129
205
  this.rl.close();
206
+ this.lastOverlay = null;
130
207
  }
131
208
  // ------------ Input handling ------------
132
209
  setupInputHandlers() {
@@ -167,10 +244,6 @@ export class UnifiedUIRenderer extends EventEmitter {
167
244
  this.emit('toggle-critical-approval');
168
245
  return;
169
246
  }
170
- if (key.ctrl && key.shift && key.name?.toLowerCase() === 'n') {
171
- this.emit('toggle-network');
172
- return;
173
- }
174
247
  if (key.ctrl && key.name === 'c') {
175
248
  // Three-stage Ctrl+C behavior:
176
249
  // 1. Clear chat box if it has text
@@ -649,6 +722,16 @@ export class UnifiedUIRenderer extends EventEmitter {
649
722
  const normalized = this.normalizeEventType(type);
650
723
  if (!normalized)
651
724
  return;
725
+ if (normalized === 'prompt' ||
726
+ normalized === 'response' ||
727
+ normalized === 'thought' ||
728
+ normalized === 'stream' ||
729
+ normalized === 'tool' ||
730
+ normalized === 'tool-result' ||
731
+ normalized === 'build' ||
732
+ normalized === 'test') {
733
+ this.hasConversationContent = true;
734
+ }
652
735
  if (this.plainMode) {
653
736
  const formatted = this.formatContent({
654
737
  type: normalized,
@@ -706,11 +789,23 @@ export class UnifiedUIRenderer extends EventEmitter {
706
789
  const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown renderer error';
707
790
  this.output.write(`\n[renderer] ${message}\n`);
708
791
  }
709
- if (event.type !== 'prompt') {
792
+ // For prompt events, ensure the overlay is rendered immediately
793
+ // This guarantees prompts are visible before async processing continues
794
+ if (event.type === 'prompt') {
795
+ if (this.output.isTTY) {
796
+ this.allowPromptRender = true;
797
+ this.renderPrompt();
798
+ }
799
+ // No delay for prompt events - render immediately
800
+ }
801
+ else {
710
802
  await this.delay(1);
711
803
  }
712
804
  }
713
- if (this.output.isTTY && this.interactive) {
805
+ // ALWAYS render prompt after queue completes to keep bottom UI persistent
806
+ // This ensures status/toggles stay pinned and responses are fully rendered
807
+ if (this.output.isTTY) {
808
+ this.allowPromptRender = true;
714
809
  this.renderPrompt();
715
810
  }
716
811
  }
@@ -725,18 +820,28 @@ export class UnifiedUIRenderer extends EventEmitter {
725
820
  */
726
821
  async flushEvents(timeoutMs = 250) {
727
822
  // Kick off processing if idle
728
- if (!this.isProcessingQueue && this.eventQueue.length > 0) {
823
+ if (!this.plainMode && !this.isProcessingQueue && this.eventQueue.length > 0) {
729
824
  void this.processQueue();
730
825
  }
731
826
  const start = Date.now();
732
827
  while ((this.isProcessingQueue || this.eventQueue.length > 0) && Date.now() - start < timeoutMs) {
733
828
  await this.delay(5);
734
829
  }
735
- if (this.output.isTTY && this.interactive) {
830
+ if (!this.plainMode && this.output.isTTY) {
831
+ this.allowPromptRender = true;
736
832
  this.renderPrompt();
737
833
  }
738
834
  }
739
835
  async renderEvent(event) {
836
+ if (this.plainMode) {
837
+ const formattedPlain = this.formatContent(event);
838
+ if (formattedPlain) {
839
+ const text = formattedPlain.endsWith('\n') ? formattedPlain : `${formattedPlain}\n`;
840
+ this.output.write(text);
841
+ this.lastOutputEndedWithNewline = text.endsWith('\n');
842
+ }
843
+ return;
844
+ }
740
845
  const formatted = this.formatContent(event);
741
846
  if (!formatted)
742
847
  return;
@@ -749,9 +854,11 @@ export class UnifiedUIRenderer extends EventEmitter {
749
854
  if (event.type !== 'prompt') {
750
855
  this.lastRenderedEventKey = signature;
751
856
  }
752
- if (this.isPromptActive) {
753
- this.clearPromptArea(true);
857
+ // Clear the prompt area before writing new content
858
+ if (this.promptHeight > 0 || this.lastOverlay) {
859
+ this.clearPromptArea();
754
860
  }
861
+ this.isPromptActive = false;
755
862
  if (event.type !== 'stream' && !this.lastOutputEndedWithNewline && formatted.trim()) {
756
863
  // Keep scrollback ordering predictable when previous output ended mid-line
757
864
  this.output.write('\n');
@@ -759,9 +866,6 @@ export class UnifiedUIRenderer extends EventEmitter {
759
866
  }
760
867
  this.output.write(formatted);
761
868
  this.lastOutputEndedWithNewline = formatted.endsWith('\n');
762
- if (this.interactive && !this.plainMode) {
763
- this.renderPrompt();
764
- }
765
869
  }
766
870
  normalizeEventType(type) {
767
871
  switch (type) {
@@ -1159,6 +1263,22 @@ export class UnifiedUIRenderer extends EventEmitter {
1159
1263
  }
1160
1264
  return result.join('\n') + '\n';
1161
1265
  }
1266
+ /**
1267
+ * Format streaming elapsed time in Claude Code style: 3m 30s
1268
+ */
1269
+ formatStreamingElapsed() {
1270
+ if (!this.streamingStartTime)
1271
+ return null;
1272
+ const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
1273
+ if (elapsed < 5)
1274
+ return null; // Don't show for very short durations
1275
+ const mins = Math.floor(elapsed / 60);
1276
+ const secs = elapsed % 60;
1277
+ if (mins > 0) {
1278
+ return `${mins}m ${secs}s`;
1279
+ }
1280
+ return `${secs}s`;
1281
+ }
1162
1282
  /**
1163
1283
  * Format a compact conversation block (Claude Code style)
1164
1284
  * Shows a visual separator with "history" label and ctrl+o hint
@@ -1209,7 +1329,8 @@ export class UnifiedUIRenderer extends EventEmitter {
1209
1329
  this.write('\n');
1210
1330
  this.lastOutputEndedWithNewline = true;
1211
1331
  }
1212
- if (this.interactive) {
1332
+ if (!this.plainMode) {
1333
+ // Always render prompt to keep bottom UI persistent (rich mode only)
1213
1334
  this.renderPrompt();
1214
1335
  }
1215
1336
  }
@@ -1220,12 +1341,15 @@ export class UnifiedUIRenderer extends EventEmitter {
1220
1341
  if (this.spinnerInterval)
1221
1342
  return; // Already running
1222
1343
  this.spinnerFrame = 0;
1344
+ this.activityStarFrame = 0;
1223
1345
  this.spinnerInterval = setInterval(() => {
1224
1346
  this.spinnerFrame = (this.spinnerFrame + 1) % spinnerFrames.braille.length;
1225
- if (this.mode === 'streaming') {
1347
+ this.activityStarFrame = (this.activityStarFrame + 1) % this.activityStarFrames.length;
1348
+ // Re-render to show updated spinner/star frame
1349
+ if (!this.plainMode && this.mode === 'streaming') {
1226
1350
  this.renderPrompt();
1227
1351
  }
1228
- }, 120);
1352
+ }, 80); // ~12 FPS for smooth spinner animation
1229
1353
  }
1230
1354
  /**
1231
1355
  * Stop the animated spinner
@@ -1236,6 +1360,7 @@ export class UnifiedUIRenderer extends EventEmitter {
1236
1360
  this.spinnerInterval = null;
1237
1361
  }
1238
1362
  this.spinnerFrame = 0;
1363
+ this.activityStarFrame = 0;
1239
1364
  this.activityMessage = null;
1240
1365
  }
1241
1366
  /**
@@ -1244,7 +1369,7 @@ export class UnifiedUIRenderer extends EventEmitter {
1244
1369
  */
1245
1370
  setActivity(message) {
1246
1371
  this.activityMessage = message;
1247
- if (this.interactive) {
1372
+ if (!this.plainMode) {
1248
1373
  this.renderPrompt();
1249
1374
  }
1250
1375
  }
@@ -1254,6 +1379,16 @@ export class UnifiedUIRenderer extends EventEmitter {
1254
1379
  updateStreamingTokens(tokens) {
1255
1380
  this.streamingTokens = tokens;
1256
1381
  }
1382
+ /**
1383
+ * Format token count as compact string (e.g., 1.2k, 24k, 128k)
1384
+ */
1385
+ formatTokenCount(tokens) {
1386
+ if (tokens < 1000)
1387
+ return String(tokens);
1388
+ if (tokens < 10000)
1389
+ return `${(tokens / 1000).toFixed(1)}k`;
1390
+ return `${Math.round(tokens / 1000)}k`;
1391
+ }
1257
1392
  getMode() {
1258
1393
  return this.mode;
1259
1394
  }
@@ -1302,19 +1437,28 @@ export class UnifiedUIRenderer extends EventEmitter {
1302
1437
  }
1303
1438
  updateModeToggles(state) {
1304
1439
  this.toggleState = { ...this.toggleState, ...state };
1440
+ if (!state.verificationHotkey &&
1441
+ !state.thinkingHotkey &&
1442
+ !state.criticalApprovalHotkey) {
1443
+ this.hotkeysInToggleLine.clear();
1444
+ }
1305
1445
  this.renderPrompt();
1306
1446
  }
1307
1447
  setInlinePanel(lines) {
1308
1448
  const normalized = (lines ?? [])
1309
1449
  .map(line => line.replace(/\s+$/g, ''))
1310
1450
  .filter(line => line.trim().length > 0);
1311
- if (!normalized.length) {
1451
+ if (JSON.stringify(normalized) === JSON.stringify(this.inlinePanel)) {
1312
1452
  return;
1313
1453
  }
1314
- this.addEvent('response', `${normalized.join('\n')}\n`);
1454
+ this.inlinePanel = normalized;
1455
+ this.renderPrompt();
1315
1456
  }
1316
1457
  clearInlinePanel() {
1317
- // No-op: inline panels render directly into scrollback
1458
+ if (!this.inlinePanel.length)
1459
+ return;
1460
+ this.inlinePanel = [];
1461
+ this.renderPrompt();
1318
1462
  }
1319
1463
  // ------------ Prompt rendering ------------
1320
1464
  renderPrompt() {
@@ -1322,42 +1466,284 @@ export class UnifiedUIRenderer extends EventEmitter {
1322
1466
  this.isPromptActive = false;
1323
1467
  return;
1324
1468
  }
1469
+ if (this.plainMode) {
1470
+ const line = `> ${this.buffer}`;
1471
+ if (!this.isPromptActive && !this.lastOutputEndedWithNewline) {
1472
+ this.write('\n');
1473
+ this.lastOutputEndedWithNewline = true;
1474
+ }
1475
+ this.write(`\r${ESC.CLEAR_LINE}${line}`);
1476
+ this.cursorVisibleColumn = line.length + 1;
1477
+ this.hasRenderedPrompt = true;
1478
+ this.isPromptActive = true;
1479
+ this.lastOutputEndedWithNewline = false; // prompt ends mid-line by design
1480
+ this.promptHeight = 1;
1481
+ return;
1482
+ }
1483
+ if (!this.allowPromptRender) {
1484
+ return;
1485
+ }
1486
+ // Rich inline mode: prompt flows naturally with content
1325
1487
  this.updateTerminalSize();
1326
- const status = this.composeStatusLabel();
1327
- const inputLine = this.buildInputLine();
1328
- const inputLines = inputLine.split('\n');
1329
- const lines = [];
1330
- if (status) {
1331
- lines.push(this.applyTone(status.text, status.tone));
1488
+ const maxWidth = this.safeWidth();
1489
+ this.lastRenderWidth = maxWidth;
1490
+ const overlay = this.buildOverlayLines();
1491
+ if (!overlay.lines.length) {
1492
+ return;
1332
1493
  }
1333
- lines.push(...inputLines);
1334
- const hadPrompt = this.isPromptActive;
1335
- this.clearPromptArea();
1336
- if (!hadPrompt && !this.lastOutputEndedWithNewline) {
1337
- this.write('\n');
1338
- this.lastOutputEndedWithNewline = true;
1494
+ const renderedLines = overlay.lines.map(line => this.truncateLine(line, maxWidth));
1495
+ if (!renderedLines.length) {
1496
+ return;
1339
1497
  }
1340
- for (let i = 0; i < lines.length; i++) {
1498
+ const promptIndex = Math.max(0, Math.min(overlay.promptIndex, renderedLines.length - 1));
1499
+ const height = renderedLines.length;
1500
+ // Clear previous prompt and handle height changes
1501
+ if (this.hasEverRenderedOverlay && this.lastOverlayHeight > 0 && this.lastOverlay) {
1502
+ // Move up from prompt row to top of overlay
1503
+ const linesToTop = this.lastOverlay.promptIndex;
1504
+ if (linesToTop > 0) {
1505
+ this.write(`\x1b[${linesToTop}A`);
1506
+ }
1507
+ // Clear all previous lines
1508
+ for (let i = 0; i < this.lastOverlayHeight; i++) {
1509
+ this.write('\r');
1510
+ this.write(ESC.CLEAR_LINE);
1511
+ if (i < this.lastOverlayHeight - 1) {
1512
+ this.write('\x1b[B');
1513
+ }
1514
+ }
1515
+ // If new height is greater, we need to add blank lines
1516
+ const extraLines = height - this.lastOverlayHeight;
1517
+ if (extraLines > 0) {
1518
+ for (let i = 0; i < extraLines; i++) {
1519
+ this.write('\n');
1520
+ }
1521
+ }
1522
+ // Move back to top of where overlay should start
1523
+ const moveBackUp = Math.max(0, height - 1);
1524
+ if (moveBackUp > 0) {
1525
+ this.write(`\x1b[${moveBackUp}A`);
1526
+ }
1527
+ }
1528
+ // Write prompt lines (no trailing newline on last line)
1529
+ for (let i = 0; i < renderedLines.length; i++) {
1341
1530
  this.write('\r');
1342
1531
  this.write(ESC.CLEAR_LINE);
1343
- this.write(lines[i] || '');
1344
- if (i < lines.length - 1) {
1532
+ this.write(renderedLines[i] || '');
1533
+ if (i < renderedLines.length - 1) {
1345
1534
  this.write('\n');
1346
1535
  }
1347
1536
  }
1348
- const cursorRow = Math.min(lines.length - 1, this.cursorVisibleRow ?? lines.length - 1);
1349
- const rowsToMoveUp = lines.length - 1 - cursorRow;
1350
- if (rowsToMoveUp > 0) {
1351
- this.write(`\x1b[${rowsToMoveUp}A`);
1352
- }
1353
- const cursorCol = Math.max(1, this.cursorVisibleColumn);
1354
- this.write(`\x1b[${cursorCol}G`);
1537
+ // Position cursor at prompt input line
1538
+ const promptCol = Math.min(Math.max(1, 3 + this.cursor), this.cols || 80);
1539
+ // Cursor is now at the last line. Move up to the prompt row.
1540
+ const linesToMoveUp = height - 1 - promptIndex;
1541
+ if (linesToMoveUp > 0) {
1542
+ this.write(`\x1b[${linesToMoveUp}A`);
1543
+ }
1544
+ this.write(`\x1b[${promptCol}G`);
1545
+ this.cursorVisibleColumn = promptCol;
1546
+ this.hasRenderedPrompt = true;
1547
+ this.hasEverRenderedOverlay = true;
1355
1548
  this.isPromptActive = true;
1356
- this.promptHeight = lines.length;
1549
+ this.lastOverlayHeight = height;
1550
+ this.lastOverlay = { lines: renderedLines, promptIndex };
1357
1551
  this.lastOutputEndedWithNewline = false;
1552
+ this.promptHeight = height;
1553
+ }
1554
+ buildOverlayLines() {
1555
+ const lines = [];
1556
+ const maxWidth = this.safeWidth();
1557
+ // Simple horizontal divider - clean and reliable
1558
+ const divider = theme.ui.muted('─'.repeat(Math.min(maxWidth, 56)));
1559
+ // Activity line (only when streaming) - shows: ✽ Moseying… (esc to interrupt · 34s)
1560
+ if (this.mode === 'streaming' && this.activityMessage) {
1561
+ // Animated sparkle
1562
+ const spinnerChars = ['✽', '✾', '✿', '❀', '❁', '❂', '❃', '✻'];
1563
+ const spinnerChar = spinnerChars[this.spinnerFrame % spinnerChars.length] ?? '✽';
1564
+ const elapsed = this.formatStreamingElapsed();
1565
+ // Use fun phrases for generic activity, otherwise show specific activity
1566
+ const genericActivities = ['Streaming', 'Thinking', 'Processing'];
1567
+ const displayActivity = genericActivities.includes(this.activityMessage)
1568
+ ? this.funActivityPhrases[this.activityPhraseIndex % this.funActivityPhrases.length]
1569
+ : this.activityMessage;
1570
+ // Format: ✽ Moseying… (esc to interrupt · 1m 19s · ↑1.2k tokens)
1571
+ const parts = ['esc to interrupt'];
1572
+ if (elapsed)
1573
+ parts.push(elapsed);
1574
+ if (this.streamingTokens > 0) {
1575
+ parts.push(`↑${this.formatTokenCount(this.streamingTokens)} tokens`);
1576
+ }
1577
+ const activityLine = `${theme.info(spinnerChar)} ${displayActivity}… ${theme.ui.muted(`(${parts.join(' · ')})`)}`;
1578
+ lines.push(this.truncateLine(activityLine, maxWidth));
1579
+ }
1580
+ // Top divider
1581
+ lines.push(divider);
1582
+ // Input prompt line
1583
+ const promptIndex = lines.length;
1584
+ const inputLine = this.buildInputLine();
1585
+ // Handle multi-line input by splitting on newlines
1586
+ const inputLines = inputLine.split('\n');
1587
+ for (const line of inputLines) {
1588
+ lines.push(this.truncateLine(line, maxWidth));
1589
+ }
1590
+ // Bottom divider
1591
+ lines.push(divider);
1592
+ // Inline panel (pinned scroll box for live output/menus)
1593
+ if (this.inlinePanel.length > 0) {
1594
+ for (const panelLine of this.inlinePanel) {
1595
+ lines.push(this.truncateLine(` ${panelLine}`, maxWidth));
1596
+ }
1597
+ // Separate inline content from suggestions/toggles
1598
+ lines.push(divider);
1599
+ }
1600
+ // Slash command suggestions
1601
+ if (this.suggestions.length > 0) {
1602
+ for (let index = 0; index < this.suggestions.length; index++) {
1603
+ const suggestion = this.suggestions[index];
1604
+ const isActive = index === this.suggestionIndex;
1605
+ const marker = isActive ? theme.primary('▸') : theme.ui.muted(' ');
1606
+ const cmdText = isActive ? theme.primary(suggestion.command) : theme.ui.muted(suggestion.command);
1607
+ const descText = isActive ? suggestion.description : theme.ui.muted(suggestion.description);
1608
+ lines.push(this.truncateLine(` ${marker} ${cmdText} — ${descText}`, maxWidth));
1609
+ }
1610
+ }
1611
+ // Model and context info
1612
+ const modelContextLine = this.buildModelContextLine();
1613
+ if (modelContextLine) {
1614
+ lines.push(this.truncateLine(` ${modelContextLine}`, maxWidth));
1615
+ }
1616
+ // Mode toggles
1617
+ const toggleLine = this.buildInlineToggleLine();
1618
+ if (toggleLine) {
1619
+ lines.push(this.truncateLine(` ${toggleLine}`, maxWidth));
1620
+ }
1621
+ // Help hint
1622
+ lines.push(this.truncateLine(` ${theme.ui.muted('? for shortcuts')}`, maxWidth));
1623
+ return { lines, promptIndex };
1624
+ }
1625
+ /**
1626
+ * Build model name and context usage line with mini progress bar
1627
+ * Format: gpt-4 · ████░░ 85% context
1628
+ */
1629
+ buildModelContextLine() {
1630
+ const parts = [];
1631
+ // Model name (provider / model or just model)
1632
+ const model = this.statusMeta.provider && this.statusMeta.model
1633
+ ? `${this.statusMeta.provider} · ${this.statusMeta.model}`
1634
+ : this.statusMeta.model || this.statusMeta.provider;
1635
+ if (model) {
1636
+ parts.push(theme.info(model));
1637
+ }
1638
+ // Context meter with mini progress bar
1639
+ if (this.statusMeta.contextPercent !== undefined) {
1640
+ const remaining = Math.max(0, 100 - this.statusMeta.contextPercent);
1641
+ const barWidth = 6;
1642
+ const filled = Math.round((remaining / 100) * barWidth);
1643
+ const empty = barWidth - filled;
1644
+ const barColor = remaining > 50 ? theme.success : remaining > 20 ? theme.warning : theme.error;
1645
+ const bar = barColor('█'.repeat(filled)) + theme.ui.muted('░'.repeat(empty));
1646
+ parts.push(`${bar} ${barColor(`${remaining}%`)} ${theme.ui.muted('ctx')}`);
1647
+ }
1648
+ return parts.length > 0 ? parts.join(theme.ui.muted(' · ')) : null;
1649
+ }
1650
+ /**
1651
+ * Build inline toggle controls - Claude Code style
1652
+ * Format: ⏵⏵ accept edits on (shift+tab to cycle)
1653
+ */
1654
+ buildInlineToggleLine() {
1655
+ const parts = [];
1656
+ // Edit acceptance mode - Claude Code style with ⏵⏵
1657
+ const editIcon = '⏵⏵';
1658
+ const editState = this.toggleState.verificationEnabled ? 'verify edits' : 'accept edits';
1659
+ const editStatus = this.toggleState.verificationEnabled ? theme.warning('on') : theme.success('on');
1660
+ parts.push(`${theme.ui.muted(editIcon)} ${editState} ${editStatus}`);
1661
+ // Thinking mode (if not default)
1662
+ const thinkingLabel = (this.toggleState.thinkingModeLabel || 'balanced').trim().toLowerCase();
1663
+ if (thinkingLabel === 'extended') {
1664
+ parts.push(`${theme.ui.muted('thinking')} ${theme.info('extended')}`);
1665
+ }
1666
+ // Approval mode (if not auto)
1667
+ const approvalMode = this.toggleState.criticalApprovalMode || 'auto';
1668
+ if (approvalMode === 'approval') {
1669
+ parts.push(`${theme.ui.muted('approvals')} ${theme.warning('ask')}`);
1670
+ }
1671
+ // Cycle hint
1672
+ const cycleHint = theme.ui.muted('(shift+tab to cycle)');
1673
+ return parts.length > 0 ? `${parts.join(theme.ui.muted(' · '))} ${cycleHint}` : null;
1674
+ }
1675
+ buildChromeLines() {
1676
+ const maxWidth = this.safeWidth();
1677
+ const statusLines = this.buildStatusBlock(maxWidth);
1678
+ const metaLines = this.buildMetaBlock(maxWidth);
1679
+ return [...statusLines, ...metaLines];
1680
+ }
1681
+ abbreviatePath(pathValue) {
1682
+ const home = homedir();
1683
+ if (home && pathValue.startsWith(home)) {
1684
+ return pathValue.replace(home, '~');
1685
+ }
1686
+ return pathValue;
1687
+ }
1688
+ buildStatusBlock(maxWidth) {
1689
+ const statusLabel = this.composeStatusLabel();
1690
+ if (!statusLabel) {
1691
+ return [];
1692
+ }
1693
+ const segments = [];
1694
+ // Add animated spinner when streaming for dynamic visual feedback
1695
+ if (this.mode === 'streaming') {
1696
+ const spinnerChars = spinnerFrames.braille;
1697
+ const spinnerChar = spinnerChars[this.spinnerFrame % spinnerChars.length] ?? '⠋';
1698
+ segments.push(`${theme.info(spinnerChar)} ${this.applyTone(statusLabel.text, statusLabel.tone)}`);
1699
+ }
1700
+ else {
1701
+ segments.push(`${theme.ui.muted('status')} ${this.applyTone(statusLabel.text, statusLabel.tone)}`);
1702
+ }
1703
+ if (this.statusMeta.sessionTime) {
1704
+ segments.push(`${theme.ui.muted('runtime')} ${theme.ui.muted(this.statusMeta.sessionTime)}`);
1705
+ }
1706
+ if (this.statusMeta.contextPercent !== undefined) {
1707
+ // Use animated context meter for smooth color transitions
1708
+ this.contextMeter.update(this.statusMeta.contextPercent);
1709
+ segments.push(this.contextMeter.render());
1710
+ }
1711
+ return this.wrapSegments(segments, maxWidth);
1712
+ }
1713
+ buildMetaBlock(maxWidth) {
1714
+ const segments = [];
1715
+ if (this.statusMeta.profile) {
1716
+ segments.push(this.formatMetaSegment('profile', this.statusMeta.profile, 'info'));
1717
+ }
1718
+ const model = this.statusMeta.provider && this.statusMeta.model
1719
+ ? `${this.statusMeta.provider} / ${this.statusMeta.model}`
1720
+ : this.statusMeta.model || this.statusMeta.provider;
1721
+ if (model) {
1722
+ segments.push(this.formatMetaSegment('model', model, 'info'));
1723
+ }
1724
+ const workspace = this.statusMeta.workspace || this.statusMeta.directory;
1725
+ if (workspace) {
1726
+ segments.push(this.formatMetaSegment('dir', this.abbreviatePath(workspace), 'muted'));
1727
+ }
1728
+ if (this.statusMeta.writes) {
1729
+ segments.push(this.formatMetaSegment('writes', this.statusMeta.writes, 'muted'));
1730
+ }
1731
+ if (this.statusMeta.toolSummary) {
1732
+ segments.push(this.formatMetaSegment('tools', this.statusMeta.toolSummary, 'muted'));
1733
+ }
1734
+ if (this.statusMeta.sessionLabel) {
1735
+ segments.push(this.formatMetaSegment('session', this.statusMeta.sessionLabel, 'muted'));
1736
+ }
1737
+ if (this.statusMeta.version) {
1738
+ segments.push(this.formatMetaSegment('build', `v${this.statusMeta.version}`, 'muted'));
1739
+ }
1740
+ if (segments.length === 0) {
1741
+ return [];
1742
+ }
1743
+ return this.wrapSegments(segments, maxWidth);
1358
1744
  }
1359
1745
  composeStatusLabel() {
1360
- const statuses = [this.activityMessage, this.statusStreaming, this.statusOverride, this.statusMessage].filter((value, index, arr) => Boolean(value) && arr.indexOf(value) === index);
1746
+ const statuses = [this.statusStreaming, this.statusOverride, this.statusMessage].filter((value, index, arr) => Boolean(value) && arr.indexOf(value) === index);
1361
1747
  const text = statuses.length > 0 ? statuses.join(' / ') : 'Ready for prompts';
1362
1748
  if (!text.trim()) {
1363
1749
  return null;
@@ -1417,6 +1803,97 @@ export class UnifiedUIRenderer extends EventEmitter {
1417
1803
  }
1418
1804
  return lines;
1419
1805
  }
1806
+ buildControlLines() {
1807
+ const lines = [];
1808
+ const toggleLine = this.buildToggleLine();
1809
+ if (toggleLine) {
1810
+ lines.push(`${theme.ui.muted('modes')} ${theme.ui.muted('›')} ${toggleLine}`);
1811
+ }
1812
+ const shortcutLine = this.buildShortcutLine();
1813
+ if (shortcutLine) {
1814
+ lines.push(`${theme.ui.muted('keys')} ${shortcutLine}`);
1815
+ }
1816
+ return lines;
1817
+ }
1818
+ /**
1819
+ * Build a compact toggle line like Claude Code:
1820
+ * "⏵⏵ accept edits on (shift+tab to cycle)"
1821
+ */
1822
+ buildCompactToggleLine() {
1823
+ // Show the most relevant mode based on current state
1824
+ const parts = [];
1825
+ // Edit mode indicator
1826
+ const editIcon = '⏵⏵';
1827
+ const editState = this.toggleState.verificationEnabled ? 'approval required' : 'accept edits';
1828
+ parts.push(`${theme.ui.muted(editIcon)} ${editState} ${theme.success('on')}`);
1829
+ // Thinking mode (if active)
1830
+ const thinkingLabel = (this.toggleState.thinkingModeLabel || '').trim().toLowerCase();
1831
+ if (thinkingLabel && thinkingLabel !== 'off') {
1832
+ parts.push(`${theme.ui.muted('thinking')} ${theme.info(thinkingLabel)}`);
1833
+ }
1834
+ // Cycle hint
1835
+ const cycleHint = theme.ui.muted('(shift+tab to cycle)');
1836
+ if (parts.length === 0) {
1837
+ return null;
1838
+ }
1839
+ return ` ${parts.join(theme.ui.muted(' · '))} ${cycleHint}`;
1840
+ }
1841
+ buildToggleLine() {
1842
+ const toggles = [];
1843
+ const addToggle = (label, on, hotkey, value) => {
1844
+ toggles.push({ label, on, hotkey: this.formatHotkey(hotkey), value });
1845
+ };
1846
+ addToggle('Verify', this.toggleState.verificationEnabled, this.toggleState.verificationHotkey);
1847
+ const approvalMode = this.toggleState.criticalApprovalMode || 'auto';
1848
+ const approvalActive = approvalMode !== 'auto';
1849
+ addToggle('Approvals', approvalActive, this.toggleState.criticalApprovalHotkey, approvalMode === 'auto' ? 'auto' : 'ask');
1850
+ const thinkingLabel = (this.toggleState.thinkingModeLabel || 'off').trim();
1851
+ const thinkingActive = thinkingLabel.toLowerCase() !== 'off';
1852
+ addToggle('Thinking', thinkingActive, this.toggleState.thinkingHotkey, thinkingLabel);
1853
+ const buildLine = (includeHotkeys) => {
1854
+ return toggles
1855
+ .map(toggle => {
1856
+ const stateText = toggle.on ? theme.success(toggle.value || 'on') : theme.ui.muted(toggle.value || 'off');
1857
+ const hotkeyText = includeHotkeys && toggle.hotkey ? theme.ui.muted(` [${toggle.hotkey}]`) : '';
1858
+ return `${theme.ui.muted(`${toggle.label}:`)} ${stateText}${hotkeyText}`;
1859
+ })
1860
+ .join(theme.ui.muted(' '));
1861
+ };
1862
+ const maxWidth = this.safeWidth();
1863
+ let line = buildLine(true);
1864
+ // Record which hotkeys are actually shown so the shortcut line can avoid duplicates
1865
+ this.hotkeysInToggleLine = new Set(toggles
1866
+ .map(toggle => (toggle.hotkey ? toggle.hotkey : null))
1867
+ .filter((key) => Boolean(key)));
1868
+ // If the line is too wide, drop hotkey hints to preserve all toggle labels
1869
+ if (this.visibleLength(line) > maxWidth) {
1870
+ this.hotkeysInToggleLine.clear();
1871
+ line = buildLine(false);
1872
+ }
1873
+ return line.trim() ? line : null;
1874
+ }
1875
+ buildShortcutLine() {
1876
+ const parts = [];
1877
+ const addHotkey = (label, combo) => {
1878
+ const normalized = this.formatHotkey(combo);
1879
+ if (!normalized)
1880
+ return;
1881
+ if (this.hotkeysInToggleLine.has(normalized)) {
1882
+ return;
1883
+ }
1884
+ parts.push(`${theme.info(normalized)} ${theme.ui.muted(label)}`);
1885
+ };
1886
+ // Core controls
1887
+ addHotkey('interrupt', 'Ctrl+C');
1888
+ addHotkey('clear input', 'Ctrl+U');
1889
+ // Feature toggles (only if hotkeys are defined)
1890
+ addHotkey('verify', this.toggleState.verificationHotkey);
1891
+ addHotkey('thinking', this.toggleState.thinkingHotkey);
1892
+ if (parts.length === 0) {
1893
+ return null;
1894
+ }
1895
+ return parts.join(theme.ui.muted(' '));
1896
+ }
1420
1897
  buildInputLine() {
1421
1898
  if (this.collapsedPaste) {
1422
1899
  const summary = `[pasted ${this.collapsedPaste.lines} lines, ${this.collapsedPaste.chars} chars] (Ctrl+L insert, Backspace discard)`;
@@ -1484,7 +1961,21 @@ export class UnifiedUIRenderer extends EventEmitter {
1484
1961
  const lastLine = result[cursorLine] ?? '';
1485
1962
  cursorCol = this.visibleLength(lastLine);
1486
1963
  }
1487
- this.cursorVisibleRow = cursorLine;
1964
+ // Add cursor highlight to the appropriate position
1965
+ if (result.length > 0) {
1966
+ const targetLine = result[cursorLine] ?? '';
1967
+ const visiblePart = this.stripAnsi(targetLine);
1968
+ const cursorPos = Math.min(cursorCol, visiblePart.length);
1969
+ // Rebuild the line with cursor highlight
1970
+ const before = visiblePart.slice(0, cursorPos);
1971
+ const at = visiblePart.charAt(cursorPos) || ' ';
1972
+ const after = visiblePart.slice(cursorPos + 1);
1973
+ // Preserve the prompt/indent styling
1974
+ const prefix = cursorLine === 0 ? prompt : continuationIndent;
1975
+ const textPart = cursorLine === 0 ? before.slice(promptWidth) : before.slice(continuationWidth);
1976
+ result[cursorLine] = `${prefix}${textPart}${ESC.REVERSE}${at}${ESC.RESET}${after}`;
1977
+ }
1978
+ // Store cursor column for terminal positioning
1488
1979
  this.cursorVisibleColumn = cursorCol + 1;
1489
1980
  return result.join('\n');
1490
1981
  }
@@ -1653,8 +2144,12 @@ export class UnifiedUIRenderer extends EventEmitter {
1653
2144
  if (!this.spinnerInterval) {
1654
2145
  this.spinnerInterval = setInterval(() => {
1655
2146
  this.spinnerFrame++;
2147
+ // Cycle activity phrase every ~4 seconds (50 frames at 80ms)
2148
+ if (this.spinnerFrame % 50 === 0) {
2149
+ this.activityPhraseIndex++;
2150
+ }
1656
2151
  this.renderPrompt();
1657
- }, 120);
2152
+ }, 80);
1658
2153
  }
1659
2154
  this.renderPrompt();
1660
2155
  }
@@ -1708,34 +2203,38 @@ export class UnifiedUIRenderer extends EventEmitter {
1708
2203
  this.lastPromptEvent = { text: normalized, at: now };
1709
2204
  this.addEvent('prompt', normalized);
1710
2205
  }
1711
- clearPromptArea(insertNewline = false) {
1712
- if (!this.isPromptActive || this.promptHeight <= 0) {
1713
- if (insertNewline && !this.lastOutputEndedWithNewline) {
1714
- this.write('\n');
1715
- this.lastOutputEndedWithNewline = true;
1716
- }
2206
+ clearPromptArea() {
2207
+ const height = this.lastOverlay?.lines.length ?? this.promptHeight ?? 0;
2208
+ if (height === 0)
1717
2209
  return;
2210
+ // Cursor is at prompt row. Move up to top of overlay first.
2211
+ if (this.lastOverlay) {
2212
+ const linesToTop = this.lastOverlay.promptIndex;
2213
+ if (linesToTop > 0) {
2214
+ this.write(`\x1b[${linesToTop}A`);
2215
+ }
1718
2216
  }
1719
- if (this.promptHeight > 1) {
1720
- readline.moveCursor(this.output, 0, -(this.promptHeight - 1));
1721
- }
1722
- for (let i = 0; i < this.promptHeight; i++) {
1723
- readline.cursorTo(this.output, 0);
1724
- readline.clearLine(this.output, 0);
1725
- if (i < this.promptHeight - 1) {
1726
- readline.moveCursor(this.output, 0, 1);
2217
+ // Now at top, clear each line downward
2218
+ for (let i = 0; i < height; i++) {
2219
+ this.write('\r');
2220
+ this.write(ESC.CLEAR_LINE);
2221
+ if (i < height - 1) {
2222
+ this.write('\x1b[B');
1727
2223
  }
1728
2224
  }
1729
- readline.cursorTo(this.output, 0);
1730
- if (insertNewline) {
1731
- this.write('\n');
1732
- this.lastOutputEndedWithNewline = true;
2225
+ // Move back to top (where content should continue from)
2226
+ if (height > 1) {
2227
+ this.write(`\x1b[${height - 1}A`);
1733
2228
  }
2229
+ this.write('\r');
2230
+ this.lastOverlay = null;
1734
2231
  this.promptHeight = 0;
2232
+ this.lastOverlayHeight = 0;
1735
2233
  this.isPromptActive = false;
1736
2234
  }
1737
2235
  updateTerminalSize() {
1738
2236
  if (this.output.isTTY) {
2237
+ this.rows = this.output.rows || 24;
1739
2238
  this.cols = this.output.columns || 80;
1740
2239
  }
1741
2240
  }