erosolar-cli 2.1.170 → 2.1.172
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/README.md +1 -1
- package/agents/erosolar-code.rules.json +2 -2
- package/agents/general.rules.json +3 -21
- package/dist/StringUtils.d.ts +8 -0
- package/dist/StringUtils.d.ts.map +1 -0
- package/dist/StringUtils.js +11 -0
- package/dist/StringUtils.js.map +1 -0
- package/dist/capabilities/statusCapability.js +2 -2
- package/dist/capabilities/statusCapability.js.map +1 -1
- package/dist/contracts/agent-schemas.json +5 -5
- package/dist/core/agent.d.ts +24 -83
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +248 -499
- package/dist/core/agent.js.map +1 -1
- package/dist/core/aiFlowSupervisor.d.ts +44 -0
- package/dist/core/aiFlowSupervisor.d.ts.map +1 -0
- package/dist/core/aiFlowSupervisor.js +299 -0
- package/dist/core/aiFlowSupervisor.js.map +1 -0
- package/dist/core/cliTestHarness.d.ts +200 -0
- package/dist/core/cliTestHarness.d.ts.map +1 -0
- package/dist/core/cliTestHarness.js +549 -0
- package/dist/core/cliTestHarness.js.map +1 -0
- package/dist/core/preferences.d.ts +0 -1
- package/dist/core/preferences.d.ts.map +1 -1
- package/dist/core/preferences.js +1 -8
- package/dist/core/preferences.js.map +1 -1
- package/dist/core/schemaValidator.js +3 -3
- package/dist/core/schemaValidator.js.map +1 -1
- package/dist/core/testUtils.d.ts +121 -0
- package/dist/core/testUtils.d.ts.map +1 -0
- package/dist/core/testUtils.js +235 -0
- package/dist/core/testUtils.js.map +1 -0
- package/dist/core/toolPreconditions.d.ts +11 -0
- package/dist/core/toolPreconditions.d.ts.map +1 -1
- package/dist/core/toolPreconditions.js +164 -33
- package/dist/core/toolPreconditions.js.map +1 -1
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +114 -9
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/core/toolValidation.d.ts +116 -0
- package/dist/core/toolValidation.d.ts.map +1 -0
- package/dist/core/toolValidation.js +282 -0
- package/dist/core/toolValidation.js.map +1 -0
- package/dist/core/updateChecker.d.ts +1 -61
- package/dist/core/updateChecker.d.ts.map +1 -1
- package/dist/core/updateChecker.js +3 -147
- package/dist/core/updateChecker.js.map +1 -1
- package/dist/headless/headlessApp.d.ts.map +1 -1
- package/dist/headless/headlessApp.js +39 -0
- package/dist/headless/headlessApp.js.map +1 -1
- package/dist/plugins/tools/nodeDefaults.d.ts.map +1 -1
- package/dist/plugins/tools/nodeDefaults.js +2 -0
- package/dist/plugins/tools/nodeDefaults.js.map +1 -1
- package/dist/providers/openaiResponsesProvider.d.ts.map +1 -1
- package/dist/providers/openaiResponsesProvider.js +74 -79
- package/dist/providers/openaiResponsesProvider.js.map +1 -1
- package/dist/runtime/agentController.d.ts.map +1 -1
- package/dist/runtime/agentController.js +3 -6
- package/dist/runtime/agentController.js.map +1 -1
- package/dist/runtime/agentSession.d.ts +2 -0
- package/dist/runtime/agentSession.d.ts.map +1 -1
- package/dist/runtime/agentSession.js +2 -2
- package/dist/runtime/agentSession.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +18 -11
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +291 -273
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/shellApp.d.ts.map +1 -1
- package/dist/shell/shellApp.js +1 -7
- package/dist/shell/shellApp.js.map +1 -1
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +15 -4
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/subagents/taskRunner.js +1 -2
- package/dist/subagents/taskRunner.js.map +1 -1
- package/dist/tools/bashTools.d.ts.map +1 -1
- package/dist/tools/bashTools.js +8 -101
- package/dist/tools/bashTools.js.map +1 -1
- package/dist/tools/diffUtils.d.ts +2 -8
- package/dist/tools/diffUtils.d.ts.map +1 -1
- package/dist/tools/diffUtils.js +13 -72
- package/dist/tools/diffUtils.js.map +1 -1
- package/dist/tools/grepTools.d.ts.map +1 -1
- package/dist/tools/grepTools.js +2 -10
- package/dist/tools/grepTools.js.map +1 -1
- package/dist/tools/planningTools.d.ts +10 -0
- package/dist/tools/planningTools.d.ts.map +1 -1
- package/dist/tools/planningTools.js +16 -0
- package/dist/tools/planningTools.js.map +1 -1
- package/dist/tools/searchTools.d.ts.map +1 -1
- package/dist/tools/searchTools.js +2 -4
- package/dist/tools/searchTools.js.map +1 -1
- package/dist/ui/PromptController.d.ts +4 -1
- package/dist/ui/PromptController.d.ts.map +1 -1
- package/dist/ui/PromptController.js +7 -1
- package/dist/ui/PromptController.js.map +1 -1
- package/dist/ui/ShellUIAdapter.d.ts +28 -292
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +121 -1513
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/UnifiedUIRenderer.d.ts +30 -133
- package/dist/ui/UnifiedUIRenderer.d.ts.map +1 -1
- package/dist/ui/UnifiedUIRenderer.js +370 -939
- package/dist/ui/UnifiedUIRenderer.js.map +1 -1
- package/dist/ui/animatedStatus.d.ts +6 -128
- package/dist/ui/animatedStatus.d.ts.map +1 -1
- package/dist/ui/animatedStatus.js +50 -383
- package/dist/ui/animatedStatus.js.map +1 -1
- package/dist/ui/display.d.ts +26 -182
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +97 -678
- package/dist/ui/display.js.map +1 -1
- package/dist/ui/layout.d.ts +1 -0
- package/dist/ui/layout.d.ts.map +1 -1
- package/dist/ui/layout.js +12 -0
- package/dist/ui/layout.js.map +1 -1
- package/dist/ui/orchestration/UIUpdateCoordinator.d.ts +7 -61
- package/dist/ui/orchestration/UIUpdateCoordinator.d.ts.map +1 -1
- package/dist/ui/orchestration/UIUpdateCoordinator.js +20 -232
- package/dist/ui/orchestration/UIUpdateCoordinator.js.map +1 -1
- package/dist/ui/planOverlay.d.ts +28 -0
- package/dist/ui/planOverlay.d.ts.map +1 -0
- package/dist/ui/planOverlay.js +156 -0
- package/dist/ui/planOverlay.js.map +1 -0
- package/dist/ui/shortcutsHelp.d.ts.map +1 -1
- package/dist/ui/shortcutsHelp.js +1 -0
- package/dist/ui/shortcutsHelp.js.map +1 -1
- package/dist/ui/streamingFormatter.d.ts +30 -0
- package/dist/ui/streamingFormatter.d.ts.map +1 -0
- package/dist/ui/streamingFormatter.js +91 -0
- package/dist/ui/streamingFormatter.js.map +1 -0
- package/dist/ui/unified/index.d.ts +1 -30
- package/dist/ui/unified/index.d.ts.map +1 -1
- package/dist/ui/unified/index.js +2 -45
- package/dist/ui/unified/index.js.map +1 -1
- package/dist/utils/errorUtils.d.ts +16 -0
- package/dist/utils/errorUtils.d.ts.map +1 -0
- package/dist/utils/errorUtils.js +66 -0
- package/dist/utils/errorUtils.js.map +1 -0
- package/package.json +2 -1
- package/dist/core/reliabilityPrompt.d.ts +0 -9
- package/dist/core/reliabilityPrompt.d.ts.map +0 -1
- package/dist/core/reliabilityPrompt.js +0 -31
- package/dist/core/reliabilityPrompt.js.map +0 -1
- package/dist/ui/UnifiedUIController.d.ts +0 -81
- package/dist/ui/UnifiedUIController.d.ts.map +0 -1
- package/dist/ui/UnifiedUIController.js +0 -212
- package/dist/ui/UnifiedUIController.js.map +0 -1
- package/dist/ui/animation/AnimationScheduler.d.ts +0 -192
- package/dist/ui/animation/AnimationScheduler.d.ts.map +0 -1
- package/dist/ui/animation/AnimationScheduler.js +0 -432
- package/dist/ui/animation/AnimationScheduler.js.map +0 -1
- package/dist/ui/inPlaceUpdater.d.ts +0 -181
- package/dist/ui/inPlaceUpdater.d.ts.map +0 -1
- package/dist/ui/inPlaceUpdater.js +0 -515
- package/dist/ui/inPlaceUpdater.js.map +0 -1
- package/dist/ui/interrupts/InterruptManager.d.ts +0 -142
- package/dist/ui/interrupts/InterruptManager.d.ts.map +0 -1
- package/dist/ui/interrupts/InterruptManager.js +0 -439
- package/dist/ui/interrupts/InterruptManager.js.map +0 -1
- package/dist/ui/telemetry/ResponseTracker.d.ts +0 -22
- package/dist/ui/telemetry/ResponseTracker.d.ts.map +0 -1
- package/dist/ui/telemetry/ResponseTracker.js +0 -60
- package/dist/ui/telemetry/ResponseTracker.js.map +0 -1
- package/dist/ui/telemetry/UITelemetry.d.ts +0 -181
- package/dist/ui/telemetry/UITelemetry.d.ts.map +0 -1
- package/dist/ui/telemetry/UITelemetry.js +0 -446
- package/dist/ui/telemetry/UITelemetry.js.map +0 -1
- package/dist/ui/unified/layout.d.ts +0 -12
- package/dist/ui/unified/layout.d.ts.map +0 -1
- package/dist/ui/unified/layout.js +0 -96
- package/dist/ui/unified/layout.js.map +0 -1
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
import * as readline from 'node:readline';
|
|
12
12
|
import { EventEmitter } from 'node:events';
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
|
-
import { theme
|
|
14
|
+
import { theme } from './theme.js';
|
|
15
15
|
import { isPlainOutputMode } from './outputMode.js';
|
|
16
|
-
import {
|
|
16
|
+
import { renderDivider } from './layout.js';
|
|
17
|
+
import { colorizeActivity, createFrameTicker, formatElapsed, formatTinyProgressBar, formatTokenDelta, } from './animatedStatus.js';
|
|
17
18
|
const ESC = {
|
|
18
19
|
HIDE_CURSOR: '\x1b[?25l',
|
|
19
20
|
SHOW_CURSOR: '\x1b[?25h',
|
|
@@ -27,11 +28,6 @@ const ESC = {
|
|
|
27
28
|
ERASE_DOWN: '\x1b[J',
|
|
28
29
|
REVERSE: '\x1b[7m',
|
|
29
30
|
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
31
|
};
|
|
36
32
|
const COLLAPSE_HINT = theme.ui.muted('…'); // subtle ellipsis, no key hint
|
|
37
33
|
const NEWLINE_PLACEHOLDER = '↵';
|
|
@@ -56,38 +52,13 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
56
52
|
hotkeysInToggleLine = new Set();
|
|
57
53
|
collapsedPaste = null;
|
|
58
54
|
mode = 'idle';
|
|
59
|
-
streamingStartTime = null;
|
|
60
55
|
statusMessage = null;
|
|
61
56
|
statusOverride = null;
|
|
62
57
|
statusStreaming = null;
|
|
63
|
-
// Animated UI components
|
|
64
|
-
streamingSpinner = null;
|
|
65
|
-
thinkingIndicator = null;
|
|
66
|
-
contextMeter;
|
|
67
|
-
spinnerFrame = 0;
|
|
68
|
-
spinnerInterval = null;
|
|
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)")
|
|
75
|
-
activityMessage = null;
|
|
76
|
-
activityPhraseIndex = 0;
|
|
77
|
-
activityStarFrame = 0;
|
|
78
|
-
activityStarFrames = ['✳', '✴', '✵', '✶', '✷', '✸'];
|
|
79
|
-
// Token count during streaming
|
|
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
|
-
];
|
|
88
58
|
statusMeta = {};
|
|
89
59
|
toggleState = {
|
|
90
60
|
verificationEnabled: false,
|
|
61
|
+
autoContinueEnabled: false,
|
|
91
62
|
criticalApprovalMode: 'auto',
|
|
92
63
|
};
|
|
93
64
|
// ------------ Helpers ------------
|
|
@@ -99,14 +70,16 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
99
70
|
lastPromptEvent = null;
|
|
100
71
|
promptHeight = 0;
|
|
101
72
|
lastOverlayHeight = 0;
|
|
73
|
+
lastPromptIndex = 0;
|
|
74
|
+
overlayBottomPadding = 1;
|
|
102
75
|
inlinePanel = [];
|
|
76
|
+
persistentPanel = [];
|
|
77
|
+
overlayInvalidated = false;
|
|
103
78
|
hasConversationContent = false;
|
|
104
79
|
isPromptActive = false;
|
|
105
|
-
inputRenderOffset = 0;
|
|
106
80
|
plainPasteIdleMs = 24;
|
|
107
81
|
plainPasteWindowMs = 60;
|
|
108
82
|
plainPasteTriggerChars = 24;
|
|
109
|
-
cursorVisibleColumn = 1;
|
|
110
83
|
inBracketedPaste = false;
|
|
111
84
|
pasteBuffer = '';
|
|
112
85
|
inPlainPaste = false;
|
|
@@ -118,9 +91,14 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
118
91
|
lastRenderedEventKey = null;
|
|
119
92
|
lastOutputEndedWithNewline = true;
|
|
120
93
|
hasRenderedPrompt = false;
|
|
121
|
-
hasEverRenderedOverlay = false; // Track if we've ever rendered
|
|
94
|
+
hasEverRenderedOverlay = false; // Track if we've ever rendered to prevent first-render scrollback pollution
|
|
122
95
|
lastOverlay = null;
|
|
123
96
|
allowPromptRender = true;
|
|
97
|
+
streamingStart = null;
|
|
98
|
+
activityInterval = null;
|
|
99
|
+
activityTicker = createFrameTicker('sparkle');
|
|
100
|
+
renderedContextPercent = null;
|
|
101
|
+
lastToolResult = null;
|
|
124
102
|
inputCapture = null;
|
|
125
103
|
constructor(output = process.stdout, input = process.stdin, options) {
|
|
126
104
|
super();
|
|
@@ -128,8 +106,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
128
106
|
this.input = input;
|
|
129
107
|
this.interactive = Boolean(this.output.isTTY && this.input.isTTY && !process.env['CI']);
|
|
130
108
|
this.plainMode = isPlainOutputMode() || !this.interactive;
|
|
131
|
-
// Initialize animated components
|
|
132
|
-
this.contextMeter = new ContextMeter();
|
|
133
109
|
this.rl = readline.createInterface({
|
|
134
110
|
input: this.input,
|
|
135
111
|
output: this.output,
|
|
@@ -161,6 +137,9 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
161
137
|
this.updateTerminalSize();
|
|
162
138
|
this.hasRenderedPrompt = false;
|
|
163
139
|
this.lastOutputEndedWithNewline = true;
|
|
140
|
+
// Don't render prompt immediately - wait for banner/content to be added first.
|
|
141
|
+
// The prompt will render after the event queue processes the welcome banner.
|
|
142
|
+
// This prevents the prompt from appearing at the bottom before the banner shows.
|
|
164
143
|
this.write(ESC.SHOW_CURSOR);
|
|
165
144
|
return;
|
|
166
145
|
}
|
|
@@ -173,27 +152,12 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
173
152
|
cleanup() {
|
|
174
153
|
this.cancelInputCapture(new Error('Renderer disposed'));
|
|
175
154
|
this.cancelPlainPasteCapture();
|
|
176
|
-
|
|
177
|
-
if (this.spinnerInterval) {
|
|
178
|
-
clearInterval(this.spinnerInterval);
|
|
179
|
-
this.spinnerInterval = null;
|
|
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();
|
|
155
|
+
this.stopActivityTimer();
|
|
191
156
|
if (!this.interactive) {
|
|
192
157
|
this.rl.close();
|
|
193
158
|
return;
|
|
194
159
|
}
|
|
195
160
|
if (!this.plainMode) {
|
|
196
|
-
// Clear the prompt area so it doesn't remain in scrollback history
|
|
197
161
|
this.clearPromptArea();
|
|
198
162
|
this.write(ESC.DISABLE_BRACKETED_PASTE);
|
|
199
163
|
this.write(ESC.SHOW_CURSOR);
|
|
@@ -245,25 +209,14 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
245
209
|
return;
|
|
246
210
|
}
|
|
247
211
|
if (key.ctrl && key.name === 'c') {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (this.buffer.length > 0) {
|
|
253
|
-
// Stage 1: Clear the input buffer
|
|
254
|
-
this.buffer = '';
|
|
255
|
-
this.cursor = 0;
|
|
256
|
-
this.renderPrompt();
|
|
257
|
-
this.emitInputChange();
|
|
212
|
+
const hadBuffer = this.buffer.length > 0;
|
|
213
|
+
this.emit('ctrl-c', { hadBuffer });
|
|
214
|
+
if (hadBuffer) {
|
|
215
|
+
this.clearBuffer();
|
|
258
216
|
}
|
|
259
217
|
else if (this.mode === 'streaming') {
|
|
260
|
-
// Stage 2: Interrupt the AI run
|
|
261
218
|
this.emit('interrupt');
|
|
262
219
|
}
|
|
263
|
-
else {
|
|
264
|
-
// Stage 3: Quit the CLI (emit exit signal)
|
|
265
|
-
this.emit('exit');
|
|
266
|
-
}
|
|
267
220
|
return;
|
|
268
221
|
}
|
|
269
222
|
if (key.ctrl && key.name === 'd') {
|
|
@@ -272,6 +225,12 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
272
225
|
}
|
|
273
226
|
return;
|
|
274
227
|
}
|
|
228
|
+
if (key.ctrl && key.name === 'o') {
|
|
229
|
+
if (!this.expandLastToolResult()) {
|
|
230
|
+
this.emit('expand-tool-result');
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
275
234
|
if (key.ctrl && key.name === 'u') {
|
|
276
235
|
this.clearBuffer();
|
|
277
236
|
return;
|
|
@@ -282,11 +241,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
282
241
|
return;
|
|
283
242
|
}
|
|
284
243
|
}
|
|
285
|
-
// Ctrl+O: Expand last tool result
|
|
286
|
-
if (key.ctrl && key.name === 'o') {
|
|
287
|
-
this.emit('expand-tool-result');
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
244
|
if (key.name === 'return' || key.name === 'enter') {
|
|
291
245
|
if (this.collapsedPaste) {
|
|
292
246
|
this.expandCollapsedPaste();
|
|
@@ -296,12 +250,9 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
296
250
|
// If a slash command suggestion is highlighted, pressing Enter submits it immediately
|
|
297
251
|
if (this.applySuggestion(true))
|
|
298
252
|
return;
|
|
299
|
-
//
|
|
253
|
+
// If buffer starts with '/' and the first suggestion exists, submit it
|
|
300
254
|
if (this.buffer.startsWith('/') && this.suggestions.length > 0) {
|
|
301
|
-
|
|
302
|
-
? this.suggestionIndex
|
|
303
|
-
: 0;
|
|
304
|
-
this.buffer = this.suggestions[safeIndex]?.command ?? this.buffer;
|
|
255
|
+
this.buffer = this.suggestions[this.suggestionIndex >= 0 ? this.suggestionIndex : 0]?.command ?? this.buffer;
|
|
305
256
|
}
|
|
306
257
|
this.submitText(this.buffer);
|
|
307
258
|
return;
|
|
@@ -347,6 +298,18 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
347
298
|
}
|
|
348
299
|
return;
|
|
349
300
|
}
|
|
301
|
+
if (key.name === 'home') {
|
|
302
|
+
this.cursor = 0;
|
|
303
|
+
this.renderPrompt();
|
|
304
|
+
this.emitInputChange();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (key.name === 'end') {
|
|
308
|
+
this.cursor = this.buffer.length;
|
|
309
|
+
this.renderPrompt();
|
|
310
|
+
this.emitInputChange();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
350
313
|
if (key.name === 'up') {
|
|
351
314
|
if (this.navigateSuggestions(-1)) {
|
|
352
315
|
return;
|
|
@@ -611,7 +574,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
611
574
|
this.inputCapture = null;
|
|
612
575
|
this.buffer = '';
|
|
613
576
|
this.cursor = 0;
|
|
614
|
-
this.inputRenderOffset = 0;
|
|
615
577
|
this.resetSuggestions();
|
|
616
578
|
this.renderPrompt();
|
|
617
579
|
this.emitInputChange();
|
|
@@ -678,11 +640,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
678
640
|
if (!this.buffer.startsWith('/') || this.suggestions.length === 0) {
|
|
679
641
|
return false;
|
|
680
642
|
}
|
|
681
|
-
|
|
682
|
-
const safeIndex = this.suggestionIndex >= 0 && this.suggestionIndex < this.suggestions.length
|
|
683
|
-
? this.suggestionIndex
|
|
684
|
-
: 0;
|
|
685
|
-
const selected = this.suggestions[safeIndex];
|
|
643
|
+
const selected = this.suggestions[this.suggestionIndex] ?? this.suggestions[0];
|
|
686
644
|
if (!selected) {
|
|
687
645
|
return false;
|
|
688
646
|
}
|
|
@@ -715,10 +673,22 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
715
673
|
this.renderPrompt();
|
|
716
674
|
return true;
|
|
717
675
|
}
|
|
676
|
+
isGarbageOutput(content) {
|
|
677
|
+
const text = content.replace(/\u001B\[[0-9;]*m/gu, '').replace(/\s+/g, '');
|
|
678
|
+
if (!text)
|
|
679
|
+
return true;
|
|
680
|
+
if (text.length <= 2 && !/[a-zA-Z0-9]/.test(text))
|
|
681
|
+
return true;
|
|
682
|
+
if (text.length < 6 && /^[\.\-_,;:~`'"!@#$%^&*+=|\\/]+$/.test(text))
|
|
683
|
+
return true;
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
718
686
|
// ------------ Event queue ------------
|
|
719
|
-
addEvent(type, content) {
|
|
687
|
+
addEvent(type, content, options) {
|
|
720
688
|
if (!content)
|
|
721
689
|
return;
|
|
690
|
+
if (this.isGarbageOutput(content))
|
|
691
|
+
return;
|
|
722
692
|
const normalized = this.normalizeEventType(type);
|
|
723
693
|
if (!normalized)
|
|
724
694
|
return;
|
|
@@ -727,7 +697,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
727
697
|
normalized === 'thought' ||
|
|
728
698
|
normalized === 'stream' ||
|
|
729
699
|
normalized === 'tool' ||
|
|
730
|
-
normalized === 'tool-result' ||
|
|
731
700
|
normalized === 'build' ||
|
|
732
701
|
normalized === 'test') {
|
|
733
702
|
this.hasConversationContent = true;
|
|
@@ -751,6 +720,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
751
720
|
rawType: type,
|
|
752
721
|
content,
|
|
753
722
|
timestamp: Date.now(),
|
|
723
|
+
isCompacted: options?.compact === true,
|
|
754
724
|
};
|
|
755
725
|
// Priority queue: prompt events are inserted at the front to ensure immediate display
|
|
756
726
|
// This guarantees user input is echoed before any async processing responses
|
|
@@ -854,7 +824,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
854
824
|
if (event.type !== 'prompt') {
|
|
855
825
|
this.lastRenderedEventKey = signature;
|
|
856
826
|
}
|
|
857
|
-
// Clear the prompt area before writing new content
|
|
858
827
|
if (this.promptHeight > 0 || this.lastOverlay) {
|
|
859
828
|
this.clearPromptArea();
|
|
860
829
|
}
|
|
@@ -866,6 +835,10 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
866
835
|
}
|
|
867
836
|
this.output.write(formatted);
|
|
868
837
|
this.lastOutputEndedWithNewline = formatted.endsWith('\n');
|
|
838
|
+
// Overlay must be re-anchored after new scrollback is written
|
|
839
|
+
this.overlayInvalidated = true;
|
|
840
|
+
// Don't re-render prompt after every event - wait for queue to finish
|
|
841
|
+
// This prevents premature prompt rendering that cuts off responses
|
|
869
842
|
}
|
|
870
843
|
normalizeEventType(type) {
|
|
871
844
|
switch (type) {
|
|
@@ -878,9 +851,8 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
878
851
|
return 'stream';
|
|
879
852
|
case 'tool':
|
|
880
853
|
case 'tool-call':
|
|
881
|
-
return 'tool';
|
|
882
854
|
case 'tool-result':
|
|
883
|
-
return 'tool
|
|
855
|
+
return 'tool';
|
|
884
856
|
case 'build':
|
|
885
857
|
return 'build';
|
|
886
858
|
case 'test':
|
|
@@ -896,388 +868,83 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
896
868
|
}
|
|
897
869
|
}
|
|
898
870
|
formatContent(event) {
|
|
899
|
-
const bullet = '⏺';
|
|
900
|
-
// Compacted blocks already have separator and formatting
|
|
871
|
+
const bullet = '⏺';
|
|
901
872
|
if (event.isCompacted) {
|
|
902
873
|
return event.content;
|
|
903
874
|
}
|
|
904
875
|
if (event.rawType === 'banner') {
|
|
905
|
-
// Banners display without bullet prefix
|
|
906
876
|
const lines = event.content.split('\n').map(line => line.trimEnd());
|
|
907
877
|
return `${lines.join('\n')}\n`;
|
|
908
878
|
}
|
|
909
|
-
// Compact, user-friendly formatting
|
|
910
879
|
switch (event.type) {
|
|
911
880
|
case 'prompt':
|
|
912
|
-
|
|
913
|
-
return `${theme.primary('>')} ${event.content}\n`;
|
|
881
|
+
return `\n> ${event.content}\n`;
|
|
914
882
|
case 'thought': {
|
|
915
|
-
|
|
916
|
-
if (this.isGarbageOutput(event.content)) {
|
|
917
|
-
return '';
|
|
918
|
-
}
|
|
919
|
-
// Strip any existing bullet prefix (○ or ⏺) and use consistent ⏺
|
|
920
|
-
const cleanContent = event.content.replace(/^[○⏺]\s*/, '');
|
|
921
|
-
return `⏺ ${cleanContent}\n`;
|
|
922
|
-
}
|
|
923
|
-
case 'tool': {
|
|
924
|
-
// Compact tool display: ⚡ToolName → result
|
|
925
|
-
const content = event.content.replace(/^[⏺⚙○]\s*/, '');
|
|
926
|
-
return this.formatCompactToolCall(content);
|
|
927
|
-
}
|
|
928
|
-
case 'tool-result': {
|
|
929
|
-
// Inline result: └─ summary
|
|
930
|
-
return this.formatCompactToolResult(event.content);
|
|
883
|
+
return `\n${this.formatBulletBlock(event.content, bullet)}\n`;
|
|
931
884
|
}
|
|
885
|
+
case 'tool':
|
|
886
|
+
if (event.rawType === 'tool-call') {
|
|
887
|
+
return `\n${this.formatBulletBlock(event.content, bullet)}\n`;
|
|
888
|
+
}
|
|
889
|
+
if (event.rawType === 'tool-result') {
|
|
890
|
+
return this.formatToolResult(event.content, event.isCompacted);
|
|
891
|
+
}
|
|
892
|
+
return `\n${event.content}\n`;
|
|
932
893
|
case 'build':
|
|
933
|
-
return
|
|
894
|
+
return `\n${this.formatBulletBlock(event.content, bullet)}\n`;
|
|
934
895
|
case 'test':
|
|
935
|
-
return
|
|
896
|
+
return `\n${this.formatBulletBlock(event.content, bullet)}\n`;
|
|
936
897
|
case 'stream':
|
|
937
898
|
return event.content;
|
|
938
899
|
case 'response':
|
|
939
900
|
default: {
|
|
940
|
-
|
|
941
|
-
if (this.isGarbageOutput(event.content)) {
|
|
942
|
-
return '';
|
|
943
|
-
}
|
|
944
|
-
// Clean response without excessive bullets
|
|
945
|
-
return this.formatCompactResponse(event.content);
|
|
901
|
+
return `\n${this.formatBulletBlock(event.content, bullet)}\n`;
|
|
946
902
|
}
|
|
947
903
|
}
|
|
948
904
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
// Structural check: contains "to=functions." or "to=tools." (internal routing)
|
|
961
|
-
if (content.includes('to=functions.') || content.includes('to=tools.')) {
|
|
962
|
-
return true;
|
|
963
|
-
}
|
|
964
|
-
// Structural check: looks like internal instruction (quoted system text)
|
|
965
|
-
if (content.startsWith('"') && content.includes('block') && content.includes('tool')) {
|
|
966
|
-
return true;
|
|
967
|
-
}
|
|
968
|
-
// Structural check: very short content that's just timing info
|
|
969
|
-
if (content.length < 30 && /elapsed|seconds?|ms\b/i.test(content)) {
|
|
970
|
-
return true;
|
|
971
|
-
}
|
|
972
|
-
// Structural check: gibberish - high ratio of non-word characters
|
|
973
|
-
const alphaCount = (content.match(/[a-zA-Z]/g) || []).length;
|
|
974
|
-
const totalCount = content.replace(/\s/g, '').length;
|
|
975
|
-
if (totalCount > 20 && alphaCount / totalCount < 0.5) {
|
|
976
|
-
return true; // Less than 50% letters = likely garbage
|
|
905
|
+
formatBulletBlock(content, bullet) {
|
|
906
|
+
const width = Math.max(24, this.safeWidth() - 4);
|
|
907
|
+
const wrapped = this.wrapText(content, width);
|
|
908
|
+
return wrapped
|
|
909
|
+
.map((line, index) => (index === 0 ? `${bullet} ${line}` : ` ${line}`))
|
|
910
|
+
.join('\n');
|
|
911
|
+
}
|
|
912
|
+
formatToolResult(content, compacted) {
|
|
913
|
+
const prefix = '⎿';
|
|
914
|
+
if (compacted) {
|
|
915
|
+
return `\n${prefix} ${content}\n`;
|
|
977
916
|
}
|
|
978
|
-
|
|
917
|
+
const width = Math.max(24, this.safeWidth() - 4);
|
|
918
|
+
const wrapped = this.wrapText(content, width);
|
|
919
|
+
const body = wrapped
|
|
920
|
+
.map((line, index) => (index === 0 ? `${prefix} ${line}` : ` ${line}`))
|
|
921
|
+
.join('\n');
|
|
922
|
+
return `\n${body}\n`;
|
|
979
923
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
const maxWidth = Math.min(this.cols - 4, 56); // Leave room for prefix and margins
|
|
990
|
-
const lines = content.split('\n');
|
|
991
|
-
const result = [];
|
|
992
|
-
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
993
|
-
const line = lines[lineIdx];
|
|
994
|
-
if (!line.trim()) {
|
|
995
|
-
result.push('');
|
|
996
|
-
continue;
|
|
997
|
-
}
|
|
998
|
-
// Word-wrap each line
|
|
999
|
-
const words = line.split(/(\s+)/);
|
|
1000
|
-
let currentLine = '';
|
|
924
|
+
wrapText(content, width) {
|
|
925
|
+
if (!content) {
|
|
926
|
+
return [''];
|
|
927
|
+
}
|
|
928
|
+
const lines = [];
|
|
929
|
+
const rawLines = content.split('\n');
|
|
930
|
+
for (const rawLine of rawLines) {
|
|
931
|
+
const words = rawLine.split(/(\s+)/);
|
|
932
|
+
let current = '';
|
|
1001
933
|
for (const word of words) {
|
|
1002
|
-
if (
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
934
|
+
if (!word)
|
|
935
|
+
continue;
|
|
936
|
+
const candidate = current ? `${current}${word}` : word;
|
|
937
|
+
if (this.visibleLength(candidate) > width && current) {
|
|
938
|
+
lines.push(current.trimEnd());
|
|
939
|
+
current = word.trimStart();
|
|
1007
940
|
}
|
|
1008
941
|
else {
|
|
1009
|
-
|
|
942
|
+
current = candidate;
|
|
1010
943
|
}
|
|
1011
944
|
}
|
|
1012
|
-
|
|
1013
|
-
const prefix = result.length === 0 && lineIdx === 0 ? `${bullet} ` : ' ';
|
|
1014
|
-
result.push(`${prefix}${currentLine.trimEnd()}`);
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
return result.join('\n') + '\n';
|
|
1018
|
-
}
|
|
1019
|
-
/**
|
|
1020
|
-
* Format a tool call in Claude Code style:
|
|
1021
|
-
* ⏺ Search(pattern: "foo", path: "src",
|
|
1022
|
-
* output_mode: "content", head_limit: 30)
|
|
1023
|
-
*/
|
|
1024
|
-
formatToolCall(content) {
|
|
1025
|
-
const bullet = '⏺';
|
|
1026
|
-
// Parse tool name and arguments
|
|
1027
|
-
const match = content.match(/^(\w+)\((.*)\)$/s);
|
|
1028
|
-
if (!match) {
|
|
1029
|
-
// Simple format without args
|
|
1030
|
-
const nameMatch = content.match(/^(\w+)/);
|
|
1031
|
-
if (nameMatch) {
|
|
1032
|
-
return `${bullet} ${theme.info(nameMatch[1])}\n`;
|
|
1033
|
-
}
|
|
1034
|
-
return `${bullet} ${content}\n`;
|
|
1035
|
-
}
|
|
1036
|
-
const toolName = match[1];
|
|
1037
|
-
const argsStr = match[2];
|
|
1038
|
-
const maxWidth = Math.min(this.cols - 4, 56);
|
|
1039
|
-
// Format: ⏺ ToolName(args...)
|
|
1040
|
-
const prefix = `${bullet} ${theme.info(toolName)}(`;
|
|
1041
|
-
const prefixLen = toolName.length + 3; // "⏺ ToolName(" visible length
|
|
1042
|
-
const indent = ' '.repeat(prefixLen + 4); // Extra indent for wrapped args
|
|
1043
|
-
// Parse and format arguments
|
|
1044
|
-
const args = this.parseToolArgs(argsStr);
|
|
1045
|
-
if (args.length === 0) {
|
|
1046
|
-
return `${prefix})\n`;
|
|
1047
|
-
}
|
|
1048
|
-
const lines = [];
|
|
1049
|
-
let currentLine = prefix;
|
|
1050
|
-
for (let i = 0; i < args.length; i++) {
|
|
1051
|
-
const arg = args[i];
|
|
1052
|
-
const argText = `${theme.ui.muted(arg.key + ':')} ${this.formatArgValue(arg.value)}`;
|
|
1053
|
-
const separator = i < args.length - 1 ? ', ' : ')';
|
|
1054
|
-
// Check if this arg fits on current line
|
|
1055
|
-
const testLine = currentLine + argText + separator;
|
|
1056
|
-
if (this.stripAnsi(testLine).length > maxWidth && currentLine !== prefix) {
|
|
1057
|
-
lines.push(currentLine.trimEnd());
|
|
1058
|
-
currentLine = indent + argText + separator;
|
|
1059
|
-
}
|
|
1060
|
-
else {
|
|
1061
|
-
currentLine += argText + separator;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
if (currentLine.trim()) {
|
|
1065
|
-
lines.push(currentLine.trimEnd());
|
|
1066
|
-
}
|
|
1067
|
-
return lines.join('\n') + '\n';
|
|
1068
|
-
}
|
|
1069
|
-
/**
|
|
1070
|
-
* Parse tool arguments from string like: key: "value", key2: value2
|
|
1071
|
-
*/
|
|
1072
|
-
parseToolArgs(argsStr) {
|
|
1073
|
-
const args = [];
|
|
1074
|
-
// Simple regex to extract key: value pairs
|
|
1075
|
-
const regex = /(\w+):\s*("(?:[^"\\]|\\.)*"|[^,\)]+)/g;
|
|
1076
|
-
let match;
|
|
1077
|
-
while ((match = regex.exec(argsStr)) !== null) {
|
|
1078
|
-
args.push({ key: match[1], value: match[2].trim() });
|
|
1079
|
-
}
|
|
1080
|
-
return args;
|
|
1081
|
-
}
|
|
1082
|
-
/**
|
|
1083
|
-
* Format an argument value (truncate long strings)
|
|
1084
|
-
*/
|
|
1085
|
-
formatArgValue(value) {
|
|
1086
|
-
// Remove surrounding quotes if present
|
|
1087
|
-
const isQuoted = value.startsWith('"') && value.endsWith('"');
|
|
1088
|
-
const inner = isQuoted ? value.slice(1, -1) : value;
|
|
1089
|
-
// Truncate long values
|
|
1090
|
-
const maxLen = 40;
|
|
1091
|
-
const truncated = inner.length > maxLen ? inner.slice(0, maxLen - 3) + '...' : inner;
|
|
1092
|
-
return isQuoted ? `"${truncated}"` : truncated;
|
|
1093
|
-
}
|
|
1094
|
-
/**
|
|
1095
|
-
* Format a tool result in Claude Code style:
|
|
1096
|
-
* ⎿ Found 12 lines (ctrl+o to expand)
|
|
1097
|
-
*/
|
|
1098
|
-
formatToolResult(content) {
|
|
1099
|
-
// Check if this is a summary line (e.g., "Found X lines")
|
|
1100
|
-
const summaryMatch = content.match(/^(Found \d+ (?:lines?|files?|matches?)|Read \d+ lines?|Wrote \d+ lines?|Edited|Created|Deleted)/i);
|
|
1101
|
-
if (summaryMatch) {
|
|
1102
|
-
return ` ${theme.ui.muted('⎿')} ${content} ${theme.ui.muted('(ctrl+o to expand)')}\n`;
|
|
1103
|
-
}
|
|
1104
|
-
// For other results, show truncated preview
|
|
1105
|
-
const lines = content.split('\n');
|
|
1106
|
-
if (lines.length > 3) {
|
|
1107
|
-
const preview = lines.slice(0, 2).join('\n');
|
|
1108
|
-
return ` ${theme.ui.muted('⎿')} ${preview}\n ${theme.ui.muted(`... ${lines.length - 2} more lines (ctrl+o to expand)`)}\n`;
|
|
1109
|
-
}
|
|
1110
|
-
return ` ${theme.ui.muted('⎿')} ${content}\n`;
|
|
1111
|
-
}
|
|
1112
|
-
/**
|
|
1113
|
-
* Format a compact tool call: ⏺ Read → file.ts
|
|
1114
|
-
*/
|
|
1115
|
-
formatCompactToolCall(content) {
|
|
1116
|
-
const bullet = '⏺';
|
|
1117
|
-
// Parse tool name and args
|
|
1118
|
-
const match = content.match(/^(\w+)\s*(?:\((.*)\))?$/s);
|
|
1119
|
-
if (!match) {
|
|
1120
|
-
return `${bullet} ${content}\n`;
|
|
1121
|
-
}
|
|
1122
|
-
const toolName = match[1];
|
|
1123
|
-
const argsStr = match[2]?.trim() || '';
|
|
1124
|
-
// If no args, just show tool name
|
|
1125
|
-
if (!argsStr) {
|
|
1126
|
-
return `${bullet} ${theme.info(toolName)}\n`;
|
|
1127
|
-
}
|
|
1128
|
-
// Format full params in Claude Code style with line wrapping
|
|
1129
|
-
// For long args, wrap them nicely with continuation indent
|
|
1130
|
-
const prefix = `${bullet} ${theme.info(toolName)}(`;
|
|
1131
|
-
const suffix = ')';
|
|
1132
|
-
const maxWidth = this.cols - 8; // Leave room for margins
|
|
1133
|
-
// Parse individual params
|
|
1134
|
-
const params = this.parseToolParams(argsStr);
|
|
1135
|
-
if (params.length === 0) {
|
|
1136
|
-
return `${prefix}${argsStr}${suffix}\n`;
|
|
1137
|
-
}
|
|
1138
|
-
// Format params with proper wrapping
|
|
1139
|
-
return this.formatToolParams(toolName, params, maxWidth);
|
|
1140
|
-
}
|
|
1141
|
-
/**
|
|
1142
|
-
* Parse tool params from args string
|
|
1143
|
-
*/
|
|
1144
|
-
parseToolParams(argsStr) {
|
|
1145
|
-
const params = [];
|
|
1146
|
-
// Match key: "value" or key: value patterns
|
|
1147
|
-
const regex = /(\w+):\s*("(?:[^"\\]|\\.)*"|[^,\n]+)/g;
|
|
1148
|
-
let match;
|
|
1149
|
-
while ((match = regex.exec(argsStr)) !== null) {
|
|
1150
|
-
params.push({ key: match[1], value: match[2].trim() });
|
|
1151
|
-
}
|
|
1152
|
-
return params;
|
|
1153
|
-
}
|
|
1154
|
-
/**
|
|
1155
|
-
* Format tool params in Claude Code style with wrapping
|
|
1156
|
-
*/
|
|
1157
|
-
formatToolParams(toolName, params, maxWidth) {
|
|
1158
|
-
const bullet = '⏺';
|
|
1159
|
-
const lines = [];
|
|
1160
|
-
const indent = ' '; // 8 spaces for continuation
|
|
1161
|
-
let currentLine = `${bullet} ${theme.info(toolName)}(`;
|
|
1162
|
-
let firstParam = true;
|
|
1163
|
-
for (const param of params) {
|
|
1164
|
-
const paramStr = firstParam
|
|
1165
|
-
? `${param.key}: ${param.value}`
|
|
1166
|
-
: `, ${param.key}: ${param.value}`;
|
|
1167
|
-
// Check if adding this param would exceed width
|
|
1168
|
-
const testLine = currentLine + paramStr;
|
|
1169
|
-
const plainLength = testLine.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
1170
|
-
if (plainLength > maxWidth && !firstParam) {
|
|
1171
|
-
// Start new line
|
|
1172
|
-
lines.push(currentLine);
|
|
1173
|
-
currentLine = indent + `${param.key}: ${param.value}`;
|
|
1174
|
-
}
|
|
1175
|
-
else {
|
|
1176
|
-
currentLine += paramStr;
|
|
1177
|
-
}
|
|
1178
|
-
firstParam = false;
|
|
1179
|
-
}
|
|
1180
|
-
currentLine += ')';
|
|
1181
|
-
lines.push(currentLine);
|
|
1182
|
-
return lines.join('\n') + '\n';
|
|
1183
|
-
}
|
|
1184
|
-
/**
|
|
1185
|
-
* Extract a short summary from tool args
|
|
1186
|
-
*/
|
|
1187
|
-
extractToolSummary(toolName, argsStr) {
|
|
1188
|
-
const tool = toolName.toLowerCase();
|
|
1189
|
-
// Extract path/file for file operations
|
|
1190
|
-
if (['read', 'write', 'edit', 'glob', 'grep', 'search'].includes(tool)) {
|
|
1191
|
-
const pathMatch = argsStr.match(/(?:path|file_path|pattern):\s*"([^"]+)"/);
|
|
1192
|
-
if (pathMatch) {
|
|
1193
|
-
const path = pathMatch[1];
|
|
1194
|
-
// Shorten long paths
|
|
1195
|
-
const short = path.length > 30 ? '…' + path.slice(-28) : path;
|
|
1196
|
-
return theme.ui.muted(short);
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
// Extract command for bash
|
|
1200
|
-
if (tool === 'bash') {
|
|
1201
|
-
const cmdMatch = argsStr.match(/command:\s*"([^"]+)"/);
|
|
1202
|
-
if (cmdMatch) {
|
|
1203
|
-
const cmd = cmdMatch[1];
|
|
1204
|
-
const short = cmd.length > 40 ? cmd.slice(0, 37) + '…' : cmd;
|
|
1205
|
-
return theme.ui.muted(short);
|
|
1206
|
-
}
|
|
945
|
+
lines.push(current.trimEnd());
|
|
1207
946
|
}
|
|
1208
|
-
return
|
|
1209
|
-
}
|
|
1210
|
-
/**
|
|
1211
|
-
* Format a compact tool result: ⎿ Found X lines (ctrl+o to expand)
|
|
1212
|
-
*/
|
|
1213
|
-
formatCompactToolResult(content) {
|
|
1214
|
-
// Parse common result patterns for summary
|
|
1215
|
-
const lineMatch = content.match(/(\d+)\s*lines?/i);
|
|
1216
|
-
const fileMatch = content.match(/(\d+)\s*(?:files?|matches?)/i);
|
|
1217
|
-
const readMatch = content.match(/read.*?(\d+)\s*lines?/i);
|
|
1218
|
-
let summary;
|
|
1219
|
-
if (readMatch) {
|
|
1220
|
-
summary = `Read ${readMatch[1]} lines`;
|
|
1221
|
-
}
|
|
1222
|
-
else if (lineMatch) {
|
|
1223
|
-
summary = `Found ${lineMatch[1]} line${lineMatch[1] === '1' ? '' : 's'}`;
|
|
1224
|
-
}
|
|
1225
|
-
else if (fileMatch) {
|
|
1226
|
-
summary = `Found ${fileMatch[1]} file${fileMatch[1] === '1' ? '' : 's'}`;
|
|
1227
|
-
}
|
|
1228
|
-
else if (content.match(/^(success|ok|done|completed|written|edited|created)/i)) {
|
|
1229
|
-
summary = '✓';
|
|
1230
|
-
}
|
|
1231
|
-
else {
|
|
1232
|
-
// Use content directly, truncated if needed
|
|
1233
|
-
summary = content.length > 40 ? content.slice(0, 37) + '…' : content;
|
|
1234
|
-
}
|
|
1235
|
-
return ` ${theme.ui.muted('⎿')} ${summary} ${theme.ui.muted('(ctrl+o to expand)')}\n`;
|
|
1236
|
-
}
|
|
1237
|
-
/**
|
|
1238
|
-
* Format a compact response with bullet on first line
|
|
1239
|
-
*/
|
|
1240
|
-
formatCompactResponse(content) {
|
|
1241
|
-
const bullet = '⏺';
|
|
1242
|
-
const trimmed = content.trim();
|
|
1243
|
-
if (!trimmed)
|
|
1244
|
-
return '';
|
|
1245
|
-
// Single line responses - bullet prefix
|
|
1246
|
-
if (!trimmed.includes('\n') && trimmed.length < 80) {
|
|
1247
|
-
return `${bullet} ${trimmed}\n`;
|
|
1248
|
-
}
|
|
1249
|
-
// Multi-line: bullet on first, indent continuation
|
|
1250
|
-
const lines = trimmed.split('\n');
|
|
1251
|
-
const result = [];
|
|
1252
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1253
|
-
const line = lines[i].trimEnd();
|
|
1254
|
-
if (!line) {
|
|
1255
|
-
result.push('');
|
|
1256
|
-
}
|
|
1257
|
-
else if (i === 0) {
|
|
1258
|
-
result.push(`${bullet} ${line}`);
|
|
1259
|
-
}
|
|
1260
|
-
else {
|
|
1261
|
-
result.push(` ${line}`);
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
return result.join('\n') + '\n';
|
|
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`;
|
|
947
|
+
return lines;
|
|
1281
948
|
}
|
|
1282
949
|
/**
|
|
1283
950
|
* Format a compact conversation block (Claude Code style)
|
|
@@ -1314,81 +981,26 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1314
981
|
setMode(mode) {
|
|
1315
982
|
const wasStreaming = this.mode === 'streaming';
|
|
1316
983
|
this.mode = mode;
|
|
1317
|
-
// Track streaming start time for elapsed display
|
|
1318
|
-
if (mode === 'streaming' && !wasStreaming) {
|
|
1319
|
-
this.streamingStartTime = Date.now();
|
|
1320
|
-
this.streamingTokens = 0; // Reset token count
|
|
1321
|
-
this.startSpinnerAnimation();
|
|
1322
|
-
}
|
|
1323
|
-
else if (mode === 'idle' && wasStreaming) {
|
|
1324
|
-
this.streamingStartTime = null;
|
|
1325
|
-
this.stopSpinnerAnimation();
|
|
1326
|
-
}
|
|
1327
984
|
if (wasStreaming && mode === 'idle' && !this.lastOutputEndedWithNewline) {
|
|
1328
985
|
// Finish streaming on a fresh line so the next prompt/event doesn't collide
|
|
1329
986
|
this.write('\n');
|
|
1330
987
|
this.lastOutputEndedWithNewline = true;
|
|
1331
988
|
}
|
|
1332
|
-
if (
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
/**
|
|
1338
|
-
* Start the animated spinner for streaming status
|
|
1339
|
-
*/
|
|
1340
|
-
startSpinnerAnimation() {
|
|
1341
|
-
if (this.spinnerInterval)
|
|
1342
|
-
return; // Already running
|
|
1343
|
-
this.spinnerFrame = 0;
|
|
1344
|
-
this.activityStarFrame = 0;
|
|
1345
|
-
this.spinnerInterval = setInterval(() => {
|
|
1346
|
-
this.spinnerFrame = (this.spinnerFrame + 1) % spinnerFrames.braille.length;
|
|
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') {
|
|
1350
|
-
this.renderPrompt();
|
|
989
|
+
if (mode === 'streaming') {
|
|
990
|
+
if (!this.streamingStart) {
|
|
991
|
+
this.streamingStart = Date.now();
|
|
1351
992
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
stopSpinnerAnimation() {
|
|
1358
|
-
if (this.spinnerInterval) {
|
|
1359
|
-
clearInterval(this.spinnerInterval);
|
|
1360
|
-
this.spinnerInterval = null;
|
|
993
|
+
this.startActivityTimer();
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
996
|
+
this.streamingStart = null;
|
|
997
|
+
this.stopActivityTimer();
|
|
1361
998
|
}
|
|
1362
|
-
this.spinnerFrame = 0;
|
|
1363
|
-
this.activityStarFrame = 0;
|
|
1364
|
-
this.activityMessage = null;
|
|
1365
|
-
}
|
|
1366
|
-
/**
|
|
1367
|
-
* Set the activity message displayed with animated star
|
|
1368
|
-
* Example: "Ruminating…" shows as "✳ Ruminating… (esc to interrupt · 34s · ↑1.2k)"
|
|
1369
|
-
*/
|
|
1370
|
-
setActivity(message) {
|
|
1371
|
-
this.activityMessage = message;
|
|
1372
999
|
if (!this.plainMode) {
|
|
1000
|
+
// Always render prompt to keep bottom UI persistent (rich mode only)
|
|
1373
1001
|
this.renderPrompt();
|
|
1374
1002
|
}
|
|
1375
1003
|
}
|
|
1376
|
-
/**
|
|
1377
|
-
* Update the token count displayed in the activity line
|
|
1378
|
-
*/
|
|
1379
|
-
updateStreamingTokens(tokens) {
|
|
1380
|
-
this.streamingTokens = tokens;
|
|
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
|
-
}
|
|
1392
1004
|
getMode() {
|
|
1393
1005
|
return this.mode;
|
|
1394
1006
|
}
|
|
@@ -1427,7 +1039,15 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1427
1039
|
}
|
|
1428
1040
|
}
|
|
1429
1041
|
updateStatusMeta(meta, options = {}) {
|
|
1430
|
-
const next = { ...this.statusMeta
|
|
1042
|
+
const next = { ...this.statusMeta };
|
|
1043
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
1044
|
+
if (value === null || value === undefined) {
|
|
1045
|
+
delete next[key];
|
|
1046
|
+
}
|
|
1047
|
+
else {
|
|
1048
|
+
next[key] = value;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1431
1051
|
const changed = JSON.stringify(next) !== JSON.stringify(this.statusMeta);
|
|
1432
1052
|
this.statusMeta = next;
|
|
1433
1053
|
const shouldRender = options.render !== false && changed;
|
|
@@ -1437,7 +1057,8 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1437
1057
|
}
|
|
1438
1058
|
updateModeToggles(state) {
|
|
1439
1059
|
this.toggleState = { ...this.toggleState, ...state };
|
|
1440
|
-
if (!state.
|
|
1060
|
+
if (!state.autoContinueHotkey &&
|
|
1061
|
+
!state.verificationHotkey &&
|
|
1441
1062
|
!state.thinkingHotkey &&
|
|
1442
1063
|
!state.criticalApprovalHotkey) {
|
|
1443
1064
|
this.hotkeysInToggleLine.clear();
|
|
@@ -1460,6 +1081,51 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1460
1081
|
this.inlinePanel = [];
|
|
1461
1082
|
this.renderPrompt();
|
|
1462
1083
|
}
|
|
1084
|
+
setPersistentPanel(lines) {
|
|
1085
|
+
const normalized = (lines ?? [])
|
|
1086
|
+
.map(line => line.replace(/\s+$/g, ''))
|
|
1087
|
+
.filter(line => line.trim().length > 0);
|
|
1088
|
+
if (JSON.stringify(normalized) === JSON.stringify(this.persistentPanel)) {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
this.persistentPanel = normalized;
|
|
1092
|
+
this.renderPrompt();
|
|
1093
|
+
}
|
|
1094
|
+
clearPersistentPanel() {
|
|
1095
|
+
if (!this.persistentPanel.length)
|
|
1096
|
+
return;
|
|
1097
|
+
this.persistentPanel = [];
|
|
1098
|
+
this.renderPrompt();
|
|
1099
|
+
}
|
|
1100
|
+
startActivityTimer() {
|
|
1101
|
+
if (this.activityInterval || this.plainMode || !this.interactive) {
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
this.activityInterval = setInterval(() => {
|
|
1105
|
+
if (this.allowPromptRender) {
|
|
1106
|
+
this.renderPrompt();
|
|
1107
|
+
}
|
|
1108
|
+
}, 120);
|
|
1109
|
+
}
|
|
1110
|
+
stopActivityTimer() {
|
|
1111
|
+
if (this.activityInterval) {
|
|
1112
|
+
clearInterval(this.activityInterval);
|
|
1113
|
+
this.activityInterval = null;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
rememberToolResult(content, summary) {
|
|
1117
|
+
const normalized = content?.trim();
|
|
1118
|
+
if (!normalized)
|
|
1119
|
+
return;
|
|
1120
|
+
this.lastToolResult = { full: normalized, summary: summary?.trim() || undefined };
|
|
1121
|
+
}
|
|
1122
|
+
expandLastToolResult() {
|
|
1123
|
+
if (!this.lastToolResult) {
|
|
1124
|
+
return false;
|
|
1125
|
+
}
|
|
1126
|
+
this.addEvent('tool-result', this.lastToolResult.full);
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1463
1129
|
// ------------ Prompt rendering ------------
|
|
1464
1130
|
renderPrompt() {
|
|
1465
1131
|
if (!this.interactive) {
|
|
@@ -1473,7 +1139,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1473
1139
|
this.lastOutputEndedWithNewline = true;
|
|
1474
1140
|
}
|
|
1475
1141
|
this.write(`\r${ESC.CLEAR_LINE}${line}`);
|
|
1476
|
-
this.cursorVisibleColumn = line.length + 1;
|
|
1477
1142
|
this.hasRenderedPrompt = true;
|
|
1478
1143
|
this.isPromptActive = true;
|
|
1479
1144
|
this.lastOutputEndedWithNewline = false; // prompt ends mid-line by design
|
|
@@ -1483,9 +1148,23 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1483
1148
|
if (!this.allowPromptRender) {
|
|
1484
1149
|
return;
|
|
1485
1150
|
}
|
|
1486
|
-
|
|
1151
|
+
if (typeof this.statusMeta.contextPercent === 'number') {
|
|
1152
|
+
this.renderedContextPercent =
|
|
1153
|
+
this.renderedContextPercent === null
|
|
1154
|
+
? this.statusMeta.contextPercent
|
|
1155
|
+
: this.renderedContextPercent +
|
|
1156
|
+
(this.statusMeta.contextPercent - this.renderedContextPercent) * 0.35;
|
|
1157
|
+
}
|
|
1158
|
+
else {
|
|
1159
|
+
this.renderedContextPercent = null;
|
|
1160
|
+
}
|
|
1161
|
+
// Rich mode: inline overlay anchored to current scrollback (no full-screen clear)
|
|
1487
1162
|
this.updateTerminalSize();
|
|
1488
1163
|
const maxWidth = this.safeWidth();
|
|
1164
|
+
if (this.lastRenderWidth !== null && maxWidth !== this.lastRenderWidth) {
|
|
1165
|
+
// Terminal resized; force a clean anchor so the overlay doesn't jitter.
|
|
1166
|
+
this.overlayInvalidated = true;
|
|
1167
|
+
}
|
|
1489
1168
|
this.lastRenderWidth = maxWidth;
|
|
1490
1169
|
const overlay = this.buildOverlayLines();
|
|
1491
1170
|
if (!overlay.lines.length) {
|
|
@@ -1495,188 +1174,99 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1495
1174
|
if (!renderedLines.length) {
|
|
1496
1175
|
return;
|
|
1497
1176
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
//
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
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
|
-
}
|
|
1177
|
+
let promptIndex = Math.max(0, Math.min(overlay.cursorRow, renderedLines.length - 1));
|
|
1178
|
+
let height = renderedLines.length;
|
|
1179
|
+
// Keep at least one free line below the overlay so typing always has breathing room
|
|
1180
|
+
const bottomPadding = this.overlayBottomPadding;
|
|
1181
|
+
const totalRows = this.rows || 24;
|
|
1182
|
+
const availableRows = Math.max(1, totalRows - bottomPadding);
|
|
1183
|
+
if (height > availableRows) {
|
|
1184
|
+
renderedLines.splice(availableRows);
|
|
1185
|
+
height = renderedLines.length;
|
|
1186
|
+
promptIndex = Math.max(0, Math.min(promptIndex, height - 1));
|
|
1187
|
+
}
|
|
1188
|
+
const startRow = Math.max(1, availableRows - height + 1);
|
|
1189
|
+
const promptRow = startRow + promptIndex;
|
|
1190
|
+
const promptCol = Math.min(Math.max(1, overlay.cursorCol), this.cols || 80);
|
|
1191
|
+
// Clear any previous overlay footprint (status, prompt, controls) to avoid leaking into scrollback
|
|
1192
|
+
this.clearOverlayRows(height, startRow);
|
|
1193
|
+
if (bottomPadding > 0 && startRow + height <= totalRows) {
|
|
1194
|
+
this.write(ESC.TO(startRow + height, 1));
|
|
1195
|
+
this.write(ESC.CLEAR_LINE);
|
|
1527
1196
|
}
|
|
1528
|
-
//
|
|
1529
|
-
for (let
|
|
1530
|
-
|
|
1197
|
+
// Render overlay lines in place without pushing scrollback
|
|
1198
|
+
for (let idx = 0; idx < height; idx++) {
|
|
1199
|
+
const row = startRow + idx;
|
|
1200
|
+
const line = renderedLines[idx] ?? '';
|
|
1201
|
+
this.write(ESC.TO(row, 1));
|
|
1531
1202
|
this.write(ESC.CLEAR_LINE);
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
this.write('\n');
|
|
1203
|
+
if (line) {
|
|
1204
|
+
this.write(line);
|
|
1535
1205
|
}
|
|
1536
1206
|
}
|
|
1537
|
-
// Position cursor at prompt
|
|
1538
|
-
|
|
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;
|
|
1207
|
+
// Position cursor at prompt row/col
|
|
1208
|
+
this.write(ESC.TO(promptRow, promptCol));
|
|
1546
1209
|
this.hasRenderedPrompt = true;
|
|
1547
|
-
this.hasEverRenderedOverlay = true;
|
|
1210
|
+
this.hasEverRenderedOverlay = true; // Mark that we've rendered at least once
|
|
1548
1211
|
this.isPromptActive = true;
|
|
1549
1212
|
this.lastOverlayHeight = height;
|
|
1213
|
+
this.lastPromptIndex = promptIndex;
|
|
1550
1214
|
this.lastOverlay = { lines: renderedLines, promptIndex };
|
|
1551
|
-
this.
|
|
1215
|
+
this.overlayInvalidated = false;
|
|
1216
|
+
this.lastOutputEndedWithNewline = true;
|
|
1552
1217
|
this.promptHeight = height;
|
|
1553
1218
|
}
|
|
1554
1219
|
buildOverlayLines() {
|
|
1555
1220
|
const lines = [];
|
|
1556
1221
|
const maxWidth = this.safeWidth();
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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) {
|
|
1222
|
+
const activity = this.buildActivityLine(maxWidth);
|
|
1223
|
+
if (activity) {
|
|
1224
|
+
lines.push(activity);
|
|
1225
|
+
}
|
|
1226
|
+
lines.push(this.truncateLine(renderDivider(Math.min(maxWidth, 96)), maxWidth));
|
|
1227
|
+
const inputOverlay = this.buildInputOverlay(maxWidth);
|
|
1228
|
+
const promptRow = lines.length + inputOverlay.cursorRow;
|
|
1229
|
+
const promptCol = inputOverlay.cursorCol;
|
|
1230
|
+
for (const line of inputOverlay.lines) {
|
|
1588
1231
|
lines.push(this.truncateLine(line, maxWidth));
|
|
1589
1232
|
}
|
|
1590
|
-
|
|
1591
|
-
lines.push(divider);
|
|
1592
|
-
// Inline panel (pinned scroll box for live output/menus)
|
|
1233
|
+
lines.push(this.truncateLine(renderDivider(Math.min(maxWidth, 96)), maxWidth));
|
|
1593
1234
|
if (this.inlinePanel.length > 0) {
|
|
1594
1235
|
for (const panelLine of this.inlinePanel) {
|
|
1595
|
-
lines.push(this.truncateLine(
|
|
1236
|
+
lines.push(this.truncateLine(panelLine, maxWidth));
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
if (this.persistentPanel.length > 0) {
|
|
1240
|
+
for (const panelLine of this.persistentPanel) {
|
|
1241
|
+
lines.push(this.truncateLine(panelLine, maxWidth));
|
|
1596
1242
|
}
|
|
1597
|
-
// Separate inline content from suggestions/toggles
|
|
1598
|
-
lines.push(divider);
|
|
1599
1243
|
}
|
|
1600
|
-
// Slash command suggestions
|
|
1601
1244
|
if (this.suggestions.length > 0) {
|
|
1602
1245
|
for (let index = 0; index < this.suggestions.length; index++) {
|
|
1603
1246
|
const suggestion = this.suggestions[index];
|
|
1604
1247
|
const isActive = index === this.suggestionIndex;
|
|
1605
|
-
const marker = isActive ? theme.primary('
|
|
1248
|
+
const marker = isActive ? theme.primary('›') : theme.ui.muted('›');
|
|
1606
1249
|
const cmdText = isActive ? theme.primary(suggestion.command) : theme.ui.muted(suggestion.command);
|
|
1607
1250
|
const descText = isActive ? suggestion.description : theme.ui.muted(suggestion.description);
|
|
1608
|
-
lines.push(this.truncateLine(
|
|
1251
|
+
lines.push(this.truncateLine(`${marker} ${cmdText} — ${descText}`, maxWidth));
|
|
1609
1252
|
}
|
|
1610
1253
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
lines.push(this.truncateLine(` ${modelContextLine}`, maxWidth));
|
|
1254
|
+
const modelLine = this.buildModelLine(maxWidth);
|
|
1255
|
+
if (modelLine) {
|
|
1256
|
+
lines.push(modelLine);
|
|
1615
1257
|
}
|
|
1616
|
-
|
|
1617
|
-
const toggleLine = this.buildInlineToggleLine();
|
|
1258
|
+
const toggleLine = this.buildToggleLine();
|
|
1618
1259
|
if (toggleLine) {
|
|
1619
|
-
lines.push(
|
|
1260
|
+
lines.push(toggleLine);
|
|
1620
1261
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
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')}`);
|
|
1262
|
+
const shortcutLine = this.buildShortcutLine();
|
|
1263
|
+
if (shortcutLine) {
|
|
1264
|
+
lines.push(shortcutLine);
|
|
1647
1265
|
}
|
|
1648
|
-
|
|
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')}`);
|
|
1266
|
+
else {
|
|
1267
|
+
lines.push(`${theme.ui.muted('?')} shortcuts`);
|
|
1670
1268
|
}
|
|
1671
|
-
|
|
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];
|
|
1269
|
+
return { lines, cursorRow: promptRow, cursorCol: promptCol };
|
|
1680
1270
|
}
|
|
1681
1271
|
abbreviatePath(pathValue) {
|
|
1682
1272
|
const home = homedir();
|
|
@@ -1685,123 +1275,57 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1685
1275
|
}
|
|
1686
1276
|
return pathValue;
|
|
1687
1277
|
}
|
|
1688
|
-
|
|
1689
|
-
const
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
const
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
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());
|
|
1278
|
+
buildActivityLine(maxWidth) {
|
|
1279
|
+
const base = this.statusStreaming || this.statusOverride || this.statusMessage || 'Ready';
|
|
1280
|
+
const spinner = this.mode === 'streaming' ? colorizeActivity(this.activityTicker()) : theme.ui.muted('∙');
|
|
1281
|
+
const elapsed = this.mode === 'streaming' ? formatElapsed(this.streamingStart) : null;
|
|
1282
|
+
const tokenText = this.mode === 'streaming' ? formatTokenDelta(this.statusMeta.tokensUsed) : null;
|
|
1283
|
+
const parts = [
|
|
1284
|
+
`${spinner} ${base}`,
|
|
1285
|
+
elapsed ? theme.ui.muted(elapsed) : null,
|
|
1286
|
+
tokenText ? theme.ui.muted(tokenText) : null,
|
|
1287
|
+
].filter(Boolean);
|
|
1288
|
+
if (!parts.length) {
|
|
1289
|
+
return null;
|
|
1710
1290
|
}
|
|
1711
|
-
return this.
|
|
1291
|
+
return this.truncateLine(parts.join(theme.ui.muted(' • ')), maxWidth);
|
|
1712
1292
|
}
|
|
1713
|
-
|
|
1293
|
+
buildModelLine(maxWidth) {
|
|
1714
1294
|
const segments = [];
|
|
1715
1295
|
if (this.statusMeta.profile) {
|
|
1716
|
-
segments.push(
|
|
1296
|
+
segments.push(`${theme.ui.muted('profile')} ${theme.info(this.statusMeta.profile)}`);
|
|
1717
1297
|
}
|
|
1718
1298
|
const model = this.statusMeta.provider && this.statusMeta.model
|
|
1719
1299
|
? `${this.statusMeta.provider} / ${this.statusMeta.model}`
|
|
1720
1300
|
: this.statusMeta.model || this.statusMeta.provider;
|
|
1721
1301
|
if (model) {
|
|
1722
|
-
segments.push(
|
|
1302
|
+
segments.push(`${theme.ui.muted('model')} ${theme.info(model)}`);
|
|
1723
1303
|
}
|
|
1724
1304
|
const workspace = this.statusMeta.workspace || this.statusMeta.directory;
|
|
1725
1305
|
if (workspace) {
|
|
1726
|
-
segments.push(
|
|
1306
|
+
segments.push(`${theme.ui.muted('dir')} ${theme.ui.muted(this.abbreviatePath(workspace))}`);
|
|
1727
1307
|
}
|
|
1728
1308
|
if (this.statusMeta.writes) {
|
|
1729
|
-
segments.push(
|
|
1309
|
+
segments.push(`${theme.ui.muted('writes')} ${theme.ui.muted(this.statusMeta.writes)}`);
|
|
1730
1310
|
}
|
|
1731
1311
|
if (this.statusMeta.toolSummary) {
|
|
1732
|
-
segments.push(
|
|
1312
|
+
segments.push(`${theme.ui.muted('tools')} ${theme.ui.muted(this.statusMeta.toolSummary)}`);
|
|
1313
|
+
}
|
|
1314
|
+
if (this.statusMeta.version) {
|
|
1315
|
+
segments.push(`${theme.ui.muted('v')} ${theme.ui.muted(this.statusMeta.version)}`);
|
|
1733
1316
|
}
|
|
1734
1317
|
if (this.statusMeta.sessionLabel) {
|
|
1735
|
-
segments.push(
|
|
1318
|
+
segments.push(`${theme.ui.muted('session')} ${theme.ui.muted(this.statusMeta.sessionLabel)}`);
|
|
1736
1319
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1320
|
+
const ctxTarget = this.renderedContextPercent ?? this.statusMeta.contextPercent;
|
|
1321
|
+
const contextBar = formatTinyProgressBar(ctxTarget, 10);
|
|
1322
|
+
if (contextBar) {
|
|
1323
|
+
segments.push(`${theme.ui.muted('ctx')} ${theme.ui.muted(contextBar)}`);
|
|
1739
1324
|
}
|
|
1740
1325
|
if (segments.length === 0) {
|
|
1741
|
-
return [];
|
|
1742
|
-
}
|
|
1743
|
-
return this.wrapSegments(segments, maxWidth);
|
|
1744
|
-
}
|
|
1745
|
-
composeStatusLabel() {
|
|
1746
|
-
const statuses = [this.statusStreaming, this.statusOverride, this.statusMessage].filter((value, index, arr) => Boolean(value) && arr.indexOf(value) === index);
|
|
1747
|
-
const text = statuses.length > 0 ? statuses.join(' / ') : 'Ready for prompts';
|
|
1748
|
-
if (!text.trim()) {
|
|
1749
1326
|
return null;
|
|
1750
1327
|
}
|
|
1751
|
-
|
|
1752
|
-
const tone = normalized.includes('ready') ? 'success' : 'info';
|
|
1753
|
-
return { text, tone };
|
|
1754
|
-
}
|
|
1755
|
-
formatMetaSegment(label, value, tone) {
|
|
1756
|
-
const colorizer = tone === 'success'
|
|
1757
|
-
? theme.success
|
|
1758
|
-
: tone === 'warn'
|
|
1759
|
-
? theme.warning
|
|
1760
|
-
: tone === 'error'
|
|
1761
|
-
? theme.error
|
|
1762
|
-
: tone === 'muted'
|
|
1763
|
-
? theme.ui.muted
|
|
1764
|
-
: theme.info;
|
|
1765
|
-
return `${theme.ui.muted(label)} ${colorizer(value)}`;
|
|
1766
|
-
}
|
|
1767
|
-
applyTone(text, tone) {
|
|
1768
|
-
switch (tone) {
|
|
1769
|
-
case 'success':
|
|
1770
|
-
return theme.success(text);
|
|
1771
|
-
case 'warn':
|
|
1772
|
-
return theme.warning(text);
|
|
1773
|
-
case 'error':
|
|
1774
|
-
return theme.error(text);
|
|
1775
|
-
case 'info':
|
|
1776
|
-
default:
|
|
1777
|
-
return theme.info(text);
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
wrapSegments(segments, maxWidth) {
|
|
1781
|
-
const lines = [];
|
|
1782
|
-
const separator = theme.ui.muted(' | ');
|
|
1783
|
-
let current = '';
|
|
1784
|
-
for (const segment of segments) {
|
|
1785
|
-
const normalized = segment.trim();
|
|
1786
|
-
if (!normalized)
|
|
1787
|
-
continue;
|
|
1788
|
-
if (!current) {
|
|
1789
|
-
current = this.truncateLine(normalized, maxWidth);
|
|
1790
|
-
continue;
|
|
1791
|
-
}
|
|
1792
|
-
const candidate = `${current}${separator}${normalized}`;
|
|
1793
|
-
if (this.visibleLength(candidate) <= maxWidth) {
|
|
1794
|
-
current = candidate;
|
|
1795
|
-
}
|
|
1796
|
-
else {
|
|
1797
|
-
lines.push(this.truncateLine(current, maxWidth));
|
|
1798
|
-
current = this.truncateLine(normalized, maxWidth);
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
if (current) {
|
|
1802
|
-
lines.push(this.truncateLine(current, maxWidth));
|
|
1803
|
-
}
|
|
1804
|
-
return lines;
|
|
1328
|
+
return this.truncateLine(segments.join(theme.ui.muted(' • ')), maxWidth);
|
|
1805
1329
|
}
|
|
1806
1330
|
buildControlLines() {
|
|
1807
1331
|
const lines = [];
|
|
@@ -1815,34 +1339,12 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1815
1339
|
}
|
|
1816
1340
|
return lines;
|
|
1817
1341
|
}
|
|
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
1342
|
buildToggleLine() {
|
|
1842
1343
|
const toggles = [];
|
|
1843
1344
|
const addToggle = (label, on, hotkey, value) => {
|
|
1844
1345
|
toggles.push({ label, on, hotkey: this.formatHotkey(hotkey), value });
|
|
1845
1346
|
};
|
|
1347
|
+
addToggle('Auto', this.toggleState.autoContinueEnabled, this.toggleState.autoContinueHotkey);
|
|
1846
1348
|
addToggle('Verify', this.toggleState.verificationEnabled, this.toggleState.verificationHotkey);
|
|
1847
1349
|
const approvalMode = this.toggleState.criticalApprovalMode || 'auto';
|
|
1848
1350
|
const approvalActive = approvalMode !== 'auto';
|
|
@@ -1887,129 +1389,86 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1887
1389
|
addHotkey('interrupt', 'Ctrl+C');
|
|
1888
1390
|
addHotkey('clear input', 'Ctrl+U');
|
|
1889
1391
|
// Feature toggles (only if hotkeys are defined)
|
|
1392
|
+
addHotkey('auto-run', this.toggleState.autoContinueHotkey);
|
|
1890
1393
|
addHotkey('verify', this.toggleState.verificationHotkey);
|
|
1891
1394
|
addHotkey('thinking', this.toggleState.thinkingHotkey);
|
|
1892
1395
|
if (parts.length === 0) {
|
|
1893
1396
|
return null;
|
|
1894
1397
|
}
|
|
1895
|
-
|
|
1398
|
+
const body = parts.join(theme.ui.muted(' '));
|
|
1399
|
+
return `${body}${theme.ui.muted(' ? shortcuts')}`;
|
|
1896
1400
|
}
|
|
1897
|
-
|
|
1401
|
+
buildInputOverlay(maxWidth) {
|
|
1402
|
+
const prompt = theme.primary('› ');
|
|
1403
|
+
const promptWidth = this.visibleLength(prompt);
|
|
1404
|
+
const usableWidth = Math.max(8, maxWidth - promptWidth);
|
|
1898
1405
|
if (this.collapsedPaste) {
|
|
1899
1406
|
const summary = `[pasted ${this.collapsedPaste.lines} lines, ${this.collapsedPaste.chars} chars] (Ctrl+L insert, Backspace discard)`;
|
|
1900
|
-
|
|
1407
|
+
const line = `${prompt}${theme.ui.muted(summary)}`;
|
|
1408
|
+
const cursorCol = Math.min(maxWidth, this.visibleLength(line) + 1);
|
|
1409
|
+
return { lines: [this.truncateLine(line, maxWidth)], cursorRow: 0, cursorCol };
|
|
1901
1410
|
}
|
|
1902
|
-
// Claude Code uses simple '>' prompt
|
|
1903
|
-
const prompt = theme.primary('> ');
|
|
1904
|
-
const promptWidth = this.visibleLength(prompt);
|
|
1905
|
-
const maxWidth = this.safeWidth();
|
|
1906
|
-
const continuationIndent = ' '; // 2 spaces for continuation lines
|
|
1907
|
-
const continuationWidth = continuationIndent.length;
|
|
1908
|
-
// Handle multi-line input - split buffer on newlines first
|
|
1909
1411
|
const normalized = this.buffer.replace(/\r/g, '\n');
|
|
1910
|
-
const
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
let
|
|
1914
|
-
|
|
1412
|
+
const cursorIndex = Math.min(this.cursor, normalized.length);
|
|
1413
|
+
const rawLines = [];
|
|
1414
|
+
let current = '';
|
|
1415
|
+
let row = 0;
|
|
1416
|
+
const limitForRow = (r) => (r === 0 ? usableWidth : usableWidth);
|
|
1417
|
+
let cursorRow = 0;
|
|
1915
1418
|
let cursorCol = 0;
|
|
1916
|
-
let
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
// Build the display line
|
|
1932
|
-
let displayLine;
|
|
1933
|
-
if (isFirstLogicalLine && isFirstDisplayLine) {
|
|
1934
|
-
displayLine = `${prompt}${chunk}`;
|
|
1935
|
-
}
|
|
1936
|
-
else {
|
|
1937
|
-
displayLine = `${continuationIndent}${chunk}`;
|
|
1938
|
-
}
|
|
1939
|
-
// Track cursor position
|
|
1940
|
-
if (!foundCursor) {
|
|
1941
|
-
const chunkStart = lineStartChar + (line.length - remaining.length - chunk.length);
|
|
1942
|
-
const chunkEnd = chunkStart + chunk.length;
|
|
1943
|
-
if (this.cursor >= chunkStart && this.cursor <= chunkEnd) {
|
|
1944
|
-
cursorLine = result.length;
|
|
1945
|
-
const offsetInChunk = this.cursor - chunkStart;
|
|
1946
|
-
cursorCol = ((isFirstLogicalLine && isFirstDisplayLine) ? promptWidth : continuationWidth) + offsetInChunk;
|
|
1947
|
-
foundCursor = true;
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
1950
|
-
result.push(displayLine);
|
|
1951
|
-
isFirstDisplayLine = false;
|
|
1952
|
-
// If nothing left and this was an empty line, we already added it
|
|
1953
|
-
if (remaining.length === 0 && chunk.length === 0)
|
|
1954
|
-
break;
|
|
1419
|
+
for (let idx = 0; idx <= normalized.length; idx += 1) {
|
|
1420
|
+
if (idx === cursorIndex) {
|
|
1421
|
+
cursorRow = row;
|
|
1422
|
+
cursorCol = current.length;
|
|
1423
|
+
}
|
|
1424
|
+
if (idx === normalized.length) {
|
|
1425
|
+
break;
|
|
1426
|
+
}
|
|
1427
|
+
const char = normalized[idx];
|
|
1428
|
+
if (char === '\n') {
|
|
1429
|
+
current += NEWLINE_PLACEHOLDER;
|
|
1430
|
+
rawLines.push(current);
|
|
1431
|
+
row += 1;
|
|
1432
|
+
current = '';
|
|
1433
|
+
continue;
|
|
1955
1434
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
}
|
|
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
|
|
1979
|
-
this.cursorVisibleColumn = cursorCol + 1;
|
|
1980
|
-
return result.join('\n');
|
|
1981
|
-
}
|
|
1982
|
-
buildInputWindow(available) {
|
|
1983
|
-
if (available <= 0) {
|
|
1984
|
-
return { text: '', cursor: 0 };
|
|
1435
|
+
const limit = limitForRow(row);
|
|
1436
|
+
if (current.length >= limit) {
|
|
1437
|
+
rawLines.push(current);
|
|
1438
|
+
row += 1;
|
|
1439
|
+
current = '';
|
|
1440
|
+
}
|
|
1441
|
+
current += char;
|
|
1985
1442
|
}
|
|
1986
|
-
if (
|
|
1987
|
-
|
|
1443
|
+
if (current || rawLines.length === 0) {
|
|
1444
|
+
rawLines.push(current);
|
|
1988
1445
|
}
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
1446
|
+
if (cursorIndex === normalized.length) {
|
|
1447
|
+
cursorRow = rawLines.length - 1;
|
|
1448
|
+
cursorCol = rawLines[cursorRow]?.length ?? 0;
|
|
1449
|
+
}
|
|
1450
|
+
const prefixPad = ' '.repeat(promptWidth);
|
|
1451
|
+
const lines = [];
|
|
1452
|
+
let cursorColumn = 1;
|
|
1453
|
+
rawLines.forEach((line, index) => {
|
|
1454
|
+
const prefix = index === 0 ? prompt : prefixPad;
|
|
1455
|
+
const onCursorLine = index === cursorRow;
|
|
1456
|
+
const text = line ?? '';
|
|
1457
|
+
const col = Math.min(Math.max(cursorCol, 0), text.length);
|
|
1458
|
+
let rendered = text;
|
|
1459
|
+
if (onCursorLine) {
|
|
1460
|
+
const before = rendered.slice(0, col);
|
|
1461
|
+
const at = rendered.charAt(col) || ' ';
|
|
1462
|
+
const after = rendered.slice(col + 1);
|
|
1463
|
+
rendered = `${before}${ESC.REVERSE}${at}${ESC.RESET}${after}`;
|
|
1464
|
+
cursorColumn = this.visibleLength(prefix) + col + 1;
|
|
1465
|
+
}
|
|
1466
|
+
lines.push(`${prefix}${rendered}`);
|
|
1467
|
+
});
|
|
2010
1468
|
return {
|
|
2011
|
-
|
|
2012
|
-
|
|
1469
|
+
lines,
|
|
1470
|
+
cursorRow,
|
|
1471
|
+
cursorCol: cursorColumn,
|
|
2013
1472
|
};
|
|
2014
1473
|
}
|
|
2015
1474
|
expandCollapsedPaste() {
|
|
@@ -2029,7 +1488,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
2029
1488
|
if (options.resetBuffer) {
|
|
2030
1489
|
this.buffer = '';
|
|
2031
1490
|
this.cursor = 0;
|
|
2032
|
-
this.inputRenderOffset = 0;
|
|
2033
1491
|
this.resetSuggestions();
|
|
2034
1492
|
this.renderPrompt();
|
|
2035
1493
|
this.emitInputChange();
|
|
@@ -2108,6 +1566,17 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
2108
1566
|
}
|
|
2109
1567
|
return result;
|
|
2110
1568
|
}
|
|
1569
|
+
clearOverlayRows(rows, startRow) {
|
|
1570
|
+
const totalRows = this.rows || 24;
|
|
1571
|
+
const limit = Math.max(0, Math.min(rows, totalRows));
|
|
1572
|
+
for (let idx = 0; idx < limit; idx++) {
|
|
1573
|
+
const row = startRow + idx;
|
|
1574
|
+
if (row < 1 || row > totalRows)
|
|
1575
|
+
continue;
|
|
1576
|
+
this.write(ESC.TO(row, 1));
|
|
1577
|
+
this.write(ESC.CLEAR_LINE);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
2111
1580
|
getBuffer() {
|
|
2112
1581
|
return this.buffer;
|
|
2113
1582
|
}
|
|
@@ -2117,7 +1586,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
2117
1586
|
setBuffer(text, cursorPos) {
|
|
2118
1587
|
this.buffer = text;
|
|
2119
1588
|
this.cursor = cursorPos ?? text.length;
|
|
2120
|
-
this.inputRenderOffset = 0;
|
|
2121
1589
|
this.updateSuggestions();
|
|
2122
1590
|
this.renderPrompt();
|
|
2123
1591
|
this.emitInputChange();
|
|
@@ -2126,7 +1594,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
2126
1594
|
this.cancelPlainPasteCapture();
|
|
2127
1595
|
this.buffer = '';
|
|
2128
1596
|
this.cursor = 0;
|
|
2129
|
-
this.inputRenderOffset = 0;
|
|
2130
1597
|
this.suggestions = [];
|
|
2131
1598
|
this.suggestionIndex = -1;
|
|
2132
1599
|
this.renderPrompt();
|
|
@@ -2135,35 +1602,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
2135
1602
|
setModeStatus(status) {
|
|
2136
1603
|
this.updateStatus(status);
|
|
2137
1604
|
}
|
|
2138
|
-
/**
|
|
2139
|
-
* Show a compacting status with animated spinner (Claude Code style)
|
|
2140
|
-
* Uses ✻ character with animation to indicate context compaction in progress
|
|
2141
|
-
*/
|
|
2142
|
-
showCompactingStatus(message) {
|
|
2143
|
-
this.statusMessage = message;
|
|
2144
|
-
if (!this.spinnerInterval) {
|
|
2145
|
-
this.spinnerInterval = setInterval(() => {
|
|
2146
|
-
this.spinnerFrame++;
|
|
2147
|
-
// Cycle activity phrase every ~4 seconds (50 frames at 80ms)
|
|
2148
|
-
if (this.spinnerFrame % 50 === 0) {
|
|
2149
|
-
this.activityPhraseIndex++;
|
|
2150
|
-
}
|
|
2151
|
-
this.renderPrompt();
|
|
2152
|
-
}, 80);
|
|
2153
|
-
}
|
|
2154
|
-
this.renderPrompt();
|
|
2155
|
-
}
|
|
2156
|
-
/**
|
|
2157
|
-
* Hide the compacting status and stop spinner animation
|
|
2158
|
-
*/
|
|
2159
|
-
hideCompactingStatus() {
|
|
2160
|
-
if (this.spinnerInterval) {
|
|
2161
|
-
clearInterval(this.spinnerInterval);
|
|
2162
|
-
this.spinnerInterval = null;
|
|
2163
|
-
}
|
|
2164
|
-
this.statusMessage = null;
|
|
2165
|
-
this.renderPrompt();
|
|
2166
|
-
}
|
|
2167
1605
|
emitPrompt(content) {
|
|
2168
1606
|
this.pushPromptEvent(content);
|
|
2169
1607
|
}
|
|
@@ -2207,30 +1645,23 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
2207
1645
|
const height = this.lastOverlay?.lines.length ?? this.promptHeight ?? 0;
|
|
2208
1646
|
if (height === 0)
|
|
2209
1647
|
return;
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
for (let i = 0; i < height; i++) {
|
|
2219
|
-
this.write('\r');
|
|
1648
|
+
this.updateTerminalSize();
|
|
1649
|
+
const totalRows = this.rows || 24;
|
|
1650
|
+
const startRow = Math.max(1, Math.max(1, totalRows - this.overlayBottomPadding) - height + 1);
|
|
1651
|
+
this.clearOverlayRows(height, startRow);
|
|
1652
|
+
// Keep the padding row clean as well
|
|
1653
|
+
const paddingRow = startRow + height;
|
|
1654
|
+
if (this.overlayBottomPadding > 0 && paddingRow <= totalRows) {
|
|
1655
|
+
this.write(ESC.TO(paddingRow, 1));
|
|
2220
1656
|
this.write(ESC.CLEAR_LINE);
|
|
2221
|
-
if (i < height - 1) {
|
|
2222
|
-
this.write('\x1b[B');
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2225
|
-
// Move back to top (where content should continue from)
|
|
2226
|
-
if (height > 1) {
|
|
2227
|
-
this.write(`\x1b[${height - 1}A`);
|
|
2228
1657
|
}
|
|
2229
|
-
|
|
1658
|
+
// Move cursor to the bottom ready for new scrollback output
|
|
1659
|
+
this.write(ESC.TO(totalRows, 1));
|
|
1660
|
+
this.lastOverlayHeight = height;
|
|
1661
|
+
this.lastPromptIndex = this.lastOverlay?.promptIndex ?? this.lastPromptIndex;
|
|
2230
1662
|
this.lastOverlay = null;
|
|
1663
|
+
this.overlayInvalidated = true;
|
|
2231
1664
|
this.promptHeight = 0;
|
|
2232
|
-
this.lastOverlayHeight = 0;
|
|
2233
|
-
this.isPromptActive = false;
|
|
2234
1665
|
}
|
|
2235
1666
|
updateTerminalSize() {
|
|
2236
1667
|
if (this.output.isTTY) {
|