erosolar-cli 2.1.171 → 2.1.173
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/capabilities/askUserCapability.js +1 -1
- package/dist/capabilities/askUserCapability.js.map +1 -1
- package/dist/codex/types.js +1 -1
- package/dist/codex/types.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +5 -0
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +18 -2
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/shellApp.d.ts.map +1 -1
- package/dist/shell/shellApp.js +0 -1
- package/dist/shell/shellApp.js.map +1 -1
- package/dist/ui/PromptController.d.ts +3 -0
- package/dist/ui/PromptController.d.ts.map +1 -1
- package/dist/ui/PromptController.js +3 -0
- package/dist/ui/PromptController.js.map +1 -1
- package/dist/ui/ShellUIAdapter.d.ts +0 -6
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +12 -40
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/UnifiedUIController.d.ts +1 -2
- package/dist/ui/UnifiedUIController.d.ts.map +1 -1
- package/dist/ui/UnifiedUIController.js +0 -1
- package/dist/ui/UnifiedUIController.js.map +1 -1
- package/dist/ui/UnifiedUIRenderer.d.ts +3 -53
- package/dist/ui/UnifiedUIRenderer.d.ts.map +1 -1
- package/dist/ui/UnifiedUIRenderer.js +72 -587
- package/dist/ui/UnifiedUIRenderer.js.map +1 -1
- package/dist/ui/display.d.ts +2 -4
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +8 -7
- package/dist/ui/display.js.map +1 -1
- package/dist/ui/orchestration/StatusOrchestrator.d.ts +1 -1
- package/dist/ui/orchestration/StatusOrchestrator.js +1 -1
- package/dist/ui/orchestration/UIUpdateCoordinator.d.ts +2 -2
- package/dist/ui/orchestration/UIUpdateCoordinator.d.ts.map +1 -1
- package/dist/ui/orchestration/UIUpdateCoordinator.js +1 -1
- package/dist/ui/orchestration/UIUpdateCoordinator.js.map +1 -1
- package/dist/ui/unified/index.d.ts +0 -2
- package/dist/ui/unified/index.d.ts.map +1 -1
- package/dist/ui/unified/index.js +0 -4
- package/dist/ui/unified/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -10,28 +10,15 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import * as readline from 'node:readline';
|
|
12
12
|
import { EventEmitter } from 'node:events';
|
|
13
|
-
import { homedir } from 'node:os';
|
|
14
13
|
import { theme, spinnerFrames } from './theme.js';
|
|
15
14
|
import { isPlainOutputMode } from './outputMode.js';
|
|
16
|
-
import { ContextMeter, disposeAnimations } from './animatedStatus.js';
|
|
17
15
|
const ESC = {
|
|
18
|
-
HIDE_CURSOR: '\x1b[?25l',
|
|
19
16
|
SHOW_CURSOR: '\x1b[?25h',
|
|
20
|
-
CLEAR_SCREEN: '\x1b[2J',
|
|
21
17
|
CLEAR_LINE: '\x1b[2K',
|
|
22
|
-
HOME: '\x1b[H',
|
|
23
18
|
ENABLE_BRACKETED_PASTE: '\x1b[?2004h',
|
|
24
19
|
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',
|
|
28
20
|
REVERSE: '\x1b[7m',
|
|
29
21
|
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',
|
|
35
22
|
};
|
|
36
23
|
const COLLAPSE_HINT = theme.ui.muted('…'); // subtle ellipsis, no key hint
|
|
37
24
|
const NEWLINE_PLACEHOLDER = '↵';
|
|
@@ -41,9 +28,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
41
28
|
rl;
|
|
42
29
|
plainMode;
|
|
43
30
|
interactive;
|
|
44
|
-
rows = 24;
|
|
45
31
|
cols = 80;
|
|
46
|
-
lastRenderWidth = null;
|
|
47
32
|
eventQueue = [];
|
|
48
33
|
isProcessingQueue = false;
|
|
49
34
|
buffer = '';
|
|
@@ -53,7 +38,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
53
38
|
suggestions = [];
|
|
54
39
|
suggestionIndex = -1;
|
|
55
40
|
availableCommands = [];
|
|
56
|
-
hotkeysInToggleLine = new Set();
|
|
57
41
|
collapsedPaste = null;
|
|
58
42
|
mode = 'idle';
|
|
59
43
|
streamingStartTime = null;
|
|
@@ -61,52 +45,26 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
61
45
|
statusOverride = null;
|
|
62
46
|
statusStreaming = null;
|
|
63
47
|
// Animated UI components
|
|
64
|
-
streamingSpinner = null;
|
|
65
|
-
thinkingIndicator = null;
|
|
66
|
-
contextMeter;
|
|
67
48
|
spinnerFrame = 0;
|
|
68
49
|
spinnerInterval = null;
|
|
69
|
-
//
|
|
70
|
-
compactingStatusMessage = '';
|
|
71
|
-
compactingStatusFrame = 0;
|
|
72
|
-
compactingStatusInterval = null;
|
|
73
|
-
compactingSpinnerFrames = ['✻', '✼', '✻', '✺'];
|
|
74
|
-
// Animated activity line (e.g., "✳ Ruminating… (esc to interrupt · 34s · ↑1.2k)")
|
|
50
|
+
// Activity/status tracking
|
|
75
51
|
activityMessage = null;
|
|
76
|
-
activityPhraseIndex = 0;
|
|
77
|
-
activityStarFrame = 0;
|
|
78
|
-
activityStarFrames = ['✳', '✴', '✵', '✶', '✷', '✸'];
|
|
79
|
-
// Token count during streaming
|
|
80
52
|
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
|
-
];
|
|
88
53
|
statusMeta = {};
|
|
89
54
|
toggleState = {
|
|
90
55
|
verificationEnabled: false,
|
|
91
56
|
criticalApprovalMode: 'auto',
|
|
92
57
|
};
|
|
93
58
|
// ------------ Helpers ------------
|
|
94
|
-
formatHotkey(combo) {
|
|
95
|
-
if (!combo?.trim())
|
|
96
|
-
return null;
|
|
97
|
-
return combo.trim().toUpperCase();
|
|
98
|
-
}
|
|
99
59
|
lastPromptEvent = null;
|
|
100
60
|
promptHeight = 0;
|
|
101
|
-
lastOverlayHeight = 0;
|
|
102
|
-
inlinePanel = [];
|
|
103
|
-
hasConversationContent = false;
|
|
104
61
|
isPromptActive = false;
|
|
105
62
|
inputRenderOffset = 0;
|
|
106
63
|
plainPasteIdleMs = 24;
|
|
107
64
|
plainPasteWindowMs = 60;
|
|
108
65
|
plainPasteTriggerChars = 24;
|
|
109
66
|
cursorVisibleColumn = 1;
|
|
67
|
+
cursorVisibleRow = 0;
|
|
110
68
|
inBracketedPaste = false;
|
|
111
69
|
pasteBuffer = '';
|
|
112
70
|
inPlainPaste = false;
|
|
@@ -117,10 +75,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
117
75
|
plainRecentChunks = [];
|
|
118
76
|
lastRenderedEventKey = null;
|
|
119
77
|
lastOutputEndedWithNewline = true;
|
|
120
|
-
hasRenderedPrompt = false;
|
|
121
|
-
hasEverRenderedOverlay = false; // Track if we've ever rendered for inline clearing
|
|
122
|
-
lastOverlay = null;
|
|
123
|
-
allowPromptRender = true;
|
|
124
78
|
inputCapture = null;
|
|
125
79
|
constructor(output = process.stdout, input = process.stdin, options) {
|
|
126
80
|
super();
|
|
@@ -128,8 +82,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
128
82
|
this.input = input;
|
|
129
83
|
this.interactive = Boolean(this.output.isTTY && this.input.isTTY && !process.env['CI']);
|
|
130
84
|
this.plainMode = isPlainOutputMode() || !this.interactive;
|
|
131
|
-
// Initialize animated components
|
|
132
|
-
this.contextMeter = new ContextMeter();
|
|
133
85
|
this.rl = readline.createInterface({
|
|
134
86
|
input: this.input,
|
|
135
87
|
output: this.output,
|
|
@@ -140,7 +92,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
140
92
|
this.rl.setPrompt('');
|
|
141
93
|
this.updateTerminalSize();
|
|
142
94
|
this.output.on('resize', () => {
|
|
143
|
-
if (
|
|
95
|
+
if (this.interactive) {
|
|
144
96
|
this.updateTerminalSize();
|
|
145
97
|
this.renderPrompt();
|
|
146
98
|
}
|
|
@@ -151,59 +103,30 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
151
103
|
if (!this.interactive) {
|
|
152
104
|
return;
|
|
153
105
|
}
|
|
154
|
-
|
|
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
|
|
106
|
+
this.write(ESC.ENABLE_BRACKETED_PASTE);
|
|
168
107
|
this.updateTerminalSize();
|
|
169
|
-
this.hasRenderedPrompt = false;
|
|
170
108
|
this.lastOutputEndedWithNewline = true;
|
|
109
|
+
this.write(ESC.SHOW_CURSOR);
|
|
171
110
|
this.renderPrompt();
|
|
172
111
|
}
|
|
173
112
|
cleanup() {
|
|
174
113
|
this.cancelInputCapture(new Error('Renderer disposed'));
|
|
175
114
|
this.cancelPlainPasteCapture();
|
|
176
|
-
// Stop any running animations
|
|
177
115
|
if (this.spinnerInterval) {
|
|
178
116
|
clearInterval(this.spinnerInterval);
|
|
179
117
|
this.spinnerInterval = null;
|
|
180
118
|
}
|
|
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();
|
|
191
119
|
if (!this.interactive) {
|
|
192
120
|
this.rl.close();
|
|
193
121
|
return;
|
|
194
122
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
this.write(ESC.DISABLE_BRACKETED_PASTE);
|
|
199
|
-
this.write(ESC.SHOW_CURSOR);
|
|
200
|
-
this.write('\n');
|
|
201
|
-
}
|
|
123
|
+
this.write(ESC.DISABLE_BRACKETED_PASTE);
|
|
124
|
+
this.write(ESC.SHOW_CURSOR);
|
|
125
|
+
this.write('\n');
|
|
202
126
|
if (this.input.isTTY) {
|
|
203
127
|
this.input.setRawMode(false);
|
|
204
128
|
}
|
|
205
129
|
this.rl.close();
|
|
206
|
-
this.lastOverlay = null;
|
|
207
130
|
}
|
|
208
131
|
// ------------ Input handling ------------
|
|
209
132
|
setupInputHandlers() {
|
|
@@ -244,6 +167,10 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
244
167
|
this.emit('toggle-critical-approval');
|
|
245
168
|
return;
|
|
246
169
|
}
|
|
170
|
+
if (key.ctrl && key.shift && key.name?.toLowerCase() === 'n') {
|
|
171
|
+
this.emit('toggle-network');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
247
174
|
if (key.ctrl && key.name === 'c') {
|
|
248
175
|
// Three-stage Ctrl+C behavior:
|
|
249
176
|
// 1. Clear chat box if it has text
|
|
@@ -722,16 +649,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
722
649
|
const normalized = this.normalizeEventType(type);
|
|
723
650
|
if (!normalized)
|
|
724
651
|
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
|
-
}
|
|
735
652
|
if (this.plainMode) {
|
|
736
653
|
const formatted = this.formatContent({
|
|
737
654
|
type: normalized,
|
|
@@ -789,23 +706,11 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
789
706
|
const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown renderer error';
|
|
790
707
|
this.output.write(`\n[renderer] ${message}\n`);
|
|
791
708
|
}
|
|
792
|
-
|
|
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 {
|
|
709
|
+
if (event.type !== 'prompt') {
|
|
802
710
|
await this.delay(1);
|
|
803
711
|
}
|
|
804
712
|
}
|
|
805
|
-
|
|
806
|
-
// This ensures status/toggles stay pinned and responses are fully rendered
|
|
807
|
-
if (this.output.isTTY) {
|
|
808
|
-
this.allowPromptRender = true;
|
|
713
|
+
if (this.output.isTTY && this.interactive) {
|
|
809
714
|
this.renderPrompt();
|
|
810
715
|
}
|
|
811
716
|
}
|
|
@@ -820,28 +725,18 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
820
725
|
*/
|
|
821
726
|
async flushEvents(timeoutMs = 250) {
|
|
822
727
|
// Kick off processing if idle
|
|
823
|
-
if (!this.
|
|
728
|
+
if (!this.isProcessingQueue && this.eventQueue.length > 0) {
|
|
824
729
|
void this.processQueue();
|
|
825
730
|
}
|
|
826
731
|
const start = Date.now();
|
|
827
732
|
while ((this.isProcessingQueue || this.eventQueue.length > 0) && Date.now() - start < timeoutMs) {
|
|
828
733
|
await this.delay(5);
|
|
829
734
|
}
|
|
830
|
-
if (
|
|
831
|
-
this.allowPromptRender = true;
|
|
735
|
+
if (this.output.isTTY && this.interactive) {
|
|
832
736
|
this.renderPrompt();
|
|
833
737
|
}
|
|
834
738
|
}
|
|
835
739
|
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
|
-
}
|
|
845
740
|
const formatted = this.formatContent(event);
|
|
846
741
|
if (!formatted)
|
|
847
742
|
return;
|
|
@@ -854,11 +749,9 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
854
749
|
if (event.type !== 'prompt') {
|
|
855
750
|
this.lastRenderedEventKey = signature;
|
|
856
751
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
this.clearPromptArea();
|
|
752
|
+
if (this.isPromptActive) {
|
|
753
|
+
this.clearPromptArea(true);
|
|
860
754
|
}
|
|
861
|
-
this.isPromptActive = false;
|
|
862
755
|
if (event.type !== 'stream' && !this.lastOutputEndedWithNewline && formatted.trim()) {
|
|
863
756
|
// Keep scrollback ordering predictable when previous output ended mid-line
|
|
864
757
|
this.output.write('\n');
|
|
@@ -866,6 +759,9 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
866
759
|
}
|
|
867
760
|
this.output.write(formatted);
|
|
868
761
|
this.lastOutputEndedWithNewline = formatted.endsWith('\n');
|
|
762
|
+
if (this.interactive && !this.plainMode) {
|
|
763
|
+
this.renderPrompt();
|
|
764
|
+
}
|
|
869
765
|
}
|
|
870
766
|
normalizeEventType(type) {
|
|
871
767
|
switch (type) {
|
|
@@ -1263,22 +1159,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1263
1159
|
}
|
|
1264
1160
|
return result.join('\n') + '\n';
|
|
1265
1161
|
}
|
|
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
|
-
}
|
|
1282
1162
|
/**
|
|
1283
1163
|
* Format a compact conversation block (Claude Code style)
|
|
1284
1164
|
* Shows a visual separator with "history" label and ctrl+o hint
|
|
@@ -1329,8 +1209,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1329
1209
|
this.write('\n');
|
|
1330
1210
|
this.lastOutputEndedWithNewline = true;
|
|
1331
1211
|
}
|
|
1332
|
-
if (
|
|
1333
|
-
// Always render prompt to keep bottom UI persistent (rich mode only)
|
|
1212
|
+
if (this.interactive) {
|
|
1334
1213
|
this.renderPrompt();
|
|
1335
1214
|
}
|
|
1336
1215
|
}
|
|
@@ -1341,15 +1220,12 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1341
1220
|
if (this.spinnerInterval)
|
|
1342
1221
|
return; // Already running
|
|
1343
1222
|
this.spinnerFrame = 0;
|
|
1344
|
-
this.activityStarFrame = 0;
|
|
1345
1223
|
this.spinnerInterval = setInterval(() => {
|
|
1346
1224
|
this.spinnerFrame = (this.spinnerFrame + 1) % spinnerFrames.braille.length;
|
|
1347
|
-
|
|
1348
|
-
// Re-render to show updated spinner/star frame
|
|
1349
|
-
if (!this.plainMode && this.mode === 'streaming') {
|
|
1225
|
+
if (this.mode === 'streaming') {
|
|
1350
1226
|
this.renderPrompt();
|
|
1351
1227
|
}
|
|
1352
|
-
},
|
|
1228
|
+
}, 120);
|
|
1353
1229
|
}
|
|
1354
1230
|
/**
|
|
1355
1231
|
* Stop the animated spinner
|
|
@@ -1360,7 +1236,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1360
1236
|
this.spinnerInterval = null;
|
|
1361
1237
|
}
|
|
1362
1238
|
this.spinnerFrame = 0;
|
|
1363
|
-
this.activityStarFrame = 0;
|
|
1364
1239
|
this.activityMessage = null;
|
|
1365
1240
|
}
|
|
1366
1241
|
/**
|
|
@@ -1369,7 +1244,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1369
1244
|
*/
|
|
1370
1245
|
setActivity(message) {
|
|
1371
1246
|
this.activityMessage = message;
|
|
1372
|
-
if (
|
|
1247
|
+
if (this.interactive) {
|
|
1373
1248
|
this.renderPrompt();
|
|
1374
1249
|
}
|
|
1375
1250
|
}
|
|
@@ -1379,16 +1254,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1379
1254
|
updateStreamingTokens(tokens) {
|
|
1380
1255
|
this.streamingTokens = tokens;
|
|
1381
1256
|
}
|
|
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
|
-
}
|
|
1392
1257
|
getMode() {
|
|
1393
1258
|
return this.mode;
|
|
1394
1259
|
}
|
|
@@ -1437,28 +1302,19 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1437
1302
|
}
|
|
1438
1303
|
updateModeToggles(state) {
|
|
1439
1304
|
this.toggleState = { ...this.toggleState, ...state };
|
|
1440
|
-
if (!state.verificationHotkey &&
|
|
1441
|
-
!state.thinkingHotkey &&
|
|
1442
|
-
!state.criticalApprovalHotkey) {
|
|
1443
|
-
this.hotkeysInToggleLine.clear();
|
|
1444
|
-
}
|
|
1445
1305
|
this.renderPrompt();
|
|
1446
1306
|
}
|
|
1447
1307
|
setInlinePanel(lines) {
|
|
1448
1308
|
const normalized = (lines ?? [])
|
|
1449
1309
|
.map(line => line.replace(/\s+$/g, ''))
|
|
1450
1310
|
.filter(line => line.trim().length > 0);
|
|
1451
|
-
if (
|
|
1311
|
+
if (!normalized.length) {
|
|
1452
1312
|
return;
|
|
1453
1313
|
}
|
|
1454
|
-
this.
|
|
1455
|
-
this.renderPrompt();
|
|
1314
|
+
this.addEvent('response', `${normalized.join('\n')}\n`);
|
|
1456
1315
|
}
|
|
1457
1316
|
clearInlinePanel() {
|
|
1458
|
-
|
|
1459
|
-
return;
|
|
1460
|
-
this.inlinePanel = [];
|
|
1461
|
-
this.renderPrompt();
|
|
1317
|
+
// No-op: inline panels render directly into scrollback
|
|
1462
1318
|
}
|
|
1463
1319
|
// ------------ Prompt rendering ------------
|
|
1464
1320
|
renderPrompt() {
|
|
@@ -1466,300 +1322,42 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1466
1322
|
this.isPromptActive = false;
|
|
1467
1323
|
return;
|
|
1468
1324
|
}
|
|
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
|
|
1487
1325
|
this.updateTerminalSize();
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1490
|
-
const
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
const renderedLines = overlay.lines.map(line => this.truncateLine(line, maxWidth));
|
|
1495
|
-
if (!renderedLines.length) {
|
|
1496
|
-
return;
|
|
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));
|
|
1497
1332
|
}
|
|
1498
|
-
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
if (
|
|
1502
|
-
|
|
1503
|
-
|
|
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
|
-
}
|
|
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;
|
|
1527
1339
|
}
|
|
1528
|
-
|
|
1529
|
-
for (let i = 0; i < renderedLines.length; i++) {
|
|
1340
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1530
1341
|
this.write('\r');
|
|
1531
1342
|
this.write(ESC.CLEAR_LINE);
|
|
1532
|
-
this.write(
|
|
1533
|
-
if (i <
|
|
1343
|
+
this.write(lines[i] || '');
|
|
1344
|
+
if (i < lines.length - 1) {
|
|
1534
1345
|
this.write('\n');
|
|
1535
1346
|
}
|
|
1536
1347
|
}
|
|
1537
|
-
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
}
|
|
1544
|
-
this.write(`\x1b[${promptCol}G`);
|
|
1545
|
-
this.cursorVisibleColumn = promptCol;
|
|
1546
|
-
this.hasRenderedPrompt = true;
|
|
1547
|
-
this.hasEverRenderedOverlay = true;
|
|
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`);
|
|
1548
1355
|
this.isPromptActive = true;
|
|
1549
|
-
this.
|
|
1550
|
-
this.lastOverlay = { lines: renderedLines, promptIndex };
|
|
1356
|
+
this.promptHeight = lines.length;
|
|
1551
1357
|
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.sandbox) {
|
|
1729
|
-
const tone = this.statusMeta.sandbox.includes('danger')
|
|
1730
|
-
? 'error'
|
|
1731
|
-
: this.statusMeta.sandbox.includes('read')
|
|
1732
|
-
? 'warn'
|
|
1733
|
-
: 'muted';
|
|
1734
|
-
segments.push(this.formatMetaSegment('sandbox', this.statusMeta.sandbox, tone));
|
|
1735
|
-
}
|
|
1736
|
-
if (this.statusMeta.network) {
|
|
1737
|
-
const tone = this.statusMeta.network === 'restricted' ? 'warn' : 'info';
|
|
1738
|
-
segments.push(this.formatMetaSegment('network', this.statusMeta.network, tone));
|
|
1739
|
-
}
|
|
1740
|
-
if (this.statusMeta.approvals) {
|
|
1741
|
-
const tone = this.statusMeta.approvals === 'auto' ? 'muted' : 'warn';
|
|
1742
|
-
segments.push(this.formatMetaSegment('approvals', this.statusMeta.approvals, tone));
|
|
1743
|
-
}
|
|
1744
|
-
if (this.statusMeta.writes) {
|
|
1745
|
-
segments.push(this.formatMetaSegment('writes', this.statusMeta.writes, 'muted'));
|
|
1746
|
-
}
|
|
1747
|
-
if (this.statusMeta.toolSummary) {
|
|
1748
|
-
segments.push(this.formatMetaSegment('tools', this.statusMeta.toolSummary, 'muted'));
|
|
1749
|
-
}
|
|
1750
|
-
if (this.statusMeta.sessionLabel) {
|
|
1751
|
-
segments.push(this.formatMetaSegment('session', this.statusMeta.sessionLabel, 'muted'));
|
|
1752
|
-
}
|
|
1753
|
-
if (this.statusMeta.version) {
|
|
1754
|
-
segments.push(this.formatMetaSegment('build', `v${this.statusMeta.version}`, 'muted'));
|
|
1755
|
-
}
|
|
1756
|
-
if (segments.length === 0) {
|
|
1757
|
-
return [];
|
|
1758
|
-
}
|
|
1759
|
-
return this.wrapSegments(segments, maxWidth);
|
|
1760
1358
|
}
|
|
1761
1359
|
composeStatusLabel() {
|
|
1762
|
-
const statuses = [this.statusStreaming, this.statusOverride, this.statusMessage].filter((value, index, arr) => Boolean(value) && arr.indexOf(value) === index);
|
|
1360
|
+
const statuses = [this.activityMessage, this.statusStreaming, this.statusOverride, this.statusMessage].filter((value, index, arr) => Boolean(value) && arr.indexOf(value) === index);
|
|
1763
1361
|
const text = statuses.length > 0 ? statuses.join(' / ') : 'Ready for prompts';
|
|
1764
1362
|
if (!text.trim()) {
|
|
1765
1363
|
return null;
|
|
@@ -1819,97 +1417,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1819
1417
|
}
|
|
1820
1418
|
return lines;
|
|
1821
1419
|
}
|
|
1822
|
-
buildControlLines() {
|
|
1823
|
-
const lines = [];
|
|
1824
|
-
const toggleLine = this.buildToggleLine();
|
|
1825
|
-
if (toggleLine) {
|
|
1826
|
-
lines.push(`${theme.ui.muted('modes')} ${theme.ui.muted('›')} ${toggleLine}`);
|
|
1827
|
-
}
|
|
1828
|
-
const shortcutLine = this.buildShortcutLine();
|
|
1829
|
-
if (shortcutLine) {
|
|
1830
|
-
lines.push(`${theme.ui.muted('keys')} ${shortcutLine}`);
|
|
1831
|
-
}
|
|
1832
|
-
return lines;
|
|
1833
|
-
}
|
|
1834
|
-
/**
|
|
1835
|
-
* Build a compact toggle line like Claude Code:
|
|
1836
|
-
* "⏵⏵ accept edits on (shift+tab to cycle)"
|
|
1837
|
-
*/
|
|
1838
|
-
buildCompactToggleLine() {
|
|
1839
|
-
// Show the most relevant mode based on current state
|
|
1840
|
-
const parts = [];
|
|
1841
|
-
// Edit mode indicator
|
|
1842
|
-
const editIcon = '⏵⏵';
|
|
1843
|
-
const editState = this.toggleState.verificationEnabled ? 'approval required' : 'accept edits';
|
|
1844
|
-
parts.push(`${theme.ui.muted(editIcon)} ${editState} ${theme.success('on')}`);
|
|
1845
|
-
// Thinking mode (if active)
|
|
1846
|
-
const thinkingLabel = (this.toggleState.thinkingModeLabel || '').trim().toLowerCase();
|
|
1847
|
-
if (thinkingLabel && thinkingLabel !== 'off') {
|
|
1848
|
-
parts.push(`${theme.ui.muted('thinking')} ${theme.info(thinkingLabel)}`);
|
|
1849
|
-
}
|
|
1850
|
-
// Cycle hint
|
|
1851
|
-
const cycleHint = theme.ui.muted('(shift+tab to cycle)');
|
|
1852
|
-
if (parts.length === 0) {
|
|
1853
|
-
return null;
|
|
1854
|
-
}
|
|
1855
|
-
return ` ${parts.join(theme.ui.muted(' · '))} ${cycleHint}`;
|
|
1856
|
-
}
|
|
1857
|
-
buildToggleLine() {
|
|
1858
|
-
const toggles = [];
|
|
1859
|
-
const addToggle = (label, on, hotkey, value) => {
|
|
1860
|
-
toggles.push({ label, on, hotkey: this.formatHotkey(hotkey), value });
|
|
1861
|
-
};
|
|
1862
|
-
addToggle('Verify', this.toggleState.verificationEnabled, this.toggleState.verificationHotkey);
|
|
1863
|
-
const approvalMode = this.toggleState.criticalApprovalMode || 'auto';
|
|
1864
|
-
const approvalActive = approvalMode !== 'auto';
|
|
1865
|
-
addToggle('Approvals', approvalActive, this.toggleState.criticalApprovalHotkey, approvalMode === 'auto' ? 'auto' : 'ask');
|
|
1866
|
-
const thinkingLabel = (this.toggleState.thinkingModeLabel || 'off').trim();
|
|
1867
|
-
const thinkingActive = thinkingLabel.toLowerCase() !== 'off';
|
|
1868
|
-
addToggle('Thinking', thinkingActive, this.toggleState.thinkingHotkey, thinkingLabel);
|
|
1869
|
-
const buildLine = (includeHotkeys) => {
|
|
1870
|
-
return toggles
|
|
1871
|
-
.map(toggle => {
|
|
1872
|
-
const stateText = toggle.on ? theme.success(toggle.value || 'on') : theme.ui.muted(toggle.value || 'off');
|
|
1873
|
-
const hotkeyText = includeHotkeys && toggle.hotkey ? theme.ui.muted(` [${toggle.hotkey}]`) : '';
|
|
1874
|
-
return `${theme.ui.muted(`${toggle.label}:`)} ${stateText}${hotkeyText}`;
|
|
1875
|
-
})
|
|
1876
|
-
.join(theme.ui.muted(' '));
|
|
1877
|
-
};
|
|
1878
|
-
const maxWidth = this.safeWidth();
|
|
1879
|
-
let line = buildLine(true);
|
|
1880
|
-
// Record which hotkeys are actually shown so the shortcut line can avoid duplicates
|
|
1881
|
-
this.hotkeysInToggleLine = new Set(toggles
|
|
1882
|
-
.map(toggle => (toggle.hotkey ? toggle.hotkey : null))
|
|
1883
|
-
.filter((key) => Boolean(key)));
|
|
1884
|
-
// If the line is too wide, drop hotkey hints to preserve all toggle labels
|
|
1885
|
-
if (this.visibleLength(line) > maxWidth) {
|
|
1886
|
-
this.hotkeysInToggleLine.clear();
|
|
1887
|
-
line = buildLine(false);
|
|
1888
|
-
}
|
|
1889
|
-
return line.trim() ? line : null;
|
|
1890
|
-
}
|
|
1891
|
-
buildShortcutLine() {
|
|
1892
|
-
const parts = [];
|
|
1893
|
-
const addHotkey = (label, combo) => {
|
|
1894
|
-
const normalized = this.formatHotkey(combo);
|
|
1895
|
-
if (!normalized)
|
|
1896
|
-
return;
|
|
1897
|
-
if (this.hotkeysInToggleLine.has(normalized)) {
|
|
1898
|
-
return;
|
|
1899
|
-
}
|
|
1900
|
-
parts.push(`${theme.info(normalized)} ${theme.ui.muted(label)}`);
|
|
1901
|
-
};
|
|
1902
|
-
// Core controls
|
|
1903
|
-
addHotkey('interrupt', 'Ctrl+C');
|
|
1904
|
-
addHotkey('clear input', 'Ctrl+U');
|
|
1905
|
-
// Feature toggles (only if hotkeys are defined)
|
|
1906
|
-
addHotkey('verify', this.toggleState.verificationHotkey);
|
|
1907
|
-
addHotkey('thinking', this.toggleState.thinkingHotkey);
|
|
1908
|
-
if (parts.length === 0) {
|
|
1909
|
-
return null;
|
|
1910
|
-
}
|
|
1911
|
-
return parts.join(theme.ui.muted(' '));
|
|
1912
|
-
}
|
|
1913
1420
|
buildInputLine() {
|
|
1914
1421
|
if (this.collapsedPaste) {
|
|
1915
1422
|
const summary = `[pasted ${this.collapsedPaste.lines} lines, ${this.collapsedPaste.chars} chars] (Ctrl+L insert, Backspace discard)`;
|
|
@@ -1977,21 +1484,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1977
1484
|
const lastLine = result[cursorLine] ?? '';
|
|
1978
1485
|
cursorCol = this.visibleLength(lastLine);
|
|
1979
1486
|
}
|
|
1980
|
-
|
|
1981
|
-
if (result.length > 0) {
|
|
1982
|
-
const targetLine = result[cursorLine] ?? '';
|
|
1983
|
-
const visiblePart = this.stripAnsi(targetLine);
|
|
1984
|
-
const cursorPos = Math.min(cursorCol, visiblePart.length);
|
|
1985
|
-
// Rebuild the line with cursor highlight
|
|
1986
|
-
const before = visiblePart.slice(0, cursorPos);
|
|
1987
|
-
const at = visiblePart.charAt(cursorPos) || ' ';
|
|
1988
|
-
const after = visiblePart.slice(cursorPos + 1);
|
|
1989
|
-
// Preserve the prompt/indent styling
|
|
1990
|
-
const prefix = cursorLine === 0 ? prompt : continuationIndent;
|
|
1991
|
-
const textPart = cursorLine === 0 ? before.slice(promptWidth) : before.slice(continuationWidth);
|
|
1992
|
-
result[cursorLine] = `${prefix}${textPart}${ESC.REVERSE}${at}${ESC.RESET}${after}`;
|
|
1993
|
-
}
|
|
1994
|
-
// Store cursor column for terminal positioning
|
|
1487
|
+
this.cursorVisibleRow = cursorLine;
|
|
1995
1488
|
this.cursorVisibleColumn = cursorCol + 1;
|
|
1996
1489
|
return result.join('\n');
|
|
1997
1490
|
}
|
|
@@ -2160,12 +1653,8 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
2160
1653
|
if (!this.spinnerInterval) {
|
|
2161
1654
|
this.spinnerInterval = setInterval(() => {
|
|
2162
1655
|
this.spinnerFrame++;
|
|
2163
|
-
// Cycle activity phrase every ~4 seconds (50 frames at 80ms)
|
|
2164
|
-
if (this.spinnerFrame % 50 === 0) {
|
|
2165
|
-
this.activityPhraseIndex++;
|
|
2166
|
-
}
|
|
2167
1656
|
this.renderPrompt();
|
|
2168
|
-
},
|
|
1657
|
+
}, 120);
|
|
2169
1658
|
}
|
|
2170
1659
|
this.renderPrompt();
|
|
2171
1660
|
}
|
|
@@ -2219,38 +1708,34 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
2219
1708
|
this.lastPromptEvent = { text: normalized, at: now };
|
|
2220
1709
|
this.addEvent('prompt', normalized);
|
|
2221
1710
|
}
|
|
2222
|
-
clearPromptArea() {
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
if (this.lastOverlay) {
|
|
2228
|
-
const linesToTop = this.lastOverlay.promptIndex;
|
|
2229
|
-
if (linesToTop > 0) {
|
|
2230
|
-
this.write(`\x1b[${linesToTop}A`);
|
|
1711
|
+
clearPromptArea(insertNewline = false) {
|
|
1712
|
+
if (!this.isPromptActive || this.promptHeight <= 0) {
|
|
1713
|
+
if (insertNewline && !this.lastOutputEndedWithNewline) {
|
|
1714
|
+
this.write('\n');
|
|
1715
|
+
this.lastOutputEndedWithNewline = true;
|
|
2231
1716
|
}
|
|
1717
|
+
return;
|
|
2232
1718
|
}
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
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);
|
|
2239
1727
|
}
|
|
2240
1728
|
}
|
|
2241
|
-
|
|
2242
|
-
if (
|
|
2243
|
-
this.write(
|
|
1729
|
+
readline.cursorTo(this.output, 0);
|
|
1730
|
+
if (insertNewline) {
|
|
1731
|
+
this.write('\n');
|
|
1732
|
+
this.lastOutputEndedWithNewline = true;
|
|
2244
1733
|
}
|
|
2245
|
-
this.write('\r');
|
|
2246
|
-
this.lastOverlay = null;
|
|
2247
1734
|
this.promptHeight = 0;
|
|
2248
|
-
this.lastOverlayHeight = 0;
|
|
2249
1735
|
this.isPromptActive = false;
|
|
2250
1736
|
}
|
|
2251
1737
|
updateTerminalSize() {
|
|
2252
1738
|
if (this.output.isTTY) {
|
|
2253
|
-
this.rows = this.output.rows || 24;
|
|
2254
1739
|
this.cols = this.output.columns || 80;
|
|
2255
1740
|
}
|
|
2256
1741
|
}
|