erosolar-cli 1.7.428 → 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 +6 -1
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +129 -92
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +13 -33
- 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 +19 -12
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +73 -56
- 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 -926
- 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 -10
- package/dist/ui/streamingFormatter.d.ts.map +1 -1
- package/dist/ui/streamingFormatter.js +9 -59
- 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,244 +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
|
-
this.withOutput(() => {
|
|
660
|
-
this.writeLine(); // Ensure clean start for the box
|
|
661
|
-
this.writeLine(wrapped);
|
|
662
|
-
this.writeLine();
|
|
663
|
-
});
|
|
197
|
+
const wrapped = isThought ? this.applySingleBulletBlock(body) : body;
|
|
198
|
+
this.enqueueEvent('raw', `\n${wrapped}\n\n`);
|
|
664
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Show narrative (thought)
|
|
202
|
+
*/
|
|
665
203
|
showNarrative(content) {
|
|
666
|
-
if (!content.trim())
|
|
204
|
+
if (!content.trim())
|
|
667
205
|
return;
|
|
668
|
-
}
|
|
669
206
|
this.showAssistantMessage(content, { isFinal: false });
|
|
670
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Show action
|
|
210
|
+
*/
|
|
671
211
|
showAction(text, status = 'info') {
|
|
672
|
-
if (!text.trim())
|
|
212
|
+
if (!text.trim())
|
|
673
213
|
return;
|
|
674
|
-
}
|
|
675
|
-
this.clearSpinnerIfActive();
|
|
676
|
-
// Claude Code style: always use ⏺ prefix for actions
|
|
677
214
|
const icon = this.formatActionIcon(status);
|
|
678
|
-
this.
|
|
679
|
-
|
|
680
|
-
});
|
|
215
|
+
const rendered = this.wrapWithPrefix(text, `${icon} `);
|
|
216
|
+
this.enqueueEvent('raw', `${rendered}\n`);
|
|
681
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Show sub-action
|
|
220
|
+
*/
|
|
682
221
|
showSubAction(text, status = 'info') {
|
|
683
|
-
if (!text.trim())
|
|
222
|
+
if (!text.trim())
|
|
684
223
|
return;
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const prefersRich = text.includes('```');
|
|
688
|
-
let rendered = prefersRich ? this.buildRichSubActionLines(text, status) : this.buildWrappedSubActionLines(text, status);
|
|
689
|
-
if (!rendered.length && prefersRich) {
|
|
690
|
-
rendered = this.buildWrappedSubActionLines(text, status);
|
|
691
|
-
}
|
|
692
|
-
if (!rendered.length) {
|
|
224
|
+
const lines = this.buildWrappedSubActionLines(text, status);
|
|
225
|
+
if (!lines.length)
|
|
693
226
|
return;
|
|
694
|
-
}
|
|
695
|
-
this.withOutput(() => {
|
|
696
|
-
this.writeLine(rendered.join('\n'));
|
|
697
|
-
this.writeLine();
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
buildWrappedSubActionLines(text, status) {
|
|
701
|
-
const lines = text.split('\n').map((line) => line.trimEnd());
|
|
702
|
-
while (lines.length && !lines[lines.length - 1]?.trim()) {
|
|
703
|
-
lines.pop();
|
|
704
|
-
}
|
|
705
|
-
if (!lines.length) {
|
|
706
|
-
return [];
|
|
707
|
-
}
|
|
708
|
-
const rendered = [];
|
|
709
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
710
|
-
const segment = lines[index] ?? '';
|
|
711
|
-
const isLast = index === lines.length - 1;
|
|
712
|
-
const { prefix, continuation } = this.buildSubActionPrefixes(status, isLast);
|
|
713
|
-
rendered.push(this.wrapWithPrefix(segment, prefix, { continuationPrefix: continuation }));
|
|
714
|
-
}
|
|
715
|
-
return rendered;
|
|
716
|
-
}
|
|
717
|
-
buildRichSubActionLines(text, status) {
|
|
718
|
-
const normalized = text.trim();
|
|
719
|
-
if (!normalized) {
|
|
720
|
-
return [];
|
|
721
|
-
}
|
|
722
|
-
const width = Math.max(DISPLAY_CONSTANTS.MIN_ACTION_WIDTH, Math.min(this.getColumnWidth(), DISPLAY_CONSTANTS.MAX_ACTION_WIDTH));
|
|
723
|
-
const samplePrefix = this.buildSubActionPrefixes(status, true).prefix;
|
|
724
|
-
const contentWidth = Math.max(DISPLAY_CONSTANTS.MIN_CONTENT_WIDTH, width - this.visibleLength(samplePrefix));
|
|
725
|
-
const blocks = formatRichContent(normalized, contentWidth);
|
|
726
|
-
if (!blocks.length) {
|
|
727
|
-
return [];
|
|
728
|
-
}
|
|
729
|
-
return blocks.map((line, index) => {
|
|
730
|
-
const isLast = index === blocks.length - 1;
|
|
731
|
-
const { prefix } = this.buildSubActionPrefixes(status, isLast);
|
|
732
|
-
if (!line.trim()) {
|
|
733
|
-
return prefix.trimEnd();
|
|
734
|
-
}
|
|
735
|
-
return `${prefix}${line}`;
|
|
736
|
-
});
|
|
227
|
+
this.enqueueEvent('raw', `${lines.join('\n')}\n\n`);
|
|
737
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Show message
|
|
231
|
+
*/
|
|
738
232
|
showMessage(content, role = 'assistant') {
|
|
739
233
|
if (role === 'system') {
|
|
740
234
|
this.showSystemMessage(content);
|
|
@@ -743,16 +237,19 @@ export class Display {
|
|
|
743
237
|
this.showAssistantMessage(content);
|
|
744
238
|
}
|
|
745
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Show system message
|
|
242
|
+
*/
|
|
746
243
|
showSystemMessage(content) {
|
|
747
|
-
this.clearSpinnerIfActive();
|
|
748
244
|
const normalized = content.trim();
|
|
749
|
-
if (!normalized)
|
|
245
|
+
if (!normalized)
|
|
750
246
|
return;
|
|
751
|
-
}
|
|
752
247
|
this.stream(`${normalized}\n\n`);
|
|
753
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Show error
|
|
251
|
+
*/
|
|
754
252
|
showError(message, error) {
|
|
755
|
-
this.clearSpinnerIfActive();
|
|
756
253
|
const details = this.formatErrorDetails(error);
|
|
757
254
|
const parts = [`${theme.error('✗')} ${message}`];
|
|
758
255
|
if (details) {
|
|
@@ -760,23 +257,26 @@ export class Display {
|
|
|
760
257
|
}
|
|
761
258
|
this.stream(`${parts.join('\n')}\n`);
|
|
762
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Show warning
|
|
262
|
+
*/
|
|
763
263
|
showWarning(message) {
|
|
764
|
-
this.clearSpinnerIfActive();
|
|
765
264
|
this.stream(`${theme.warning('!')} ${message}\n`);
|
|
766
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Show info
|
|
268
|
+
*/
|
|
767
269
|
showInfo(message) {
|
|
768
|
-
this.clearSpinnerIfActive();
|
|
769
270
|
this.stream(`${theme.info('ℹ')} ${message}\n`);
|
|
770
271
|
}
|
|
771
272
|
/**
|
|
772
|
-
* Show
|
|
273
|
+
* Show success
|
|
773
274
|
*/
|
|
774
275
|
showSuccess(message) {
|
|
775
|
-
this.clearSpinnerIfActive();
|
|
776
276
|
this.stream(`${theme.success('✓')} ${message}\n`);
|
|
777
277
|
}
|
|
778
278
|
/**
|
|
779
|
-
* Show
|
|
279
|
+
* Show progress badge
|
|
780
280
|
*/
|
|
781
281
|
showProgressBadge(label, current, total) {
|
|
782
282
|
const percentage = Math.round((current / total) * 100);
|
|
@@ -785,66 +285,133 @@ export class Display {
|
|
|
785
285
|
const empty = barWidth - filled;
|
|
786
286
|
const progressBar = `${'█'.repeat(filled)}${'░'.repeat(empty)}`;
|
|
787
287
|
const badge = `[${label}] ${progressBar} ${percentage}%`;
|
|
788
|
-
this.
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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,
|
|
793
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
|
|
794
358
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
}
|
|
801
|
-
const elapsed = this.formatElapsed(elapsedMs);
|
|
802
|
-
const parts = [
|
|
803
|
-
{ text: `${theme.success('✓')} ${normalized}`, tone: 'success' },
|
|
804
|
-
elapsed ? { text: `(${elapsed})`, tone: 'muted' } : null,
|
|
805
|
-
].filter(Boolean);
|
|
806
|
-
const line = renderStatusLine(parts, this.getColumnWidth() - 2);
|
|
807
|
-
this.withOutput(() => {
|
|
808
|
-
this.writeLine(line);
|
|
809
|
-
});
|
|
359
|
+
/**
|
|
360
|
+
* Update streaming status (routes to renderer)
|
|
361
|
+
*/
|
|
362
|
+
updateStreamingStatus(status) {
|
|
363
|
+
this.renderer?.updateStatus(status);
|
|
810
364
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
365
|
+
/**
|
|
366
|
+
* Clear streaming status
|
|
367
|
+
*/
|
|
368
|
+
clearStreamingStatus() {
|
|
369
|
+
this.renderer?.updateStatus(null);
|
|
815
370
|
}
|
|
816
371
|
/**
|
|
817
|
-
*
|
|
372
|
+
* Check if streaming status is visible
|
|
818
373
|
*/
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
const panel = this.buildCommandPalette(commands, options);
|
|
824
|
-
if (!panel.trim()) {
|
|
825
|
-
return;
|
|
826
|
-
}
|
|
827
|
-
this.withOutput(() => {
|
|
828
|
-
this.writeLine();
|
|
829
|
-
this.writeLine(panel);
|
|
830
|
-
this.writeLine();
|
|
831
|
-
});
|
|
374
|
+
isStreamingStatusVisible() {
|
|
375
|
+
return false; // Renderer manages this
|
|
832
376
|
}
|
|
833
377
|
/**
|
|
834
|
-
*
|
|
835
|
-
* Note: Commands are now shown in the banner, so this is a no-op
|
|
378
|
+
* Legacy compatibility methods (no-ops)
|
|
836
379
|
*/
|
|
837
|
-
|
|
838
|
-
|
|
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();
|
|
839
408
|
}
|
|
840
409
|
formatErrorDetails(error) {
|
|
841
|
-
if (!error)
|
|
410
|
+
if (!error)
|
|
842
411
|
return null;
|
|
843
|
-
}
|
|
844
412
|
if (error instanceof Error) {
|
|
845
|
-
if (error.stack)
|
|
413
|
+
if (error.stack)
|
|
846
414
|
return highlightError(error.stack);
|
|
847
|
-
}
|
|
848
415
|
return highlightError(error.message);
|
|
849
416
|
}
|
|
850
417
|
if (typeof error === 'string') {
|
|
@@ -857,83 +424,22 @@ export class Display {
|
|
|
857
424
|
return null;
|
|
858
425
|
}
|
|
859
426
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
if (index < 1 || total < 1 || index > total) {
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
const width = Math.max(DISPLAY_CONSTANTS.MIN_THOUGHT_WIDTH, Math.min(this.getColumnWidth(), DISPLAY_CONSTANTS.MAX_MESSAGE_WIDTH));
|
|
869
|
-
const heading = renderSectionHeading(`Plan ${index}/${total}`, {
|
|
870
|
-
subtitle: step,
|
|
871
|
-
icon: icons.arrow,
|
|
872
|
-
tone: 'info',
|
|
873
|
-
width,
|
|
874
|
-
});
|
|
875
|
-
this.withOutput(() => {
|
|
876
|
-
this.writeLine(heading);
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
clear() {
|
|
880
|
-
this.withOutput(() => {
|
|
881
|
-
try {
|
|
882
|
-
cursorTo(this.outputStream, 0, 0);
|
|
883
|
-
clearScreenDown(this.outputStream);
|
|
884
|
-
}
|
|
885
|
-
catch {
|
|
886
|
-
this.write('\x1Bc');
|
|
887
|
-
}
|
|
888
|
-
});
|
|
889
|
-
this.stdoutTracker.reset();
|
|
890
|
-
// Banner is streamed content - no re-render on clear
|
|
891
|
-
}
|
|
892
|
-
formatModelLabel(model) {
|
|
893
|
-
if (/gpt-5\.1-?codex/i.test(model)) {
|
|
894
|
-
return model;
|
|
895
|
-
}
|
|
896
|
-
if (/sonnet-4[-.]?5/i.test(model)) {
|
|
897
|
-
return 'Sonnet 4.5';
|
|
898
|
-
}
|
|
899
|
-
if (/opus-4[-.]?1/i.test(model)) {
|
|
900
|
-
return 'Opus 4.1';
|
|
901
|
-
}
|
|
902
|
-
if (/haiku-4[-.]?5/i.test(model)) {
|
|
903
|
-
return 'Haiku 4.5';
|
|
904
|
-
}
|
|
905
|
-
if (/gpt-5\.1/i.test(model)) {
|
|
906
|
-
return 'GPT-5.1';
|
|
907
|
-
}
|
|
908
|
-
if (/gpt-5-?pro/i.test(model)) {
|
|
909
|
-
return 'GPT-5 Pro';
|
|
910
|
-
}
|
|
911
|
-
if (/gpt-5-?mini/i.test(model)) {
|
|
912
|
-
return 'GPT-5 Mini';
|
|
913
|
-
}
|
|
914
|
-
if (/gpt-5-?nano/i.test(model)) {
|
|
915
|
-
return 'GPT-5 Nano';
|
|
916
|
-
}
|
|
917
|
-
return model;
|
|
918
|
-
}
|
|
919
|
-
compactPath(path, maxLen) {
|
|
920
|
-
if (!path) {
|
|
921
|
-
return '';
|
|
922
|
-
}
|
|
923
|
-
if (this.visibleLength(path) <= maxLen) {
|
|
924
|
-
return path;
|
|
427
|
+
formatElapsed(elapsedMs) {
|
|
428
|
+
if (typeof elapsedMs !== 'number' || !Number.isFinite(elapsedMs) || elapsedMs < 0) {
|
|
429
|
+
return null;
|
|
925
430
|
}
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
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`;
|
|
929
436
|
}
|
|
930
|
-
return `${
|
|
437
|
+
return `${seconds}s`;
|
|
931
438
|
}
|
|
932
439
|
buildChatBox(content, metadata) {
|
|
933
440
|
const normalized = content.trim();
|
|
934
|
-
if (!normalized)
|
|
441
|
+
if (!normalized)
|
|
935
442
|
return '';
|
|
936
|
-
}
|
|
937
443
|
if (isPlainOutputMode()) {
|
|
938
444
|
const body = renderMessageBody(normalized, this.resolveMessageWidth());
|
|
939
445
|
const telemetry = this.formatTelemetryLine(metadata);
|
|
@@ -948,19 +454,15 @@ export class Display {
|
|
|
948
454
|
borderColor: theme.ui.border,
|
|
949
455
|
});
|
|
950
456
|
const telemetry = this.formatTelemetryLine(metadata);
|
|
951
|
-
|
|
952
|
-
return panel;
|
|
953
|
-
}
|
|
954
|
-
return `${panel}\n${telemetry}`;
|
|
457
|
+
return telemetry ? `${panel}\n${telemetry}` : panel;
|
|
955
458
|
}
|
|
956
459
|
resolveMessageWidth() {
|
|
957
460
|
const columns = this.getColumnWidth();
|
|
958
461
|
return Math.max(DISPLAY_CONSTANTS.MIN_MESSAGE_WIDTH, Math.min(columns - DISPLAY_CONSTANTS.MESSAGE_PADDING, DISPLAY_CONSTANTS.MAX_MESSAGE_WIDTH));
|
|
959
462
|
}
|
|
960
463
|
formatTelemetryLine(metadata) {
|
|
961
|
-
if (!metadata)
|
|
464
|
+
if (!metadata)
|
|
962
465
|
return '';
|
|
963
|
-
}
|
|
964
466
|
const parts = [];
|
|
965
467
|
const elapsed = this.formatElapsed(metadata.elapsedMs);
|
|
966
468
|
if (elapsed) {
|
|
@@ -968,28 +470,67 @@ export class Display {
|
|
|
968
470
|
const elapsedValue = theme.metrics?.elapsedValue ?? theme.secondary;
|
|
969
471
|
parts.push(`${elapsedLabel('elapsed')} ${elapsedValue(elapsed)}`);
|
|
970
472
|
}
|
|
971
|
-
if (!parts.length)
|
|
473
|
+
if (!parts.length)
|
|
972
474
|
return '';
|
|
973
|
-
}
|
|
974
475
|
const separator = theme.ui.muted(' • ');
|
|
975
476
|
return ` ${parts.join(separator)}`;
|
|
976
477
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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}`);
|
|
980
491
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
const seconds = totalSeconds % 60;
|
|
984
|
-
if (minutes > 0) {
|
|
985
|
-
return `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
|
|
492
|
+
else {
|
|
493
|
+
lines.push(`${theme.info('✻')} ${thinkingStyle.label('Thinking…')}`);
|
|
986
494
|
}
|
|
987
|
-
|
|
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');
|
|
988
530
|
}
|
|
989
531
|
buildCommandPalette(commands, options) {
|
|
990
|
-
if (!commands.length)
|
|
532
|
+
if (!commands.length)
|
|
991
533
|
return '';
|
|
992
|
-
}
|
|
993
534
|
const width = Math.max(DISPLAY_CONSTANTS.MIN_MESSAGE_WIDTH, Math.min(this.getColumnWidth() - 2, DISPLAY_CONSTANTS.MAX_MESSAGE_WIDTH));
|
|
994
535
|
const indent = ' ';
|
|
995
536
|
const grouped = this.groupPaletteCommands(commands);
|
|
@@ -1048,39 +589,25 @@ export class Display {
|
|
|
1048
589
|
return Math.min(maxAllowed, budget);
|
|
1049
590
|
}
|
|
1050
591
|
formatPaletteCategory(category) {
|
|
1051
|
-
if (!category)
|
|
592
|
+
if (!category)
|
|
1052
593
|
return 'Other';
|
|
1053
|
-
}
|
|
1054
594
|
switch (category.toLowerCase()) {
|
|
1055
|
-
case 'configuration':
|
|
1056
|
-
|
|
1057
|
-
case '
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
return 'Diagnostics';
|
|
1061
|
-
case 'other':
|
|
1062
|
-
return 'Other';
|
|
1063
|
-
default:
|
|
1064
|
-
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);
|
|
1065
600
|
}
|
|
1066
601
|
}
|
|
1067
602
|
colorizePaletteText(text, tone) {
|
|
1068
603
|
switch (tone) {
|
|
1069
|
-
case 'warn':
|
|
1070
|
-
|
|
1071
|
-
case '
|
|
1072
|
-
return theme.success(text);
|
|
1073
|
-
case 'info':
|
|
1074
|
-
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);
|
|
1075
607
|
case 'muted':
|
|
1076
|
-
default:
|
|
1077
|
-
return theme.ui.muted(text);
|
|
608
|
+
default: return theme.ui.muted(text);
|
|
1078
609
|
}
|
|
1079
610
|
}
|
|
1080
|
-
/**
|
|
1081
|
-
* Wraps text with a prefix on the first line and optional continuation prefix.
|
|
1082
|
-
* Handles multi-line text and word wrapping intelligently.
|
|
1083
|
-
*/
|
|
1084
611
|
wrapWithPrefix(text, prefix, options) {
|
|
1085
612
|
if (!text) {
|
|
1086
613
|
return prefix.trimEnd();
|
|
@@ -1113,93 +640,25 @@ export class Display {
|
|
|
1113
640
|
}
|
|
1114
641
|
return lines.join('\n');
|
|
1115
642
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
case 'error':
|
|
1121
|
-
return theme.error;
|
|
1122
|
-
case 'warning':
|
|
1123
|
-
return theme.warning;
|
|
1124
|
-
case 'pending':
|
|
1125
|
-
return theme.info;
|
|
1126
|
-
default:
|
|
1127
|
-
return theme.secondary;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
formatActionIcon(status) {
|
|
1131
|
-
const colorize = this.resolveStatusColor(status);
|
|
1132
|
-
return colorize(`${icons.action}`);
|
|
1133
|
-
}
|
|
1134
|
-
/**
|
|
1135
|
-
* Prefix a multi-line block with a single bullet, indenting subsequent lines.
|
|
1136
|
-
* Keeps entire assistant responses as one visual event.
|
|
1137
|
-
*/
|
|
1138
|
-
applySingleBulletBlock(text) {
|
|
1139
|
-
const lines = text.split('\n');
|
|
1140
|
-
const bullet = `${icons.action} `;
|
|
1141
|
-
const prefix = theme.info(bullet);
|
|
1142
|
-
const indent = ' '.repeat(this.visibleLength(this.stripAnsi(bullet)));
|
|
1143
|
-
return lines
|
|
1144
|
-
.map((line, index) => (index === 0 ? `${prefix}${line}` : `${indent}${line}`))
|
|
1145
|
-
.join('\n');
|
|
1146
|
-
}
|
|
1147
|
-
buildClaudeStyleThought(content, durationMs) {
|
|
1148
|
-
// Claude Code style: ∴ Thought for Xs or ✻ Thinking…
|
|
1149
|
-
const thinkingStyle = theme.thinking || {
|
|
1150
|
-
icon: theme.info,
|
|
1151
|
-
text: theme.ui.muted,
|
|
1152
|
-
border: theme.ui.border,
|
|
1153
|
-
label: theme.info,
|
|
1154
|
-
};
|
|
1155
|
-
const width = Math.min(this.getColumnWidth() - 4, 70);
|
|
1156
|
-
const lines = [];
|
|
1157
|
-
// Header: "∴ Thought for Xs" for completed, "✻ Thinking…" for active
|
|
1158
|
-
if (durationMs !== undefined) {
|
|
1159
|
-
const elapsed = this.formatElapsedTime(Math.floor(durationMs / 1000));
|
|
1160
|
-
lines.push(`${theme.info('∴')} Thought for ${elapsed}`);
|
|
1161
|
-
}
|
|
1162
|
-
else {
|
|
1163
|
-
lines.push(`${theme.info('✻')} ${thinkingStyle.label('Thinking…')}`);
|
|
1164
|
-
}
|
|
1165
|
-
// Parse and format the thinking content with simple indentation
|
|
1166
|
-
const contentLines = content.split('\n');
|
|
1167
|
-
const hasContent = contentLines.some(line => line.trim().length > 0);
|
|
1168
|
-
if (hasContent) {
|
|
1169
|
-
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();
|
|
1170
647
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
lines.push(` ${thinkingStyle.text(wrappedLine)}`);
|
|
1180
|
-
}
|
|
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 }));
|
|
1181
656
|
}
|
|
1182
|
-
return
|
|
1183
|
-
}
|
|
1184
|
-
/**
|
|
1185
|
-
* Show a thinking block with rich formatting (public method for external use)
|
|
1186
|
-
* @param content The thinking content to display
|
|
1187
|
-
* @param durationMs Optional duration in milliseconds to show "Thought for Xs"
|
|
1188
|
-
*/
|
|
1189
|
-
showThinkingBlock(content, durationMs) {
|
|
1190
|
-
this.clearSpinnerIfActive();
|
|
1191
|
-
const block = this.buildClaudeStyleThought(content, durationMs);
|
|
1192
|
-
this.withOutput(() => {
|
|
1193
|
-
this.writeLine();
|
|
1194
|
-
this.writeLine(block);
|
|
1195
|
-
this.writeLine();
|
|
1196
|
-
this.writeLine(); // Extra newline for better visual separation
|
|
1197
|
-
});
|
|
657
|
+
return rendered;
|
|
1198
658
|
}
|
|
1199
659
|
buildSubActionPrefixes(status, isLast) {
|
|
1200
660
|
if (isLast) {
|
|
1201
661
|
const colorize = this.resolveStatusColor(status);
|
|
1202
|
-
// Claude Code style: use ⎿ for sub-action result/detail prefix
|
|
1203
662
|
return {
|
|
1204
663
|
prefix: ` ${colorize(icons.subaction)} `,
|
|
1205
664
|
continuation: ' ',
|
|
@@ -1211,40 +670,55 @@ export class Display {
|
|
|
1211
670
|
continuation: ` ${branch} `,
|
|
1212
671
|
};
|
|
1213
672
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
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
|
+
}
|
|
1218
686
|
wrapLine(text, width) {
|
|
1219
|
-
|
|
1220
|
-
if (width <= 0) {
|
|
687
|
+
if (width <= 0)
|
|
1221
688
|
return [text];
|
|
1222
|
-
|
|
1223
|
-
if (!text) {
|
|
689
|
+
if (!text)
|
|
1224
690
|
return [''];
|
|
1225
|
-
|
|
1226
|
-
if (text.length <= width) {
|
|
691
|
+
if (text.length <= width)
|
|
1227
692
|
return [text];
|
|
1228
|
-
}
|
|
1229
693
|
const words = text.split(/\s+/).filter(Boolean);
|
|
1230
|
-
|
|
1231
|
-
if (!words.length) {
|
|
694
|
+
if (!words.length)
|
|
1232
695
|
return this.chunkWord(text, width);
|
|
1233
|
-
}
|
|
1234
696
|
const lines = [];
|
|
1235
697
|
let current = '';
|
|
1236
698
|
for (const word of words) {
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
+
}
|
|
1240
708
|
}
|
|
1241
|
-
if (
|
|
1242
|
-
|
|
1243
|
-
lines.push(...appendResult.chunks.slice(0, -1));
|
|
1244
|
-
current = appendResult.chunks[appendResult.chunks.length - 1] ?? '';
|
|
709
|
+
else if (current.length + 1 + word.length <= width) {
|
|
710
|
+
current = `${current} ${word}`;
|
|
1245
711
|
}
|
|
1246
712
|
else {
|
|
1247
|
-
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
|
+
}
|
|
1248
722
|
}
|
|
1249
723
|
}
|
|
1250
724
|
if (current) {
|
|
@@ -1252,64 +726,23 @@ export class Display {
|
|
|
1252
726
|
}
|
|
1253
727
|
return lines.length ? lines : [''];
|
|
1254
728
|
}
|
|
1255
|
-
/**
|
|
1256
|
-
* Attempts to append a word to the current line.
|
|
1257
|
-
* Returns instructions on how to handle the word.
|
|
1258
|
-
*/
|
|
1259
|
-
tryAppendWord(current, word, width) {
|
|
1260
|
-
if (!word) {
|
|
1261
|
-
return { shouldFlush: false, newCurrent: current, chunks: [] };
|
|
1262
|
-
}
|
|
1263
|
-
// Empty current line - start new line with word
|
|
1264
|
-
if (!current) {
|
|
1265
|
-
if (word.length <= width) {
|
|
1266
|
-
return { shouldFlush: false, newCurrent: word, chunks: [] };
|
|
1267
|
-
}
|
|
1268
|
-
// Word too long, need to chunk it
|
|
1269
|
-
return { shouldFlush: false, newCurrent: '', chunks: this.chunkWord(word, width) };
|
|
1270
|
-
}
|
|
1271
|
-
// Word fits on current line with space
|
|
1272
|
-
if (current.length + 1 + word.length <= width) {
|
|
1273
|
-
return { shouldFlush: false, newCurrent: `${current} ${word}`, chunks: [] };
|
|
1274
|
-
}
|
|
1275
|
-
// Word doesn't fit - flush current and start new line
|
|
1276
|
-
if (word.length <= width) {
|
|
1277
|
-
return { shouldFlush: true, newCurrent: word, chunks: [] };
|
|
1278
|
-
}
|
|
1279
|
-
// Word doesn't fit and is too long - flush current and chunk word
|
|
1280
|
-
return { shouldFlush: true, newCurrent: '', chunks: this.chunkWord(word, width) };
|
|
1281
|
-
}
|
|
1282
|
-
/**
|
|
1283
|
-
* Splits a long word into chunks that fit within the specified width.
|
|
1284
|
-
* Used when a single word is too long to fit on one line.
|
|
1285
|
-
*/
|
|
1286
729
|
chunkWord(word, width) {
|
|
1287
|
-
if (width <= 0 || !word)
|
|
730
|
+
if (width <= 0 || !word)
|
|
1288
731
|
return word ? [word] : [''];
|
|
1289
|
-
}
|
|
1290
732
|
const chunks = [];
|
|
1291
733
|
for (let i = 0; i < word.length; i += width) {
|
|
1292
734
|
chunks.push(word.slice(i, i + width));
|
|
1293
735
|
}
|
|
1294
736
|
return chunks.length > 0 ? chunks : [''];
|
|
1295
737
|
}
|
|
1296
|
-
/**
|
|
1297
|
-
* Returns the visible length of a string, excluding ANSI escape codes.
|
|
1298
|
-
*/
|
|
1299
738
|
visibleLength(value) {
|
|
1300
|
-
if (!value)
|
|
739
|
+
if (!value)
|
|
1301
740
|
return 0;
|
|
1302
|
-
}
|
|
1303
741
|
return this.stripAnsi(value).length;
|
|
1304
742
|
}
|
|
1305
|
-
/**
|
|
1306
|
-
* Removes ANSI escape codes from a string to get the visible text.
|
|
1307
|
-
* Uses the standard ANSI escape sequence pattern.
|
|
1308
|
-
*/
|
|
1309
743
|
stripAnsi(value) {
|
|
1310
|
-
if (!value)
|
|
744
|
+
if (!value)
|
|
1311
745
|
return '';
|
|
1312
|
-
}
|
|
1313
746
|
return value.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, '');
|
|
1314
747
|
}
|
|
1315
748
|
}
|