erosolar-cli 2.1.173 → 2.1.174
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/headless/evalMode.d.ts.map +1 -1
- package/dist/headless/evalMode.js +0 -6
- package/dist/headless/evalMode.js.map +1 -1
- package/dist/headless/headlessApp.d.ts.map +1 -1
- package/dist/headless/headlessApp.js +0 -6
- package/dist/headless/headlessApp.js.map +1 -1
- package/dist/mcp/sseClient.d.ts +1 -4
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +2 -36
- package/dist/mcp/sseClient.js.map +1 -1
- package/dist/mcp/stdioClient.d.ts +1 -4
- package/dist/mcp/stdioClient.d.ts.map +1 -1
- package/dist/mcp/stdioClient.js +1 -41
- package/dist/mcp/stdioClient.js.map +1 -1
- package/dist/mcp/toolBridge.d.ts +0 -3
- package/dist/mcp/toolBridge.d.ts.map +1 -1
- package/dist/mcp/toolBridge.js +2 -2
- package/dist/mcp/toolBridge.js.map +1 -1
- package/dist/mcp/types.d.ts +0 -18
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/shell/interactiveShell.d.ts +0 -14
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +1 -73
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/shellApp.d.ts.map +1 -1
- package/dist/shell/shellApp.js +8 -9
- package/dist/shell/shellApp.js.map +1 -1
- package/dist/ui/PromptController.d.ts +0 -6
- package/dist/ui/PromptController.d.ts.map +1 -1
- package/dist/ui/PromptController.js +0 -3
- package/dist/ui/PromptController.js.map +1 -1
- package/dist/ui/ShellUIAdapter.d.ts +6 -0
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +40 -12
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/UnifiedUIController.d.ts +2 -1
- package/dist/ui/UnifiedUIController.d.ts.map +1 -1
- package/dist/ui/UnifiedUIController.js +1 -0
- package/dist/ui/UnifiedUIController.js.map +1 -1
- package/dist/ui/UnifiedUIRenderer.d.ts +53 -6
- package/dist/ui/UnifiedUIRenderer.d.ts.map +1 -1
- package/dist/ui/UnifiedUIRenderer.js +571 -72
- package/dist/ui/UnifiedUIRenderer.js.map +1 -1
- package/dist/ui/display.d.ts +4 -2
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +7 -8
- 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 +2 -0
- package/dist/ui/unified/index.d.ts.map +1 -1
- package/dist/ui/unified/index.js +4 -0
- package/dist/ui/unified/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/codex/capabilities/codexCoreCapability.d.ts +0 -6
- package/dist/codex/capabilities/codexCoreCapability.d.ts.map +0 -1
- package/dist/codex/capabilities/codexCoreCapability.js +0 -516
- package/dist/codex/capabilities/codexCoreCapability.js.map +0 -1
- package/dist/codex/fs.d.ts +0 -4
- package/dist/codex/fs.d.ts.map +0 -1
- package/dist/codex/fs.js +0 -25
- package/dist/codex/fs.js.map +0 -1
- package/dist/codex/persistence/planStore.d.ts +0 -4
- package/dist/codex/persistence/planStore.d.ts.map +0 -1
- package/dist/codex/persistence/planStore.js +0 -59
- package/dist/codex/persistence/planStore.js.map +0 -1
- package/dist/codex/pluginAllowlist.d.ts +0 -4
- package/dist/codex/pluginAllowlist.d.ts.map +0 -1
- package/dist/codex/pluginAllowlist.js +0 -14
- package/dist/codex/pluginAllowlist.js.map +0 -1
- package/dist/codex/types.d.ts +0 -21
- package/dist/codex/types.d.ts.map +0 -1
- package/dist/codex/types.js +0 -62
- 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
|
-
//
|
|
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.
|
|
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.
|
|
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.
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
753
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
},
|
|
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.
|
|
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 (
|
|
1451
|
+
if (JSON.stringify(normalized) === JSON.stringify(this.inlinePanel)) {
|
|
1312
1452
|
return;
|
|
1313
1453
|
}
|
|
1314
|
-
this.
|
|
1454
|
+
this.inlinePanel = normalized;
|
|
1455
|
+
this.renderPrompt();
|
|
1315
1456
|
}
|
|
1316
1457
|
clearInlinePanel() {
|
|
1317
|
-
|
|
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
|
|
1327
|
-
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
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.
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
-
|
|
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(
|
|
1344
|
-
if (i <
|
|
1532
|
+
this.write(renderedLines[i] || '');
|
|
1533
|
+
if (i < renderedLines.length - 1) {
|
|
1345
1534
|
this.write('\n');
|
|
1346
1535
|
}
|
|
1347
1536
|
}
|
|
1348
|
-
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
},
|
|
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(
|
|
1712
|
-
|
|
1713
|
-
|
|
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
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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
|
-
|
|
1730
|
-
if (
|
|
1731
|
-
this.write(
|
|
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
|
}
|