erosolar-cli 1.7.429 → 1.7.431
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/preferences.d.ts +3 -1
- package/dist/core/preferences.d.ts.map +1 -1
- package/dist/core/preferences.js +4 -2
- package/dist/core/preferences.js.map +1 -1
- 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 +120 -88
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +14 -34
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/shell/terminalInputAdapter.d.ts +77 -85
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -1
- package/dist/shell/terminalInputAdapter.js +163 -223
- 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 +188 -0
- package/dist/ui/UnifiedUIRenderer.d.ts.map +1 -0
- package/dist/ui/UnifiedUIRenderer.js +581 -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 +364 -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 -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
|
|
29
|
+
* Display class - now a thin wrapper around UnifiedUIRenderer
|
|
165
30
|
*
|
|
166
|
-
*
|
|
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
|
|
171
|
-
*
|
|
172
|
-
* Claude Code Style Formatting:
|
|
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 (
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
const normalized = this.normalizeStreamingContent(content);
|
|
309
|
-
if (!normalized) {
|
|
101
|
+
if (!content)
|
|
310
102
|
return;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (isStreamingMode()) {
|
|
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');
|
|
103
|
+
this.notifyBeforeOutput();
|
|
104
|
+
if (this.enqueueEvent('raw', content)) {
|
|
105
|
+
this.notifyAfterOutput(content);
|
|
327
106
|
return;
|
|
328
107
|
}
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
*
|
|
408
|
-
*/
|
|
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.
|
|
140
|
+
* Show thinking indicator
|
|
415
141
|
*/
|
|
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,76 @@ 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;
|
|
197
|
+
const wrapped = isThought ? this.applySingleBulletBlock(body) : body;
|
|
198
|
+
const output = `\n${wrapped}\n\n`;
|
|
199
|
+
this.notifyBeforeOutput();
|
|
200
|
+
if (!this.enqueueEvent('raw', output)) {
|
|
201
|
+
// Fallback if no renderer
|
|
202
|
+
this.outputStream.write(output);
|
|
657
203
|
}
|
|
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
|
-
});
|
|
204
|
+
this.notifyAfterOutput(output);
|
|
665
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Show narrative (thought)
|
|
208
|
+
*/
|
|
666
209
|
showNarrative(content) {
|
|
667
|
-
if (!content.trim())
|
|
210
|
+
if (!content.trim())
|
|
668
211
|
return;
|
|
669
|
-
}
|
|
670
212
|
this.showAssistantMessage(content, { isFinal: false });
|
|
671
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Show action
|
|
216
|
+
*/
|
|
672
217
|
showAction(text, status = 'info') {
|
|
673
|
-
if (!text.trim())
|
|
218
|
+
if (!text.trim())
|
|
674
219
|
return;
|
|
675
|
-
}
|
|
676
|
-
this.clearSpinnerIfActive();
|
|
677
|
-
// Claude Code style: always use ⏺ prefix for actions
|
|
678
220
|
const icon = this.formatActionIcon(status);
|
|
679
|
-
this.
|
|
680
|
-
|
|
681
|
-
});
|
|
221
|
+
const rendered = this.wrapWithPrefix(text, `${icon} `);
|
|
222
|
+
this.enqueueEvent('raw', `${rendered}\n`);
|
|
682
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Show sub-action
|
|
226
|
+
*/
|
|
683
227
|
showSubAction(text, status = 'info') {
|
|
684
|
-
if (!text.trim())
|
|
228
|
+
if (!text.trim())
|
|
685
229
|
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) {
|
|
230
|
+
const lines = this.buildWrappedSubActionLines(text, status);
|
|
231
|
+
if (!lines.length)
|
|
694
232
|
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
|
-
});
|
|
233
|
+
this.enqueueEvent('raw', `${lines.join('\n')}\n\n`);
|
|
738
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Show message
|
|
237
|
+
*/
|
|
739
238
|
showMessage(content, role = 'assistant') {
|
|
740
239
|
if (role === 'system') {
|
|
741
240
|
this.showSystemMessage(content);
|
|
@@ -744,16 +243,19 @@ export class Display {
|
|
|
744
243
|
this.showAssistantMessage(content);
|
|
745
244
|
}
|
|
746
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Show system message
|
|
248
|
+
*/
|
|
747
249
|
showSystemMessage(content) {
|
|
748
|
-
this.clearSpinnerIfActive();
|
|
749
250
|
const normalized = content.trim();
|
|
750
|
-
if (!normalized)
|
|
251
|
+
if (!normalized)
|
|
751
252
|
return;
|
|
752
|
-
}
|
|
753
253
|
this.stream(`${normalized}\n\n`);
|
|
754
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Show error
|
|
257
|
+
*/
|
|
755
258
|
showError(message, error) {
|
|
756
|
-
this.clearSpinnerIfActive();
|
|
757
259
|
const details = this.formatErrorDetails(error);
|
|
758
260
|
const parts = [`${theme.error('✗')} ${message}`];
|
|
759
261
|
if (details) {
|
|
@@ -761,23 +263,26 @@ export class Display {
|
|
|
761
263
|
}
|
|
762
264
|
this.stream(`${parts.join('\n')}\n`);
|
|
763
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Show warning
|
|
268
|
+
*/
|
|
764
269
|
showWarning(message) {
|
|
765
|
-
this.clearSpinnerIfActive();
|
|
766
270
|
this.stream(`${theme.warning('!')} ${message}\n`);
|
|
767
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Show info
|
|
274
|
+
*/
|
|
768
275
|
showInfo(message) {
|
|
769
|
-
this.clearSpinnerIfActive();
|
|
770
276
|
this.stream(`${theme.info('ℹ')} ${message}\n`);
|
|
771
277
|
}
|
|
772
278
|
/**
|
|
773
|
-
* Show
|
|
279
|
+
* Show success
|
|
774
280
|
*/
|
|
775
281
|
showSuccess(message) {
|
|
776
|
-
this.clearSpinnerIfActive();
|
|
777
282
|
this.stream(`${theme.success('✓')} ${message}\n`);
|
|
778
283
|
}
|
|
779
284
|
/**
|
|
780
|
-
* Show
|
|
285
|
+
* Show progress badge
|
|
781
286
|
*/
|
|
782
287
|
showProgressBadge(label, current, total) {
|
|
783
288
|
const percentage = Math.round((current / total) * 100);
|
|
@@ -786,66 +291,133 @@ export class Display {
|
|
|
786
291
|
const empty = barWidth - filled;
|
|
787
292
|
const progressBar = `${'█'.repeat(filled)}${'░'.repeat(empty)}`;
|
|
788
293
|
const badge = `[${label}] ${progressBar} ${percentage}%`;
|
|
789
|
-
this.
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
294
|
+
this.stream(`\r${theme.info(badge)}`);
|
|
295
|
+
if (current >= total) {
|
|
296
|
+
this.stream('\n');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Show status line
|
|
301
|
+
*/
|
|
302
|
+
showStatusLine(status, elapsedMs, _context) {
|
|
303
|
+
const normalized = status?.trim();
|
|
304
|
+
if (!normalized)
|
|
305
|
+
return;
|
|
306
|
+
const elapsed = this.formatElapsed(elapsedMs);
|
|
307
|
+
const parts = [];
|
|
308
|
+
parts.push(`${theme.success('✓')} ${normalized}`);
|
|
309
|
+
if (elapsed) {
|
|
310
|
+
parts.push(`(${elapsed})`);
|
|
311
|
+
}
|
|
312
|
+
this.stream(`${parts.join(' ')}\n`);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Show available tools (no-op)
|
|
316
|
+
*/
|
|
317
|
+
showAvailableTools(_tools) {
|
|
318
|
+
// Hidden by default
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Show command palette
|
|
322
|
+
*/
|
|
323
|
+
showCommandPalette(commands, options) {
|
|
324
|
+
if (!commands || commands.length === 0)
|
|
325
|
+
return;
|
|
326
|
+
const panel = this.buildCommandPalette(commands, options);
|
|
327
|
+
if (!panel.trim())
|
|
328
|
+
return;
|
|
329
|
+
this.enqueueEvent('raw', `\n${panel}\n\n`);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Show ready with hints (no-op)
|
|
333
|
+
*/
|
|
334
|
+
showReadyWithHints() {
|
|
335
|
+
// Commands hint is now in the renderer
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Show planning step
|
|
339
|
+
*/
|
|
340
|
+
showPlanningStep(step, index, total) {
|
|
341
|
+
if (!step?.trim() || index < 1 || total < 1 || index > total)
|
|
342
|
+
return;
|
|
343
|
+
const width = Math.max(DISPLAY_CONSTANTS.MIN_THOUGHT_WIDTH, Math.min(this.getColumnWidth(), DISPLAY_CONSTANTS.MAX_MESSAGE_WIDTH));
|
|
344
|
+
const heading = renderSectionHeading(`Plan ${index}/${total}`, {
|
|
345
|
+
subtitle: step,
|
|
346
|
+
icon: icons.arrow,
|
|
347
|
+
tone: 'info',
|
|
348
|
+
width,
|
|
794
349
|
});
|
|
350
|
+
this.enqueueEvent('raw', `${heading}\n`);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Show thinking block
|
|
354
|
+
*/
|
|
355
|
+
showThinkingBlock(content, durationMs) {
|
|
356
|
+
const block = this.buildClaudeStyleThought(content, durationMs);
|
|
357
|
+
this.enqueueEvent('raw', `\n${block}\n\n`);
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Clear screen
|
|
361
|
+
*/
|
|
362
|
+
clear() {
|
|
363
|
+
// Renderer handles this
|
|
795
364
|
}
|
|
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
|
-
});
|
|
365
|
+
/**
|
|
366
|
+
* Update streaming status (routes to renderer)
|
|
367
|
+
*/
|
|
368
|
+
updateStreamingStatus(status) {
|
|
369
|
+
this.renderer?.updateStatus(status);
|
|
811
370
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
371
|
+
/**
|
|
372
|
+
* Clear streaming status
|
|
373
|
+
*/
|
|
374
|
+
clearStreamingStatus() {
|
|
375
|
+
this.renderer?.updateStatus(null);
|
|
816
376
|
}
|
|
817
377
|
/**
|
|
818
|
-
*
|
|
378
|
+
* Check if streaming status is visible
|
|
819
379
|
*/
|
|
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
|
-
});
|
|
380
|
+
isStreamingStatusVisible() {
|
|
381
|
+
return false; // Renderer manages this
|
|
833
382
|
}
|
|
834
383
|
/**
|
|
835
|
-
*
|
|
836
|
-
* Note: Commands are now shown in the banner, so this is a no-op
|
|
384
|
+
* Legacy compatibility methods (no-ops)
|
|
837
385
|
*/
|
|
838
|
-
|
|
839
|
-
|
|
386
|
+
getTotalWrittenLines() {
|
|
387
|
+
return 0;
|
|
388
|
+
}
|
|
389
|
+
getPinnedHeaderLines() {
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
392
|
+
setupScrollRegion() {
|
|
393
|
+
// No-op - renderer handles layout
|
|
394
|
+
}
|
|
395
|
+
teardownScrollRegion() {
|
|
396
|
+
// No-op
|
|
397
|
+
}
|
|
398
|
+
isScrollRegionActive() {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
parallelAgentStatus(content) {
|
|
402
|
+
if (!content)
|
|
403
|
+
return;
|
|
404
|
+
this.enqueueEvent('streaming', `${content}\n`);
|
|
405
|
+
}
|
|
406
|
+
// ==================== Private Helper Methods ====================
|
|
407
|
+
getColumnWidth() {
|
|
408
|
+
if (typeof this.outputStream.columns === 'number' &&
|
|
409
|
+
Number.isFinite(this.outputStream.columns) &&
|
|
410
|
+
this.outputStream.columns > 0) {
|
|
411
|
+
return this.outputStream.columns;
|
|
412
|
+
}
|
|
413
|
+
return getTerminalColumns();
|
|
840
414
|
}
|
|
841
415
|
formatErrorDetails(error) {
|
|
842
|
-
if (!error)
|
|
416
|
+
if (!error)
|
|
843
417
|
return null;
|
|
844
|
-
}
|
|
845
418
|
if (error instanceof Error) {
|
|
846
|
-
if (error.stack)
|
|
419
|
+
if (error.stack)
|
|
847
420
|
return highlightError(error.stack);
|
|
848
|
-
}
|
|
849
421
|
return highlightError(error.message);
|
|
850
422
|
}
|
|
851
423
|
if (typeof error === 'string') {
|
|
@@ -858,83 +430,22 @@ export class Display {
|
|
|
858
430
|
return null;
|
|
859
431
|
}
|
|
860
432
|
}
|
|
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;
|
|
433
|
+
formatElapsed(elapsedMs) {
|
|
434
|
+
if (typeof elapsedMs !== 'number' || !Number.isFinite(elapsedMs) || elapsedMs < 0) {
|
|
435
|
+
return null;
|
|
926
436
|
}
|
|
927
|
-
const
|
|
928
|
-
|
|
929
|
-
|
|
437
|
+
const totalSeconds = Math.max(0, Math.round(elapsedMs / 1000));
|
|
438
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
439
|
+
const seconds = totalSeconds % 60;
|
|
440
|
+
if (minutes > 0) {
|
|
441
|
+
return `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
|
|
930
442
|
}
|
|
931
|
-
return `${
|
|
443
|
+
return `${seconds}s`;
|
|
932
444
|
}
|
|
933
445
|
buildChatBox(content, metadata) {
|
|
934
446
|
const normalized = content.trim();
|
|
935
|
-
if (!normalized)
|
|
447
|
+
if (!normalized)
|
|
936
448
|
return '';
|
|
937
|
-
}
|
|
938
449
|
if (isPlainOutputMode()) {
|
|
939
450
|
const body = renderMessageBody(normalized, this.resolveMessageWidth());
|
|
940
451
|
const telemetry = this.formatTelemetryLine(metadata);
|
|
@@ -949,19 +460,15 @@ export class Display {
|
|
|
949
460
|
borderColor: theme.ui.border,
|
|
950
461
|
});
|
|
951
462
|
const telemetry = this.formatTelemetryLine(metadata);
|
|
952
|
-
|
|
953
|
-
return panel;
|
|
954
|
-
}
|
|
955
|
-
return `${panel}\n${telemetry}`;
|
|
463
|
+
return telemetry ? `${panel}\n${telemetry}` : panel;
|
|
956
464
|
}
|
|
957
465
|
resolveMessageWidth() {
|
|
958
466
|
const columns = this.getColumnWidth();
|
|
959
467
|
return Math.max(DISPLAY_CONSTANTS.MIN_MESSAGE_WIDTH, Math.min(columns - DISPLAY_CONSTANTS.MESSAGE_PADDING, DISPLAY_CONSTANTS.MAX_MESSAGE_WIDTH));
|
|
960
468
|
}
|
|
961
469
|
formatTelemetryLine(metadata) {
|
|
962
|
-
if (!metadata)
|
|
470
|
+
if (!metadata)
|
|
963
471
|
return '';
|
|
964
|
-
}
|
|
965
472
|
const parts = [];
|
|
966
473
|
const elapsed = this.formatElapsed(metadata.elapsedMs);
|
|
967
474
|
if (elapsed) {
|
|
@@ -969,28 +476,67 @@ export class Display {
|
|
|
969
476
|
const elapsedValue = theme.metrics?.elapsedValue ?? theme.secondary;
|
|
970
477
|
parts.push(`${elapsedLabel('elapsed')} ${elapsedValue(elapsed)}`);
|
|
971
478
|
}
|
|
972
|
-
if (!parts.length)
|
|
479
|
+
if (!parts.length)
|
|
973
480
|
return '';
|
|
974
|
-
}
|
|
975
481
|
const separator = theme.ui.muted(' • ');
|
|
976
482
|
return ` ${parts.join(separator)}`;
|
|
977
483
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
484
|
+
buildClaudeStyleThought(content, durationMs) {
|
|
485
|
+
const thinkingStyle = theme.thinking || {
|
|
486
|
+
icon: theme.info,
|
|
487
|
+
text: theme.ui.muted,
|
|
488
|
+
border: theme.ui.border,
|
|
489
|
+
label: theme.info,
|
|
490
|
+
};
|
|
491
|
+
const width = Math.min(this.getColumnWidth() - 4, 70);
|
|
492
|
+
const lines = [];
|
|
493
|
+
// Header
|
|
494
|
+
if (durationMs !== undefined) {
|
|
495
|
+
const elapsed = this.formatElapsedTime(Math.floor(durationMs / 1000));
|
|
496
|
+
lines.push(`${theme.info('∴')} Thought for ${elapsed}`);
|
|
981
497
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
const seconds = totalSeconds % 60;
|
|
985
|
-
if (minutes > 0) {
|
|
986
|
-
return `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
|
|
498
|
+
else {
|
|
499
|
+
lines.push(`${theme.info('✻')} ${thinkingStyle.label('Thinking…')}`);
|
|
987
500
|
}
|
|
988
|
-
|
|
501
|
+
// Content
|
|
502
|
+
const contentLines = content.split('\n');
|
|
503
|
+
const hasContent = contentLines.some(line => line.trim().length > 0);
|
|
504
|
+
if (hasContent) {
|
|
505
|
+
lines.push('');
|
|
506
|
+
}
|
|
507
|
+
for (const line of contentLines) {
|
|
508
|
+
const trimmed = line.replace(/\s+$/, '');
|
|
509
|
+
if (!trimmed.trim()) {
|
|
510
|
+
lines.push('');
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
const wrapped = this.wrapLine(trimmed, width - 4);
|
|
514
|
+
for (const wrappedLine of wrapped) {
|
|
515
|
+
lines.push(` ${thinkingStyle.text(wrappedLine)}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return lines.join('\n');
|
|
519
|
+
}
|
|
520
|
+
formatElapsedTime(seconds) {
|
|
521
|
+
if (seconds < 60) {
|
|
522
|
+
return `${seconds}s`;
|
|
523
|
+
}
|
|
524
|
+
const mins = Math.floor(seconds / 60);
|
|
525
|
+
const secs = seconds % 60;
|
|
526
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
527
|
+
}
|
|
528
|
+
applySingleBulletBlock(text) {
|
|
529
|
+
const lines = text.split('\n');
|
|
530
|
+
const bullet = `${icons.action} `;
|
|
531
|
+
const prefix = theme.info(bullet);
|
|
532
|
+
const indent = ' '.repeat(this.visibleLength(this.stripAnsi(bullet)));
|
|
533
|
+
return lines
|
|
534
|
+
.map((line, index) => (index === 0 ? `${prefix}${line}` : `${indent}${line}`))
|
|
535
|
+
.join('\n');
|
|
989
536
|
}
|
|
990
537
|
buildCommandPalette(commands, options) {
|
|
991
|
-
if (!commands.length)
|
|
538
|
+
if (!commands.length)
|
|
992
539
|
return '';
|
|
993
|
-
}
|
|
994
540
|
const width = Math.max(DISPLAY_CONSTANTS.MIN_MESSAGE_WIDTH, Math.min(this.getColumnWidth() - 2, DISPLAY_CONSTANTS.MAX_MESSAGE_WIDTH));
|
|
995
541
|
const indent = ' ';
|
|
996
542
|
const grouped = this.groupPaletteCommands(commands);
|
|
@@ -1049,39 +595,25 @@ export class Display {
|
|
|
1049
595
|
return Math.min(maxAllowed, budget);
|
|
1050
596
|
}
|
|
1051
597
|
formatPaletteCategory(category) {
|
|
1052
|
-
if (!category)
|
|
598
|
+
if (!category)
|
|
1053
599
|
return 'Other';
|
|
1054
|
-
}
|
|
1055
600
|
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);
|
|
601
|
+
case 'configuration': return 'Configuration';
|
|
602
|
+
case 'workspace': return 'Workspace';
|
|
603
|
+
case 'diagnostics': return 'Diagnostics';
|
|
604
|
+
case 'other': return 'Other';
|
|
605
|
+
default: return category[0]?.toUpperCase() + category.slice(1);
|
|
1066
606
|
}
|
|
1067
607
|
}
|
|
1068
608
|
colorizePaletteText(text, tone) {
|
|
1069
609
|
switch (tone) {
|
|
1070
|
-
case 'warn':
|
|
1071
|
-
|
|
1072
|
-
case '
|
|
1073
|
-
return theme.success(text);
|
|
1074
|
-
case 'info':
|
|
1075
|
-
return theme.info(text);
|
|
610
|
+
case 'warn': return theme.warning(text);
|
|
611
|
+
case 'success': return theme.success(text);
|
|
612
|
+
case 'info': return theme.info(text);
|
|
1076
613
|
case 'muted':
|
|
1077
|
-
default:
|
|
1078
|
-
return theme.ui.muted(text);
|
|
614
|
+
default: return theme.ui.muted(text);
|
|
1079
615
|
}
|
|
1080
616
|
}
|
|
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
617
|
wrapWithPrefix(text, prefix, options) {
|
|
1086
618
|
if (!text) {
|
|
1087
619
|
return prefix.trimEnd();
|
|
@@ -1114,93 +646,25 @@ export class Display {
|
|
|
1114
646
|
}
|
|
1115
647
|
return lines.join('\n');
|
|
1116
648
|
}
|
|
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
|
|
649
|
+
buildWrappedSubActionLines(text, status) {
|
|
650
|
+
const lines = text.split('\n').map((line) => line.trimEnd());
|
|
651
|
+
while (lines.length && !lines[lines.length - 1]?.trim()) {
|
|
652
|
+
lines.pop();
|
|
1171
653
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1180
|
-
lines.push(` ${thinkingStyle.text(wrappedLine)}`);
|
|
1181
|
-
}
|
|
654
|
+
if (!lines.length)
|
|
655
|
+
return [];
|
|
656
|
+
const rendered = [];
|
|
657
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
658
|
+
const segment = lines[index] ?? '';
|
|
659
|
+
const isLast = index === lines.length - 1;
|
|
660
|
+
const { prefix, continuation } = this.buildSubActionPrefixes(status, isLast);
|
|
661
|
+
rendered.push(this.wrapWithPrefix(segment, prefix, { continuationPrefix: continuation }));
|
|
1182
662
|
}
|
|
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
|
-
});
|
|
663
|
+
return rendered;
|
|
1199
664
|
}
|
|
1200
665
|
buildSubActionPrefixes(status, isLast) {
|
|
1201
666
|
if (isLast) {
|
|
1202
667
|
const colorize = this.resolveStatusColor(status);
|
|
1203
|
-
// Claude Code style: use ⎿ for sub-action result/detail prefix
|
|
1204
668
|
return {
|
|
1205
669
|
prefix: ` ${colorize(icons.subaction)} `,
|
|
1206
670
|
continuation: ' ',
|
|
@@ -1212,40 +676,55 @@ export class Display {
|
|
|
1212
676
|
continuation: ` ${branch} `,
|
|
1213
677
|
};
|
|
1214
678
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
679
|
+
resolveStatusColor(status) {
|
|
680
|
+
switch (status) {
|
|
681
|
+
case 'success': return theme.success;
|
|
682
|
+
case 'error': return theme.error;
|
|
683
|
+
case 'warning': return theme.warning;
|
|
684
|
+
case 'pending': return theme.info;
|
|
685
|
+
default: return theme.secondary;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
formatActionIcon(status) {
|
|
689
|
+
const colorize = this.resolveStatusColor(status);
|
|
690
|
+
return colorize(`${icons.action}`);
|
|
691
|
+
}
|
|
1219
692
|
wrapLine(text, width) {
|
|
1220
|
-
|
|
1221
|
-
if (width <= 0) {
|
|
693
|
+
if (width <= 0)
|
|
1222
694
|
return [text];
|
|
1223
|
-
|
|
1224
|
-
if (!text) {
|
|
695
|
+
if (!text)
|
|
1225
696
|
return [''];
|
|
1226
|
-
|
|
1227
|
-
if (text.length <= width) {
|
|
697
|
+
if (text.length <= width)
|
|
1228
698
|
return [text];
|
|
1229
|
-
}
|
|
1230
699
|
const words = text.split(/\s+/).filter(Boolean);
|
|
1231
|
-
|
|
1232
|
-
if (!words.length) {
|
|
700
|
+
if (!words.length)
|
|
1233
701
|
return this.chunkWord(text, width);
|
|
1234
|
-
}
|
|
1235
702
|
const lines = [];
|
|
1236
703
|
let current = '';
|
|
1237
704
|
for (const word of words) {
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
705
|
+
if (!current) {
|
|
706
|
+
if (word.length <= width) {
|
|
707
|
+
current = word;
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
const chunks = this.chunkWord(word, width);
|
|
711
|
+
lines.push(...chunks.slice(0, -1));
|
|
712
|
+
current = chunks[chunks.length - 1] ?? '';
|
|
713
|
+
}
|
|
1241
714
|
}
|
|
1242
|
-
if (
|
|
1243
|
-
|
|
1244
|
-
lines.push(...appendResult.chunks.slice(0, -1));
|
|
1245
|
-
current = appendResult.chunks[appendResult.chunks.length - 1] ?? '';
|
|
715
|
+
else if (current.length + 1 + word.length <= width) {
|
|
716
|
+
current = `${current} ${word}`;
|
|
1246
717
|
}
|
|
1247
718
|
else {
|
|
1248
|
-
current
|
|
719
|
+
lines.push(current);
|
|
720
|
+
if (word.length <= width) {
|
|
721
|
+
current = word;
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
const chunks = this.chunkWord(word, width);
|
|
725
|
+
lines.push(...chunks.slice(0, -1));
|
|
726
|
+
current = chunks[chunks.length - 1] ?? '';
|
|
727
|
+
}
|
|
1249
728
|
}
|
|
1250
729
|
}
|
|
1251
730
|
if (current) {
|
|
@@ -1253,64 +732,23 @@ export class Display {
|
|
|
1253
732
|
}
|
|
1254
733
|
return lines.length ? lines : [''];
|
|
1255
734
|
}
|
|
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
735
|
chunkWord(word, width) {
|
|
1288
|
-
if (width <= 0 || !word)
|
|
736
|
+
if (width <= 0 || !word)
|
|
1289
737
|
return word ? [word] : [''];
|
|
1290
|
-
}
|
|
1291
738
|
const chunks = [];
|
|
1292
739
|
for (let i = 0; i < word.length; i += width) {
|
|
1293
740
|
chunks.push(word.slice(i, i + width));
|
|
1294
741
|
}
|
|
1295
742
|
return chunks.length > 0 ? chunks : [''];
|
|
1296
743
|
}
|
|
1297
|
-
/**
|
|
1298
|
-
* Returns the visible length of a string, excluding ANSI escape codes.
|
|
1299
|
-
*/
|
|
1300
744
|
visibleLength(value) {
|
|
1301
|
-
if (!value)
|
|
745
|
+
if (!value)
|
|
1302
746
|
return 0;
|
|
1303
|
-
}
|
|
1304
747
|
return this.stripAnsi(value).length;
|
|
1305
748
|
}
|
|
1306
|
-
/**
|
|
1307
|
-
* Removes ANSI escape codes from a string to get the visible text.
|
|
1308
|
-
* Uses the standard ANSI escape sequence pattern.
|
|
1309
|
-
*/
|
|
1310
749
|
stripAnsi(value) {
|
|
1311
|
-
if (!value)
|
|
750
|
+
if (!value)
|
|
1312
751
|
return '';
|
|
1313
|
-
}
|
|
1314
752
|
return value.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, '');
|
|
1315
753
|
}
|
|
1316
754
|
}
|