erosolar-cli 1.7.429 → 1.7.430
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/enhancedGitCapability.js +3 -3
- package/dist/capabilities/enhancedGitCapability.js.map +1 -1
- package/dist/capabilities/learnCapability.d.ts +1 -1
- package/dist/capabilities/learnCapability.d.ts.map +1 -1
- package/dist/capabilities/learnCapability.js +1 -1
- package/dist/capabilities/learnCapability.js.map +1 -1
- package/dist/core/checkpoint.d.ts +1 -1
- package/dist/core/checkpoint.js +1 -1
- package/dist/core/costTracker.d.ts +1 -1
- package/dist/core/costTracker.js +1 -1
- package/dist/core/hooks.d.ts +1 -1
- package/dist/core/hooks.js +1 -1
- package/dist/core/memorySystem.d.ts +2 -2
- package/dist/core/memorySystem.js +2 -2
- package/dist/core/outputStyles.d.ts +2 -2
- package/dist/core/outputStyles.js +2 -2
- package/dist/core/validationRunner.d.ts +1 -1
- package/dist/core/validationRunner.js +1 -1
- package/dist/shell/interactiveShell.d.ts +5 -1
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +115 -83
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +13 -34
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +75 -83
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +159 -220
- package/dist/shell/terminalInputAdapter.js.map +1 -1
- package/dist/shell/vimMode.d.ts +1 -1
- package/dist/shell/vimMode.js +1 -1
- package/dist/tools/buildTools.d.ts +1 -1
- package/dist/tools/buildTools.js +1 -1
- package/dist/tools/diffUtils.d.ts +2 -2
- package/dist/tools/diffUtils.js +2 -2
- package/dist/tools/editTools.js +4 -4
- package/dist/tools/editTools.js.map +1 -1
- package/dist/tools/localExplore.d.ts +3 -3
- package/dist/tools/localExplore.js +3 -3
- package/dist/tools/skillTools.js +2 -2
- package/dist/tools/skillTools.js.map +1 -1
- package/dist/tools/validationTools.js +1 -1
- package/dist/ui/DisplayEventQueue.d.ts +99 -0
- package/dist/ui/DisplayEventQueue.d.ts.map +1 -0
- package/dist/ui/DisplayEventQueue.js +167 -0
- package/dist/ui/DisplayEventQueue.js.map +1 -0
- package/dist/ui/SequentialRenderer.d.ts +69 -0
- package/dist/ui/SequentialRenderer.d.ts.map +1 -0
- package/dist/ui/SequentialRenderer.js +137 -0
- package/dist/ui/SequentialRenderer.js.map +1 -0
- package/dist/ui/ShellUIAdapter.d.ts +18 -6
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +65 -14
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/UnifiedUIRenderer.d.ts +184 -0
- package/dist/ui/UnifiedUIRenderer.d.ts.map +1 -0
- package/dist/ui/UnifiedUIRenderer.js +567 -0
- package/dist/ui/UnifiedUIRenderer.js.map +1 -0
- package/dist/ui/display.d.ts +100 -173
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +359 -927
- package/dist/ui/display.js.map +1 -1
- package/dist/ui/errorFormatter.d.ts +1 -1
- package/dist/ui/errorFormatter.js +1 -1
- package/dist/ui/shortcutsHelp.d.ts +6 -6
- package/dist/ui/shortcutsHelp.js +6 -6
- package/dist/ui/streamingFormatter.d.ts +2 -5
- package/dist/ui/streamingFormatter.d.ts.map +1 -1
- package/dist/ui/streamingFormatter.js +9 -33
- package/dist/ui/streamingFormatter.js.map +1 -1
- package/dist/ui/textHighlighter.d.ts +8 -8
- package/dist/ui/textHighlighter.js +9 -9
- package/dist/ui/textHighlighter.js.map +1 -1
- package/dist/ui/theme.d.ts +2 -2
- package/dist/ui/theme.js +4 -4
- package/dist/ui/theme.js.map +1 -1
- package/dist/ui/toolDisplay.d.ts +8 -8
- package/dist/ui/toolDisplay.js +8 -8
- package/package.json +1 -1
- package/dist/shell/terminalInput.d.ts +0 -619
- package/dist/shell/terminalInput.d.ts.map +0 -1
- package/dist/shell/terminalInput.js +0 -2699
- package/dist/shell/terminalInput.js.map +0 -1
package/dist/ui/display.js
CHANGED
|
@@ -1,146 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Display - Simplified UI facade that routes all output through UnifiedUIRenderer
|
|
3
|
+
*
|
|
4
|
+
* This class now serves as a compatibility layer, providing the same API
|
|
5
|
+
* but delegating all actual rendering to UnifiedUIRenderer.
|
|
6
|
+
*/
|
|
3
7
|
import { theme, icons } from './theme.js';
|
|
4
|
-
import {
|
|
8
|
+
import { renderMessagePanel, renderMessageBody } from './richText.js';
|
|
5
9
|
import { getTerminalColumns } from './layout.js';
|
|
6
10
|
import { highlightError } from './textHighlighter.js';
|
|
7
11
|
import { renderSectionHeading } from './designSystem.js';
|
|
8
12
|
import { isPlainOutputMode } from './outputMode.js';
|
|
9
|
-
import { writeLock } from './writeLock.js';
|
|
10
|
-
import { renderStatusLine } from './unified/layout.js';
|
|
11
|
-
import { isStreamingMode } from './globalWriteLock.js';
|
|
12
|
-
/**
|
|
13
|
-
* Output lock to prevent race conditions during spinner/stream output.
|
|
14
|
-
* Ensures that spinner frames don't interleave with streamed content.
|
|
15
|
-
* Uses a simple lock mechanism suitable for Node.js single-threaded event loop.
|
|
16
|
-
*/
|
|
17
|
-
class OutputLock {
|
|
18
|
-
static instance = null;
|
|
19
|
-
locked = false;
|
|
20
|
-
pendingCallbacks = [];
|
|
21
|
-
static getInstance() {
|
|
22
|
-
if (!OutputLock.instance) {
|
|
23
|
-
OutputLock.instance = new OutputLock();
|
|
24
|
-
}
|
|
25
|
-
return OutputLock.instance;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Synchronously check if output is locked (spinner is active).
|
|
29
|
-
* Used to prevent stream writes during spinner animation.
|
|
30
|
-
*/
|
|
31
|
-
isLocked() {
|
|
32
|
-
return this.locked;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Lock output during spinner animation.
|
|
36
|
-
*/
|
|
37
|
-
lock() {
|
|
38
|
-
this.locked = true;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Unlock output and process any pending callbacks.
|
|
42
|
-
*/
|
|
43
|
-
unlock() {
|
|
44
|
-
this.locked = false;
|
|
45
|
-
// Process any pending writes
|
|
46
|
-
while (this.pendingCallbacks.length > 0) {
|
|
47
|
-
const callback = this.pendingCallbacks.shift();
|
|
48
|
-
if (callback) {
|
|
49
|
-
callback();
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Execute a callback safely, queueing if output is locked.
|
|
55
|
-
*/
|
|
56
|
-
safeWrite(callback) {
|
|
57
|
-
if (this.locked) {
|
|
58
|
-
this.pendingCallbacks.push(callback);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
callback();
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Tracks line output to stdout for banner rewriting and cursor positioning.
|
|
66
|
-
* Instances are cached per stream to keep banner calculations consistent.
|
|
67
|
-
*/
|
|
68
|
-
class StdoutLineTracker {
|
|
69
|
-
static instances = new WeakMap();
|
|
70
|
-
static getInstance(stream = process.stdout) {
|
|
71
|
-
const existing = StdoutLineTracker.instances.get(stream);
|
|
72
|
-
if (existing) {
|
|
73
|
-
return existing;
|
|
74
|
-
}
|
|
75
|
-
const tracker = new StdoutLineTracker(stream);
|
|
76
|
-
StdoutLineTracker.instances.set(stream, tracker);
|
|
77
|
-
return tracker;
|
|
78
|
-
}
|
|
79
|
-
linesWritten = 0;
|
|
80
|
-
suspended = false;
|
|
81
|
-
stream;
|
|
82
|
-
originalWrite;
|
|
83
|
-
constructor(stream) {
|
|
84
|
-
this.stream = stream;
|
|
85
|
-
this.originalWrite = stream.write.bind(stream);
|
|
86
|
-
this.patchStream();
|
|
87
|
-
}
|
|
88
|
-
get totalLines() {
|
|
89
|
-
return this.linesWritten;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Temporarily suspends line tracking while executing a function.
|
|
93
|
-
* Useful for rewriting content without incrementing line count.
|
|
94
|
-
*/
|
|
95
|
-
withSuspended(fn) {
|
|
96
|
-
const wasSuspended = this.suspended;
|
|
97
|
-
this.suspended = true;
|
|
98
|
-
try {
|
|
99
|
-
return fn();
|
|
100
|
-
}
|
|
101
|
-
finally {
|
|
102
|
-
this.suspended = wasSuspended;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
reset() {
|
|
106
|
-
this.linesWritten = 0;
|
|
107
|
-
}
|
|
108
|
-
patchStream() {
|
|
109
|
-
const tracker = this;
|
|
110
|
-
this.stream.write = function patched(chunk, encoding, callback) {
|
|
111
|
-
const actualEncoding = typeof encoding === 'function' ? undefined : encoding;
|
|
112
|
-
tracker.recordChunk(chunk, actualEncoding);
|
|
113
|
-
return tracker.originalWrite.call(this, chunk, encoding, callback);
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
recordChunk(chunk, encoding) {
|
|
117
|
-
if (this.suspended) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
const text = this.chunkToString(chunk, encoding);
|
|
121
|
-
if (!text) {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
this.countNewlines(text);
|
|
125
|
-
}
|
|
126
|
-
countNewlines(text) {
|
|
127
|
-
for (const char of text) {
|
|
128
|
-
if (char === '\n') {
|
|
129
|
-
this.linesWritten += 1;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
chunkToString(chunk, encoding) {
|
|
134
|
-
if (typeof chunk === 'string') {
|
|
135
|
-
return chunk;
|
|
136
|
-
}
|
|
137
|
-
if (chunk instanceof Uint8Array) {
|
|
138
|
-
const enc = encoding ?? 'utf8';
|
|
139
|
-
return Buffer.from(chunk).toString(enc);
|
|
140
|
-
}
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
13
|
// Display configuration constants
|
|
145
14
|
const DISPLAY_CONSTANTS = {
|
|
146
15
|
MIN_BANNER_WIDTH: 32,
|
|
@@ -155,60 +24,33 @@ const DISPLAY_CONSTANTS = {
|
|
|
155
24
|
MAX_THOUGHT_WIDTH: 96,
|
|
156
25
|
MIN_CONTENT_WIDTH: 10,
|
|
157
26
|
MIN_WRAP_WIDTH: 12,
|
|
158
|
-
SPINNER_INTERVAL: 80,
|
|
159
27
|
};
|
|
160
|
-
// Claude Code style spinner frames: ✻ as primary thinking indicator
|
|
161
|
-
// Alternates between ✻ and ◐ for visual interest
|
|
162
|
-
const SPINNER_FRAMES = ['✻', '◐', '✻', '◓', '✻', '◑', '✻', '◒'];
|
|
163
28
|
/**
|
|
164
|
-
* Display class
|
|
165
|
-
*
|
|
166
|
-
* Architecture:
|
|
167
|
-
* - Per-stream line tracking via StdoutLineTracker for consistent banner updates
|
|
168
|
-
* - Output interceptor pattern for live update integration
|
|
169
|
-
* - Banner state management for in-place updates
|
|
170
|
-
* - Configurable width constraints via DISPLAY_CONSTANTS
|
|
29
|
+
* Display class - now a thin wrapper around UnifiedUIRenderer
|
|
171
30
|
*
|
|
172
|
-
*
|
|
173
|
-
* - ⏺ prefix for tool calls, actions, and thinking/reasoning
|
|
174
|
-
* - ⎿ prefix for results, details, and nested information
|
|
175
|
-
* - ─ horizontal separators for dividing sections (edit diffs, etc.)
|
|
176
|
-
* - > prefix for user prompts (handled in theme.ts formatUserPrompt)
|
|
177
|
-
* - Compact epsilon spinner: ∴, ε, ✻
|
|
178
|
-
*
|
|
179
|
-
* Key responsibilities:
|
|
180
|
-
* - Welcome banners and session information display
|
|
181
|
-
* - Message formatting (assistant, system, errors, warnings)
|
|
182
|
-
* - Spinner/thinking indicators
|
|
183
|
-
* - Action and sub-action formatting with tree-style prefixes
|
|
184
|
-
* - Text wrapping and layout management
|
|
185
|
-
*
|
|
186
|
-
* Error handling:
|
|
187
|
-
* - Graceful degradation for non-TTY environments
|
|
188
|
-
* - Input validation on public methods
|
|
189
|
-
* - Safe cursor manipulation with fallback
|
|
31
|
+
* Provides backward-compatible API while routing all output through the renderer.
|
|
190
32
|
*/
|
|
191
33
|
export class Display {
|
|
192
|
-
stdoutTracker;
|
|
193
34
|
outputStream;
|
|
194
35
|
errorStream;
|
|
195
|
-
|
|
36
|
+
renderer = null;
|
|
196
37
|
outputInterceptors = new Set();
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
captureStack = [];
|
|
38
|
+
// Legacy spinner support for compatibility with existing tests/callers
|
|
39
|
+
activeSpinner = null;
|
|
200
40
|
thinkingStartTime = null;
|
|
201
|
-
thinkingElapsedTimer = null;
|
|
202
|
-
thinkingBaseMessage = 'Thinking...';
|
|
203
|
-
pendingCarriageReturn = false;
|
|
204
|
-
// Streaming status line (Claude Code style - fixed at bottom using scroll region)
|
|
205
|
-
streamingStatusVisible = false;
|
|
206
|
-
scrollRegionActive = false;
|
|
207
|
-
savedCursorRow = 0;
|
|
208
41
|
constructor(stream = process.stdout, errorStream) {
|
|
209
42
|
this.outputStream = stream;
|
|
210
43
|
this.errorStream = errorStream ?? stream;
|
|
211
|
-
|
|
44
|
+
}
|
|
45
|
+
setRenderer(renderer) {
|
|
46
|
+
this.renderer = renderer;
|
|
47
|
+
}
|
|
48
|
+
enqueueEvent(type, content) {
|
|
49
|
+
if (!this.renderer || !content) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
this.renderer.addEvent(type, content);
|
|
53
|
+
return true;
|
|
212
54
|
}
|
|
213
55
|
registerOutputInterceptor(interceptor) {
|
|
214
56
|
if (!interceptor) {
|
|
@@ -219,38 +61,6 @@ export class Display {
|
|
|
219
61
|
this.outputInterceptors.delete(interceptor);
|
|
220
62
|
};
|
|
221
63
|
}
|
|
222
|
-
/**
|
|
223
|
-
* Execute output with proper write lock coordination.
|
|
224
|
-
* All display output goes through this method to prevent race conditions.
|
|
225
|
-
*
|
|
226
|
-
* Behavior:
|
|
227
|
-
* - If write lock is already held, execute directly (we're in a protected context)
|
|
228
|
-
* - Otherwise acquire lock during output to prevent interleaving
|
|
229
|
-
* - Notifies interceptors before/after output for cursor positioning
|
|
230
|
-
*/
|
|
231
|
-
withOutput(fn) {
|
|
232
|
-
const run = () => {
|
|
233
|
-
this.notifyBeforeOutput();
|
|
234
|
-
this.pushCaptureBuffer();
|
|
235
|
-
try {
|
|
236
|
-
fn();
|
|
237
|
-
}
|
|
238
|
-
finally {
|
|
239
|
-
const captured = this.popCaptureBuffer();
|
|
240
|
-
this.notifyAfterOutput(captured || undefined);
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
// If lock is already held, execute directly - we're in a protected context
|
|
244
|
-
// This prevents queuing issues where content gets delayed
|
|
245
|
-
if (writeLock.isLocked()) {
|
|
246
|
-
run();
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
// Acquire lock during output to prevent interleaved writes
|
|
250
|
-
writeLock.withLock(() => {
|
|
251
|
-
run();
|
|
252
|
-
}, 'display.withOutput');
|
|
253
|
-
}
|
|
254
64
|
notifyBeforeOutput() {
|
|
255
65
|
for (const interceptor of this.outputInterceptors) {
|
|
256
66
|
interceptor.beforeWrite?.();
|
|
@@ -262,234 +72,92 @@ export class Display {
|
|
|
262
72
|
interceptors[index]?.afterWrite?.(content);
|
|
263
73
|
}
|
|
264
74
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Clear any active spinner for backward compatibility with legacy rendering.
|
|
77
|
+
* Optionally emits a newline to separate subsequent output.
|
|
78
|
+
*/
|
|
79
|
+
clearActiveSpinner(addNewLine) {
|
|
80
|
+
if (!this.activeSpinner) {
|
|
270
81
|
return;
|
|
271
82
|
}
|
|
272
|
-
const
|
|
273
|
-
this.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (this.captureStack.length === 0) {
|
|
277
|
-
return '';
|
|
83
|
+
const spinner = this.activeSpinner;
|
|
84
|
+
this.activeSpinner = null;
|
|
85
|
+
if (typeof spinner.clear === 'function') {
|
|
86
|
+
spinner.clear();
|
|
278
87
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
write(value, target = this.outputStream) {
|
|
282
|
-
// Write directly - this method is called from within locked contexts
|
|
283
|
-
// like withOutput(), so we don't need additional locking here.
|
|
284
|
-
// The globalWriteLock wrapper will handle coordination if needed.
|
|
285
|
-
try {
|
|
286
|
-
target.write(value);
|
|
287
|
-
this.appendCapturedOutput(value);
|
|
88
|
+
else if (typeof spinner.stop === 'function') {
|
|
89
|
+
spinner.stop();
|
|
288
90
|
}
|
|
289
|
-
|
|
290
|
-
|
|
91
|
+
if (addNewLine) {
|
|
92
|
+
if (!this.enqueueEvent('raw', '\n')) {
|
|
93
|
+
this.outputStream.write('\n');
|
|
94
|
+
}
|
|
291
95
|
}
|
|
292
96
|
}
|
|
293
|
-
writeLine(value = '', target = this.outputStream) {
|
|
294
|
-
this.write(`${value}\n`, target);
|
|
295
|
-
}
|
|
296
97
|
/**
|
|
297
|
-
* Write raw content directly
|
|
298
|
-
* For streaming output chunks - writes directly without locks during streaming mode.
|
|
299
|
-
*
|
|
300
|
-
* During streaming mode, content is written directly to stdout without any
|
|
301
|
-
* lock coordination. This ensures streaming content flows smoothly without
|
|
302
|
-
* being delayed by other UI components.
|
|
98
|
+
* Write raw content directly
|
|
303
99
|
*/
|
|
304
100
|
writeRaw(content) {
|
|
305
|
-
if (
|
|
101
|
+
if (!content)
|
|
306
102
|
return;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
103
|
+
this.notifyBeforeOutput();
|
|
104
|
+
if (this.enqueueEvent('raw', content)) {
|
|
105
|
+
this.notifyAfterOutput(content);
|
|
310
106
|
return;
|
|
311
107
|
}
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
writeLock.withLock(() => {
|
|
316
|
-
this.notifyBeforeOutput();
|
|
317
|
-
try {
|
|
318
|
-
this.outputStream.write(normalized);
|
|
319
|
-
}
|
|
320
|
-
catch {
|
|
321
|
-
// Ignore write failures to keep UI resilient
|
|
322
|
-
}
|
|
323
|
-
finally {
|
|
324
|
-
this.notifyAfterOutput(normalized);
|
|
325
|
-
}
|
|
326
|
-
}, 'display.writeRaw.streaming');
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
// Outside streaming mode, use locks to coordinate with other UI
|
|
330
|
-
writeLock.withLock(() => {
|
|
331
|
-
this.notifyBeforeOutput();
|
|
332
|
-
try {
|
|
333
|
-
this.write(normalized);
|
|
334
|
-
}
|
|
335
|
-
finally {
|
|
336
|
-
this.notifyAfterOutput(normalized);
|
|
337
|
-
}
|
|
338
|
-
}, 'display.writeRaw');
|
|
108
|
+
// Fallback if no renderer
|
|
109
|
+
this.outputStream.write(content);
|
|
110
|
+
this.notifyAfterOutput(content);
|
|
339
111
|
}
|
|
340
112
|
/**
|
|
341
|
-
*
|
|
342
|
-
* - Preserve CR-based rewrites instead of converting to newlines
|
|
343
|
-
* - Clear lines when rewriting to avoid leftover characters
|
|
344
|
-
* - Carry trailing CR across chunks so the next write can clear before drawing
|
|
345
|
-
*/
|
|
346
|
-
normalizeStreamingContent(content) {
|
|
347
|
-
// Normalize CRLF first
|
|
348
|
-
let text = content.replace(/\r\n/g, '\n');
|
|
349
|
-
let result = '';
|
|
350
|
-
// If the previous chunk ended with a bare CR, clear the line before new text
|
|
351
|
-
if (this.pendingCarriageReturn) {
|
|
352
|
-
if (text.length === 0) {
|
|
353
|
-
return '';
|
|
354
|
-
}
|
|
355
|
-
if (text[0] !== '\n') {
|
|
356
|
-
result += '\r\x1b[K';
|
|
357
|
-
}
|
|
358
|
-
this.pendingCarriageReturn = false;
|
|
359
|
-
}
|
|
360
|
-
let trailingCarriageReturn = false;
|
|
361
|
-
for (let i = 0; i < text.length; i++) {
|
|
362
|
-
const char = text[i];
|
|
363
|
-
if (char === '\r') {
|
|
364
|
-
const next = text[i + 1];
|
|
365
|
-
if (next === '\n') {
|
|
366
|
-
// CRLF already normalized above; skip CR
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
const isLastChar = i === text.length - 1;
|
|
370
|
-
if (isLastChar) {
|
|
371
|
-
// Trailing CR: keep it so cursor is at column 0, but delay clearing
|
|
372
|
-
result += '\r';
|
|
373
|
-
trailingCarriageReturn = true;
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
// In-place update: move to start and clear the line before new text
|
|
377
|
-
result += '\r\x1b[K';
|
|
378
|
-
}
|
|
379
|
-
continue;
|
|
380
|
-
}
|
|
381
|
-
result += char;
|
|
382
|
-
}
|
|
383
|
-
this.pendingCarriageReturn = trailingCarriageReturn;
|
|
384
|
-
return result;
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* Stream chunk helper - writes directly to stdout during streaming.
|
|
388
|
-
* This is the primary method for streaming AI responses.
|
|
113
|
+
* Stream chunk (for streaming responses)
|
|
389
114
|
*/
|
|
390
115
|
stream(chunk) {
|
|
391
|
-
|
|
116
|
+
if (!chunk)
|
|
117
|
+
return;
|
|
118
|
+
this.notifyBeforeOutput();
|
|
119
|
+
if (this.enqueueEvent('streaming', chunk)) {
|
|
120
|
+
this.notifyAfterOutput(chunk);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Fallback
|
|
124
|
+
this.outputStream.write(chunk);
|
|
125
|
+
this.notifyAfterOutput(chunk);
|
|
392
126
|
}
|
|
393
127
|
/**
|
|
394
|
-
* Backward-compatible alias
|
|
128
|
+
* Backward-compatible alias
|
|
395
129
|
*/
|
|
396
130
|
writeStreamChunk(chunk) {
|
|
397
131
|
this.stream(chunk);
|
|
398
132
|
}
|
|
399
133
|
/**
|
|
400
|
-
* Get the output stream for direct access
|
|
401
|
-
* Prefer writeRaw/writeStreamChunk for interceptor support.
|
|
134
|
+
* Get the output stream for direct access
|
|
402
135
|
*/
|
|
403
136
|
getOutputStream() {
|
|
404
137
|
return this.outputStream;
|
|
405
138
|
}
|
|
406
139
|
/**
|
|
407
|
-
*
|
|
140
|
+
* Show thinking indicator
|
|
408
141
|
*/
|
|
409
|
-
getTotalWrittenLines() {
|
|
410
|
-
return this.stdoutTracker.totalLines;
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Number of lines at the top of the terminal that belong to the pinned banner.
|
|
414
|
-
* For integrated scroll-region layouts, banners are not pinned, so this is 0.
|
|
415
|
-
*/
|
|
416
|
-
getPinnedHeaderLines() {
|
|
417
|
-
return 0;
|
|
418
|
-
}
|
|
419
|
-
getColumnWidth() {
|
|
420
|
-
if (typeof this.outputStream.columns === 'number' &&
|
|
421
|
-
Number.isFinite(this.outputStream.columns) &&
|
|
422
|
-
this.outputStream.columns > 0) {
|
|
423
|
-
return this.outputStream.columns;
|
|
424
|
-
}
|
|
425
|
-
return getTerminalColumns();
|
|
426
|
-
}
|
|
427
|
-
// Banner is now streamed by the shell - no storage needed
|
|
428
142
|
showThinking(message = 'Thinking…') {
|
|
429
|
-
// If we already have a spinner, just update its text instead of creating a new one
|
|
430
|
-
if (this.activeSpinner) {
|
|
431
|
-
this.thinkingBaseMessage = message;
|
|
432
|
-
this.activeSpinner.update({ text: message });
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
// Lock output to prevent stream writes from interleaving with spinner frames
|
|
436
|
-
this.outputLock.lock();
|
|
437
|
-
// Track when thinking started for elapsed time display
|
|
438
143
|
this.thinkingStartTime = Date.now();
|
|
439
|
-
this.
|
|
440
|
-
// Use Claude Code style spinner with ✻ as primary thinking indicator
|
|
441
|
-
this.activeSpinner = createSpinner(message, {
|
|
442
|
-
stream: this.outputStream,
|
|
443
|
-
spinner: {
|
|
444
|
-
interval: DISPLAY_CONSTANTS.SPINNER_INTERVAL,
|
|
445
|
-
frames: this.spinnerFrames,
|
|
446
|
-
},
|
|
447
|
-
}).start();
|
|
448
|
-
// Update spinner with elapsed time every second (Claude Code style)
|
|
449
|
-
this.thinkingElapsedTimer = setInterval(() => {
|
|
450
|
-
if (this.activeSpinner && this.thinkingStartTime) {
|
|
451
|
-
const elapsed = Math.floor((Date.now() - this.thinkingStartTime) / 1000);
|
|
452
|
-
if (elapsed > 0) {
|
|
453
|
-
const elapsedText = this.formatElapsedTime(elapsed);
|
|
454
|
-
this.activeSpinner.update({ text: `${this.thinkingBaseMessage} ${theme.ui.muted(`(${elapsedText})`)}` });
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}, 1000);
|
|
144
|
+
this.enqueueEvent('response', message);
|
|
458
145
|
}
|
|
459
146
|
/**
|
|
460
|
-
*
|
|
147
|
+
* Update thinking message
|
|
461
148
|
*/
|
|
462
|
-
formatElapsedTime(seconds) {
|
|
463
|
-
if (seconds < 60) {
|
|
464
|
-
return `${seconds}s`;
|
|
465
|
-
}
|
|
466
|
-
const mins = Math.floor(seconds / 60);
|
|
467
|
-
const secs = seconds % 60;
|
|
468
|
-
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
469
|
-
}
|
|
470
149
|
updateThinking(message) {
|
|
471
|
-
this.
|
|
472
|
-
if (this.activeSpinner) {
|
|
473
|
-
// If we have elapsed time, include it in the update
|
|
474
|
-
if (this.thinkingStartTime) {
|
|
475
|
-
const elapsed = Math.floor((Date.now() - this.thinkingStartTime) / 1000);
|
|
476
|
-
if (elapsed > 0) {
|
|
477
|
-
const elapsedText = this.formatElapsedTime(elapsed);
|
|
478
|
-
this.activeSpinner.update({ text: `${message} ${theme.ui.muted(`(${elapsedText})`)}` });
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
this.activeSpinner.update({ text: message });
|
|
483
|
-
}
|
|
484
|
-
else {
|
|
485
|
-
this.showThinking(message);
|
|
486
|
-
}
|
|
150
|
+
this.enqueueEvent('response', message);
|
|
487
151
|
}
|
|
488
|
-
|
|
489
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Stop thinking
|
|
154
|
+
*/
|
|
155
|
+
stopThinking(_addNewLine = true) {
|
|
156
|
+
this.clearActiveSpinner(_addNewLine);
|
|
157
|
+
this.thinkingStartTime = null;
|
|
490
158
|
}
|
|
491
159
|
/**
|
|
492
|
-
* Get
|
|
160
|
+
* Get thinking elapsed time
|
|
493
161
|
*/
|
|
494
162
|
getThinkingElapsedMs() {
|
|
495
163
|
if (!this.thinkingStartTime) {
|
|
@@ -497,245 +165,70 @@ export class Display {
|
|
|
497
165
|
}
|
|
498
166
|
return Date.now() - this.thinkingStartTime;
|
|
499
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Check if spinner is active (always false with new renderer)
|
|
170
|
+
*/
|
|
500
171
|
isSpinnerActive() {
|
|
501
|
-
return
|
|
172
|
+
return false;
|
|
502
173
|
}
|
|
503
174
|
/**
|
|
504
|
-
* Check if output is
|
|
505
|
-
* Used by external callers to coordinate output timing.
|
|
175
|
+
* Check if output is locked (always false with event queue)
|
|
506
176
|
*/
|
|
507
177
|
isOutputLocked() {
|
|
508
|
-
return
|
|
178
|
+
return false;
|
|
509
179
|
}
|
|
510
180
|
/**
|
|
511
|
-
*
|
|
512
|
-
* If spinner is active, the write will be queued until spinner stops.
|
|
181
|
+
* Safe write (just calls callback immediately with event queue)
|
|
513
182
|
*/
|
|
514
183
|
safeWrite(callback) {
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
/**
|
|
518
|
-
* Get terminal rows, with fallback
|
|
519
|
-
*/
|
|
520
|
-
getTerminalRows() {
|
|
521
|
-
const stream = this.outputStream;
|
|
522
|
-
return stream.rows ?? 24;
|
|
523
|
-
}
|
|
524
|
-
/**
|
|
525
|
-
* Set up scroll region to reserve bottom line for status.
|
|
526
|
-
* This allows content to scroll while status stays fixed.
|
|
527
|
-
*/
|
|
528
|
-
setupScrollRegion() {
|
|
529
|
-
if (this.scrollRegionActive || !this.isTTY()) {
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
const rows = this.getTerminalRows();
|
|
533
|
-
// Reserve bottom 2 lines (separator + status)
|
|
534
|
-
// Set scroll region from row 1 to row (rows - 2)
|
|
535
|
-
this.outputStream.write(`\x1b[1;${rows - 2}r`);
|
|
536
|
-
// Move cursor to end of scroll region
|
|
537
|
-
this.outputStream.write(`\x1b[${rows - 2};1H`);
|
|
538
|
-
this.scrollRegionActive = true;
|
|
539
|
-
this.savedCursorRow = rows - 2;
|
|
540
|
-
}
|
|
541
|
-
/**
|
|
542
|
-
* Tear down scroll region and restore normal terminal.
|
|
543
|
-
*/
|
|
544
|
-
teardownScrollRegion() {
|
|
545
|
-
if (!this.scrollRegionActive) {
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
const rows = this.getTerminalRows();
|
|
549
|
-
// Reset scroll region to full screen
|
|
550
|
-
this.outputStream.write('\x1b[r');
|
|
551
|
-
// Clear the status lines at bottom
|
|
552
|
-
this.outputStream.write(`\x1b[${rows - 1};1H\x1b[2K`);
|
|
553
|
-
this.outputStream.write(`\x1b[${rows};1H\x1b[2K`);
|
|
554
|
-
// Move cursor back to where content was
|
|
555
|
-
this.outputStream.write(`\x1b[${this.savedCursorRow};1H`);
|
|
556
|
-
this.scrollRegionActive = false;
|
|
557
|
-
this.streamingStatusVisible = false;
|
|
558
|
-
}
|
|
559
|
-
/**
|
|
560
|
-
* Check if we're in a TTY (can use escape codes)
|
|
561
|
-
*/
|
|
562
|
-
isTTY() {
|
|
563
|
-
const stream = this.outputStream;
|
|
564
|
-
return stream.isTTY === true;
|
|
565
|
-
}
|
|
566
|
-
/**
|
|
567
|
-
* Update the streaming status line (Claude Code style).
|
|
568
|
-
* When scroll region is active, updates the fixed status area at bottom.
|
|
569
|
-
* Call with null to clear the status line.
|
|
570
|
-
* Uses shared writeLock to prevent race conditions with other output.
|
|
571
|
-
*/
|
|
572
|
-
updateStreamingStatus(status) {
|
|
573
|
-
// Use shared writeLock to coordinate with terminalInput and streaming output
|
|
574
|
-
writeLock.withLock(() => {
|
|
575
|
-
this.updateStreamingStatusInternal(status);
|
|
576
|
-
}, 'display.updateStreamingStatus');
|
|
577
|
-
}
|
|
578
|
-
/**
|
|
579
|
-
* Internal implementation of updateStreamingStatus (called with lock held)
|
|
580
|
-
* NOTE: During streaming, we do NOT write status lines to stdout to prevent
|
|
581
|
-
* race conditions with stream chunks. The status is tracked internally and
|
|
582
|
-
* the terminalInput's status message display handles showing it in the
|
|
583
|
-
* reserved input area, separate from the main content stream.
|
|
584
|
-
*/
|
|
585
|
-
updateStreamingStatusInternal(status) {
|
|
586
|
-
if (!status) {
|
|
587
|
-
this.streamingStatusVisible = false;
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
// Just track the status - don't write to stdout during streaming
|
|
591
|
-
this.streamingStatusVisible = true;
|
|
592
|
-
}
|
|
593
|
-
/**
|
|
594
|
-
* Check if streaming status is currently visible.
|
|
595
|
-
*/
|
|
596
|
-
isStreamingStatusVisible() {
|
|
597
|
-
return this.streamingStatusVisible;
|
|
598
|
-
}
|
|
599
|
-
/**
|
|
600
|
-
* Check if scroll region is active.
|
|
601
|
-
*/
|
|
602
|
-
isScrollRegionActive() {
|
|
603
|
-
return this.scrollRegionActive;
|
|
604
|
-
}
|
|
605
|
-
/**
|
|
606
|
-
* Clear streaming status and reset state.
|
|
607
|
-
*/
|
|
608
|
-
clearStreamingStatus() {
|
|
609
|
-
this.streamingStatusVisible = false;
|
|
184
|
+
callback();
|
|
610
185
|
}
|
|
611
186
|
/**
|
|
612
|
-
*
|
|
613
|
-
* Shows a tree of running agents with their progress.
|
|
187
|
+
* Show assistant message
|
|
614
188
|
*/
|
|
615
|
-
parallelAgentStatus(content) {
|
|
616
|
-
if (!content)
|
|
617
|
-
return;
|
|
618
|
-
// During streaming, write directly to output
|
|
619
|
-
this.withOutput(() => {
|
|
620
|
-
// Clear current line and write agent status
|
|
621
|
-
this.writeRaw('\r\x1b[K');
|
|
622
|
-
this.writeLine(content);
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
clearSpinnerIfActive(addNewLine = true) {
|
|
626
|
-
if (!this.activeSpinner) {
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
// Clear the elapsed time update timer
|
|
630
|
-
if (this.thinkingElapsedTimer) {
|
|
631
|
-
clearInterval(this.thinkingElapsedTimer);
|
|
632
|
-
this.thinkingElapsedTimer = null;
|
|
633
|
-
}
|
|
634
|
-
this.thinkingStartTime = null;
|
|
635
|
-
const spinner = this.activeSpinner;
|
|
636
|
-
this.activeSpinner = null;
|
|
637
|
-
// Use stop() instead of clear() so nanospinner removes its SIGINT/SIGTERM listeners.
|
|
638
|
-
// clear() leaves the listeners attached, which triggers MaxListenersExceededWarning over time.
|
|
639
|
-
spinner.stop();
|
|
640
|
-
// Unlock output to process any pending writes
|
|
641
|
-
this.outputLock.unlock();
|
|
642
|
-
if (addNewLine) {
|
|
643
|
-
this.withOutput(() => {
|
|
644
|
-
this.writeLine();
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
189
|
showAssistantMessage(content, metadata) {
|
|
649
|
-
|
|
190
|
+
this.clearActiveSpinner(false);
|
|
191
|
+
if (!content.trim())
|
|
650
192
|
return;
|
|
651
|
-
}
|
|
652
|
-
this.clearSpinnerIfActive();
|
|
653
193
|
const isThought = metadata?.isFinal === false;
|
|
654
194
|
const body = isThought ? this.buildClaudeStyleThought(content) : this.buildChatBox(content, metadata);
|
|
655
|
-
if (!body.trim())
|
|
195
|
+
if (!body.trim())
|
|
656
196
|
return;
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const wrapped = this.applySingleBulletBlock(body);
|
|
660
|
-
this.withOutput(() => {
|
|
661
|
-
this.writeLine(); // Ensure clean start for the box
|
|
662
|
-
this.writeLine(wrapped);
|
|
663
|
-
this.writeLine();
|
|
664
|
-
});
|
|
197
|
+
const wrapped = isThought ? this.applySingleBulletBlock(body) : body;
|
|
198
|
+
this.enqueueEvent('raw', `\n${wrapped}\n\n`);
|
|
665
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Show narrative (thought)
|
|
202
|
+
*/
|
|
666
203
|
showNarrative(content) {
|
|
667
|
-
if (!content.trim())
|
|
204
|
+
if (!content.trim())
|
|
668
205
|
return;
|
|
669
|
-
}
|
|
670
206
|
this.showAssistantMessage(content, { isFinal: false });
|
|
671
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Show action
|
|
210
|
+
*/
|
|
672
211
|
showAction(text, status = 'info') {
|
|
673
|
-
if (!text.trim())
|
|
212
|
+
if (!text.trim())
|
|
674
213
|
return;
|
|
675
|
-
}
|
|
676
|
-
this.clearSpinnerIfActive();
|
|
677
|
-
// Claude Code style: always use ⏺ prefix for actions
|
|
678
214
|
const icon = this.formatActionIcon(status);
|
|
679
|
-
this.
|
|
680
|
-
|
|
681
|
-
});
|
|
215
|
+
const rendered = this.wrapWithPrefix(text, `${icon} `);
|
|
216
|
+
this.enqueueEvent('raw', `${rendered}\n`);
|
|
682
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Show sub-action
|
|
220
|
+
*/
|
|
683
221
|
showSubAction(text, status = 'info') {
|
|
684
|
-
if (!text.trim())
|
|
222
|
+
if (!text.trim())
|
|
685
223
|
return;
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
const prefersRich = text.includes('```');
|
|
689
|
-
let rendered = prefersRich ? this.buildRichSubActionLines(text, status) : this.buildWrappedSubActionLines(text, status);
|
|
690
|
-
if (!rendered.length && prefersRich) {
|
|
691
|
-
rendered = this.buildWrappedSubActionLines(text, status);
|
|
692
|
-
}
|
|
693
|
-
if (!rendered.length) {
|
|
224
|
+
const lines = this.buildWrappedSubActionLines(text, status);
|
|
225
|
+
if (!lines.length)
|
|
694
226
|
return;
|
|
695
|
-
}
|
|
696
|
-
this.withOutput(() => {
|
|
697
|
-
this.writeLine(rendered.join('\n'));
|
|
698
|
-
this.writeLine();
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
buildWrappedSubActionLines(text, status) {
|
|
702
|
-
const lines = text.split('\n').map((line) => line.trimEnd());
|
|
703
|
-
while (lines.length && !lines[lines.length - 1]?.trim()) {
|
|
704
|
-
lines.pop();
|
|
705
|
-
}
|
|
706
|
-
if (!lines.length) {
|
|
707
|
-
return [];
|
|
708
|
-
}
|
|
709
|
-
const rendered = [];
|
|
710
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
711
|
-
const segment = lines[index] ?? '';
|
|
712
|
-
const isLast = index === lines.length - 1;
|
|
713
|
-
const { prefix, continuation } = this.buildSubActionPrefixes(status, isLast);
|
|
714
|
-
rendered.push(this.wrapWithPrefix(segment, prefix, { continuationPrefix: continuation }));
|
|
715
|
-
}
|
|
716
|
-
return rendered;
|
|
717
|
-
}
|
|
718
|
-
buildRichSubActionLines(text, status) {
|
|
719
|
-
const normalized = text.trim();
|
|
720
|
-
if (!normalized) {
|
|
721
|
-
return [];
|
|
722
|
-
}
|
|
723
|
-
const width = Math.max(DISPLAY_CONSTANTS.MIN_ACTION_WIDTH, Math.min(this.getColumnWidth(), DISPLAY_CONSTANTS.MAX_ACTION_WIDTH));
|
|
724
|
-
const samplePrefix = this.buildSubActionPrefixes(status, true).prefix;
|
|
725
|
-
const contentWidth = Math.max(DISPLAY_CONSTANTS.MIN_CONTENT_WIDTH, width - this.visibleLength(samplePrefix));
|
|
726
|
-
const blocks = formatRichContent(normalized, contentWidth);
|
|
727
|
-
if (!blocks.length) {
|
|
728
|
-
return [];
|
|
729
|
-
}
|
|
730
|
-
return blocks.map((line, index) => {
|
|
731
|
-
const isLast = index === blocks.length - 1;
|
|
732
|
-
const { prefix } = this.buildSubActionPrefixes(status, isLast);
|
|
733
|
-
if (!line.trim()) {
|
|
734
|
-
return prefix.trimEnd();
|
|
735
|
-
}
|
|
736
|
-
return `${prefix}${line}`;
|
|
737
|
-
});
|
|
227
|
+
this.enqueueEvent('raw', `${lines.join('\n')}\n\n`);
|
|
738
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Show message
|
|
231
|
+
*/
|
|
739
232
|
showMessage(content, role = 'assistant') {
|
|
740
233
|
if (role === 'system') {
|
|
741
234
|
this.showSystemMessage(content);
|
|
@@ -744,16 +237,19 @@ export class Display {
|
|
|
744
237
|
this.showAssistantMessage(content);
|
|
745
238
|
}
|
|
746
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Show system message
|
|
242
|
+
*/
|
|
747
243
|
showSystemMessage(content) {
|
|
748
|
-
this.clearSpinnerIfActive();
|
|
749
244
|
const normalized = content.trim();
|
|
750
|
-
if (!normalized)
|
|
245
|
+
if (!normalized)
|
|
751
246
|
return;
|
|
752
|
-
}
|
|
753
247
|
this.stream(`${normalized}\n\n`);
|
|
754
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Show error
|
|
251
|
+
*/
|
|
755
252
|
showError(message, error) {
|
|
756
|
-
this.clearSpinnerIfActive();
|
|
757
253
|
const details = this.formatErrorDetails(error);
|
|
758
254
|
const parts = [`${theme.error('✗')} ${message}`];
|
|
759
255
|
if (details) {
|
|
@@ -761,23 +257,26 @@ export class Display {
|
|
|
761
257
|
}
|
|
762
258
|
this.stream(`${parts.join('\n')}\n`);
|
|
763
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Show warning
|
|
262
|
+
*/
|
|
764
263
|
showWarning(message) {
|
|
765
|
-
this.clearSpinnerIfActive();
|
|
766
264
|
this.stream(`${theme.warning('!')} ${message}\n`);
|
|
767
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Show info
|
|
268
|
+
*/
|
|
768
269
|
showInfo(message) {
|
|
769
|
-
this.clearSpinnerIfActive();
|
|
770
270
|
this.stream(`${theme.info('ℹ')} ${message}\n`);
|
|
771
271
|
}
|
|
772
272
|
/**
|
|
773
|
-
* Show
|
|
273
|
+
* Show success
|
|
774
274
|
*/
|
|
775
275
|
showSuccess(message) {
|
|
776
|
-
this.clearSpinnerIfActive();
|
|
777
276
|
this.stream(`${theme.success('✓')} ${message}\n`);
|
|
778
277
|
}
|
|
779
278
|
/**
|
|
780
|
-
* Show
|
|
279
|
+
* Show progress badge
|
|
781
280
|
*/
|
|
782
281
|
showProgressBadge(label, current, total) {
|
|
783
282
|
const percentage = Math.round((current / total) * 100);
|
|
@@ -786,66 +285,133 @@ export class Display {
|
|
|
786
285
|
const empty = barWidth - filled;
|
|
787
286
|
const progressBar = `${'█'.repeat(filled)}${'░'.repeat(empty)}`;
|
|
788
287
|
const badge = `[${label}] ${progressBar} ${percentage}%`;
|
|
789
|
-
this.
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
288
|
+
this.stream(`\r${theme.info(badge)}`);
|
|
289
|
+
if (current >= total) {
|
|
290
|
+
this.stream('\n');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Show status line
|
|
295
|
+
*/
|
|
296
|
+
showStatusLine(status, elapsedMs, _context) {
|
|
297
|
+
const normalized = status?.trim();
|
|
298
|
+
if (!normalized)
|
|
299
|
+
return;
|
|
300
|
+
const elapsed = this.formatElapsed(elapsedMs);
|
|
301
|
+
const parts = [];
|
|
302
|
+
parts.push(`${theme.success('✓')} ${normalized}`);
|
|
303
|
+
if (elapsed) {
|
|
304
|
+
parts.push(`(${elapsed})`);
|
|
305
|
+
}
|
|
306
|
+
this.stream(`${parts.join(' ')}\n`);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Show available tools (no-op)
|
|
310
|
+
*/
|
|
311
|
+
showAvailableTools(_tools) {
|
|
312
|
+
// Hidden by default
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Show command palette
|
|
316
|
+
*/
|
|
317
|
+
showCommandPalette(commands, options) {
|
|
318
|
+
if (!commands || commands.length === 0)
|
|
319
|
+
return;
|
|
320
|
+
const panel = this.buildCommandPalette(commands, options);
|
|
321
|
+
if (!panel.trim())
|
|
322
|
+
return;
|
|
323
|
+
this.enqueueEvent('raw', `\n${panel}\n\n`);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Show ready with hints (no-op)
|
|
327
|
+
*/
|
|
328
|
+
showReadyWithHints() {
|
|
329
|
+
// Commands hint is now in the renderer
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Show planning step
|
|
333
|
+
*/
|
|
334
|
+
showPlanningStep(step, index, total) {
|
|
335
|
+
if (!step?.trim() || index < 1 || total < 1 || index > total)
|
|
336
|
+
return;
|
|
337
|
+
const width = Math.max(DISPLAY_CONSTANTS.MIN_THOUGHT_WIDTH, Math.min(this.getColumnWidth(), DISPLAY_CONSTANTS.MAX_MESSAGE_WIDTH));
|
|
338
|
+
const heading = renderSectionHeading(`Plan ${index}/${total}`, {
|
|
339
|
+
subtitle: step,
|
|
340
|
+
icon: icons.arrow,
|
|
341
|
+
tone: 'info',
|
|
342
|
+
width,
|
|
794
343
|
});
|
|
344
|
+
this.enqueueEvent('raw', `${heading}\n`);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Show thinking block
|
|
348
|
+
*/
|
|
349
|
+
showThinkingBlock(content, durationMs) {
|
|
350
|
+
const block = this.buildClaudeStyleThought(content, durationMs);
|
|
351
|
+
this.enqueueEvent('raw', `\n${block}\n\n`);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Clear screen
|
|
355
|
+
*/
|
|
356
|
+
clear() {
|
|
357
|
+
// Renderer handles this
|
|
795
358
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
}
|
|
802
|
-
const elapsed = this.formatElapsed(elapsedMs);
|
|
803
|
-
const parts = [
|
|
804
|
-
{ text: `${theme.success('✓')} ${normalized}`, tone: 'success' },
|
|
805
|
-
elapsed ? { text: `(${elapsed})`, tone: 'muted' } : null,
|
|
806
|
-
].filter(Boolean);
|
|
807
|
-
const line = renderStatusLine(parts, this.getColumnWidth() - 2);
|
|
808
|
-
this.withOutput(() => {
|
|
809
|
-
this.writeLine(line);
|
|
810
|
-
});
|
|
359
|
+
/**
|
|
360
|
+
* Update streaming status (routes to renderer)
|
|
361
|
+
*/
|
|
362
|
+
updateStreamingStatus(status) {
|
|
363
|
+
this.renderer?.updateStatus(status);
|
|
811
364
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
365
|
+
/**
|
|
366
|
+
* Clear streaming status
|
|
367
|
+
*/
|
|
368
|
+
clearStreamingStatus() {
|
|
369
|
+
this.renderer?.updateStatus(null);
|
|
816
370
|
}
|
|
817
371
|
/**
|
|
818
|
-
*
|
|
372
|
+
* Check if streaming status is visible
|
|
819
373
|
*/
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
const panel = this.buildCommandPalette(commands, options);
|
|
825
|
-
if (!panel.trim()) {
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
this.withOutput(() => {
|
|
829
|
-
this.writeLine();
|
|
830
|
-
this.writeLine(panel);
|
|
831
|
-
this.writeLine();
|
|
832
|
-
});
|
|
374
|
+
isStreamingStatusVisible() {
|
|
375
|
+
return false; // Renderer manages this
|
|
833
376
|
}
|
|
834
377
|
/**
|
|
835
|
-
*
|
|
836
|
-
* Note: Commands are now shown in the banner, so this is a no-op
|
|
378
|
+
* Legacy compatibility methods (no-ops)
|
|
837
379
|
*/
|
|
838
|
-
|
|
839
|
-
|
|
380
|
+
getTotalWrittenLines() {
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
getPinnedHeaderLines() {
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
setupScrollRegion() {
|
|
387
|
+
// No-op - renderer handles layout
|
|
388
|
+
}
|
|
389
|
+
teardownScrollRegion() {
|
|
390
|
+
// No-op
|
|
391
|
+
}
|
|
392
|
+
isScrollRegionActive() {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
parallelAgentStatus(content) {
|
|
396
|
+
if (!content)
|
|
397
|
+
return;
|
|
398
|
+
this.enqueueEvent('streaming', `${content}\n`);
|
|
399
|
+
}
|
|
400
|
+
// ==================== Private Helper Methods ====================
|
|
401
|
+
getColumnWidth() {
|
|
402
|
+
if (typeof this.outputStream.columns === 'number' &&
|
|
403
|
+
Number.isFinite(this.outputStream.columns) &&
|
|
404
|
+
this.outputStream.columns > 0) {
|
|
405
|
+
return this.outputStream.columns;
|
|
406
|
+
}
|
|
407
|
+
return getTerminalColumns();
|
|
840
408
|
}
|
|
841
409
|
formatErrorDetails(error) {
|
|
842
|
-
if (!error)
|
|
410
|
+
if (!error)
|
|
843
411
|
return null;
|
|
844
|
-
}
|
|
845
412
|
if (error instanceof Error) {
|
|
846
|
-
if (error.stack)
|
|
413
|
+
if (error.stack)
|
|
847
414
|
return highlightError(error.stack);
|
|
848
|
-
}
|
|
849
415
|
return highlightError(error.message);
|
|
850
416
|
}
|
|
851
417
|
if (typeof error === 'string') {
|
|
@@ -858,83 +424,22 @@ export class Display {
|
|
|
858
424
|
return null;
|
|
859
425
|
}
|
|
860
426
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
if (index < 1 || total < 1 || index > total) {
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
const width = Math.max(DISPLAY_CONSTANTS.MIN_THOUGHT_WIDTH, Math.min(this.getColumnWidth(), DISPLAY_CONSTANTS.MAX_MESSAGE_WIDTH));
|
|
870
|
-
const heading = renderSectionHeading(`Plan ${index}/${total}`, {
|
|
871
|
-
subtitle: step,
|
|
872
|
-
icon: icons.arrow,
|
|
873
|
-
tone: 'info',
|
|
874
|
-
width,
|
|
875
|
-
});
|
|
876
|
-
this.withOutput(() => {
|
|
877
|
-
this.writeLine(heading);
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
clear() {
|
|
881
|
-
this.withOutput(() => {
|
|
882
|
-
try {
|
|
883
|
-
cursorTo(this.outputStream, 0, 0);
|
|
884
|
-
clearScreenDown(this.outputStream);
|
|
885
|
-
}
|
|
886
|
-
catch {
|
|
887
|
-
this.write('\x1Bc');
|
|
888
|
-
}
|
|
889
|
-
});
|
|
890
|
-
this.stdoutTracker.reset();
|
|
891
|
-
// Banner is streamed content - no re-render on clear
|
|
892
|
-
}
|
|
893
|
-
formatModelLabel(model) {
|
|
894
|
-
if (/gpt-5\.1-?codex/i.test(model)) {
|
|
895
|
-
return model;
|
|
896
|
-
}
|
|
897
|
-
if (/sonnet-4[-.]?5/i.test(model)) {
|
|
898
|
-
return 'Sonnet 4.5';
|
|
899
|
-
}
|
|
900
|
-
if (/opus-4[-.]?1/i.test(model)) {
|
|
901
|
-
return 'Opus 4.1';
|
|
902
|
-
}
|
|
903
|
-
if (/haiku-4[-.]?5/i.test(model)) {
|
|
904
|
-
return 'Haiku 4.5';
|
|
905
|
-
}
|
|
906
|
-
if (/gpt-5\.1/i.test(model)) {
|
|
907
|
-
return 'GPT-5.1';
|
|
908
|
-
}
|
|
909
|
-
if (/gpt-5-?pro/i.test(model)) {
|
|
910
|
-
return 'GPT-5 Pro';
|
|
911
|
-
}
|
|
912
|
-
if (/gpt-5-?mini/i.test(model)) {
|
|
913
|
-
return 'GPT-5 Mini';
|
|
914
|
-
}
|
|
915
|
-
if (/gpt-5-?nano/i.test(model)) {
|
|
916
|
-
return 'GPT-5 Nano';
|
|
917
|
-
}
|
|
918
|
-
return model;
|
|
919
|
-
}
|
|
920
|
-
compactPath(path, maxLen) {
|
|
921
|
-
if (!path) {
|
|
922
|
-
return '';
|
|
923
|
-
}
|
|
924
|
-
if (this.visibleLength(path) <= maxLen) {
|
|
925
|
-
return path;
|
|
427
|
+
formatElapsed(elapsedMs) {
|
|
428
|
+
if (typeof elapsedMs !== 'number' || !Number.isFinite(elapsedMs) || elapsedMs < 0) {
|
|
429
|
+
return null;
|
|
926
430
|
}
|
|
927
|
-
const
|
|
928
|
-
|
|
929
|
-
|
|
431
|
+
const totalSeconds = Math.max(0, Math.round(elapsedMs / 1000));
|
|
432
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
433
|
+
const seconds = totalSeconds % 60;
|
|
434
|
+
if (minutes > 0) {
|
|
435
|
+
return `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
|
|
930
436
|
}
|
|
931
|
-
return `${
|
|
437
|
+
return `${seconds}s`;
|
|
932
438
|
}
|
|
933
439
|
buildChatBox(content, metadata) {
|
|
934
440
|
const normalized = content.trim();
|
|
935
|
-
if (!normalized)
|
|
441
|
+
if (!normalized)
|
|
936
442
|
return '';
|
|
937
|
-
}
|
|
938
443
|
if (isPlainOutputMode()) {
|
|
939
444
|
const body = renderMessageBody(normalized, this.resolveMessageWidth());
|
|
940
445
|
const telemetry = this.formatTelemetryLine(metadata);
|
|
@@ -949,19 +454,15 @@ export class Display {
|
|
|
949
454
|
borderColor: theme.ui.border,
|
|
950
455
|
});
|
|
951
456
|
const telemetry = this.formatTelemetryLine(metadata);
|
|
952
|
-
|
|
953
|
-
return panel;
|
|
954
|
-
}
|
|
955
|
-
return `${panel}\n${telemetry}`;
|
|
457
|
+
return telemetry ? `${panel}\n${telemetry}` : panel;
|
|
956
458
|
}
|
|
957
459
|
resolveMessageWidth() {
|
|
958
460
|
const columns = this.getColumnWidth();
|
|
959
461
|
return Math.max(DISPLAY_CONSTANTS.MIN_MESSAGE_WIDTH, Math.min(columns - DISPLAY_CONSTANTS.MESSAGE_PADDING, DISPLAY_CONSTANTS.MAX_MESSAGE_WIDTH));
|
|
960
462
|
}
|
|
961
463
|
formatTelemetryLine(metadata) {
|
|
962
|
-
if (!metadata)
|
|
464
|
+
if (!metadata)
|
|
963
465
|
return '';
|
|
964
|
-
}
|
|
965
466
|
const parts = [];
|
|
966
467
|
const elapsed = this.formatElapsed(metadata.elapsedMs);
|
|
967
468
|
if (elapsed) {
|
|
@@ -969,28 +470,67 @@ export class Display {
|
|
|
969
470
|
const elapsedValue = theme.metrics?.elapsedValue ?? theme.secondary;
|
|
970
471
|
parts.push(`${elapsedLabel('elapsed')} ${elapsedValue(elapsed)}`);
|
|
971
472
|
}
|
|
972
|
-
if (!parts.length)
|
|
473
|
+
if (!parts.length)
|
|
973
474
|
return '';
|
|
974
|
-
}
|
|
975
475
|
const separator = theme.ui.muted(' • ');
|
|
976
476
|
return ` ${parts.join(separator)}`;
|
|
977
477
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
478
|
+
buildClaudeStyleThought(content, durationMs) {
|
|
479
|
+
const thinkingStyle = theme.thinking || {
|
|
480
|
+
icon: theme.info,
|
|
481
|
+
text: theme.ui.muted,
|
|
482
|
+
border: theme.ui.border,
|
|
483
|
+
label: theme.info,
|
|
484
|
+
};
|
|
485
|
+
const width = Math.min(this.getColumnWidth() - 4, 70);
|
|
486
|
+
const lines = [];
|
|
487
|
+
// Header
|
|
488
|
+
if (durationMs !== undefined) {
|
|
489
|
+
const elapsed = this.formatElapsedTime(Math.floor(durationMs / 1000));
|
|
490
|
+
lines.push(`${theme.info('∴')} Thought for ${elapsed}`);
|
|
981
491
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
const seconds = totalSeconds % 60;
|
|
985
|
-
if (minutes > 0) {
|
|
986
|
-
return `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
|
|
492
|
+
else {
|
|
493
|
+
lines.push(`${theme.info('✻')} ${thinkingStyle.label('Thinking…')}`);
|
|
987
494
|
}
|
|
988
|
-
|
|
495
|
+
// Content
|
|
496
|
+
const contentLines = content.split('\n');
|
|
497
|
+
const hasContent = contentLines.some(line => line.trim().length > 0);
|
|
498
|
+
if (hasContent) {
|
|
499
|
+
lines.push('');
|
|
500
|
+
}
|
|
501
|
+
for (const line of contentLines) {
|
|
502
|
+
const trimmed = line.replace(/\s+$/, '');
|
|
503
|
+
if (!trimmed.trim()) {
|
|
504
|
+
lines.push('');
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const wrapped = this.wrapLine(trimmed, width - 4);
|
|
508
|
+
for (const wrappedLine of wrapped) {
|
|
509
|
+
lines.push(` ${thinkingStyle.text(wrappedLine)}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return lines.join('\n');
|
|
513
|
+
}
|
|
514
|
+
formatElapsedTime(seconds) {
|
|
515
|
+
if (seconds < 60) {
|
|
516
|
+
return `${seconds}s`;
|
|
517
|
+
}
|
|
518
|
+
const mins = Math.floor(seconds / 60);
|
|
519
|
+
const secs = seconds % 60;
|
|
520
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
521
|
+
}
|
|
522
|
+
applySingleBulletBlock(text) {
|
|
523
|
+
const lines = text.split('\n');
|
|
524
|
+
const bullet = `${icons.action} `;
|
|
525
|
+
const prefix = theme.info(bullet);
|
|
526
|
+
const indent = ' '.repeat(this.visibleLength(this.stripAnsi(bullet)));
|
|
527
|
+
return lines
|
|
528
|
+
.map((line, index) => (index === 0 ? `${prefix}${line}` : `${indent}${line}`))
|
|
529
|
+
.join('\n');
|
|
989
530
|
}
|
|
990
531
|
buildCommandPalette(commands, options) {
|
|
991
|
-
if (!commands.length)
|
|
532
|
+
if (!commands.length)
|
|
992
533
|
return '';
|
|
993
|
-
}
|
|
994
534
|
const width = Math.max(DISPLAY_CONSTANTS.MIN_MESSAGE_WIDTH, Math.min(this.getColumnWidth() - 2, DISPLAY_CONSTANTS.MAX_MESSAGE_WIDTH));
|
|
995
535
|
const indent = ' ';
|
|
996
536
|
const grouped = this.groupPaletteCommands(commands);
|
|
@@ -1049,39 +589,25 @@ export class Display {
|
|
|
1049
589
|
return Math.min(maxAllowed, budget);
|
|
1050
590
|
}
|
|
1051
591
|
formatPaletteCategory(category) {
|
|
1052
|
-
if (!category)
|
|
592
|
+
if (!category)
|
|
1053
593
|
return 'Other';
|
|
1054
|
-
}
|
|
1055
594
|
switch (category.toLowerCase()) {
|
|
1056
|
-
case 'configuration':
|
|
1057
|
-
|
|
1058
|
-
case '
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
return 'Diagnostics';
|
|
1062
|
-
case 'other':
|
|
1063
|
-
return 'Other';
|
|
1064
|
-
default:
|
|
1065
|
-
return category[0]?.toUpperCase() + category.slice(1);
|
|
595
|
+
case 'configuration': return 'Configuration';
|
|
596
|
+
case 'workspace': return 'Workspace';
|
|
597
|
+
case 'diagnostics': return 'Diagnostics';
|
|
598
|
+
case 'other': return 'Other';
|
|
599
|
+
default: return category[0]?.toUpperCase() + category.slice(1);
|
|
1066
600
|
}
|
|
1067
601
|
}
|
|
1068
602
|
colorizePaletteText(text, tone) {
|
|
1069
603
|
switch (tone) {
|
|
1070
|
-
case 'warn':
|
|
1071
|
-
|
|
1072
|
-
case '
|
|
1073
|
-
return theme.success(text);
|
|
1074
|
-
case 'info':
|
|
1075
|
-
return theme.info(text);
|
|
604
|
+
case 'warn': return theme.warning(text);
|
|
605
|
+
case 'success': return theme.success(text);
|
|
606
|
+
case 'info': return theme.info(text);
|
|
1076
607
|
case 'muted':
|
|
1077
|
-
default:
|
|
1078
|
-
return theme.ui.muted(text);
|
|
608
|
+
default: return theme.ui.muted(text);
|
|
1079
609
|
}
|
|
1080
610
|
}
|
|
1081
|
-
/**
|
|
1082
|
-
* Wraps text with a prefix on the first line and optional continuation prefix.
|
|
1083
|
-
* Handles multi-line text and word wrapping intelligently.
|
|
1084
|
-
*/
|
|
1085
611
|
wrapWithPrefix(text, prefix, options) {
|
|
1086
612
|
if (!text) {
|
|
1087
613
|
return prefix.trimEnd();
|
|
@@ -1114,93 +640,25 @@ export class Display {
|
|
|
1114
640
|
}
|
|
1115
641
|
return lines.join('\n');
|
|
1116
642
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
case 'error':
|
|
1122
|
-
return theme.error;
|
|
1123
|
-
case 'warning':
|
|
1124
|
-
return theme.warning;
|
|
1125
|
-
case 'pending':
|
|
1126
|
-
return theme.info;
|
|
1127
|
-
default:
|
|
1128
|
-
return theme.secondary;
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
formatActionIcon(status) {
|
|
1132
|
-
const colorize = this.resolveStatusColor(status);
|
|
1133
|
-
return colorize(`${icons.action}`);
|
|
1134
|
-
}
|
|
1135
|
-
/**
|
|
1136
|
-
* Prefix a multi-line block with a single bullet, indenting subsequent lines.
|
|
1137
|
-
* Keeps entire assistant responses as one visual event.
|
|
1138
|
-
*/
|
|
1139
|
-
applySingleBulletBlock(text) {
|
|
1140
|
-
const lines = text.split('\n');
|
|
1141
|
-
const bullet = `${icons.action} `;
|
|
1142
|
-
const prefix = theme.info(bullet);
|
|
1143
|
-
const indent = ' '.repeat(this.visibleLength(this.stripAnsi(bullet)));
|
|
1144
|
-
return lines
|
|
1145
|
-
.map((line, index) => (index === 0 ? `${prefix}${line}` : `${indent}${line}`))
|
|
1146
|
-
.join('\n');
|
|
1147
|
-
}
|
|
1148
|
-
buildClaudeStyleThought(content, durationMs) {
|
|
1149
|
-
// Claude Code style: ∴ Thought for Xs or ✻ Thinking…
|
|
1150
|
-
const thinkingStyle = theme.thinking || {
|
|
1151
|
-
icon: theme.info,
|
|
1152
|
-
text: theme.ui.muted,
|
|
1153
|
-
border: theme.ui.border,
|
|
1154
|
-
label: theme.info,
|
|
1155
|
-
};
|
|
1156
|
-
const width = Math.min(this.getColumnWidth() - 4, 70);
|
|
1157
|
-
const lines = [];
|
|
1158
|
-
// Header: "∴ Thought for Xs" for completed, "✻ Thinking…" for active
|
|
1159
|
-
if (durationMs !== undefined) {
|
|
1160
|
-
const elapsed = this.formatElapsedTime(Math.floor(durationMs / 1000));
|
|
1161
|
-
lines.push(`${theme.info('∴')} Thought for ${elapsed}`);
|
|
1162
|
-
}
|
|
1163
|
-
else {
|
|
1164
|
-
lines.push(`${theme.info('✻')} ${thinkingStyle.label('Thinking…')}`);
|
|
1165
|
-
}
|
|
1166
|
-
// Parse and format the thinking content with simple indentation
|
|
1167
|
-
const contentLines = content.split('\n');
|
|
1168
|
-
const hasContent = contentLines.some(line => line.trim().length > 0);
|
|
1169
|
-
if (hasContent) {
|
|
1170
|
-
lines.push(''); // Visual gap between header and content
|
|
643
|
+
buildWrappedSubActionLines(text, status) {
|
|
644
|
+
const lines = text.split('\n').map((line) => line.trimEnd());
|
|
645
|
+
while (lines.length && !lines[lines.length - 1]?.trim()) {
|
|
646
|
+
lines.pop();
|
|
1171
647
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1180
|
-
lines.push(` ${thinkingStyle.text(wrappedLine)}`);
|
|
1181
|
-
}
|
|
648
|
+
if (!lines.length)
|
|
649
|
+
return [];
|
|
650
|
+
const rendered = [];
|
|
651
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
652
|
+
const segment = lines[index] ?? '';
|
|
653
|
+
const isLast = index === lines.length - 1;
|
|
654
|
+
const { prefix, continuation } = this.buildSubActionPrefixes(status, isLast);
|
|
655
|
+
rendered.push(this.wrapWithPrefix(segment, prefix, { continuationPrefix: continuation }));
|
|
1182
656
|
}
|
|
1183
|
-
return
|
|
1184
|
-
}
|
|
1185
|
-
/**
|
|
1186
|
-
* Show a thinking block with rich formatting (public method for external use)
|
|
1187
|
-
* @param content The thinking content to display
|
|
1188
|
-
* @param durationMs Optional duration in milliseconds to show "Thought for Xs"
|
|
1189
|
-
*/
|
|
1190
|
-
showThinkingBlock(content, durationMs) {
|
|
1191
|
-
this.clearSpinnerIfActive();
|
|
1192
|
-
const block = this.buildClaudeStyleThought(content, durationMs);
|
|
1193
|
-
this.withOutput(() => {
|
|
1194
|
-
this.writeLine();
|
|
1195
|
-
this.writeLine(block);
|
|
1196
|
-
this.writeLine();
|
|
1197
|
-
this.writeLine(); // Extra newline for better visual separation
|
|
1198
|
-
});
|
|
657
|
+
return rendered;
|
|
1199
658
|
}
|
|
1200
659
|
buildSubActionPrefixes(status, isLast) {
|
|
1201
660
|
if (isLast) {
|
|
1202
661
|
const colorize = this.resolveStatusColor(status);
|
|
1203
|
-
// Claude Code style: use ⎿ for sub-action result/detail prefix
|
|
1204
662
|
return {
|
|
1205
663
|
prefix: ` ${colorize(icons.subaction)} `,
|
|
1206
664
|
continuation: ' ',
|
|
@@ -1212,40 +670,55 @@ export class Display {
|
|
|
1212
670
|
continuation: ` ${branch} `,
|
|
1213
671
|
};
|
|
1214
672
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
673
|
+
resolveStatusColor(status) {
|
|
674
|
+
switch (status) {
|
|
675
|
+
case 'success': return theme.success;
|
|
676
|
+
case 'error': return theme.error;
|
|
677
|
+
case 'warning': return theme.warning;
|
|
678
|
+
case 'pending': return theme.info;
|
|
679
|
+
default: return theme.secondary;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
formatActionIcon(status) {
|
|
683
|
+
const colorize = this.resolveStatusColor(status);
|
|
684
|
+
return colorize(`${icons.action}`);
|
|
685
|
+
}
|
|
1219
686
|
wrapLine(text, width) {
|
|
1220
|
-
|
|
1221
|
-
if (width <= 0) {
|
|
687
|
+
if (width <= 0)
|
|
1222
688
|
return [text];
|
|
1223
|
-
|
|
1224
|
-
if (!text) {
|
|
689
|
+
if (!text)
|
|
1225
690
|
return [''];
|
|
1226
|
-
|
|
1227
|
-
if (text.length <= width) {
|
|
691
|
+
if (text.length <= width)
|
|
1228
692
|
return [text];
|
|
1229
|
-
}
|
|
1230
693
|
const words = text.split(/\s+/).filter(Boolean);
|
|
1231
|
-
|
|
1232
|
-
if (!words.length) {
|
|
694
|
+
if (!words.length)
|
|
1233
695
|
return this.chunkWord(text, width);
|
|
1234
|
-
}
|
|
1235
696
|
const lines = [];
|
|
1236
697
|
let current = '';
|
|
1237
698
|
for (const word of words) {
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
699
|
+
if (!current) {
|
|
700
|
+
if (word.length <= width) {
|
|
701
|
+
current = word;
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
const chunks = this.chunkWord(word, width);
|
|
705
|
+
lines.push(...chunks.slice(0, -1));
|
|
706
|
+
current = chunks[chunks.length - 1] ?? '';
|
|
707
|
+
}
|
|
1241
708
|
}
|
|
1242
|
-
if (
|
|
1243
|
-
|
|
1244
|
-
lines.push(...appendResult.chunks.slice(0, -1));
|
|
1245
|
-
current = appendResult.chunks[appendResult.chunks.length - 1] ?? '';
|
|
709
|
+
else if (current.length + 1 + word.length <= width) {
|
|
710
|
+
current = `${current} ${word}`;
|
|
1246
711
|
}
|
|
1247
712
|
else {
|
|
1248
|
-
current
|
|
713
|
+
lines.push(current);
|
|
714
|
+
if (word.length <= width) {
|
|
715
|
+
current = word;
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
const chunks = this.chunkWord(word, width);
|
|
719
|
+
lines.push(...chunks.slice(0, -1));
|
|
720
|
+
current = chunks[chunks.length - 1] ?? '';
|
|
721
|
+
}
|
|
1249
722
|
}
|
|
1250
723
|
}
|
|
1251
724
|
if (current) {
|
|
@@ -1253,64 +726,23 @@ export class Display {
|
|
|
1253
726
|
}
|
|
1254
727
|
return lines.length ? lines : [''];
|
|
1255
728
|
}
|
|
1256
|
-
/**
|
|
1257
|
-
* Attempts to append a word to the current line.
|
|
1258
|
-
* Returns instructions on how to handle the word.
|
|
1259
|
-
*/
|
|
1260
|
-
tryAppendWord(current, word, width) {
|
|
1261
|
-
if (!word) {
|
|
1262
|
-
return { shouldFlush: false, newCurrent: current, chunks: [] };
|
|
1263
|
-
}
|
|
1264
|
-
// Empty current line - start new line with word
|
|
1265
|
-
if (!current) {
|
|
1266
|
-
if (word.length <= width) {
|
|
1267
|
-
return { shouldFlush: false, newCurrent: word, chunks: [] };
|
|
1268
|
-
}
|
|
1269
|
-
// Word too long, need to chunk it
|
|
1270
|
-
return { shouldFlush: false, newCurrent: '', chunks: this.chunkWord(word, width) };
|
|
1271
|
-
}
|
|
1272
|
-
// Word fits on current line with space
|
|
1273
|
-
if (current.length + 1 + word.length <= width) {
|
|
1274
|
-
return { shouldFlush: false, newCurrent: `${current} ${word}`, chunks: [] };
|
|
1275
|
-
}
|
|
1276
|
-
// Word doesn't fit - flush current and start new line
|
|
1277
|
-
if (word.length <= width) {
|
|
1278
|
-
return { shouldFlush: true, newCurrent: word, chunks: [] };
|
|
1279
|
-
}
|
|
1280
|
-
// Word doesn't fit and is too long - flush current and chunk word
|
|
1281
|
-
return { shouldFlush: true, newCurrent: '', chunks: this.chunkWord(word, width) };
|
|
1282
|
-
}
|
|
1283
|
-
/**
|
|
1284
|
-
* Splits a long word into chunks that fit within the specified width.
|
|
1285
|
-
* Used when a single word is too long to fit on one line.
|
|
1286
|
-
*/
|
|
1287
729
|
chunkWord(word, width) {
|
|
1288
|
-
if (width <= 0 || !word)
|
|
730
|
+
if (width <= 0 || !word)
|
|
1289
731
|
return word ? [word] : [''];
|
|
1290
|
-
}
|
|
1291
732
|
const chunks = [];
|
|
1292
733
|
for (let i = 0; i < word.length; i += width) {
|
|
1293
734
|
chunks.push(word.slice(i, i + width));
|
|
1294
735
|
}
|
|
1295
736
|
return chunks.length > 0 ? chunks : [''];
|
|
1296
737
|
}
|
|
1297
|
-
/**
|
|
1298
|
-
* Returns the visible length of a string, excluding ANSI escape codes.
|
|
1299
|
-
*/
|
|
1300
738
|
visibleLength(value) {
|
|
1301
|
-
if (!value)
|
|
739
|
+
if (!value)
|
|
1302
740
|
return 0;
|
|
1303
|
-
}
|
|
1304
741
|
return this.stripAnsi(value).length;
|
|
1305
742
|
}
|
|
1306
|
-
/**
|
|
1307
|
-
* Removes ANSI escape codes from a string to get the visible text.
|
|
1308
|
-
* Uses the standard ANSI escape sequence pattern.
|
|
1309
|
-
*/
|
|
1310
743
|
stripAnsi(value) {
|
|
1311
|
-
if (!value)
|
|
744
|
+
if (!value)
|
|
1312
745
|
return '';
|
|
1313
|
-
}
|
|
1314
746
|
return value.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, '');
|
|
1315
747
|
}
|
|
1316
748
|
}
|