erosolar-cli 2.1.166 → 2.1.168
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/agents/erosolar-code.rules.json +2 -2
- package/agents/general.rules.json +3 -21
- package/dist/StringUtils.d.ts +8 -0
- package/dist/StringUtils.d.ts.map +1 -0
- package/dist/StringUtils.js +11 -0
- package/dist/StringUtils.js.map +1 -0
- package/dist/capabilities/statusCapability.js +2 -2
- package/dist/capabilities/statusCapability.js.map +1 -1
- package/dist/contracts/agent-schemas.json +0 -5
- package/dist/core/agent.d.ts +11 -70
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +180 -854
- package/dist/core/agent.js.map +1 -1
- package/dist/core/aiFlowSupervisor.d.ts +44 -0
- package/dist/core/aiFlowSupervisor.d.ts.map +1 -0
- package/dist/core/aiFlowSupervisor.js +299 -0
- package/dist/core/aiFlowSupervisor.js.map +1 -0
- package/dist/core/cliTestHarness.d.ts +200 -0
- package/dist/core/cliTestHarness.d.ts.map +1 -0
- package/dist/core/cliTestHarness.js +549 -0
- package/dist/core/cliTestHarness.js.map +1 -0
- package/dist/core/preferences.d.ts +0 -1
- package/dist/core/preferences.d.ts.map +1 -1
- package/dist/core/preferences.js +2 -9
- package/dist/core/preferences.js.map +1 -1
- package/dist/core/schemaValidator.js +3 -3
- package/dist/core/schemaValidator.js.map +1 -1
- package/dist/core/testUtils.d.ts +121 -0
- package/dist/core/testUtils.d.ts.map +1 -0
- package/dist/core/testUtils.js +235 -0
- package/dist/core/testUtils.js.map +1 -0
- package/dist/core/toolPreconditions.d.ts +11 -0
- package/dist/core/toolPreconditions.d.ts.map +1 -1
- package/dist/core/toolPreconditions.js +164 -33
- package/dist/core/toolPreconditions.js.map +1 -1
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +114 -9
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/core/toolValidation.d.ts +116 -0
- package/dist/core/toolValidation.d.ts.map +1 -0
- package/dist/core/toolValidation.js +282 -0
- package/dist/core/toolValidation.js.map +1 -0
- package/dist/core/updateChecker.d.ts +1 -61
- package/dist/core/updateChecker.d.ts.map +1 -1
- package/dist/core/updateChecker.js +3 -147
- package/dist/core/updateChecker.js.map +1 -1
- package/dist/headless/headlessApp.d.ts.map +1 -1
- package/dist/headless/headlessApp.js +39 -0
- package/dist/headless/headlessApp.js.map +1 -1
- package/dist/plugins/tools/nodeDefaults.d.ts.map +1 -1
- package/dist/plugins/tools/nodeDefaults.js +2 -0
- package/dist/plugins/tools/nodeDefaults.js.map +1 -1
- package/dist/providers/openaiResponsesProvider.d.ts.map +1 -1
- package/dist/providers/openaiResponsesProvider.js +74 -79
- package/dist/providers/openaiResponsesProvider.js.map +1 -1
- package/dist/runtime/agentController.d.ts.map +1 -1
- package/dist/runtime/agentController.js +0 -2
- package/dist/runtime/agentController.js.map +1 -1
- package/dist/runtime/agentSession.d.ts.map +1 -1
- package/dist/runtime/agentSession.js +2 -3
- package/dist/runtime/agentSession.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +8 -16
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +151 -378
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +15 -4
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/subagents/taskRunner.js +1 -2
- package/dist/subagents/taskRunner.js.map +1 -1
- package/dist/tools/bashTools.d.ts.map +1 -1
- package/dist/tools/bashTools.js +8 -101
- package/dist/tools/bashTools.js.map +1 -1
- package/dist/tools/diffUtils.d.ts +2 -8
- package/dist/tools/diffUtils.d.ts.map +1 -1
- package/dist/tools/diffUtils.js +13 -72
- package/dist/tools/diffUtils.js.map +1 -1
- package/dist/tools/grepTools.d.ts.map +1 -1
- package/dist/tools/grepTools.js +2 -10
- package/dist/tools/grepTools.js.map +1 -1
- package/dist/tools/searchTools.d.ts.map +1 -1
- package/dist/tools/searchTools.js +2 -4
- package/dist/tools/searchTools.js.map +1 -1
- package/dist/ui/PromptController.d.ts +0 -2
- package/dist/ui/PromptController.d.ts.map +1 -1
- package/dist/ui/PromptController.js +0 -2
- package/dist/ui/PromptController.js.map +1 -1
- package/dist/ui/ShellUIAdapter.d.ts +18 -71
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +139 -237
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/UnifiedUIController.d.ts +1 -0
- package/dist/ui/UnifiedUIController.d.ts.map +1 -1
- package/dist/ui/UnifiedUIController.js +1 -0
- package/dist/ui/UnifiedUIController.js.map +1 -1
- package/dist/ui/UnifiedUIRenderer.d.ts +5 -122
- package/dist/ui/UnifiedUIRenderer.d.ts.map +1 -1
- package/dist/ui/UnifiedUIRenderer.js +125 -830
- package/dist/ui/UnifiedUIRenderer.js.map +1 -1
- package/dist/ui/compactRenderer.d.ts +139 -0
- package/dist/ui/compactRenderer.d.ts.map +1 -0
- package/dist/ui/compactRenderer.js +398 -0
- package/dist/ui/compactRenderer.js.map +1 -0
- package/dist/ui/display.d.ts +48 -12
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +105 -22
- package/dist/ui/display.js.map +1 -1
- package/dist/ui/streamingFormatter.d.ts +30 -0
- package/dist/ui/streamingFormatter.d.ts.map +1 -0
- package/dist/ui/streamingFormatter.js +91 -0
- package/dist/ui/streamingFormatter.js.map +1 -0
- package/dist/ui/unified/index.d.ts +1 -1
- package/dist/ui/unified/index.d.ts.map +1 -1
- package/dist/ui/unified/index.js +2 -0
- package/dist/ui/unified/index.js.map +1 -1
- package/dist/utils/errorUtils.d.ts +16 -0
- package/dist/utils/errorUtils.d.ts.map +1 -0
- package/dist/utils/errorUtils.js +66 -0
- package/dist/utils/errorUtils.js.map +1 -0
- package/package.json +2 -1
- package/dist/core/reliabilityPrompt.d.ts +0 -9
- package/dist/core/reliabilityPrompt.d.ts.map +0 -1
- package/dist/core/reliabilityPrompt.js +0 -30
- package/dist/core/reliabilityPrompt.js.map +0 -1
- package/dist/ui/animatedStatus.d.ts +0 -129
- package/dist/ui/animatedStatus.d.ts.map +0 -1
- package/dist/ui/animatedStatus.js +0 -384
- package/dist/ui/animatedStatus.js.map +0 -1
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
import * as readline from 'node:readline';
|
|
12
12
|
import { EventEmitter } from 'node:events';
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
|
-
import { theme
|
|
14
|
+
import { theme } from './theme.js';
|
|
15
15
|
import { isPlainOutputMode } from './outputMode.js';
|
|
16
|
-
import {
|
|
16
|
+
import { renderDivider } from './unified/layout.js';
|
|
17
17
|
const ESC = {
|
|
18
18
|
HIDE_CURSOR: '\x1b[?25l',
|
|
19
19
|
SHOW_CURSOR: '\x1b[?25h',
|
|
@@ -27,11 +27,6 @@ const ESC = {
|
|
|
27
27
|
ERASE_DOWN: '\x1b[J',
|
|
28
28
|
REVERSE: '\x1b[7m',
|
|
29
29
|
RESET: '\x1b[0m',
|
|
30
|
-
// Scroll region control - CRITICAL for fixed bottom overlay
|
|
31
|
-
SET_SCROLL_REGION: (top, bottom) => `\x1b[${top};${bottom}r`,
|
|
32
|
-
RESET_SCROLL_REGION: '\x1b[r',
|
|
33
|
-
SAVE_CURSOR: '\x1b[s',
|
|
34
|
-
RESTORE_CURSOR: '\x1b[u',
|
|
35
30
|
};
|
|
36
31
|
const COLLAPSE_HINT = theme.ui.muted('…'); // subtle ellipsis, no key hint
|
|
37
32
|
const NEWLINE_PLACEHOLDER = '↵';
|
|
@@ -56,35 +51,9 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
56
51
|
hotkeysInToggleLine = new Set();
|
|
57
52
|
collapsedPaste = null;
|
|
58
53
|
mode = 'idle';
|
|
59
|
-
streamingStartTime = null;
|
|
60
54
|
statusMessage = null;
|
|
61
55
|
statusOverride = null;
|
|
62
56
|
statusStreaming = null;
|
|
63
|
-
// Animated UI components
|
|
64
|
-
streamingSpinner = null;
|
|
65
|
-
thinkingIndicator = null;
|
|
66
|
-
contextMeter;
|
|
67
|
-
spinnerFrame = 0;
|
|
68
|
-
spinnerInterval = null;
|
|
69
|
-
// Compacting status animation
|
|
70
|
-
compactingStatusMessage = '';
|
|
71
|
-
compactingStatusFrame = 0;
|
|
72
|
-
compactingStatusInterval = null;
|
|
73
|
-
compactingSpinnerFrames = ['✻', '✼', '✻', '✺'];
|
|
74
|
-
// Animated activity line (e.g., "✳ Ruminating… (esc to interrupt · 34s · ↑1.2k)")
|
|
75
|
-
activityMessage = null;
|
|
76
|
-
activityPhraseIndex = 0;
|
|
77
|
-
activityStarFrame = 0;
|
|
78
|
-
activityStarFrames = ['✳', '✴', '✵', '✶', '✷', '✸'];
|
|
79
|
-
// Token count during streaming
|
|
80
|
-
streamingTokens = 0;
|
|
81
|
-
// Fun phrases to cycle through when no specific activity is provided
|
|
82
|
-
funActivityPhrases = [
|
|
83
|
-
'Moseying', 'Ruminating', 'Pondering', 'Cogitating', 'Mulling',
|
|
84
|
-
'Contemplating', 'Deliberating', 'Noodling', 'Percolating', 'Stewing',
|
|
85
|
-
'Brewing', 'Simmering', 'Churning', 'Puzzling', 'Meandering',
|
|
86
|
-
'Wandering', 'Musing', 'Daydreaming', 'Woolgathering', 'Chewing',
|
|
87
|
-
];
|
|
88
57
|
statusMeta = {};
|
|
89
58
|
toggleState = {
|
|
90
59
|
verificationEnabled: false,
|
|
@@ -100,7 +69,10 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
100
69
|
lastPromptEvent = null;
|
|
101
70
|
promptHeight = 0;
|
|
102
71
|
lastOverlayHeight = 0;
|
|
72
|
+
lastPromptIndex = 0;
|
|
73
|
+
overlayBottomPadding = 1;
|
|
103
74
|
inlinePanel = [];
|
|
75
|
+
overlayInvalidated = false;
|
|
104
76
|
hasConversationContent = false;
|
|
105
77
|
isPromptActive = false;
|
|
106
78
|
inputRenderOffset = 0;
|
|
@@ -119,7 +91,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
119
91
|
lastRenderedEventKey = null;
|
|
120
92
|
lastOutputEndedWithNewline = true;
|
|
121
93
|
hasRenderedPrompt = false;
|
|
122
|
-
hasEverRenderedOverlay = false; // Track if we've ever rendered
|
|
94
|
+
hasEverRenderedOverlay = false; // Track if we've ever rendered to prevent first-render scrollback pollution
|
|
123
95
|
lastOverlay = null;
|
|
124
96
|
allowPromptRender = true;
|
|
125
97
|
inputCapture = null;
|
|
@@ -129,8 +101,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
129
101
|
this.input = input;
|
|
130
102
|
this.interactive = Boolean(this.output.isTTY && this.input.isTTY && !process.env['CI']);
|
|
131
103
|
this.plainMode = isPlainOutputMode() || !this.interactive;
|
|
132
|
-
// Initialize animated components
|
|
133
|
-
this.contextMeter = new ContextMeter();
|
|
134
104
|
this.rl = readline.createInterface({
|
|
135
105
|
input: this.input,
|
|
136
106
|
output: this.output,
|
|
@@ -162,6 +132,9 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
162
132
|
this.updateTerminalSize();
|
|
163
133
|
this.hasRenderedPrompt = false;
|
|
164
134
|
this.lastOutputEndedWithNewline = true;
|
|
135
|
+
// Don't render prompt immediately - wait for banner/content to be added first.
|
|
136
|
+
// The prompt will render after the event queue processes the welcome banner.
|
|
137
|
+
// This prevents the prompt from appearing at the bottom before the banner shows.
|
|
165
138
|
this.write(ESC.SHOW_CURSOR);
|
|
166
139
|
return;
|
|
167
140
|
}
|
|
@@ -174,27 +147,11 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
174
147
|
cleanup() {
|
|
175
148
|
this.cancelInputCapture(new Error('Renderer disposed'));
|
|
176
149
|
this.cancelPlainPasteCapture();
|
|
177
|
-
// Stop any running animations
|
|
178
|
-
if (this.spinnerInterval) {
|
|
179
|
-
clearInterval(this.spinnerInterval);
|
|
180
|
-
this.spinnerInterval = null;
|
|
181
|
-
}
|
|
182
|
-
if (this.streamingSpinner) {
|
|
183
|
-
this.streamingSpinner.stop();
|
|
184
|
-
this.streamingSpinner = null;
|
|
185
|
-
}
|
|
186
|
-
if (this.thinkingIndicator) {
|
|
187
|
-
this.thinkingIndicator.stop();
|
|
188
|
-
this.thinkingIndicator = null;
|
|
189
|
-
}
|
|
190
|
-
this.contextMeter.dispose();
|
|
191
|
-
disposeAnimations();
|
|
192
150
|
if (!this.interactive) {
|
|
193
151
|
this.rl.close();
|
|
194
152
|
return;
|
|
195
153
|
}
|
|
196
154
|
if (!this.plainMode) {
|
|
197
|
-
// Clear the prompt area so it doesn't remain in scrollback history
|
|
198
155
|
this.clearPromptArea();
|
|
199
156
|
this.write(ESC.DISABLE_BRACKETED_PASTE);
|
|
200
157
|
this.write(ESC.SHOW_CURSOR);
|
|
@@ -246,25 +203,15 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
246
203
|
return;
|
|
247
204
|
}
|
|
248
205
|
if (key.ctrl && key.name === 'c') {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (this.buffer.length > 0) {
|
|
254
|
-
// Stage 1: Clear the input buffer
|
|
206
|
+
if (this.buffer.length === 0) {
|
|
207
|
+
this.emit('interrupt');
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
255
210
|
this.buffer = '';
|
|
256
211
|
this.cursor = 0;
|
|
257
212
|
this.renderPrompt();
|
|
258
213
|
this.emitInputChange();
|
|
259
214
|
}
|
|
260
|
-
else if (this.mode === 'streaming') {
|
|
261
|
-
// Stage 2: Interrupt the AI run
|
|
262
|
-
this.emit('interrupt');
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
// Stage 3: Quit the CLI (emit exit signal)
|
|
266
|
-
this.emit('exit');
|
|
267
|
-
}
|
|
268
215
|
return;
|
|
269
216
|
}
|
|
270
217
|
if (key.ctrl && key.name === 'd') {
|
|
@@ -283,11 +230,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
283
230
|
return;
|
|
284
231
|
}
|
|
285
232
|
}
|
|
286
|
-
// Ctrl+O: Expand last tool result
|
|
287
|
-
if (key.ctrl && key.name === 'o') {
|
|
288
|
-
this.emit('expand-tool-result');
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
233
|
if (key.name === 'return' || key.name === 'enter') {
|
|
292
234
|
if (this.collapsedPaste) {
|
|
293
235
|
this.expandCollapsedPaste();
|
|
@@ -297,12 +239,9 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
297
239
|
// If a slash command suggestion is highlighted, pressing Enter submits it immediately
|
|
298
240
|
if (this.applySuggestion(true))
|
|
299
241
|
return;
|
|
300
|
-
//
|
|
242
|
+
// If buffer starts with '/' and the first suggestion exists, submit it
|
|
301
243
|
if (this.buffer.startsWith('/') && this.suggestions.length > 0) {
|
|
302
|
-
|
|
303
|
-
? this.suggestionIndex
|
|
304
|
-
: 0;
|
|
305
|
-
this.buffer = this.suggestions[safeIndex]?.command ?? this.buffer;
|
|
244
|
+
this.buffer = this.suggestions[this.suggestionIndex >= 0 ? this.suggestionIndex : 0]?.command ?? this.buffer;
|
|
306
245
|
}
|
|
307
246
|
this.submitText(this.buffer);
|
|
308
247
|
return;
|
|
@@ -679,11 +618,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
679
618
|
if (!this.buffer.startsWith('/') || this.suggestions.length === 0) {
|
|
680
619
|
return false;
|
|
681
620
|
}
|
|
682
|
-
|
|
683
|
-
const safeIndex = this.suggestionIndex >= 0 && this.suggestionIndex < this.suggestions.length
|
|
684
|
-
? this.suggestionIndex
|
|
685
|
-
: 0;
|
|
686
|
-
const selected = this.suggestions[safeIndex];
|
|
621
|
+
const selected = this.suggestions[this.suggestionIndex] ?? this.suggestions[0];
|
|
687
622
|
if (!selected) {
|
|
688
623
|
return false;
|
|
689
624
|
}
|
|
@@ -728,7 +663,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
728
663
|
normalized === 'thought' ||
|
|
729
664
|
normalized === 'stream' ||
|
|
730
665
|
normalized === 'tool' ||
|
|
731
|
-
normalized === 'tool-result' ||
|
|
732
666
|
normalized === 'build' ||
|
|
733
667
|
normalized === 'test') {
|
|
734
668
|
this.hasConversationContent = true;
|
|
@@ -855,7 +789,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
855
789
|
if (event.type !== 'prompt') {
|
|
856
790
|
this.lastRenderedEventKey = signature;
|
|
857
791
|
}
|
|
858
|
-
// Clear the prompt area before writing new content
|
|
859
792
|
if (this.promptHeight > 0 || this.lastOverlay) {
|
|
860
793
|
this.clearPromptArea();
|
|
861
794
|
}
|
|
@@ -867,6 +800,10 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
867
800
|
}
|
|
868
801
|
this.output.write(formatted);
|
|
869
802
|
this.lastOutputEndedWithNewline = formatted.endsWith('\n');
|
|
803
|
+
// Overlay must be re-anchored after new scrollback is written
|
|
804
|
+
this.overlayInvalidated = true;
|
|
805
|
+
// Don't re-render prompt after every event - wait for queue to finish
|
|
806
|
+
// This prevents premature prompt rendering that cuts off responses
|
|
870
807
|
}
|
|
871
808
|
normalizeEventType(type) {
|
|
872
809
|
switch (type) {
|
|
@@ -879,9 +816,8 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
879
816
|
return 'stream';
|
|
880
817
|
case 'tool':
|
|
881
818
|
case 'tool-call':
|
|
882
|
-
return 'tool';
|
|
883
819
|
case 'tool-result':
|
|
884
|
-
return 'tool
|
|
820
|
+
return 'tool';
|
|
885
821
|
case 'build':
|
|
886
822
|
return 'build';
|
|
887
823
|
case 'test':
|
|
@@ -907,378 +843,35 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
907
843
|
const lines = event.content.split('\n').map(line => line.trimEnd());
|
|
908
844
|
return `${lines.join('\n')}\n`;
|
|
909
845
|
}
|
|
910
|
-
// Compact, user-friendly formatting
|
|
911
846
|
switch (event.type) {
|
|
912
847
|
case 'prompt':
|
|
913
|
-
|
|
914
|
-
return `${theme.primary('>')} ${event.content}\n`;
|
|
848
|
+
return `\n> ${event.content}\n`; // Plain > like Claude Code
|
|
915
849
|
case 'thought': {
|
|
916
|
-
//
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
return `⏺ ${cleanContent}\n`;
|
|
923
|
-
}
|
|
924
|
-
case 'tool': {
|
|
925
|
-
// Compact tool display: ⚡ToolName → result
|
|
926
|
-
const content = event.content.replace(/^[⏺⚙○]\s*/, '');
|
|
927
|
-
return this.formatCompactToolCall(content);
|
|
928
|
-
}
|
|
929
|
-
case 'tool-result': {
|
|
930
|
-
// Inline result: └─ summary
|
|
931
|
-
return this.formatCompactToolResult(event.content);
|
|
850
|
+
// Claude Code style: ⏺ first line, then indented continuation
|
|
851
|
+
const lines = event.content.split('\n');
|
|
852
|
+
const formatted = lines
|
|
853
|
+
.map((line, i) => (i === 0 ? `${bullet} ${line}` : ` ${line}`))
|
|
854
|
+
.join('\n');
|
|
855
|
+
return `\n${formatted}\n`;
|
|
932
856
|
}
|
|
857
|
+
case 'tool':
|
|
858
|
+
// Tool calls don't have bullet in Claude Code - just the name
|
|
859
|
+
return `\n${event.content}\n`;
|
|
933
860
|
case 'build':
|
|
934
|
-
return
|
|
861
|
+
return `\n${event.content}\n`;
|
|
935
862
|
case 'test':
|
|
936
|
-
return
|
|
863
|
+
return `\n${event.content}\n`;
|
|
937
864
|
case 'stream':
|
|
938
865
|
return event.content;
|
|
939
866
|
case 'response':
|
|
940
867
|
default: {
|
|
941
|
-
//
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
return this.formatCompactResponse(event.content);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
/**
|
|
951
|
-
* Programmatic garbage detection - checks if content looks like internal/system output
|
|
952
|
-
* that shouldn't be shown to users. Uses structural checks, not pattern matching.
|
|
953
|
-
*/
|
|
954
|
-
isGarbageOutput(content) {
|
|
955
|
-
if (!content || content.trim().length === 0)
|
|
956
|
-
return true;
|
|
957
|
-
// Structural check: content starting with < that isn't valid markdown/code
|
|
958
|
-
if (content.startsWith('<') && !content.startsWith('<http') && !content.startsWith('<!')) {
|
|
959
|
-
return true;
|
|
960
|
-
}
|
|
961
|
-
// Structural check: contains "to=functions." or "to=tools." (internal routing)
|
|
962
|
-
if (content.includes('to=functions.') || content.includes('to=tools.')) {
|
|
963
|
-
return true;
|
|
964
|
-
}
|
|
965
|
-
// Structural check: looks like internal instruction (quoted system text)
|
|
966
|
-
if (content.startsWith('"') && content.includes('block') && content.includes('tool')) {
|
|
967
|
-
return true;
|
|
968
|
-
}
|
|
969
|
-
// Structural check: very short content that's just timing info
|
|
970
|
-
if (content.length < 30 && /elapsed|seconds?|ms\b/i.test(content)) {
|
|
971
|
-
return true;
|
|
972
|
-
}
|
|
973
|
-
// Structural check: gibberish - high ratio of non-word characters
|
|
974
|
-
const alphaCount = (content.match(/[a-zA-Z]/g) || []).length;
|
|
975
|
-
const totalCount = content.replace(/\s/g, '').length;
|
|
976
|
-
if (totalCount > 20 && alphaCount / totalCount < 0.5) {
|
|
977
|
-
return true; // Less than 50% letters = likely garbage
|
|
978
|
-
}
|
|
979
|
-
return false;
|
|
980
|
-
}
|
|
981
|
-
/**
|
|
982
|
-
* Format text in Claude Code style: ⏺ prefix with wrapped continuation lines
|
|
983
|
-
* Example:
|
|
984
|
-
* ⏺ The AI ran tools but gave no response. Need to fix
|
|
985
|
-
* the response handling. Let me check where the AI's
|
|
986
|
-
* text response should be displayed:
|
|
987
|
-
*/
|
|
988
|
-
formatClaudeCodeBlock(content) {
|
|
989
|
-
const bullet = '⏺';
|
|
990
|
-
const maxWidth = Math.min(this.cols - 4, 56); // Leave room for prefix and margins
|
|
991
|
-
const lines = content.split('\n');
|
|
992
|
-
const result = [];
|
|
993
|
-
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
994
|
-
const line = lines[lineIdx];
|
|
995
|
-
if (!line.trim()) {
|
|
996
|
-
result.push('');
|
|
997
|
-
continue;
|
|
998
|
-
}
|
|
999
|
-
// Word-wrap each line
|
|
1000
|
-
const words = line.split(/(\s+)/);
|
|
1001
|
-
let currentLine = '';
|
|
1002
|
-
for (const word of words) {
|
|
1003
|
-
if ((currentLine + word).length > maxWidth && currentLine.trim()) {
|
|
1004
|
-
// First line of this paragraph gets ⏺, rest get indent
|
|
1005
|
-
const prefix = result.length === 0 && lineIdx === 0 ? `${bullet} ` : ' ';
|
|
1006
|
-
result.push(`${prefix}${currentLine.trimEnd()}`);
|
|
1007
|
-
currentLine = word.trimStart();
|
|
1008
|
-
}
|
|
1009
|
-
else {
|
|
1010
|
-
currentLine += word;
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
if (currentLine.trim()) {
|
|
1014
|
-
const prefix = result.length === 0 && lineIdx === 0 ? `${bullet} ` : ' ';
|
|
1015
|
-
result.push(`${prefix}${currentLine.trimEnd()}`);
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
return result.join('\n') + '\n';
|
|
1019
|
-
}
|
|
1020
|
-
/**
|
|
1021
|
-
* Format a tool call in Claude Code style:
|
|
1022
|
-
* ⏺ Search(pattern: "foo", path: "src",
|
|
1023
|
-
* output_mode: "content", head_limit: 30)
|
|
1024
|
-
*/
|
|
1025
|
-
formatToolCall(content) {
|
|
1026
|
-
const bullet = '⏺';
|
|
1027
|
-
// Parse tool name and arguments
|
|
1028
|
-
const match = content.match(/^(\w+)\((.*)\)$/s);
|
|
1029
|
-
if (!match) {
|
|
1030
|
-
// Simple format without args
|
|
1031
|
-
const nameMatch = content.match(/^(\w+)/);
|
|
1032
|
-
if (nameMatch) {
|
|
1033
|
-
return `${bullet} ${theme.info(nameMatch[1])}\n`;
|
|
1034
|
-
}
|
|
1035
|
-
return `${bullet} ${content}\n`;
|
|
1036
|
-
}
|
|
1037
|
-
const toolName = match[1];
|
|
1038
|
-
const argsStr = match[2];
|
|
1039
|
-
const maxWidth = Math.min(this.cols - 4, 56);
|
|
1040
|
-
// Format: ⏺ ToolName(args...)
|
|
1041
|
-
const prefix = `${bullet} ${theme.info(toolName)}(`;
|
|
1042
|
-
const prefixLen = toolName.length + 3; // "⏺ ToolName(" visible length
|
|
1043
|
-
const indent = ' '.repeat(prefixLen + 4); // Extra indent for wrapped args
|
|
1044
|
-
// Parse and format arguments
|
|
1045
|
-
const args = this.parseToolArgs(argsStr);
|
|
1046
|
-
if (args.length === 0) {
|
|
1047
|
-
return `${prefix})\n`;
|
|
1048
|
-
}
|
|
1049
|
-
const lines = [];
|
|
1050
|
-
let currentLine = prefix;
|
|
1051
|
-
for (let i = 0; i < args.length; i++) {
|
|
1052
|
-
const arg = args[i];
|
|
1053
|
-
const argText = `${theme.ui.muted(arg.key + ':')} ${this.formatArgValue(arg.value)}`;
|
|
1054
|
-
const separator = i < args.length - 1 ? ', ' : ')';
|
|
1055
|
-
// Check if this arg fits on current line
|
|
1056
|
-
const testLine = currentLine + argText + separator;
|
|
1057
|
-
if (this.stripAnsi(testLine).length > maxWidth && currentLine !== prefix) {
|
|
1058
|
-
lines.push(currentLine.trimEnd());
|
|
1059
|
-
currentLine = indent + argText + separator;
|
|
1060
|
-
}
|
|
1061
|
-
else {
|
|
1062
|
-
currentLine += argText + separator;
|
|
868
|
+
// Claude Code style: ⏺ for first line, indent rest
|
|
869
|
+
const lines = event.content.split('\n');
|
|
870
|
+
return (`\n${lines
|
|
871
|
+
.map((line, i) => (i === 0 ? `${bullet} ${line}` : ` ${line}`))
|
|
872
|
+
.join('\n')}\n`);
|
|
1063
873
|
}
|
|
1064
874
|
}
|
|
1065
|
-
if (currentLine.trim()) {
|
|
1066
|
-
lines.push(currentLine.trimEnd());
|
|
1067
|
-
}
|
|
1068
|
-
return lines.join('\n') + '\n';
|
|
1069
|
-
}
|
|
1070
|
-
/**
|
|
1071
|
-
* Parse tool arguments from string like: key: "value", key2: value2
|
|
1072
|
-
*/
|
|
1073
|
-
parseToolArgs(argsStr) {
|
|
1074
|
-
const args = [];
|
|
1075
|
-
// Simple regex to extract key: value pairs
|
|
1076
|
-
const regex = /(\w+):\s*("(?:[^"\\]|\\.)*"|[^,\)]+)/g;
|
|
1077
|
-
let match;
|
|
1078
|
-
while ((match = regex.exec(argsStr)) !== null) {
|
|
1079
|
-
args.push({ key: match[1], value: match[2].trim() });
|
|
1080
|
-
}
|
|
1081
|
-
return args;
|
|
1082
|
-
}
|
|
1083
|
-
/**
|
|
1084
|
-
* Format an argument value (truncate long strings)
|
|
1085
|
-
*/
|
|
1086
|
-
formatArgValue(value) {
|
|
1087
|
-
// Remove surrounding quotes if present
|
|
1088
|
-
const isQuoted = value.startsWith('"') && value.endsWith('"');
|
|
1089
|
-
const inner = isQuoted ? value.slice(1, -1) : value;
|
|
1090
|
-
// Truncate long values
|
|
1091
|
-
const maxLen = 40;
|
|
1092
|
-
const truncated = inner.length > maxLen ? inner.slice(0, maxLen - 3) + '...' : inner;
|
|
1093
|
-
return isQuoted ? `"${truncated}"` : truncated;
|
|
1094
|
-
}
|
|
1095
|
-
/**
|
|
1096
|
-
* Format a tool result in Claude Code style:
|
|
1097
|
-
* ⎿ Found 12 lines (ctrl+o to expand)
|
|
1098
|
-
*/
|
|
1099
|
-
formatToolResult(content) {
|
|
1100
|
-
// Check if this is a summary line (e.g., "Found X lines")
|
|
1101
|
-
const summaryMatch = content.match(/^(Found \d+ (?:lines?|files?|matches?)|Read \d+ lines?|Wrote \d+ lines?|Edited|Created|Deleted)/i);
|
|
1102
|
-
if (summaryMatch) {
|
|
1103
|
-
return ` ${theme.ui.muted('⎿')} ${content} ${theme.ui.muted('(ctrl+o to expand)')}\n`;
|
|
1104
|
-
}
|
|
1105
|
-
// For other results, show truncated preview
|
|
1106
|
-
const lines = content.split('\n');
|
|
1107
|
-
if (lines.length > 3) {
|
|
1108
|
-
const preview = lines.slice(0, 2).join('\n');
|
|
1109
|
-
return ` ${theme.ui.muted('⎿')} ${preview}\n ${theme.ui.muted(`... ${lines.length - 2} more lines (ctrl+o to expand)`)}\n`;
|
|
1110
|
-
}
|
|
1111
|
-
return ` ${theme.ui.muted('⎿')} ${content}\n`;
|
|
1112
|
-
}
|
|
1113
|
-
/**
|
|
1114
|
-
* Format a compact tool call: ⏺ Read → file.ts
|
|
1115
|
-
*/
|
|
1116
|
-
formatCompactToolCall(content) {
|
|
1117
|
-
const bullet = '⏺';
|
|
1118
|
-
// Parse tool name and args
|
|
1119
|
-
const match = content.match(/^(\w+)\s*(?:\((.*)\))?$/s);
|
|
1120
|
-
if (!match) {
|
|
1121
|
-
return `${bullet} ${content}\n`;
|
|
1122
|
-
}
|
|
1123
|
-
const toolName = match[1];
|
|
1124
|
-
const argsStr = match[2]?.trim() || '';
|
|
1125
|
-
// If no args, just show tool name
|
|
1126
|
-
if (!argsStr) {
|
|
1127
|
-
return `${bullet} ${theme.info(toolName)}\n`;
|
|
1128
|
-
}
|
|
1129
|
-
// Format full params in Claude Code style with line wrapping
|
|
1130
|
-
// For long args, wrap them nicely with continuation indent
|
|
1131
|
-
const prefix = `${bullet} ${theme.info(toolName)}(`;
|
|
1132
|
-
const suffix = ')';
|
|
1133
|
-
const maxWidth = this.cols - 8; // Leave room for margins
|
|
1134
|
-
// Parse individual params
|
|
1135
|
-
const params = this.parseToolParams(argsStr);
|
|
1136
|
-
if (params.length === 0) {
|
|
1137
|
-
return `${prefix}${argsStr}${suffix}\n`;
|
|
1138
|
-
}
|
|
1139
|
-
// Format params with proper wrapping
|
|
1140
|
-
return this.formatToolParams(toolName, params, maxWidth);
|
|
1141
|
-
}
|
|
1142
|
-
/**
|
|
1143
|
-
* Parse tool params from args string
|
|
1144
|
-
*/
|
|
1145
|
-
parseToolParams(argsStr) {
|
|
1146
|
-
const params = [];
|
|
1147
|
-
// Match key: "value" or key: value patterns
|
|
1148
|
-
const regex = /(\w+):\s*("(?:[^"\\]|\\.)*"|[^,\n]+)/g;
|
|
1149
|
-
let match;
|
|
1150
|
-
while ((match = regex.exec(argsStr)) !== null) {
|
|
1151
|
-
params.push({ key: match[1], value: match[2].trim() });
|
|
1152
|
-
}
|
|
1153
|
-
return params;
|
|
1154
|
-
}
|
|
1155
|
-
/**
|
|
1156
|
-
* Format tool params in Claude Code style with wrapping
|
|
1157
|
-
*/
|
|
1158
|
-
formatToolParams(toolName, params, maxWidth) {
|
|
1159
|
-
const bullet = '⏺';
|
|
1160
|
-
const lines = [];
|
|
1161
|
-
const indent = ' '; // 8 spaces for continuation
|
|
1162
|
-
let currentLine = `${bullet} ${theme.info(toolName)}(`;
|
|
1163
|
-
let firstParam = true;
|
|
1164
|
-
for (const param of params) {
|
|
1165
|
-
const paramStr = firstParam
|
|
1166
|
-
? `${param.key}: ${param.value}`
|
|
1167
|
-
: `, ${param.key}: ${param.value}`;
|
|
1168
|
-
// Check if adding this param would exceed width
|
|
1169
|
-
const testLine = currentLine + paramStr;
|
|
1170
|
-
const plainLength = testLine.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
1171
|
-
if (plainLength > maxWidth && !firstParam) {
|
|
1172
|
-
// Start new line
|
|
1173
|
-
lines.push(currentLine);
|
|
1174
|
-
currentLine = indent + `${param.key}: ${param.value}`;
|
|
1175
|
-
}
|
|
1176
|
-
else {
|
|
1177
|
-
currentLine += paramStr;
|
|
1178
|
-
}
|
|
1179
|
-
firstParam = false;
|
|
1180
|
-
}
|
|
1181
|
-
currentLine += ')';
|
|
1182
|
-
lines.push(currentLine);
|
|
1183
|
-
return lines.join('\n') + '\n';
|
|
1184
|
-
}
|
|
1185
|
-
/**
|
|
1186
|
-
* Extract a short summary from tool args
|
|
1187
|
-
*/
|
|
1188
|
-
extractToolSummary(toolName, argsStr) {
|
|
1189
|
-
const tool = toolName.toLowerCase();
|
|
1190
|
-
// Extract path/file for file operations
|
|
1191
|
-
if (['read', 'write', 'edit', 'glob', 'grep', 'search'].includes(tool)) {
|
|
1192
|
-
const pathMatch = argsStr.match(/(?:path|file_path|pattern):\s*"([^"]+)"/);
|
|
1193
|
-
if (pathMatch) {
|
|
1194
|
-
const path = pathMatch[1];
|
|
1195
|
-
// Shorten long paths
|
|
1196
|
-
const short = path.length > 30 ? '…' + path.slice(-28) : path;
|
|
1197
|
-
return theme.ui.muted(short);
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
// Extract command for bash
|
|
1201
|
-
if (tool === 'bash') {
|
|
1202
|
-
const cmdMatch = argsStr.match(/command:\s*"([^"]+)"/);
|
|
1203
|
-
if (cmdMatch) {
|
|
1204
|
-
const cmd = cmdMatch[1];
|
|
1205
|
-
const short = cmd.length > 40 ? cmd.slice(0, 37) + '…' : cmd;
|
|
1206
|
-
return theme.ui.muted(short);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
return null;
|
|
1210
|
-
}
|
|
1211
|
-
/**
|
|
1212
|
-
* Format a compact tool result: ⎿ Found X lines (ctrl+o to expand)
|
|
1213
|
-
*/
|
|
1214
|
-
formatCompactToolResult(content) {
|
|
1215
|
-
// Parse common result patterns for summary
|
|
1216
|
-
const lineMatch = content.match(/(\d+)\s*lines?/i);
|
|
1217
|
-
const fileMatch = content.match(/(\d+)\s*(?:files?|matches?)/i);
|
|
1218
|
-
const readMatch = content.match(/read.*?(\d+)\s*lines?/i);
|
|
1219
|
-
let summary;
|
|
1220
|
-
if (readMatch) {
|
|
1221
|
-
summary = `Read ${readMatch[1]} lines`;
|
|
1222
|
-
}
|
|
1223
|
-
else if (lineMatch) {
|
|
1224
|
-
summary = `Found ${lineMatch[1]} line${lineMatch[1] === '1' ? '' : 's'}`;
|
|
1225
|
-
}
|
|
1226
|
-
else if (fileMatch) {
|
|
1227
|
-
summary = `Found ${fileMatch[1]} file${fileMatch[1] === '1' ? '' : 's'}`;
|
|
1228
|
-
}
|
|
1229
|
-
else if (content.match(/^(success|ok|done|completed|written|edited|created)/i)) {
|
|
1230
|
-
summary = '✓';
|
|
1231
|
-
}
|
|
1232
|
-
else {
|
|
1233
|
-
// Use content directly, truncated if needed
|
|
1234
|
-
summary = content.length > 40 ? content.slice(0, 37) + '…' : content;
|
|
1235
|
-
}
|
|
1236
|
-
return ` ${theme.ui.muted('⎿')} ${summary} ${theme.ui.muted('(ctrl+o to expand)')}\n`;
|
|
1237
|
-
}
|
|
1238
|
-
/**
|
|
1239
|
-
* Format a compact response with bullet on first line
|
|
1240
|
-
*/
|
|
1241
|
-
formatCompactResponse(content) {
|
|
1242
|
-
const bullet = '⏺';
|
|
1243
|
-
const trimmed = content.trim();
|
|
1244
|
-
if (!trimmed)
|
|
1245
|
-
return '';
|
|
1246
|
-
// Single line responses - bullet prefix
|
|
1247
|
-
if (!trimmed.includes('\n') && trimmed.length < 80) {
|
|
1248
|
-
return `${bullet} ${trimmed}\n`;
|
|
1249
|
-
}
|
|
1250
|
-
// Multi-line: bullet on first, indent continuation
|
|
1251
|
-
const lines = trimmed.split('\n');
|
|
1252
|
-
const result = [];
|
|
1253
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1254
|
-
const line = lines[i].trimEnd();
|
|
1255
|
-
if (!line) {
|
|
1256
|
-
result.push('');
|
|
1257
|
-
}
|
|
1258
|
-
else if (i === 0) {
|
|
1259
|
-
result.push(`${bullet} ${line}`);
|
|
1260
|
-
}
|
|
1261
|
-
else {
|
|
1262
|
-
result.push(` ${line}`);
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
return result.join('\n') + '\n';
|
|
1266
|
-
}
|
|
1267
|
-
/**
|
|
1268
|
-
* Format streaming elapsed time in Claude Code style: 3m 30s
|
|
1269
|
-
*/
|
|
1270
|
-
formatStreamingElapsed() {
|
|
1271
|
-
if (!this.streamingStartTime)
|
|
1272
|
-
return null;
|
|
1273
|
-
const elapsed = Math.floor((Date.now() - this.streamingStartTime) / 1000);
|
|
1274
|
-
if (elapsed < 5)
|
|
1275
|
-
return null; // Don't show for very short durations
|
|
1276
|
-
const mins = Math.floor(elapsed / 60);
|
|
1277
|
-
const secs = elapsed % 60;
|
|
1278
|
-
if (mins > 0) {
|
|
1279
|
-
return `${mins}m ${secs}s`;
|
|
1280
|
-
}
|
|
1281
|
-
return `${secs}s`;
|
|
1282
875
|
}
|
|
1283
876
|
/**
|
|
1284
877
|
* Format a compact conversation block (Claude Code style)
|
|
@@ -1315,16 +908,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1315
908
|
setMode(mode) {
|
|
1316
909
|
const wasStreaming = this.mode === 'streaming';
|
|
1317
910
|
this.mode = mode;
|
|
1318
|
-
// Track streaming start time for elapsed display
|
|
1319
|
-
if (mode === 'streaming' && !wasStreaming) {
|
|
1320
|
-
this.streamingStartTime = Date.now();
|
|
1321
|
-
this.streamingTokens = 0; // Reset token count
|
|
1322
|
-
this.startSpinnerAnimation();
|
|
1323
|
-
}
|
|
1324
|
-
else if (mode === 'idle' && wasStreaming) {
|
|
1325
|
-
this.streamingStartTime = null;
|
|
1326
|
-
this.stopSpinnerAnimation();
|
|
1327
|
-
}
|
|
1328
911
|
if (wasStreaming && mode === 'idle' && !this.lastOutputEndedWithNewline) {
|
|
1329
912
|
// Finish streaming on a fresh line so the next prompt/event doesn't collide
|
|
1330
913
|
this.write('\n');
|
|
@@ -1335,61 +918,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1335
918
|
this.renderPrompt();
|
|
1336
919
|
}
|
|
1337
920
|
}
|
|
1338
|
-
/**
|
|
1339
|
-
* Start the animated spinner for streaming status
|
|
1340
|
-
*/
|
|
1341
|
-
startSpinnerAnimation() {
|
|
1342
|
-
if (this.spinnerInterval)
|
|
1343
|
-
return; // Already running
|
|
1344
|
-
this.spinnerFrame = 0;
|
|
1345
|
-
this.activityStarFrame = 0;
|
|
1346
|
-
this.spinnerInterval = setInterval(() => {
|
|
1347
|
-
this.spinnerFrame = (this.spinnerFrame + 1) % spinnerFrames.braille.length;
|
|
1348
|
-
this.activityStarFrame = (this.activityStarFrame + 1) % this.activityStarFrames.length;
|
|
1349
|
-
// Re-render to show updated spinner/star frame
|
|
1350
|
-
if (!this.plainMode && this.mode === 'streaming') {
|
|
1351
|
-
this.renderPrompt();
|
|
1352
|
-
}
|
|
1353
|
-
}, 80); // ~12 FPS for smooth spinner animation
|
|
1354
|
-
}
|
|
1355
|
-
/**
|
|
1356
|
-
* Stop the animated spinner
|
|
1357
|
-
*/
|
|
1358
|
-
stopSpinnerAnimation() {
|
|
1359
|
-
if (this.spinnerInterval) {
|
|
1360
|
-
clearInterval(this.spinnerInterval);
|
|
1361
|
-
this.spinnerInterval = null;
|
|
1362
|
-
}
|
|
1363
|
-
this.spinnerFrame = 0;
|
|
1364
|
-
this.activityStarFrame = 0;
|
|
1365
|
-
this.activityMessage = null;
|
|
1366
|
-
}
|
|
1367
|
-
/**
|
|
1368
|
-
* Set the activity message displayed with animated star
|
|
1369
|
-
* Example: "Ruminating…" shows as "✳ Ruminating… (esc to interrupt · 34s · ↑1.2k)"
|
|
1370
|
-
*/
|
|
1371
|
-
setActivity(message) {
|
|
1372
|
-
this.activityMessage = message;
|
|
1373
|
-
if (!this.plainMode) {
|
|
1374
|
-
this.renderPrompt();
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
/**
|
|
1378
|
-
* Update the token count displayed in the activity line
|
|
1379
|
-
*/
|
|
1380
|
-
updateStreamingTokens(tokens) {
|
|
1381
|
-
this.streamingTokens = tokens;
|
|
1382
|
-
}
|
|
1383
|
-
/**
|
|
1384
|
-
* Format token count as compact string (e.g., 1.2k, 24k, 128k)
|
|
1385
|
-
*/
|
|
1386
|
-
formatTokenCount(tokens) {
|
|
1387
|
-
if (tokens < 1000)
|
|
1388
|
-
return String(tokens);
|
|
1389
|
-
if (tokens < 10000)
|
|
1390
|
-
return `${(tokens / 1000).toFixed(1)}k`;
|
|
1391
|
-
return `${Math.round(tokens / 1000)}k`;
|
|
1392
|
-
}
|
|
1393
921
|
getMode() {
|
|
1394
922
|
return this.mode;
|
|
1395
923
|
}
|
|
@@ -1485,9 +1013,13 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1485
1013
|
if (!this.allowPromptRender) {
|
|
1486
1014
|
return;
|
|
1487
1015
|
}
|
|
1488
|
-
// Rich
|
|
1016
|
+
// Rich mode: inline overlay anchored to current scrollback (no full-screen clear)
|
|
1489
1017
|
this.updateTerminalSize();
|
|
1490
1018
|
const maxWidth = this.safeWidth();
|
|
1019
|
+
if (this.lastRenderWidth !== null && maxWidth !== this.lastRenderWidth) {
|
|
1020
|
+
// Terminal resized; force a clean anchor so the overlay doesn't jitter.
|
|
1021
|
+
this.overlayInvalidated = true;
|
|
1022
|
+
}
|
|
1491
1023
|
this.lastRenderWidth = maxWidth;
|
|
1492
1024
|
const overlay = this.buildOverlayLines();
|
|
1493
1025
|
if (!overlay.lines.length) {
|
|
@@ -1497,187 +1029,81 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1497
1029
|
if (!renderedLines.length) {
|
|
1498
1030
|
return;
|
|
1499
1031
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
//
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
if (extraLines > 0) {
|
|
1520
|
-
for (let i = 0; i < extraLines; i++) {
|
|
1521
|
-
this.write('\n');
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
// Move back to top of where overlay should start
|
|
1525
|
-
const moveBackUp = Math.max(0, height - 1);
|
|
1526
|
-
if (moveBackUp > 0) {
|
|
1527
|
-
this.write(`\x1b[${moveBackUp}A`);
|
|
1528
|
-
}
|
|
1032
|
+
let promptIndex = Math.max(0, Math.min(overlay.promptIndex, renderedLines.length - 1));
|
|
1033
|
+
let height = renderedLines.length;
|
|
1034
|
+
// Keep at least one free line below the overlay so typing always has breathing room
|
|
1035
|
+
const bottomPadding = this.overlayBottomPadding;
|
|
1036
|
+
const totalRows = this.rows || 24;
|
|
1037
|
+
const availableRows = Math.max(1, totalRows - bottomPadding);
|
|
1038
|
+
if (height > availableRows) {
|
|
1039
|
+
renderedLines.splice(availableRows);
|
|
1040
|
+
height = renderedLines.length;
|
|
1041
|
+
promptIndex = Math.max(0, Math.min(promptIndex, height - 1));
|
|
1042
|
+
}
|
|
1043
|
+
const startRow = Math.max(1, availableRows - height + 1);
|
|
1044
|
+
const promptRow = startRow + promptIndex;
|
|
1045
|
+
const promptCol = Math.min(Math.max(1, 3 + this.cursor), this.cols || 80);
|
|
1046
|
+
// Clear any previous overlay footprint (status, prompt, controls) to avoid leaking into scrollback
|
|
1047
|
+
this.clearOverlayRows(height, startRow);
|
|
1048
|
+
if (bottomPadding > 0 && startRow + height <= totalRows) {
|
|
1049
|
+
this.write(ESC.TO(startRow + height, 1));
|
|
1050
|
+
this.write(ESC.CLEAR_LINE);
|
|
1529
1051
|
}
|
|
1530
|
-
//
|
|
1531
|
-
for (let
|
|
1532
|
-
|
|
1052
|
+
// Render overlay lines in place without pushing scrollback
|
|
1053
|
+
for (let idx = 0; idx < height; idx++) {
|
|
1054
|
+
const row = startRow + idx;
|
|
1055
|
+
const line = renderedLines[idx] ?? '';
|
|
1056
|
+
this.write(ESC.TO(row, 1));
|
|
1533
1057
|
this.write(ESC.CLEAR_LINE);
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
this.write('\n');
|
|
1058
|
+
if (line) {
|
|
1059
|
+
this.write(line);
|
|
1537
1060
|
}
|
|
1538
1061
|
}
|
|
1539
|
-
// Position cursor at prompt
|
|
1540
|
-
|
|
1541
|
-
// Cursor is now at the last line. Move up to the prompt row.
|
|
1542
|
-
const linesToMoveUp = height - 1 - promptIndex;
|
|
1543
|
-
if (linesToMoveUp > 0) {
|
|
1544
|
-
this.write(`\x1b[${linesToMoveUp}A`);
|
|
1545
|
-
}
|
|
1546
|
-
this.write(`\x1b[${promptCol}G`);
|
|
1062
|
+
// Position cursor at prompt row/col
|
|
1063
|
+
this.write(ESC.TO(promptRow, promptCol));
|
|
1547
1064
|
this.cursorVisibleColumn = promptCol;
|
|
1548
1065
|
this.hasRenderedPrompt = true;
|
|
1549
|
-
this.hasEverRenderedOverlay = true;
|
|
1066
|
+
this.hasEverRenderedOverlay = true; // Mark that we've rendered at least once
|
|
1550
1067
|
this.isPromptActive = true;
|
|
1551
1068
|
this.lastOverlayHeight = height;
|
|
1069
|
+
this.lastPromptIndex = promptIndex;
|
|
1552
1070
|
this.lastOverlay = { lines: renderedLines, promptIndex };
|
|
1553
|
-
this.
|
|
1071
|
+
this.overlayInvalidated = false;
|
|
1072
|
+
this.lastOutputEndedWithNewline = true;
|
|
1554
1073
|
this.promptHeight = height;
|
|
1555
1074
|
}
|
|
1556
1075
|
buildOverlayLines() {
|
|
1557
1076
|
const lines = [];
|
|
1558
1077
|
const maxWidth = this.safeWidth();
|
|
1559
|
-
|
|
1560
|
-
const
|
|
1561
|
-
// Activity line (only when streaming) - shows: ✽ Moseying… (esc to interrupt · 34s)
|
|
1562
|
-
if (this.mode === 'streaming' && this.activityMessage) {
|
|
1563
|
-
// Animated sparkle
|
|
1564
|
-
const spinnerChars = ['✽', '✾', '✿', '❀', '❁', '❂', '❃', '✻'];
|
|
1565
|
-
const spinnerChar = spinnerChars[this.spinnerFrame % spinnerChars.length] ?? '✽';
|
|
1566
|
-
const elapsed = this.formatStreamingElapsed();
|
|
1567
|
-
// Use fun phrases for generic activity, otherwise show specific activity
|
|
1568
|
-
const genericActivities = ['Streaming', 'Thinking', 'Processing'];
|
|
1569
|
-
const displayActivity = genericActivities.includes(this.activityMessage)
|
|
1570
|
-
? this.funActivityPhrases[this.activityPhraseIndex % this.funActivityPhrases.length]
|
|
1571
|
-
: this.activityMessage;
|
|
1572
|
-
// Format: ✽ Moseying… (esc to interrupt · 1m 19s · ↑1.2k tokens)
|
|
1573
|
-
const parts = ['esc to interrupt'];
|
|
1574
|
-
if (elapsed)
|
|
1575
|
-
parts.push(elapsed);
|
|
1576
|
-
if (this.streamingTokens > 0) {
|
|
1577
|
-
parts.push(`↑${this.formatTokenCount(this.streamingTokens)} tokens`);
|
|
1578
|
-
}
|
|
1579
|
-
const activityLine = `${theme.info(spinnerChar)} ${displayActivity}… ${theme.ui.muted(`(${parts.join(' · ')})`)}`;
|
|
1580
|
-
lines.push(this.truncateLine(activityLine, maxWidth));
|
|
1581
|
-
}
|
|
1582
|
-
// Top divider
|
|
1583
|
-
lines.push(divider);
|
|
1584
|
-
// Input prompt line
|
|
1585
|
-
const promptIndex = lines.length;
|
|
1586
|
-
const inputLine = this.buildInputLine();
|
|
1587
|
-
// Handle multi-line input by splitting on newlines
|
|
1588
|
-
const inputLines = inputLine.split('\n');
|
|
1589
|
-
for (const line of inputLines) {
|
|
1078
|
+
const chromeLines = this.buildChromeLines();
|
|
1079
|
+
for (const line of chromeLines) {
|
|
1590
1080
|
lines.push(this.truncateLine(line, maxWidth));
|
|
1591
1081
|
}
|
|
1592
|
-
|
|
1593
|
-
lines.push(divider);
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
for (const panelLine of this.inlinePanel) {
|
|
1597
|
-
lines.push(this.truncateLine(` ${panelLine}`, maxWidth));
|
|
1598
|
-
}
|
|
1599
|
-
// Separate inline content from suggestions/toggles
|
|
1600
|
-
lines.push(divider);
|
|
1601
|
-
}
|
|
1602
|
-
// Slash command suggestions
|
|
1082
|
+
const divider = renderDivider(Math.min(maxWidth, 96), 'prompt');
|
|
1083
|
+
lines.push(this.truncateLine(divider, maxWidth));
|
|
1084
|
+
const promptIndex = lines.length;
|
|
1085
|
+
lines.push(this.truncateLine(this.buildInputLine(), maxWidth));
|
|
1603
1086
|
if (this.suggestions.length > 0) {
|
|
1604
1087
|
for (let index = 0; index < this.suggestions.length; index++) {
|
|
1605
1088
|
const suggestion = this.suggestions[index];
|
|
1606
1089
|
const isActive = index === this.suggestionIndex;
|
|
1607
|
-
const marker = isActive ? theme.primary('
|
|
1090
|
+
const marker = isActive ? theme.primary('›') : theme.ui.muted('›');
|
|
1608
1091
|
const cmdText = isActive ? theme.primary(suggestion.command) : theme.ui.muted(suggestion.command);
|
|
1609
1092
|
const descText = isActive ? suggestion.description : theme.ui.muted(suggestion.description);
|
|
1610
|
-
lines.push(this.truncateLine(
|
|
1093
|
+
lines.push(this.truncateLine(`${marker} ${cmdText} — ${descText}`, maxWidth));
|
|
1611
1094
|
}
|
|
1612
1095
|
}
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1096
|
+
if (this.inlinePanel.length > 0) {
|
|
1097
|
+
for (const panelLine of this.inlinePanel) {
|
|
1098
|
+
lines.push(this.truncateLine(panelLine, maxWidth));
|
|
1099
|
+
}
|
|
1617
1100
|
}
|
|
1618
|
-
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
lines.push(this.truncateLine(` ${toggleLine}`, maxWidth));
|
|
1101
|
+
const controlLines = this.buildControlLines();
|
|
1102
|
+
for (const control of controlLines) {
|
|
1103
|
+
lines.push(this.truncateLine(control, maxWidth));
|
|
1622
1104
|
}
|
|
1623
|
-
// Help hint
|
|
1624
|
-
lines.push(this.truncateLine(` ${theme.ui.muted('? for shortcuts')}`, maxWidth));
|
|
1625
1105
|
return { lines, promptIndex };
|
|
1626
1106
|
}
|
|
1627
|
-
/**
|
|
1628
|
-
* Build model name and context usage line with mini progress bar
|
|
1629
|
-
* Format: gpt-4 · ████░░ 85% context
|
|
1630
|
-
*/
|
|
1631
|
-
buildModelContextLine() {
|
|
1632
|
-
const parts = [];
|
|
1633
|
-
// Model name (provider / model or just model)
|
|
1634
|
-
const model = this.statusMeta.provider && this.statusMeta.model
|
|
1635
|
-
? `${this.statusMeta.provider} · ${this.statusMeta.model}`
|
|
1636
|
-
: this.statusMeta.model || this.statusMeta.provider;
|
|
1637
|
-
if (model) {
|
|
1638
|
-
parts.push(theme.info(model));
|
|
1639
|
-
}
|
|
1640
|
-
// Context meter with mini progress bar
|
|
1641
|
-
if (this.statusMeta.contextPercent !== undefined) {
|
|
1642
|
-
const remaining = Math.max(0, 100 - this.statusMeta.contextPercent);
|
|
1643
|
-
const barWidth = 6;
|
|
1644
|
-
const filled = Math.round((remaining / 100) * barWidth);
|
|
1645
|
-
const empty = barWidth - filled;
|
|
1646
|
-
const barColor = remaining > 50 ? theme.success : remaining > 20 ? theme.warning : theme.error;
|
|
1647
|
-
const bar = barColor('█'.repeat(filled)) + theme.ui.muted('░'.repeat(empty));
|
|
1648
|
-
parts.push(`${bar} ${barColor(`${remaining}%`)} ${theme.ui.muted('ctx')}`);
|
|
1649
|
-
}
|
|
1650
|
-
return parts.length > 0 ? parts.join(theme.ui.muted(' · ')) : null;
|
|
1651
|
-
}
|
|
1652
|
-
/**
|
|
1653
|
-
* Build inline toggle controls - Claude Code style
|
|
1654
|
-
* Format: ⏵⏵ accept edits on (shift+tab to cycle)
|
|
1655
|
-
*/
|
|
1656
|
-
buildInlineToggleLine() {
|
|
1657
|
-
const parts = [];
|
|
1658
|
-
// Edit acceptance mode - Claude Code style with ⏵⏵
|
|
1659
|
-
const editIcon = '⏵⏵';
|
|
1660
|
-
const editState = this.toggleState.verificationEnabled ? 'verify edits' : 'accept edits';
|
|
1661
|
-
const editStatus = this.toggleState.verificationEnabled ? theme.warning('on') : theme.success('on');
|
|
1662
|
-
parts.push(`${theme.ui.muted(editIcon)} ${editState} ${editStatus}`);
|
|
1663
|
-
// Auto-continue
|
|
1664
|
-
if (this.toggleState.autoContinueEnabled) {
|
|
1665
|
-
parts.push(`${theme.ui.muted('auto')} ${theme.success('on')}`);
|
|
1666
|
-
}
|
|
1667
|
-
// Thinking mode (if not default)
|
|
1668
|
-
const thinkingLabel = (this.toggleState.thinkingModeLabel || 'balanced').trim().toLowerCase();
|
|
1669
|
-
if (thinkingLabel === 'extended') {
|
|
1670
|
-
parts.push(`${theme.ui.muted('thinking')} ${theme.info('extended')}`);
|
|
1671
|
-
}
|
|
1672
|
-
// Approval mode (if not auto)
|
|
1673
|
-
const approvalMode = this.toggleState.criticalApprovalMode || 'auto';
|
|
1674
|
-
if (approvalMode === 'approval') {
|
|
1675
|
-
parts.push(`${theme.ui.muted('approvals')} ${theme.warning('ask')}`);
|
|
1676
|
-
}
|
|
1677
|
-
// Cycle hint
|
|
1678
|
-
const cycleHint = theme.ui.muted('(shift+tab to cycle)');
|
|
1679
|
-
return parts.length > 0 ? `${parts.join(theme.ui.muted(' · '))} ${cycleHint}` : null;
|
|
1680
|
-
}
|
|
1681
1107
|
buildChromeLines() {
|
|
1682
1108
|
const maxWidth = this.safeWidth();
|
|
1683
1109
|
const statusLines = this.buildStatusBlock(maxWidth);
|
|
@@ -1697,22 +1123,15 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1697
1123
|
return [];
|
|
1698
1124
|
}
|
|
1699
1125
|
const segments = [];
|
|
1700
|
-
|
|
1701
|
-
if (this.mode === 'streaming') {
|
|
1702
|
-
const spinnerChars = spinnerFrames.braille;
|
|
1703
|
-
const spinnerChar = spinnerChars[this.spinnerFrame % spinnerChars.length] ?? '⠋';
|
|
1704
|
-
segments.push(`${theme.info(spinnerChar)} ${this.applyTone(statusLabel.text, statusLabel.tone)}`);
|
|
1705
|
-
}
|
|
1706
|
-
else {
|
|
1707
|
-
segments.push(`${theme.ui.muted('status')} ${this.applyTone(statusLabel.text, statusLabel.tone)}`);
|
|
1708
|
-
}
|
|
1126
|
+
segments.push(`${theme.ui.muted('status')} ${this.applyTone(statusLabel.text, statusLabel.tone)}`);
|
|
1709
1127
|
if (this.statusMeta.sessionTime) {
|
|
1710
1128
|
segments.push(`${theme.ui.muted('runtime')} ${theme.ui.muted(this.statusMeta.sessionTime)}`);
|
|
1711
1129
|
}
|
|
1712
1130
|
if (this.statusMeta.contextPercent !== undefined) {
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1131
|
+
const ctx = this.statusMeta.contextPercent;
|
|
1132
|
+
const tone = ctx > 90 ? 'error' : ctx > 70 ? 'warn' : 'muted';
|
|
1133
|
+
const color = tone === 'error' ? theme.error : tone === 'warn' ? theme.warning : theme.ui.muted;
|
|
1134
|
+
segments.push(`${theme.ui.muted('ctx')} ${color(`${ctx}%`)}`);
|
|
1716
1135
|
}
|
|
1717
1136
|
return this.wrapSegments(segments, maxWidth);
|
|
1718
1137
|
}
|
|
@@ -1821,33 +1240,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1821
1240
|
}
|
|
1822
1241
|
return lines;
|
|
1823
1242
|
}
|
|
1824
|
-
/**
|
|
1825
|
-
* Build a compact toggle line like Claude Code:
|
|
1826
|
-
* "⏵⏵ accept edits on (shift+tab to cycle)"
|
|
1827
|
-
*/
|
|
1828
|
-
buildCompactToggleLine() {
|
|
1829
|
-
// Show the most relevant mode based on current state
|
|
1830
|
-
const parts = [];
|
|
1831
|
-
// Edit mode indicator
|
|
1832
|
-
const editIcon = '⏵⏵';
|
|
1833
|
-
const editState = this.toggleState.verificationEnabled ? 'approval required' : 'accept edits';
|
|
1834
|
-
parts.push(`${theme.ui.muted(editIcon)} ${editState} ${theme.success('on')}`);
|
|
1835
|
-
// Auto-continue indicator (if enabled)
|
|
1836
|
-
if (this.toggleState.autoContinueEnabled) {
|
|
1837
|
-
parts.push(`${theme.ui.muted('auto')} ${theme.success('on')}`);
|
|
1838
|
-
}
|
|
1839
|
-
// Thinking mode (if active)
|
|
1840
|
-
const thinkingLabel = (this.toggleState.thinkingModeLabel || '').trim().toLowerCase();
|
|
1841
|
-
if (thinkingLabel && thinkingLabel !== 'off') {
|
|
1842
|
-
parts.push(`${theme.ui.muted('thinking')} ${theme.info(thinkingLabel)}`);
|
|
1843
|
-
}
|
|
1844
|
-
// Cycle hint
|
|
1845
|
-
const cycleHint = theme.ui.muted('(shift+tab to cycle)');
|
|
1846
|
-
if (parts.length === 0) {
|
|
1847
|
-
return null;
|
|
1848
|
-
}
|
|
1849
|
-
return ` ${parts.join(theme.ui.muted(' · '))} ${cycleHint}`;
|
|
1850
|
-
}
|
|
1851
1243
|
buildToggleLine() {
|
|
1852
1244
|
const toggles = [];
|
|
1853
1245
|
const addToggle = (label, on, hotkey, value) => {
|
|
@@ -1909,87 +1301,15 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1909
1301
|
buildInputLine() {
|
|
1910
1302
|
if (this.collapsedPaste) {
|
|
1911
1303
|
const summary = `[pasted ${this.collapsedPaste.lines} lines, ${this.collapsedPaste.chars} chars] (Ctrl+L insert, Backspace discard)`;
|
|
1912
|
-
return this.truncateLine(`${theme.primary('
|
|
1304
|
+
return this.truncateLine(`${theme.primary('› ')}${theme.ui.muted(summary)}`, this.safeWidth());
|
|
1913
1305
|
}
|
|
1914
|
-
|
|
1915
|
-
const prompt = theme.primary('> ');
|
|
1306
|
+
const prompt = theme.primary('› ');
|
|
1916
1307
|
const promptWidth = this.visibleLength(prompt);
|
|
1917
1308
|
const maxWidth = this.safeWidth();
|
|
1918
|
-
const
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
const bufferLines = normalized.split('\n');
|
|
1923
|
-
// Wrap each logical line to fit terminal width, expanding vertically
|
|
1924
|
-
const result = [];
|
|
1925
|
-
let totalChars = 0;
|
|
1926
|
-
let cursorLine = 0;
|
|
1927
|
-
let cursorCol = 0;
|
|
1928
|
-
let foundCursor = false;
|
|
1929
|
-
for (let lineIndex = 0; lineIndex < bufferLines.length; lineIndex++) {
|
|
1930
|
-
const line = bufferLines[lineIndex] ?? '';
|
|
1931
|
-
const isFirstLogicalLine = lineIndex === 0;
|
|
1932
|
-
const lineStartChar = totalChars;
|
|
1933
|
-
// Determine available width for this line
|
|
1934
|
-
const firstLineWidth = maxWidth - promptWidth;
|
|
1935
|
-
const contLineWidth = maxWidth - continuationWidth;
|
|
1936
|
-
// Wrap this logical line into display lines
|
|
1937
|
-
let remaining = line;
|
|
1938
|
-
let isFirstDisplayLine = true;
|
|
1939
|
-
while (remaining.length > 0 || isFirstDisplayLine) {
|
|
1940
|
-
const availableWidth = (isFirstLogicalLine && isFirstDisplayLine) ? firstLineWidth : contLineWidth;
|
|
1941
|
-
const chunk = remaining.slice(0, availableWidth);
|
|
1942
|
-
remaining = remaining.slice(availableWidth);
|
|
1943
|
-
// Build the display line
|
|
1944
|
-
let displayLine;
|
|
1945
|
-
if (isFirstLogicalLine && isFirstDisplayLine) {
|
|
1946
|
-
displayLine = `${prompt}${chunk}`;
|
|
1947
|
-
}
|
|
1948
|
-
else {
|
|
1949
|
-
displayLine = `${continuationIndent}${chunk}`;
|
|
1950
|
-
}
|
|
1951
|
-
// Track cursor position
|
|
1952
|
-
if (!foundCursor) {
|
|
1953
|
-
const chunkStart = lineStartChar + (line.length - remaining.length - chunk.length);
|
|
1954
|
-
const chunkEnd = chunkStart + chunk.length;
|
|
1955
|
-
if (this.cursor >= chunkStart && this.cursor <= chunkEnd) {
|
|
1956
|
-
cursorLine = result.length;
|
|
1957
|
-
const offsetInChunk = this.cursor - chunkStart;
|
|
1958
|
-
cursorCol = ((isFirstLogicalLine && isFirstDisplayLine) ? promptWidth : continuationWidth) + offsetInChunk;
|
|
1959
|
-
foundCursor = true;
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
result.push(displayLine);
|
|
1963
|
-
isFirstDisplayLine = false;
|
|
1964
|
-
// If nothing left and this was an empty line, we already added it
|
|
1965
|
-
if (remaining.length === 0 && chunk.length === 0)
|
|
1966
|
-
break;
|
|
1967
|
-
}
|
|
1968
|
-
totalChars += line.length + 1; // +1 for the newline separator
|
|
1969
|
-
}
|
|
1970
|
-
// Handle cursor at very end
|
|
1971
|
-
if (!foundCursor) {
|
|
1972
|
-
cursorLine = Math.max(0, result.length - 1);
|
|
1973
|
-
const lastLine = result[cursorLine] ?? '';
|
|
1974
|
-
cursorCol = this.visibleLength(lastLine);
|
|
1975
|
-
}
|
|
1976
|
-
// Add cursor highlight to the appropriate position
|
|
1977
|
-
if (result.length > 0) {
|
|
1978
|
-
const targetLine = result[cursorLine] ?? '';
|
|
1979
|
-
const visiblePart = this.stripAnsi(targetLine);
|
|
1980
|
-
const cursorPos = Math.min(cursorCol, visiblePart.length);
|
|
1981
|
-
// Rebuild the line with cursor highlight
|
|
1982
|
-
const before = visiblePart.slice(0, cursorPos);
|
|
1983
|
-
const at = visiblePart.charAt(cursorPos) || ' ';
|
|
1984
|
-
const after = visiblePart.slice(cursorPos + 1);
|
|
1985
|
-
// Preserve the prompt/indent styling
|
|
1986
|
-
const prefix = cursorLine === 0 ? prompt : continuationIndent;
|
|
1987
|
-
const textPart = cursorLine === 0 ? before.slice(promptWidth) : before.slice(continuationWidth);
|
|
1988
|
-
result[cursorLine] = `${prefix}${textPart}${ESC.REVERSE}${at}${ESC.RESET}${after}`;
|
|
1989
|
-
}
|
|
1990
|
-
// Store cursor column for terminal positioning
|
|
1991
|
-
this.cursorVisibleColumn = cursorCol + 1;
|
|
1992
|
-
return result.join('\n');
|
|
1309
|
+
const available = Math.max(1, maxWidth - promptWidth);
|
|
1310
|
+
const window = this.buildInputWindow(available);
|
|
1311
|
+
this.cursorVisibleColumn = Math.min(maxWidth, promptWidth + window.cursor + 1);
|
|
1312
|
+
return this.truncateLine(`${prompt}${window.text}`, maxWidth);
|
|
1993
1313
|
}
|
|
1994
1314
|
buildInputWindow(available) {
|
|
1995
1315
|
if (available <= 0) {
|
|
@@ -2120,6 +1440,17 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
2120
1440
|
}
|
|
2121
1441
|
return result;
|
|
2122
1442
|
}
|
|
1443
|
+
clearOverlayRows(rows, startRow) {
|
|
1444
|
+
const totalRows = this.rows || 24;
|
|
1445
|
+
const limit = Math.max(0, Math.min(rows, totalRows));
|
|
1446
|
+
for (let idx = 0; idx < limit; idx++) {
|
|
1447
|
+
const row = startRow + idx;
|
|
1448
|
+
if (row < 1 || row > totalRows)
|
|
1449
|
+
continue;
|
|
1450
|
+
this.write(ESC.TO(row, 1));
|
|
1451
|
+
this.write(ESC.CLEAR_LINE);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
2123
1454
|
getBuffer() {
|
|
2124
1455
|
return this.buffer;
|
|
2125
1456
|
}
|
|
@@ -2147,35 +1478,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
2147
1478
|
setModeStatus(status) {
|
|
2148
1479
|
this.updateStatus(status);
|
|
2149
1480
|
}
|
|
2150
|
-
/**
|
|
2151
|
-
* Show a compacting status with animated spinner (Claude Code style)
|
|
2152
|
-
* Uses ✻ character with animation to indicate context compaction in progress
|
|
2153
|
-
*/
|
|
2154
|
-
showCompactingStatus(message) {
|
|
2155
|
-
this.statusMessage = message;
|
|
2156
|
-
if (!this.spinnerInterval) {
|
|
2157
|
-
this.spinnerInterval = setInterval(() => {
|
|
2158
|
-
this.spinnerFrame++;
|
|
2159
|
-
// Cycle activity phrase every ~4 seconds (50 frames at 80ms)
|
|
2160
|
-
if (this.spinnerFrame % 50 === 0) {
|
|
2161
|
-
this.activityPhraseIndex++;
|
|
2162
|
-
}
|
|
2163
|
-
this.renderPrompt();
|
|
2164
|
-
}, 80);
|
|
2165
|
-
}
|
|
2166
|
-
this.renderPrompt();
|
|
2167
|
-
}
|
|
2168
|
-
/**
|
|
2169
|
-
* Hide the compacting status and stop spinner animation
|
|
2170
|
-
*/
|
|
2171
|
-
hideCompactingStatus() {
|
|
2172
|
-
if (this.spinnerInterval) {
|
|
2173
|
-
clearInterval(this.spinnerInterval);
|
|
2174
|
-
this.spinnerInterval = null;
|
|
2175
|
-
}
|
|
2176
|
-
this.statusMessage = null;
|
|
2177
|
-
this.renderPrompt();
|
|
2178
|
-
}
|
|
2179
1481
|
emitPrompt(content) {
|
|
2180
1482
|
this.pushPromptEvent(content);
|
|
2181
1483
|
}
|
|
@@ -2219,30 +1521,23 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
2219
1521
|
const height = this.lastOverlay?.lines.length ?? this.promptHeight ?? 0;
|
|
2220
1522
|
if (height === 0)
|
|
2221
1523
|
return;
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
for (let i = 0; i < height; i++) {
|
|
2231
|
-
this.write('\r');
|
|
1524
|
+
this.updateTerminalSize();
|
|
1525
|
+
const totalRows = this.rows || 24;
|
|
1526
|
+
const startRow = Math.max(1, Math.max(1, totalRows - this.overlayBottomPadding) - height + 1);
|
|
1527
|
+
this.clearOverlayRows(height, startRow);
|
|
1528
|
+
// Keep the padding row clean as well
|
|
1529
|
+
const paddingRow = startRow + height;
|
|
1530
|
+
if (this.overlayBottomPadding > 0 && paddingRow <= totalRows) {
|
|
1531
|
+
this.write(ESC.TO(paddingRow, 1));
|
|
2232
1532
|
this.write(ESC.CLEAR_LINE);
|
|
2233
|
-
if (i < height - 1) {
|
|
2234
|
-
this.write('\x1b[B');
|
|
2235
|
-
}
|
|
2236
1533
|
}
|
|
2237
|
-
// Move
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
this.write('\r');
|
|
1534
|
+
// Move cursor to the bottom ready for new scrollback output
|
|
1535
|
+
this.write(ESC.TO(totalRows, 1));
|
|
1536
|
+
this.lastOverlayHeight = height;
|
|
1537
|
+
this.lastPromptIndex = this.lastOverlay?.promptIndex ?? this.lastPromptIndex;
|
|
2242
1538
|
this.lastOverlay = null;
|
|
1539
|
+
this.overlayInvalidated = true;
|
|
2243
1540
|
this.promptHeight = 0;
|
|
2244
|
-
this.lastOverlayHeight = 0;
|
|
2245
|
-
this.isPromptActive = false;
|
|
2246
1541
|
}
|
|
2247
1542
|
updateTerminalSize() {
|
|
2248
1543
|
if (this.output.isTTY) {
|