erosolar-cli 2.1.172 → 2.1.173
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/agents/erosolar-code.rules.json +2 -2
- package/agents/general.rules.json +21 -3
- package/dist/capabilities/askUserCapability.js +1 -1
- package/dist/capabilities/askUserCapability.js.map +1 -1
- package/dist/capabilities/statusCapability.js +2 -2
- package/dist/capabilities/statusCapability.js.map +1 -1
- package/dist/codex/capabilities/codexCoreCapability.d.ts +6 -0
- package/dist/codex/capabilities/codexCoreCapability.d.ts.map +1 -0
- package/dist/codex/capabilities/codexCoreCapability.js +516 -0
- package/dist/codex/capabilities/codexCoreCapability.js.map +1 -0
- package/dist/codex/fs.d.ts +4 -0
- package/dist/codex/fs.d.ts.map +1 -0
- package/dist/codex/fs.js +25 -0
- package/dist/codex/fs.js.map +1 -0
- package/dist/codex/persistence/planStore.d.ts +4 -0
- package/dist/codex/persistence/planStore.d.ts.map +1 -0
- package/dist/codex/persistence/planStore.js +59 -0
- package/dist/codex/persistence/planStore.js.map +1 -0
- package/dist/codex/pluginAllowlist.d.ts +4 -0
- package/dist/codex/pluginAllowlist.d.ts.map +1 -0
- package/dist/codex/pluginAllowlist.js +14 -0
- package/dist/codex/pluginAllowlist.js.map +1 -0
- package/dist/codex/types.d.ts +21 -0
- package/dist/codex/types.d.ts.map +1 -0
- package/dist/codex/types.js +62 -0
- package/dist/codex/types.js.map +1 -0
- package/dist/contracts/agent-schemas.json +5 -5
- package/dist/core/agent.d.ts +83 -24
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +499 -248
- package/dist/core/agent.js.map +1 -1
- package/dist/core/preferences.d.ts +1 -0
- package/dist/core/preferences.d.ts.map +1 -1
- package/dist/core/preferences.js +8 -1
- package/dist/core/preferences.js.map +1 -1
- package/dist/core/reliabilityPrompt.d.ts +9 -0
- package/dist/core/reliabilityPrompt.d.ts.map +1 -0
- package/dist/core/reliabilityPrompt.js +31 -0
- package/dist/core/reliabilityPrompt.js.map +1 -0
- package/dist/core/schemaValidator.js +3 -3
- package/dist/core/schemaValidator.js.map +1 -1
- package/dist/core/toolPreconditions.d.ts +0 -11
- package/dist/core/toolPreconditions.d.ts.map +1 -1
- package/dist/core/toolPreconditions.js +33 -164
- package/dist/core/toolPreconditions.js.map +1 -1
- package/dist/core/toolRuntime.d.ts.map +1 -1
- package/dist/core/toolRuntime.js +9 -114
- package/dist/core/toolRuntime.js.map +1 -1
- package/dist/core/updateChecker.d.ts +61 -1
- package/dist/core/updateChecker.d.ts.map +1 -1
- package/dist/core/updateChecker.js +147 -3
- package/dist/core/updateChecker.js.map +1 -1
- package/dist/headless/evalMode.d.ts.map +1 -1
- package/dist/headless/evalMode.js +6 -0
- package/dist/headless/evalMode.js.map +1 -1
- package/dist/headless/headlessApp.d.ts.map +1 -1
- package/dist/headless/headlessApp.js +6 -39
- package/dist/headless/headlessApp.js.map +1 -1
- package/dist/mcp/sseClient.d.ts +4 -1
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +36 -2
- package/dist/mcp/sseClient.js.map +1 -1
- package/dist/mcp/stdioClient.d.ts +4 -1
- package/dist/mcp/stdioClient.d.ts.map +1 -1
- package/dist/mcp/stdioClient.js +41 -1
- package/dist/mcp/stdioClient.js.map +1 -1
- package/dist/mcp/toolBridge.d.ts +3 -0
- package/dist/mcp/toolBridge.d.ts.map +1 -1
- package/dist/mcp/toolBridge.js +2 -2
- package/dist/mcp/toolBridge.js.map +1 -1
- package/dist/mcp/types.d.ts +18 -0
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/plugins/tools/nodeDefaults.d.ts.map +1 -1
- package/dist/plugins/tools/nodeDefaults.js +0 -2
- package/dist/plugins/tools/nodeDefaults.js.map +1 -1
- package/dist/providers/openaiResponsesProvider.d.ts.map +1 -1
- package/dist/providers/openaiResponsesProvider.js +79 -74
- package/dist/providers/openaiResponsesProvider.js.map +1 -1
- package/dist/runtime/agentController.d.ts.map +1 -1
- package/dist/runtime/agentController.js +6 -3
- package/dist/runtime/agentController.js.map +1 -1
- package/dist/runtime/agentSession.d.ts +0 -2
- package/dist/runtime/agentSession.d.ts.map +1 -1
- package/dist/runtime/agentSession.js +2 -2
- package/dist/runtime/agentSession.js.map +1 -1
- package/dist/shell/interactiveShell.d.ts +25 -18
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +345 -291
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/shellApp.d.ts.map +1 -1
- package/dist/shell/shellApp.js +15 -8
- package/dist/shell/shellApp.js.map +1 -1
- package/dist/shell/systemPrompt.d.ts.map +1 -1
- package/dist/shell/systemPrompt.js +4 -15
- package/dist/shell/systemPrompt.js.map +1 -1
- package/dist/subagents/taskRunner.js +2 -1
- package/dist/subagents/taskRunner.js.map +1 -1
- package/dist/tools/bashTools.d.ts.map +1 -1
- package/dist/tools/bashTools.js +101 -8
- package/dist/tools/bashTools.js.map +1 -1
- package/dist/tools/diffUtils.d.ts +8 -2
- package/dist/tools/diffUtils.d.ts.map +1 -1
- package/dist/tools/diffUtils.js +72 -13
- package/dist/tools/diffUtils.js.map +1 -1
- package/dist/tools/grepTools.d.ts.map +1 -1
- package/dist/tools/grepTools.js +10 -2
- package/dist/tools/grepTools.js.map +1 -1
- package/dist/tools/planningTools.d.ts +0 -10
- package/dist/tools/planningTools.d.ts.map +1 -1
- package/dist/tools/planningTools.js +0 -16
- package/dist/tools/planningTools.js.map +1 -1
- package/dist/tools/searchTools.d.ts.map +1 -1
- package/dist/tools/searchTools.js +4 -2
- package/dist/tools/searchTools.js.map +1 -1
- package/dist/ui/PromptController.d.ts +7 -4
- package/dist/ui/PromptController.d.ts.map +1 -1
- package/dist/ui/PromptController.js +4 -7
- package/dist/ui/PromptController.js.map +1 -1
- package/dist/ui/ShellUIAdapter.d.ts +286 -28
- package/dist/ui/ShellUIAdapter.d.ts.map +1 -1
- package/dist/ui/ShellUIAdapter.js +1485 -121
- package/dist/ui/ShellUIAdapter.js.map +1 -1
- package/dist/ui/UnifiedUIController.d.ts +80 -0
- package/dist/ui/UnifiedUIController.d.ts.map +1 -0
- package/dist/ui/UnifiedUIController.js +211 -0
- package/dist/ui/UnifiedUIController.js.map +1 -0
- package/dist/ui/UnifiedUIRenderer.d.ts +102 -46
- package/dist/ui/UnifiedUIRenderer.d.ts.map +1 -1
- package/dist/ui/UnifiedUIRenderer.js +680 -610
- package/dist/ui/UnifiedUIRenderer.js.map +1 -1
- package/dist/ui/animatedStatus.d.ts +128 -6
- package/dist/ui/animatedStatus.d.ts.map +1 -1
- package/dist/ui/animatedStatus.js +383 -50
- package/dist/ui/animatedStatus.js.map +1 -1
- package/dist/ui/animation/AnimationScheduler.d.ts +192 -0
- package/dist/ui/animation/AnimationScheduler.d.ts.map +1 -0
- package/dist/ui/animation/AnimationScheduler.js +432 -0
- package/dist/ui/animation/AnimationScheduler.js.map +1 -0
- package/dist/ui/display.d.ts +179 -25
- package/dist/ui/display.d.ts.map +1 -1
- package/dist/ui/display.js +678 -96
- package/dist/ui/display.js.map +1 -1
- package/dist/ui/inPlaceUpdater.d.ts +181 -0
- package/dist/ui/inPlaceUpdater.d.ts.map +1 -0
- package/dist/ui/inPlaceUpdater.js +515 -0
- package/dist/ui/inPlaceUpdater.js.map +1 -0
- package/dist/ui/interrupts/InterruptManager.d.ts +142 -0
- package/dist/ui/interrupts/InterruptManager.d.ts.map +1 -0
- package/dist/ui/interrupts/InterruptManager.js +439 -0
- package/dist/ui/interrupts/InterruptManager.js.map +1 -0
- package/dist/ui/layout.d.ts +0 -1
- package/dist/ui/layout.d.ts.map +1 -1
- package/dist/ui/layout.js +0 -12
- package/dist/ui/layout.js.map +1 -1
- package/dist/ui/orchestration/StatusOrchestrator.d.ts +1 -1
- package/dist/ui/orchestration/StatusOrchestrator.js +1 -1
- package/dist/ui/orchestration/UIUpdateCoordinator.d.ts +61 -7
- package/dist/ui/orchestration/UIUpdateCoordinator.d.ts.map +1 -1
- package/dist/ui/orchestration/UIUpdateCoordinator.js +232 -20
- package/dist/ui/orchestration/UIUpdateCoordinator.js.map +1 -1
- package/dist/ui/shortcutsHelp.d.ts.map +1 -1
- package/dist/ui/shortcutsHelp.js +0 -1
- package/dist/ui/shortcutsHelp.js.map +1 -1
- package/dist/ui/telemetry/ResponseTracker.d.ts +22 -0
- package/dist/ui/telemetry/ResponseTracker.d.ts.map +1 -0
- package/dist/ui/telemetry/ResponseTracker.js +60 -0
- package/dist/ui/telemetry/ResponseTracker.js.map +1 -0
- package/dist/ui/telemetry/UITelemetry.d.ts +181 -0
- package/dist/ui/telemetry/UITelemetry.d.ts.map +1 -0
- package/dist/ui/telemetry/UITelemetry.js +446 -0
- package/dist/ui/telemetry/UITelemetry.js.map +1 -0
- package/dist/ui/unified/index.d.ts +28 -1
- package/dist/ui/unified/index.d.ts.map +1 -1
- package/dist/ui/unified/index.js +41 -2
- package/dist/ui/unified/index.js.map +1 -1
- package/dist/ui/unified/layout.d.ts +12 -0
- package/dist/ui/unified/layout.d.ts.map +1 -0
- package/dist/ui/unified/layout.js +96 -0
- package/dist/ui/unified/layout.js.map +1 -0
- package/package.json +1 -2
- package/dist/StringUtils.d.ts +0 -8
- package/dist/StringUtils.d.ts.map +0 -1
- package/dist/StringUtils.js +0 -11
- package/dist/StringUtils.js.map +0 -1
- package/dist/core/aiFlowSupervisor.d.ts +0 -44
- package/dist/core/aiFlowSupervisor.d.ts.map +0 -1
- package/dist/core/aiFlowSupervisor.js +0 -299
- package/dist/core/aiFlowSupervisor.js.map +0 -1
- package/dist/core/cliTestHarness.d.ts +0 -200
- package/dist/core/cliTestHarness.d.ts.map +0 -1
- package/dist/core/cliTestHarness.js +0 -549
- package/dist/core/cliTestHarness.js.map +0 -1
- package/dist/core/testUtils.d.ts +0 -121
- package/dist/core/testUtils.d.ts.map +0 -1
- package/dist/core/testUtils.js +0 -235
- package/dist/core/testUtils.js.map +0 -1
- package/dist/core/toolValidation.d.ts +0 -116
- package/dist/core/toolValidation.d.ts.map +0 -1
- package/dist/core/toolValidation.js +0 -282
- package/dist/core/toolValidation.js.map +0 -1
- package/dist/ui/planOverlay.d.ts +0 -28
- package/dist/ui/planOverlay.d.ts.map +0 -1
- package/dist/ui/planOverlay.js +0 -156
- package/dist/ui/planOverlay.js.map +0 -1
- package/dist/ui/streamingFormatter.d.ts +0 -30
- package/dist/ui/streamingFormatter.d.ts.map +0 -1
- package/dist/ui/streamingFormatter.js +0 -91
- package/dist/ui/streamingFormatter.js.map +0 -1
- package/dist/utils/errorUtils.d.ts +0 -16
- package/dist/utils/errorUtils.d.ts.map +0 -1
- package/dist/utils/errorUtils.js +0 -66
- package/dist/utils/errorUtils.js.map +0 -1
|
@@ -10,22 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import * as readline from 'node:readline';
|
|
12
12
|
import { EventEmitter } from 'node:events';
|
|
13
|
-
import {
|
|
14
|
-
import { theme } from './theme.js';
|
|
13
|
+
import { theme, spinnerFrames } from './theme.js';
|
|
15
14
|
import { isPlainOutputMode } from './outputMode.js';
|
|
16
|
-
import { renderDivider } from './layout.js';
|
|
17
|
-
import { colorizeActivity, createFrameTicker, formatElapsed, formatTinyProgressBar, formatTokenDelta, } from './animatedStatus.js';
|
|
18
15
|
const ESC = {
|
|
19
|
-
HIDE_CURSOR: '\x1b[?25l',
|
|
20
16
|
SHOW_CURSOR: '\x1b[?25h',
|
|
21
|
-
CLEAR_SCREEN: '\x1b[2J',
|
|
22
17
|
CLEAR_LINE: '\x1b[2K',
|
|
23
|
-
HOME: '\x1b[H',
|
|
24
18
|
ENABLE_BRACKETED_PASTE: '\x1b[?2004h',
|
|
25
19
|
DISABLE_BRACKETED_PASTE: '\x1b[?2004l',
|
|
26
|
-
TO: (row, col) => `\x1b[${row};${col}H`,
|
|
27
|
-
TO_COL: (col) => `\x1b[${col}G`,
|
|
28
|
-
ERASE_DOWN: '\x1b[J',
|
|
29
20
|
REVERSE: '\x1b[7m',
|
|
30
21
|
RESET: '\x1b[0m',
|
|
31
22
|
};
|
|
@@ -37,9 +28,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
37
28
|
rl;
|
|
38
29
|
plainMode;
|
|
39
30
|
interactive;
|
|
40
|
-
rows = 24;
|
|
41
31
|
cols = 80;
|
|
42
|
-
lastRenderWidth = null;
|
|
43
32
|
eventQueue = [];
|
|
44
33
|
isProcessingQueue = false;
|
|
45
34
|
buffer = '';
|
|
@@ -49,37 +38,33 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
49
38
|
suggestions = [];
|
|
50
39
|
suggestionIndex = -1;
|
|
51
40
|
availableCommands = [];
|
|
52
|
-
hotkeysInToggleLine = new Set();
|
|
53
41
|
collapsedPaste = null;
|
|
54
42
|
mode = 'idle';
|
|
43
|
+
streamingStartTime = null;
|
|
55
44
|
statusMessage = null;
|
|
56
45
|
statusOverride = null;
|
|
57
46
|
statusStreaming = null;
|
|
47
|
+
// Animated UI components
|
|
48
|
+
spinnerFrame = 0;
|
|
49
|
+
spinnerInterval = null;
|
|
50
|
+
// Activity/status tracking
|
|
51
|
+
activityMessage = null;
|
|
52
|
+
streamingTokens = 0;
|
|
58
53
|
statusMeta = {};
|
|
59
54
|
toggleState = {
|
|
60
55
|
verificationEnabled: false,
|
|
61
|
-
autoContinueEnabled: false,
|
|
62
56
|
criticalApprovalMode: 'auto',
|
|
63
57
|
};
|
|
64
58
|
// ------------ Helpers ------------
|
|
65
|
-
formatHotkey(combo) {
|
|
66
|
-
if (!combo?.trim())
|
|
67
|
-
return null;
|
|
68
|
-
return combo.trim().toUpperCase();
|
|
69
|
-
}
|
|
70
59
|
lastPromptEvent = null;
|
|
71
60
|
promptHeight = 0;
|
|
72
|
-
lastOverlayHeight = 0;
|
|
73
|
-
lastPromptIndex = 0;
|
|
74
|
-
overlayBottomPadding = 1;
|
|
75
|
-
inlinePanel = [];
|
|
76
|
-
persistentPanel = [];
|
|
77
|
-
overlayInvalidated = false;
|
|
78
|
-
hasConversationContent = false;
|
|
79
61
|
isPromptActive = false;
|
|
62
|
+
inputRenderOffset = 0;
|
|
80
63
|
plainPasteIdleMs = 24;
|
|
81
64
|
plainPasteWindowMs = 60;
|
|
82
65
|
plainPasteTriggerChars = 24;
|
|
66
|
+
cursorVisibleColumn = 1;
|
|
67
|
+
cursorVisibleRow = 0;
|
|
83
68
|
inBracketedPaste = false;
|
|
84
69
|
pasteBuffer = '';
|
|
85
70
|
inPlainPaste = false;
|
|
@@ -90,15 +75,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
90
75
|
plainRecentChunks = [];
|
|
91
76
|
lastRenderedEventKey = null;
|
|
92
77
|
lastOutputEndedWithNewline = true;
|
|
93
|
-
hasRenderedPrompt = false;
|
|
94
|
-
hasEverRenderedOverlay = false; // Track if we've ever rendered to prevent first-render scrollback pollution
|
|
95
|
-
lastOverlay = null;
|
|
96
|
-
allowPromptRender = true;
|
|
97
|
-
streamingStart = null;
|
|
98
|
-
activityInterval = null;
|
|
99
|
-
activityTicker = createFrameTicker('sparkle');
|
|
100
|
-
renderedContextPercent = null;
|
|
101
|
-
lastToolResult = null;
|
|
102
78
|
inputCapture = null;
|
|
103
79
|
constructor(output = process.stdout, input = process.stdin, options) {
|
|
104
80
|
super();
|
|
@@ -116,7 +92,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
116
92
|
this.rl.setPrompt('');
|
|
117
93
|
this.updateTerminalSize();
|
|
118
94
|
this.output.on('resize', () => {
|
|
119
|
-
if (
|
|
95
|
+
if (this.interactive) {
|
|
120
96
|
this.updateTerminalSize();
|
|
121
97
|
this.renderPrompt();
|
|
122
98
|
}
|
|
@@ -127,47 +103,30 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
127
103
|
if (!this.interactive) {
|
|
128
104
|
return;
|
|
129
105
|
}
|
|
130
|
-
|
|
131
|
-
// If an overlay was already rendered before initialization (e.g., banner emitted early),
|
|
132
|
-
// clear it so initialize() doesn't stack a second control bar in scrollback.
|
|
133
|
-
if (this.hasRenderedPrompt || this.lastOverlay) {
|
|
134
|
-
this.clearPromptArea();
|
|
135
|
-
}
|
|
136
|
-
this.write(ESC.ENABLE_BRACKETED_PASTE);
|
|
137
|
-
this.updateTerminalSize();
|
|
138
|
-
this.hasRenderedPrompt = false;
|
|
139
|
-
this.lastOutputEndedWithNewline = true;
|
|
140
|
-
// Don't render prompt immediately - wait for banner/content to be added first.
|
|
141
|
-
// The prompt will render after the event queue processes the welcome banner.
|
|
142
|
-
// This prevents the prompt from appearing at the bottom before the banner shows.
|
|
143
|
-
this.write(ESC.SHOW_CURSOR);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
// Plain mode: minimal setup, still render a simple prompt line
|
|
106
|
+
this.write(ESC.ENABLE_BRACKETED_PASTE);
|
|
147
107
|
this.updateTerminalSize();
|
|
148
|
-
this.hasRenderedPrompt = false;
|
|
149
108
|
this.lastOutputEndedWithNewline = true;
|
|
109
|
+
this.write(ESC.SHOW_CURSOR);
|
|
150
110
|
this.renderPrompt();
|
|
151
111
|
}
|
|
152
112
|
cleanup() {
|
|
153
113
|
this.cancelInputCapture(new Error('Renderer disposed'));
|
|
154
114
|
this.cancelPlainPasteCapture();
|
|
155
|
-
this.
|
|
115
|
+
if (this.spinnerInterval) {
|
|
116
|
+
clearInterval(this.spinnerInterval);
|
|
117
|
+
this.spinnerInterval = null;
|
|
118
|
+
}
|
|
156
119
|
if (!this.interactive) {
|
|
157
120
|
this.rl.close();
|
|
158
121
|
return;
|
|
159
122
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
this.write(ESC.SHOW_CURSOR);
|
|
164
|
-
this.write('\n');
|
|
165
|
-
}
|
|
123
|
+
this.write(ESC.DISABLE_BRACKETED_PASTE);
|
|
124
|
+
this.write(ESC.SHOW_CURSOR);
|
|
125
|
+
this.write('\n');
|
|
166
126
|
if (this.input.isTTY) {
|
|
167
127
|
this.input.setRawMode(false);
|
|
168
128
|
}
|
|
169
129
|
this.rl.close();
|
|
170
|
-
this.lastOverlay = null;
|
|
171
130
|
}
|
|
172
131
|
// ------------ Input handling ------------
|
|
173
132
|
setupInputHandlers() {
|
|
@@ -208,15 +167,30 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
208
167
|
this.emit('toggle-critical-approval');
|
|
209
168
|
return;
|
|
210
169
|
}
|
|
170
|
+
if (key.ctrl && key.shift && key.name?.toLowerCase() === 'n') {
|
|
171
|
+
this.emit('toggle-network');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
211
174
|
if (key.ctrl && key.name === 'c') {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if
|
|
215
|
-
|
|
175
|
+
// Three-stage Ctrl+C behavior:
|
|
176
|
+
// 1. Clear chat box if it has text
|
|
177
|
+
// 2. Interrupt/pause the AI if streaming
|
|
178
|
+
// 3. Quit the CLI if already idle
|
|
179
|
+
if (this.buffer.length > 0) {
|
|
180
|
+
// Stage 1: Clear the input buffer
|
|
181
|
+
this.buffer = '';
|
|
182
|
+
this.cursor = 0;
|
|
183
|
+
this.renderPrompt();
|
|
184
|
+
this.emitInputChange();
|
|
216
185
|
}
|
|
217
186
|
else if (this.mode === 'streaming') {
|
|
187
|
+
// Stage 2: Interrupt the AI run
|
|
218
188
|
this.emit('interrupt');
|
|
219
189
|
}
|
|
190
|
+
else {
|
|
191
|
+
// Stage 3: Quit the CLI (emit exit signal)
|
|
192
|
+
this.emit('exit');
|
|
193
|
+
}
|
|
220
194
|
return;
|
|
221
195
|
}
|
|
222
196
|
if (key.ctrl && key.name === 'd') {
|
|
@@ -225,12 +199,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
225
199
|
}
|
|
226
200
|
return;
|
|
227
201
|
}
|
|
228
|
-
if (key.ctrl && key.name === 'o') {
|
|
229
|
-
if (!this.expandLastToolResult()) {
|
|
230
|
-
this.emit('expand-tool-result');
|
|
231
|
-
}
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
202
|
if (key.ctrl && key.name === 'u') {
|
|
235
203
|
this.clearBuffer();
|
|
236
204
|
return;
|
|
@@ -241,6 +209,11 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
241
209
|
return;
|
|
242
210
|
}
|
|
243
211
|
}
|
|
212
|
+
// Ctrl+O: Expand last tool result
|
|
213
|
+
if (key.ctrl && key.name === 'o') {
|
|
214
|
+
this.emit('expand-tool-result');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
244
217
|
if (key.name === 'return' || key.name === 'enter') {
|
|
245
218
|
if (this.collapsedPaste) {
|
|
246
219
|
this.expandCollapsedPaste();
|
|
@@ -250,9 +223,12 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
250
223
|
// If a slash command suggestion is highlighted, pressing Enter submits it immediately
|
|
251
224
|
if (this.applySuggestion(true))
|
|
252
225
|
return;
|
|
253
|
-
//
|
|
226
|
+
// Fallback: if buffer starts with '/' and suggestions exist, use the selected/first one
|
|
254
227
|
if (this.buffer.startsWith('/') && this.suggestions.length > 0) {
|
|
255
|
-
|
|
228
|
+
const safeIndex = this.suggestionIndex >= 0 && this.suggestionIndex < this.suggestions.length
|
|
229
|
+
? this.suggestionIndex
|
|
230
|
+
: 0;
|
|
231
|
+
this.buffer = this.suggestions[safeIndex]?.command ?? this.buffer;
|
|
256
232
|
}
|
|
257
233
|
this.submitText(this.buffer);
|
|
258
234
|
return;
|
|
@@ -298,18 +274,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
298
274
|
}
|
|
299
275
|
return;
|
|
300
276
|
}
|
|
301
|
-
if (key.name === 'home') {
|
|
302
|
-
this.cursor = 0;
|
|
303
|
-
this.renderPrompt();
|
|
304
|
-
this.emitInputChange();
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
if (key.name === 'end') {
|
|
308
|
-
this.cursor = this.buffer.length;
|
|
309
|
-
this.renderPrompt();
|
|
310
|
-
this.emitInputChange();
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
277
|
if (key.name === 'up') {
|
|
314
278
|
if (this.navigateSuggestions(-1)) {
|
|
315
279
|
return;
|
|
@@ -574,6 +538,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
574
538
|
this.inputCapture = null;
|
|
575
539
|
this.buffer = '';
|
|
576
540
|
this.cursor = 0;
|
|
541
|
+
this.inputRenderOffset = 0;
|
|
577
542
|
this.resetSuggestions();
|
|
578
543
|
this.renderPrompt();
|
|
579
544
|
this.emitInputChange();
|
|
@@ -640,7 +605,11 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
640
605
|
if (!this.buffer.startsWith('/') || this.suggestions.length === 0) {
|
|
641
606
|
return false;
|
|
642
607
|
}
|
|
643
|
-
|
|
608
|
+
// Ensure suggestionIndex is valid, default to first item
|
|
609
|
+
const safeIndex = this.suggestionIndex >= 0 && this.suggestionIndex < this.suggestions.length
|
|
610
|
+
? this.suggestionIndex
|
|
611
|
+
: 0;
|
|
612
|
+
const selected = this.suggestions[safeIndex];
|
|
644
613
|
if (!selected) {
|
|
645
614
|
return false;
|
|
646
615
|
}
|
|
@@ -673,34 +642,13 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
673
642
|
this.renderPrompt();
|
|
674
643
|
return true;
|
|
675
644
|
}
|
|
676
|
-
isGarbageOutput(content) {
|
|
677
|
-
const text = content.replace(/\u001B\[[0-9;]*m/gu, '').replace(/\s+/g, '');
|
|
678
|
-
if (!text)
|
|
679
|
-
return true;
|
|
680
|
-
if (text.length <= 2 && !/[a-zA-Z0-9]/.test(text))
|
|
681
|
-
return true;
|
|
682
|
-
if (text.length < 6 && /^[\.\-_,;:~`'"!@#$%^&*+=|\\/]+$/.test(text))
|
|
683
|
-
return true;
|
|
684
|
-
return false;
|
|
685
|
-
}
|
|
686
645
|
// ------------ Event queue ------------
|
|
687
|
-
addEvent(type, content
|
|
646
|
+
addEvent(type, content) {
|
|
688
647
|
if (!content)
|
|
689
648
|
return;
|
|
690
|
-
if (this.isGarbageOutput(content))
|
|
691
|
-
return;
|
|
692
649
|
const normalized = this.normalizeEventType(type);
|
|
693
650
|
if (!normalized)
|
|
694
651
|
return;
|
|
695
|
-
if (normalized === 'prompt' ||
|
|
696
|
-
normalized === 'response' ||
|
|
697
|
-
normalized === 'thought' ||
|
|
698
|
-
normalized === 'stream' ||
|
|
699
|
-
normalized === 'tool' ||
|
|
700
|
-
normalized === 'build' ||
|
|
701
|
-
normalized === 'test') {
|
|
702
|
-
this.hasConversationContent = true;
|
|
703
|
-
}
|
|
704
652
|
if (this.plainMode) {
|
|
705
653
|
const formatted = this.formatContent({
|
|
706
654
|
type: normalized,
|
|
@@ -720,7 +668,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
720
668
|
rawType: type,
|
|
721
669
|
content,
|
|
722
670
|
timestamp: Date.now(),
|
|
723
|
-
isCompacted: options?.compact === true,
|
|
724
671
|
};
|
|
725
672
|
// Priority queue: prompt events are inserted at the front to ensure immediate display
|
|
726
673
|
// This guarantees user input is echoed before any async processing responses
|
|
@@ -759,23 +706,11 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
759
706
|
const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown renderer error';
|
|
760
707
|
this.output.write(`\n[renderer] ${message}\n`);
|
|
761
708
|
}
|
|
762
|
-
|
|
763
|
-
// This guarantees prompts are visible before async processing continues
|
|
764
|
-
if (event.type === 'prompt') {
|
|
765
|
-
if (this.output.isTTY) {
|
|
766
|
-
this.allowPromptRender = true;
|
|
767
|
-
this.renderPrompt();
|
|
768
|
-
}
|
|
769
|
-
// No delay for prompt events - render immediately
|
|
770
|
-
}
|
|
771
|
-
else {
|
|
709
|
+
if (event.type !== 'prompt') {
|
|
772
710
|
await this.delay(1);
|
|
773
711
|
}
|
|
774
712
|
}
|
|
775
|
-
|
|
776
|
-
// This ensures status/toggles stay pinned and responses are fully rendered
|
|
777
|
-
if (this.output.isTTY) {
|
|
778
|
-
this.allowPromptRender = true;
|
|
713
|
+
if (this.output.isTTY && this.interactive) {
|
|
779
714
|
this.renderPrompt();
|
|
780
715
|
}
|
|
781
716
|
}
|
|
@@ -790,28 +725,18 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
790
725
|
*/
|
|
791
726
|
async flushEvents(timeoutMs = 250) {
|
|
792
727
|
// Kick off processing if idle
|
|
793
|
-
if (!this.
|
|
728
|
+
if (!this.isProcessingQueue && this.eventQueue.length > 0) {
|
|
794
729
|
void this.processQueue();
|
|
795
730
|
}
|
|
796
731
|
const start = Date.now();
|
|
797
732
|
while ((this.isProcessingQueue || this.eventQueue.length > 0) && Date.now() - start < timeoutMs) {
|
|
798
733
|
await this.delay(5);
|
|
799
734
|
}
|
|
800
|
-
if (
|
|
801
|
-
this.allowPromptRender = true;
|
|
735
|
+
if (this.output.isTTY && this.interactive) {
|
|
802
736
|
this.renderPrompt();
|
|
803
737
|
}
|
|
804
738
|
}
|
|
805
739
|
async renderEvent(event) {
|
|
806
|
-
if (this.plainMode) {
|
|
807
|
-
const formattedPlain = this.formatContent(event);
|
|
808
|
-
if (formattedPlain) {
|
|
809
|
-
const text = formattedPlain.endsWith('\n') ? formattedPlain : `${formattedPlain}\n`;
|
|
810
|
-
this.output.write(text);
|
|
811
|
-
this.lastOutputEndedWithNewline = text.endsWith('\n');
|
|
812
|
-
}
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
740
|
const formatted = this.formatContent(event);
|
|
816
741
|
if (!formatted)
|
|
817
742
|
return;
|
|
@@ -824,10 +749,9 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
824
749
|
if (event.type !== 'prompt') {
|
|
825
750
|
this.lastRenderedEventKey = signature;
|
|
826
751
|
}
|
|
827
|
-
if (this.
|
|
828
|
-
this.clearPromptArea();
|
|
752
|
+
if (this.isPromptActive) {
|
|
753
|
+
this.clearPromptArea(true);
|
|
829
754
|
}
|
|
830
|
-
this.isPromptActive = false;
|
|
831
755
|
if (event.type !== 'stream' && !this.lastOutputEndedWithNewline && formatted.trim()) {
|
|
832
756
|
// Keep scrollback ordering predictable when previous output ended mid-line
|
|
833
757
|
this.output.write('\n');
|
|
@@ -835,10 +759,9 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
835
759
|
}
|
|
836
760
|
this.output.write(formatted);
|
|
837
761
|
this.lastOutputEndedWithNewline = formatted.endsWith('\n');
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
// This prevents premature prompt rendering that cuts off responses
|
|
762
|
+
if (this.interactive && !this.plainMode) {
|
|
763
|
+
this.renderPrompt();
|
|
764
|
+
}
|
|
842
765
|
}
|
|
843
766
|
normalizeEventType(type) {
|
|
844
767
|
switch (type) {
|
|
@@ -851,8 +774,9 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
851
774
|
return 'stream';
|
|
852
775
|
case 'tool':
|
|
853
776
|
case 'tool-call':
|
|
854
|
-
case 'tool-result':
|
|
855
777
|
return 'tool';
|
|
778
|
+
case 'tool-result':
|
|
779
|
+
return 'tool-result';
|
|
856
780
|
case 'build':
|
|
857
781
|
return 'build';
|
|
858
782
|
case 'test':
|
|
@@ -868,83 +792,372 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
868
792
|
}
|
|
869
793
|
}
|
|
870
794
|
formatContent(event) {
|
|
871
|
-
const bullet = '⏺';
|
|
795
|
+
const bullet = '⏺'; // Claude Code uses plain bullet, no color
|
|
796
|
+
// Compacted blocks already have separator and formatting
|
|
872
797
|
if (event.isCompacted) {
|
|
873
798
|
return event.content;
|
|
874
799
|
}
|
|
875
800
|
if (event.rawType === 'banner') {
|
|
801
|
+
// Banners display without bullet prefix
|
|
876
802
|
const lines = event.content.split('\n').map(line => line.trimEnd());
|
|
877
803
|
return `${lines.join('\n')}\n`;
|
|
878
804
|
}
|
|
805
|
+
// Compact, user-friendly formatting
|
|
879
806
|
switch (event.type) {
|
|
880
807
|
case 'prompt':
|
|
881
|
-
|
|
808
|
+
// User prompt - just the text (prompt box handles styling)
|
|
809
|
+
return `${theme.primary('>')} ${event.content}\n`;
|
|
882
810
|
case 'thought': {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
if (event.rawType === 'tool-call') {
|
|
887
|
-
return `\n${this.formatBulletBlock(event.content, bullet)}\n`;
|
|
811
|
+
// Programmatic filter: reject content that looks like internal/garbage output
|
|
812
|
+
if (this.isGarbageOutput(event.content)) {
|
|
813
|
+
return '';
|
|
888
814
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
}
|
|
892
|
-
|
|
815
|
+
// Strip any existing bullet prefix (○ or ⏺) and use consistent ⏺
|
|
816
|
+
const cleanContent = event.content.replace(/^[○⏺]\s*/, '');
|
|
817
|
+
return `⏺ ${cleanContent}\n`;
|
|
818
|
+
}
|
|
819
|
+
case 'tool': {
|
|
820
|
+
// Compact tool display: ⚡ToolName → result
|
|
821
|
+
const content = event.content.replace(/^[⏺⚙○]\s*/, '');
|
|
822
|
+
return this.formatCompactToolCall(content);
|
|
823
|
+
}
|
|
824
|
+
case 'tool-result': {
|
|
825
|
+
// Inline result: └─ summary
|
|
826
|
+
return this.formatCompactToolResult(event.content);
|
|
827
|
+
}
|
|
893
828
|
case 'build':
|
|
894
|
-
return
|
|
829
|
+
return `${bullet} ${theme.warning('Build')} ${theme.ui.muted('→')} ${event.content}\n`;
|
|
895
830
|
case 'test':
|
|
896
|
-
return
|
|
831
|
+
return `${bullet} ${theme.info('Test')} ${theme.ui.muted('→')} ${event.content}\n`;
|
|
897
832
|
case 'stream':
|
|
898
833
|
return event.content;
|
|
899
834
|
case 'response':
|
|
900
835
|
default: {
|
|
901
|
-
|
|
836
|
+
// Programmatic filter: reject content that looks like internal/garbage output
|
|
837
|
+
if (this.isGarbageOutput(event.content)) {
|
|
838
|
+
return '';
|
|
839
|
+
}
|
|
840
|
+
// Clean response without excessive bullets
|
|
841
|
+
return this.formatCompactResponse(event.content);
|
|
902
842
|
}
|
|
903
843
|
}
|
|
904
844
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
return `\n${prefix} ${content}\n`;
|
|
845
|
+
/**
|
|
846
|
+
* Programmatic garbage detection - checks if content looks like internal/system output
|
|
847
|
+
* that shouldn't be shown to users. Uses structural checks, not pattern matching.
|
|
848
|
+
*/
|
|
849
|
+
isGarbageOutput(content) {
|
|
850
|
+
if (!content || content.trim().length === 0)
|
|
851
|
+
return true;
|
|
852
|
+
// Structural check: content starting with < that isn't valid markdown/code
|
|
853
|
+
if (content.startsWith('<') && !content.startsWith('<http') && !content.startsWith('<!')) {
|
|
854
|
+
return true;
|
|
916
855
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
.map((line, index) => (index === 0 ? `${prefix} ${line}` : ` ${line}`))
|
|
921
|
-
.join('\n');
|
|
922
|
-
return `\n${body}\n`;
|
|
923
|
-
}
|
|
924
|
-
wrapText(content, width) {
|
|
925
|
-
if (!content) {
|
|
926
|
-
return [''];
|
|
856
|
+
// Structural check: contains "to=functions." or "to=tools." (internal routing)
|
|
857
|
+
if (content.includes('to=functions.') || content.includes('to=tools.')) {
|
|
858
|
+
return true;
|
|
927
859
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
860
|
+
// Structural check: looks like internal instruction (quoted system text)
|
|
861
|
+
if (content.startsWith('"') && content.includes('block') && content.includes('tool')) {
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
// Structural check: very short content that's just timing info
|
|
865
|
+
if (content.length < 30 && /elapsed|seconds?|ms\b/i.test(content)) {
|
|
866
|
+
return true;
|
|
867
|
+
}
|
|
868
|
+
// Structural check: gibberish - high ratio of non-word characters
|
|
869
|
+
const alphaCount = (content.match(/[a-zA-Z]/g) || []).length;
|
|
870
|
+
const totalCount = content.replace(/\s/g, '').length;
|
|
871
|
+
if (totalCount > 20 && alphaCount / totalCount < 0.5) {
|
|
872
|
+
return true; // Less than 50% letters = likely garbage
|
|
873
|
+
}
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Format text in Claude Code style: ⏺ prefix with wrapped continuation lines
|
|
878
|
+
* Example:
|
|
879
|
+
* ⏺ The AI ran tools but gave no response. Need to fix
|
|
880
|
+
* the response handling. Let me check where the AI's
|
|
881
|
+
* text response should be displayed:
|
|
882
|
+
*/
|
|
883
|
+
formatClaudeCodeBlock(content) {
|
|
884
|
+
const bullet = '⏺';
|
|
885
|
+
const maxWidth = Math.min(this.cols - 4, 56); // Leave room for prefix and margins
|
|
886
|
+
const lines = content.split('\n');
|
|
887
|
+
const result = [];
|
|
888
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
889
|
+
const line = lines[lineIdx];
|
|
890
|
+
if (!line.trim()) {
|
|
891
|
+
result.push('');
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
// Word-wrap each line
|
|
895
|
+
const words = line.split(/(\s+)/);
|
|
896
|
+
let currentLine = '';
|
|
933
897
|
for (const word of words) {
|
|
934
|
-
if (
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
current = word.trimStart();
|
|
898
|
+
if ((currentLine + word).length > maxWidth && currentLine.trim()) {
|
|
899
|
+
// First line of this paragraph gets ⏺, rest get indent
|
|
900
|
+
const prefix = result.length === 0 && lineIdx === 0 ? `${bullet} ` : ' ';
|
|
901
|
+
result.push(`${prefix}${currentLine.trimEnd()}`);
|
|
902
|
+
currentLine = word.trimStart();
|
|
940
903
|
}
|
|
941
904
|
else {
|
|
942
|
-
|
|
905
|
+
currentLine += word;
|
|
943
906
|
}
|
|
944
907
|
}
|
|
945
|
-
|
|
908
|
+
if (currentLine.trim()) {
|
|
909
|
+
const prefix = result.length === 0 && lineIdx === 0 ? `${bullet} ` : ' ';
|
|
910
|
+
result.push(`${prefix}${currentLine.trimEnd()}`);
|
|
911
|
+
}
|
|
946
912
|
}
|
|
947
|
-
return
|
|
913
|
+
return result.join('\n') + '\n';
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Format a tool call in Claude Code style:
|
|
917
|
+
* ⏺ Search(pattern: "foo", path: "src",
|
|
918
|
+
* output_mode: "content", head_limit: 30)
|
|
919
|
+
*/
|
|
920
|
+
formatToolCall(content) {
|
|
921
|
+
const bullet = '⏺';
|
|
922
|
+
// Parse tool name and arguments
|
|
923
|
+
const match = content.match(/^(\w+)\((.*)\)$/s);
|
|
924
|
+
if (!match) {
|
|
925
|
+
// Simple format without args
|
|
926
|
+
const nameMatch = content.match(/^(\w+)/);
|
|
927
|
+
if (nameMatch) {
|
|
928
|
+
return `${bullet} ${theme.info(nameMatch[1])}\n`;
|
|
929
|
+
}
|
|
930
|
+
return `${bullet} ${content}\n`;
|
|
931
|
+
}
|
|
932
|
+
const toolName = match[1];
|
|
933
|
+
const argsStr = match[2];
|
|
934
|
+
const maxWidth = Math.min(this.cols - 4, 56);
|
|
935
|
+
// Format: ⏺ ToolName(args...)
|
|
936
|
+
const prefix = `${bullet} ${theme.info(toolName)}(`;
|
|
937
|
+
const prefixLen = toolName.length + 3; // "⏺ ToolName(" visible length
|
|
938
|
+
const indent = ' '.repeat(prefixLen + 4); // Extra indent for wrapped args
|
|
939
|
+
// Parse and format arguments
|
|
940
|
+
const args = this.parseToolArgs(argsStr);
|
|
941
|
+
if (args.length === 0) {
|
|
942
|
+
return `${prefix})\n`;
|
|
943
|
+
}
|
|
944
|
+
const lines = [];
|
|
945
|
+
let currentLine = prefix;
|
|
946
|
+
for (let i = 0; i < args.length; i++) {
|
|
947
|
+
const arg = args[i];
|
|
948
|
+
const argText = `${theme.ui.muted(arg.key + ':')} ${this.formatArgValue(arg.value)}`;
|
|
949
|
+
const separator = i < args.length - 1 ? ', ' : ')';
|
|
950
|
+
// Check if this arg fits on current line
|
|
951
|
+
const testLine = currentLine + argText + separator;
|
|
952
|
+
if (this.stripAnsi(testLine).length > maxWidth && currentLine !== prefix) {
|
|
953
|
+
lines.push(currentLine.trimEnd());
|
|
954
|
+
currentLine = indent + argText + separator;
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
currentLine += argText + separator;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (currentLine.trim()) {
|
|
961
|
+
lines.push(currentLine.trimEnd());
|
|
962
|
+
}
|
|
963
|
+
return lines.join('\n') + '\n';
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Parse tool arguments from string like: key: "value", key2: value2
|
|
967
|
+
*/
|
|
968
|
+
parseToolArgs(argsStr) {
|
|
969
|
+
const args = [];
|
|
970
|
+
// Simple regex to extract key: value pairs
|
|
971
|
+
const regex = /(\w+):\s*("(?:[^"\\]|\\.)*"|[^,\)]+)/g;
|
|
972
|
+
let match;
|
|
973
|
+
while ((match = regex.exec(argsStr)) !== null) {
|
|
974
|
+
args.push({ key: match[1], value: match[2].trim() });
|
|
975
|
+
}
|
|
976
|
+
return args;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Format an argument value (truncate long strings)
|
|
980
|
+
*/
|
|
981
|
+
formatArgValue(value) {
|
|
982
|
+
// Remove surrounding quotes if present
|
|
983
|
+
const isQuoted = value.startsWith('"') && value.endsWith('"');
|
|
984
|
+
const inner = isQuoted ? value.slice(1, -1) : value;
|
|
985
|
+
// Truncate long values
|
|
986
|
+
const maxLen = 40;
|
|
987
|
+
const truncated = inner.length > maxLen ? inner.slice(0, maxLen - 3) + '...' : inner;
|
|
988
|
+
return isQuoted ? `"${truncated}"` : truncated;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Format a tool result in Claude Code style:
|
|
992
|
+
* ⎿ Found 12 lines (ctrl+o to expand)
|
|
993
|
+
*/
|
|
994
|
+
formatToolResult(content) {
|
|
995
|
+
// Check if this is a summary line (e.g., "Found X lines")
|
|
996
|
+
const summaryMatch = content.match(/^(Found \d+ (?:lines?|files?|matches?)|Read \d+ lines?|Wrote \d+ lines?|Edited|Created|Deleted)/i);
|
|
997
|
+
if (summaryMatch) {
|
|
998
|
+
return ` ${theme.ui.muted('⎿')} ${content} ${theme.ui.muted('(ctrl+o to expand)')}\n`;
|
|
999
|
+
}
|
|
1000
|
+
// For other results, show truncated preview
|
|
1001
|
+
const lines = content.split('\n');
|
|
1002
|
+
if (lines.length > 3) {
|
|
1003
|
+
const preview = lines.slice(0, 2).join('\n');
|
|
1004
|
+
return ` ${theme.ui.muted('⎿')} ${preview}\n ${theme.ui.muted(`... ${lines.length - 2} more lines (ctrl+o to expand)`)}\n`;
|
|
1005
|
+
}
|
|
1006
|
+
return ` ${theme.ui.muted('⎿')} ${content}\n`;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Format a compact tool call: ⏺ Read → file.ts
|
|
1010
|
+
*/
|
|
1011
|
+
formatCompactToolCall(content) {
|
|
1012
|
+
const bullet = '⏺';
|
|
1013
|
+
// Parse tool name and args
|
|
1014
|
+
const match = content.match(/^(\w+)\s*(?:\((.*)\))?$/s);
|
|
1015
|
+
if (!match) {
|
|
1016
|
+
return `${bullet} ${content}\n`;
|
|
1017
|
+
}
|
|
1018
|
+
const toolName = match[1];
|
|
1019
|
+
const argsStr = match[2]?.trim() || '';
|
|
1020
|
+
// If no args, just show tool name
|
|
1021
|
+
if (!argsStr) {
|
|
1022
|
+
return `${bullet} ${theme.info(toolName)}\n`;
|
|
1023
|
+
}
|
|
1024
|
+
// Format full params in Claude Code style with line wrapping
|
|
1025
|
+
// For long args, wrap them nicely with continuation indent
|
|
1026
|
+
const prefix = `${bullet} ${theme.info(toolName)}(`;
|
|
1027
|
+
const suffix = ')';
|
|
1028
|
+
const maxWidth = this.cols - 8; // Leave room for margins
|
|
1029
|
+
// Parse individual params
|
|
1030
|
+
const params = this.parseToolParams(argsStr);
|
|
1031
|
+
if (params.length === 0) {
|
|
1032
|
+
return `${prefix}${argsStr}${suffix}\n`;
|
|
1033
|
+
}
|
|
1034
|
+
// Format params with proper wrapping
|
|
1035
|
+
return this.formatToolParams(toolName, params, maxWidth);
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Parse tool params from args string
|
|
1039
|
+
*/
|
|
1040
|
+
parseToolParams(argsStr) {
|
|
1041
|
+
const params = [];
|
|
1042
|
+
// Match key: "value" or key: value patterns
|
|
1043
|
+
const regex = /(\w+):\s*("(?:[^"\\]|\\.)*"|[^,\n]+)/g;
|
|
1044
|
+
let match;
|
|
1045
|
+
while ((match = regex.exec(argsStr)) !== null) {
|
|
1046
|
+
params.push({ key: match[1], value: match[2].trim() });
|
|
1047
|
+
}
|
|
1048
|
+
return params;
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Format tool params in Claude Code style with wrapping
|
|
1052
|
+
*/
|
|
1053
|
+
formatToolParams(toolName, params, maxWidth) {
|
|
1054
|
+
const bullet = '⏺';
|
|
1055
|
+
const lines = [];
|
|
1056
|
+
const indent = ' '; // 8 spaces for continuation
|
|
1057
|
+
let currentLine = `${bullet} ${theme.info(toolName)}(`;
|
|
1058
|
+
let firstParam = true;
|
|
1059
|
+
for (const param of params) {
|
|
1060
|
+
const paramStr = firstParam
|
|
1061
|
+
? `${param.key}: ${param.value}`
|
|
1062
|
+
: `, ${param.key}: ${param.value}`;
|
|
1063
|
+
// Check if adding this param would exceed width
|
|
1064
|
+
const testLine = currentLine + paramStr;
|
|
1065
|
+
const plainLength = testLine.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
1066
|
+
if (plainLength > maxWidth && !firstParam) {
|
|
1067
|
+
// Start new line
|
|
1068
|
+
lines.push(currentLine);
|
|
1069
|
+
currentLine = indent + `${param.key}: ${param.value}`;
|
|
1070
|
+
}
|
|
1071
|
+
else {
|
|
1072
|
+
currentLine += paramStr;
|
|
1073
|
+
}
|
|
1074
|
+
firstParam = false;
|
|
1075
|
+
}
|
|
1076
|
+
currentLine += ')';
|
|
1077
|
+
lines.push(currentLine);
|
|
1078
|
+
return lines.join('\n') + '\n';
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Extract a short summary from tool args
|
|
1082
|
+
*/
|
|
1083
|
+
extractToolSummary(toolName, argsStr) {
|
|
1084
|
+
const tool = toolName.toLowerCase();
|
|
1085
|
+
// Extract path/file for file operations
|
|
1086
|
+
if (['read', 'write', 'edit', 'glob', 'grep', 'search'].includes(tool)) {
|
|
1087
|
+
const pathMatch = argsStr.match(/(?:path|file_path|pattern):\s*"([^"]+)"/);
|
|
1088
|
+
if (pathMatch) {
|
|
1089
|
+
const path = pathMatch[1];
|
|
1090
|
+
// Shorten long paths
|
|
1091
|
+
const short = path.length > 30 ? '…' + path.slice(-28) : path;
|
|
1092
|
+
return theme.ui.muted(short);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
// Extract command for bash
|
|
1096
|
+
if (tool === 'bash') {
|
|
1097
|
+
const cmdMatch = argsStr.match(/command:\s*"([^"]+)"/);
|
|
1098
|
+
if (cmdMatch) {
|
|
1099
|
+
const cmd = cmdMatch[1];
|
|
1100
|
+
const short = cmd.length > 40 ? cmd.slice(0, 37) + '…' : cmd;
|
|
1101
|
+
return theme.ui.muted(short);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Format a compact tool result: ⎿ Found X lines (ctrl+o to expand)
|
|
1108
|
+
*/
|
|
1109
|
+
formatCompactToolResult(content) {
|
|
1110
|
+
// Parse common result patterns for summary
|
|
1111
|
+
const lineMatch = content.match(/(\d+)\s*lines?/i);
|
|
1112
|
+
const fileMatch = content.match(/(\d+)\s*(?:files?|matches?)/i);
|
|
1113
|
+
const readMatch = content.match(/read.*?(\d+)\s*lines?/i);
|
|
1114
|
+
let summary;
|
|
1115
|
+
if (readMatch) {
|
|
1116
|
+
summary = `Read ${readMatch[1]} lines`;
|
|
1117
|
+
}
|
|
1118
|
+
else if (lineMatch) {
|
|
1119
|
+
summary = `Found ${lineMatch[1]} line${lineMatch[1] === '1' ? '' : 's'}`;
|
|
1120
|
+
}
|
|
1121
|
+
else if (fileMatch) {
|
|
1122
|
+
summary = `Found ${fileMatch[1]} file${fileMatch[1] === '1' ? '' : 's'}`;
|
|
1123
|
+
}
|
|
1124
|
+
else if (content.match(/^(success|ok|done|completed|written|edited|created)/i)) {
|
|
1125
|
+
summary = '✓';
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
// Use content directly, truncated if needed
|
|
1129
|
+
summary = content.length > 40 ? content.slice(0, 37) + '…' : content;
|
|
1130
|
+
}
|
|
1131
|
+
return ` ${theme.ui.muted('⎿')} ${summary} ${theme.ui.muted('(ctrl+o to expand)')}\n`;
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Format a compact response with bullet on first line
|
|
1135
|
+
*/
|
|
1136
|
+
formatCompactResponse(content) {
|
|
1137
|
+
const bullet = '⏺';
|
|
1138
|
+
const trimmed = content.trim();
|
|
1139
|
+
if (!trimmed)
|
|
1140
|
+
return '';
|
|
1141
|
+
// Single line responses - bullet prefix
|
|
1142
|
+
if (!trimmed.includes('\n') && trimmed.length < 80) {
|
|
1143
|
+
return `${bullet} ${trimmed}\n`;
|
|
1144
|
+
}
|
|
1145
|
+
// Multi-line: bullet on first, indent continuation
|
|
1146
|
+
const lines = trimmed.split('\n');
|
|
1147
|
+
const result = [];
|
|
1148
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1149
|
+
const line = lines[i].trimEnd();
|
|
1150
|
+
if (!line) {
|
|
1151
|
+
result.push('');
|
|
1152
|
+
}
|
|
1153
|
+
else if (i === 0) {
|
|
1154
|
+
result.push(`${bullet} ${line}`);
|
|
1155
|
+
}
|
|
1156
|
+
else {
|
|
1157
|
+
result.push(` ${line}`);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return result.join('\n') + '\n';
|
|
948
1161
|
}
|
|
949
1162
|
/**
|
|
950
1163
|
* Format a compact conversation block (Claude Code style)
|
|
@@ -981,26 +1194,66 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
981
1194
|
setMode(mode) {
|
|
982
1195
|
const wasStreaming = this.mode === 'streaming';
|
|
983
1196
|
this.mode = mode;
|
|
1197
|
+
// Track streaming start time for elapsed display
|
|
1198
|
+
if (mode === 'streaming' && !wasStreaming) {
|
|
1199
|
+
this.streamingStartTime = Date.now();
|
|
1200
|
+
this.streamingTokens = 0; // Reset token count
|
|
1201
|
+
this.startSpinnerAnimation();
|
|
1202
|
+
}
|
|
1203
|
+
else if (mode === 'idle' && wasStreaming) {
|
|
1204
|
+
this.streamingStartTime = null;
|
|
1205
|
+
this.stopSpinnerAnimation();
|
|
1206
|
+
}
|
|
984
1207
|
if (wasStreaming && mode === 'idle' && !this.lastOutputEndedWithNewline) {
|
|
985
1208
|
// Finish streaming on a fresh line so the next prompt/event doesn't collide
|
|
986
1209
|
this.write('\n');
|
|
987
1210
|
this.lastOutputEndedWithNewline = true;
|
|
988
1211
|
}
|
|
989
|
-
if (
|
|
990
|
-
|
|
991
|
-
this.streamingStart = Date.now();
|
|
992
|
-
}
|
|
993
|
-
this.startActivityTimer();
|
|
1212
|
+
if (this.interactive) {
|
|
1213
|
+
this.renderPrompt();
|
|
994
1214
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Start the animated spinner for streaming status
|
|
1218
|
+
*/
|
|
1219
|
+
startSpinnerAnimation() {
|
|
1220
|
+
if (this.spinnerInterval)
|
|
1221
|
+
return; // Already running
|
|
1222
|
+
this.spinnerFrame = 0;
|
|
1223
|
+
this.spinnerInterval = setInterval(() => {
|
|
1224
|
+
this.spinnerFrame = (this.spinnerFrame + 1) % spinnerFrames.braille.length;
|
|
1225
|
+
if (this.mode === 'streaming') {
|
|
1226
|
+
this.renderPrompt();
|
|
1227
|
+
}
|
|
1228
|
+
}, 120);
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Stop the animated spinner
|
|
1232
|
+
*/
|
|
1233
|
+
stopSpinnerAnimation() {
|
|
1234
|
+
if (this.spinnerInterval) {
|
|
1235
|
+
clearInterval(this.spinnerInterval);
|
|
1236
|
+
this.spinnerInterval = null;
|
|
998
1237
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1238
|
+
this.spinnerFrame = 0;
|
|
1239
|
+
this.activityMessage = null;
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Set the activity message displayed with animated star
|
|
1243
|
+
* Example: "Ruminating…" shows as "✳ Ruminating… (esc to interrupt · 34s · ↑1.2k)"
|
|
1244
|
+
*/
|
|
1245
|
+
setActivity(message) {
|
|
1246
|
+
this.activityMessage = message;
|
|
1247
|
+
if (this.interactive) {
|
|
1001
1248
|
this.renderPrompt();
|
|
1002
1249
|
}
|
|
1003
1250
|
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Update the token count displayed in the activity line
|
|
1253
|
+
*/
|
|
1254
|
+
updateStreamingTokens(tokens) {
|
|
1255
|
+
this.streamingTokens = tokens;
|
|
1256
|
+
}
|
|
1004
1257
|
getMode() {
|
|
1005
1258
|
return this.mode;
|
|
1006
1259
|
}
|
|
@@ -1039,15 +1292,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1039
1292
|
}
|
|
1040
1293
|
}
|
|
1041
1294
|
updateStatusMeta(meta, options = {}) {
|
|
1042
|
-
const next = { ...this.statusMeta };
|
|
1043
|
-
for (const [key, value] of Object.entries(meta)) {
|
|
1044
|
-
if (value === null || value === undefined) {
|
|
1045
|
-
delete next[key];
|
|
1046
|
-
}
|
|
1047
|
-
else {
|
|
1048
|
-
next[key] = value;
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1295
|
+
const next = { ...this.statusMeta, ...meta };
|
|
1051
1296
|
const changed = JSON.stringify(next) !== JSON.stringify(this.statusMeta);
|
|
1052
1297
|
this.statusMeta = next;
|
|
1053
1298
|
const shouldRender = options.render !== false && changed;
|
|
@@ -1057,74 +1302,19 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1057
1302
|
}
|
|
1058
1303
|
updateModeToggles(state) {
|
|
1059
1304
|
this.toggleState = { ...this.toggleState, ...state };
|
|
1060
|
-
if (!state.autoContinueHotkey &&
|
|
1061
|
-
!state.verificationHotkey &&
|
|
1062
|
-
!state.thinkingHotkey &&
|
|
1063
|
-
!state.criticalApprovalHotkey) {
|
|
1064
|
-
this.hotkeysInToggleLine.clear();
|
|
1065
|
-
}
|
|
1066
1305
|
this.renderPrompt();
|
|
1067
1306
|
}
|
|
1068
1307
|
setInlinePanel(lines) {
|
|
1069
1308
|
const normalized = (lines ?? [])
|
|
1070
1309
|
.map(line => line.replace(/\s+$/g, ''))
|
|
1071
1310
|
.filter(line => line.trim().length > 0);
|
|
1072
|
-
if (
|
|
1311
|
+
if (!normalized.length) {
|
|
1073
1312
|
return;
|
|
1074
1313
|
}
|
|
1075
|
-
this.
|
|
1076
|
-
this.renderPrompt();
|
|
1314
|
+
this.addEvent('response', `${normalized.join('\n')}\n`);
|
|
1077
1315
|
}
|
|
1078
1316
|
clearInlinePanel() {
|
|
1079
|
-
|
|
1080
|
-
return;
|
|
1081
|
-
this.inlinePanel = [];
|
|
1082
|
-
this.renderPrompt();
|
|
1083
|
-
}
|
|
1084
|
-
setPersistentPanel(lines) {
|
|
1085
|
-
const normalized = (lines ?? [])
|
|
1086
|
-
.map(line => line.replace(/\s+$/g, ''))
|
|
1087
|
-
.filter(line => line.trim().length > 0);
|
|
1088
|
-
if (JSON.stringify(normalized) === JSON.stringify(this.persistentPanel)) {
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
this.persistentPanel = normalized;
|
|
1092
|
-
this.renderPrompt();
|
|
1093
|
-
}
|
|
1094
|
-
clearPersistentPanel() {
|
|
1095
|
-
if (!this.persistentPanel.length)
|
|
1096
|
-
return;
|
|
1097
|
-
this.persistentPanel = [];
|
|
1098
|
-
this.renderPrompt();
|
|
1099
|
-
}
|
|
1100
|
-
startActivityTimer() {
|
|
1101
|
-
if (this.activityInterval || this.plainMode || !this.interactive) {
|
|
1102
|
-
return;
|
|
1103
|
-
}
|
|
1104
|
-
this.activityInterval = setInterval(() => {
|
|
1105
|
-
if (this.allowPromptRender) {
|
|
1106
|
-
this.renderPrompt();
|
|
1107
|
-
}
|
|
1108
|
-
}, 120);
|
|
1109
|
-
}
|
|
1110
|
-
stopActivityTimer() {
|
|
1111
|
-
if (this.activityInterval) {
|
|
1112
|
-
clearInterval(this.activityInterval);
|
|
1113
|
-
this.activityInterval = null;
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
rememberToolResult(content, summary) {
|
|
1117
|
-
const normalized = content?.trim();
|
|
1118
|
-
if (!normalized)
|
|
1119
|
-
return;
|
|
1120
|
-
this.lastToolResult = { full: normalized, summary: summary?.trim() || undefined };
|
|
1121
|
-
}
|
|
1122
|
-
expandLastToolResult() {
|
|
1123
|
-
if (!this.lastToolResult) {
|
|
1124
|
-
return false;
|
|
1125
|
-
}
|
|
1126
|
-
this.addEvent('tool-result', this.lastToolResult.full);
|
|
1127
|
-
return true;
|
|
1317
|
+
// No-op: inline panels render directly into scrollback
|
|
1128
1318
|
}
|
|
1129
1319
|
// ------------ Prompt rendering ------------
|
|
1130
1320
|
renderPrompt() {
|
|
@@ -1132,343 +1322,203 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1132
1322
|
this.isPromptActive = false;
|
|
1133
1323
|
return;
|
|
1134
1324
|
}
|
|
1135
|
-
if (this.plainMode) {
|
|
1136
|
-
const line = `> ${this.buffer}`;
|
|
1137
|
-
if (!this.isPromptActive && !this.lastOutputEndedWithNewline) {
|
|
1138
|
-
this.write('\n');
|
|
1139
|
-
this.lastOutputEndedWithNewline = true;
|
|
1140
|
-
}
|
|
1141
|
-
this.write(`\r${ESC.CLEAR_LINE}${line}`);
|
|
1142
|
-
this.hasRenderedPrompt = true;
|
|
1143
|
-
this.isPromptActive = true;
|
|
1144
|
-
this.lastOutputEndedWithNewline = false; // prompt ends mid-line by design
|
|
1145
|
-
this.promptHeight = 1;
|
|
1146
|
-
return;
|
|
1147
|
-
}
|
|
1148
|
-
if (!this.allowPromptRender) {
|
|
1149
|
-
return;
|
|
1150
|
-
}
|
|
1151
|
-
if (typeof this.statusMeta.contextPercent === 'number') {
|
|
1152
|
-
this.renderedContextPercent =
|
|
1153
|
-
this.renderedContextPercent === null
|
|
1154
|
-
? this.statusMeta.contextPercent
|
|
1155
|
-
: this.renderedContextPercent +
|
|
1156
|
-
(this.statusMeta.contextPercent - this.renderedContextPercent) * 0.35;
|
|
1157
|
-
}
|
|
1158
|
-
else {
|
|
1159
|
-
this.renderedContextPercent = null;
|
|
1160
|
-
}
|
|
1161
|
-
// Rich mode: inline overlay anchored to current scrollback (no full-screen clear)
|
|
1162
1325
|
this.updateTerminalSize();
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
const overlay = this.buildOverlayLines();
|
|
1170
|
-
if (!overlay.lines.length) {
|
|
1171
|
-
return;
|
|
1172
|
-
}
|
|
1173
|
-
const renderedLines = overlay.lines.map(line => this.truncateLine(line, maxWidth));
|
|
1174
|
-
if (!renderedLines.length) {
|
|
1175
|
-
return;
|
|
1326
|
+
const status = this.composeStatusLabel();
|
|
1327
|
+
const inputLine = this.buildInputLine();
|
|
1328
|
+
const inputLines = inputLine.split('\n');
|
|
1329
|
+
const lines = [];
|
|
1330
|
+
if (status) {
|
|
1331
|
+
lines.push(this.applyTone(status.text, status.tone));
|
|
1176
1332
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
if (height > availableRows) {
|
|
1184
|
-
renderedLines.splice(availableRows);
|
|
1185
|
-
height = renderedLines.length;
|
|
1186
|
-
promptIndex = Math.max(0, Math.min(promptIndex, height - 1));
|
|
1187
|
-
}
|
|
1188
|
-
const startRow = Math.max(1, availableRows - height + 1);
|
|
1189
|
-
const promptRow = startRow + promptIndex;
|
|
1190
|
-
const promptCol = Math.min(Math.max(1, overlay.cursorCol), this.cols || 80);
|
|
1191
|
-
// Clear any previous overlay footprint (status, prompt, controls) to avoid leaking into scrollback
|
|
1192
|
-
this.clearOverlayRows(height, startRow);
|
|
1193
|
-
if (bottomPadding > 0 && startRow + height <= totalRows) {
|
|
1194
|
-
this.write(ESC.TO(startRow + height, 1));
|
|
1195
|
-
this.write(ESC.CLEAR_LINE);
|
|
1333
|
+
lines.push(...inputLines);
|
|
1334
|
+
const hadPrompt = this.isPromptActive;
|
|
1335
|
+
this.clearPromptArea();
|
|
1336
|
+
if (!hadPrompt && !this.lastOutputEndedWithNewline) {
|
|
1337
|
+
this.write('\n');
|
|
1338
|
+
this.lastOutputEndedWithNewline = true;
|
|
1196
1339
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
const row = startRow + idx;
|
|
1200
|
-
const line = renderedLines[idx] ?? '';
|
|
1201
|
-
this.write(ESC.TO(row, 1));
|
|
1340
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1341
|
+
this.write('\r');
|
|
1202
1342
|
this.write(ESC.CLEAR_LINE);
|
|
1203
|
-
|
|
1204
|
-
|
|
1343
|
+
this.write(lines[i] || '');
|
|
1344
|
+
if (i < lines.length - 1) {
|
|
1345
|
+
this.write('\n');
|
|
1205
1346
|
}
|
|
1206
1347
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1348
|
+
const cursorRow = Math.min(lines.length - 1, this.cursorVisibleRow ?? lines.length - 1);
|
|
1349
|
+
const rowsToMoveUp = lines.length - 1 - cursorRow;
|
|
1350
|
+
if (rowsToMoveUp > 0) {
|
|
1351
|
+
this.write(`\x1b[${rowsToMoveUp}A`);
|
|
1352
|
+
}
|
|
1353
|
+
const cursorCol = Math.max(1, this.cursorVisibleColumn);
|
|
1354
|
+
this.write(`\x1b[${cursorCol}G`);
|
|
1211
1355
|
this.isPromptActive = true;
|
|
1212
|
-
this.
|
|
1213
|
-
this.
|
|
1214
|
-
this.lastOverlay = { lines: renderedLines, promptIndex };
|
|
1215
|
-
this.overlayInvalidated = false;
|
|
1216
|
-
this.lastOutputEndedWithNewline = true;
|
|
1217
|
-
this.promptHeight = height;
|
|
1356
|
+
this.promptHeight = lines.length;
|
|
1357
|
+
this.lastOutputEndedWithNewline = false;
|
|
1218
1358
|
}
|
|
1219
|
-
|
|
1220
|
-
const
|
|
1221
|
-
const
|
|
1222
|
-
|
|
1223
|
-
if (activity) {
|
|
1224
|
-
lines.push(activity);
|
|
1225
|
-
}
|
|
1226
|
-
lines.push(this.truncateLine(renderDivider(Math.min(maxWidth, 96)), maxWidth));
|
|
1227
|
-
const inputOverlay = this.buildInputOverlay(maxWidth);
|
|
1228
|
-
const promptRow = lines.length + inputOverlay.cursorRow;
|
|
1229
|
-
const promptCol = inputOverlay.cursorCol;
|
|
1230
|
-
for (const line of inputOverlay.lines) {
|
|
1231
|
-
lines.push(this.truncateLine(line, maxWidth));
|
|
1232
|
-
}
|
|
1233
|
-
lines.push(this.truncateLine(renderDivider(Math.min(maxWidth, 96)), maxWidth));
|
|
1234
|
-
if (this.inlinePanel.length > 0) {
|
|
1235
|
-
for (const panelLine of this.inlinePanel) {
|
|
1236
|
-
lines.push(this.truncateLine(panelLine, maxWidth));
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
if (this.persistentPanel.length > 0) {
|
|
1240
|
-
for (const panelLine of this.persistentPanel) {
|
|
1241
|
-
lines.push(this.truncateLine(panelLine, maxWidth));
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
if (this.suggestions.length > 0) {
|
|
1245
|
-
for (let index = 0; index < this.suggestions.length; index++) {
|
|
1246
|
-
const suggestion = this.suggestions[index];
|
|
1247
|
-
const isActive = index === this.suggestionIndex;
|
|
1248
|
-
const marker = isActive ? theme.primary('›') : theme.ui.muted('›');
|
|
1249
|
-
const cmdText = isActive ? theme.primary(suggestion.command) : theme.ui.muted(suggestion.command);
|
|
1250
|
-
const descText = isActive ? suggestion.description : theme.ui.muted(suggestion.description);
|
|
1251
|
-
lines.push(this.truncateLine(`${marker} ${cmdText} — ${descText}`, maxWidth));
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
const modelLine = this.buildModelLine(maxWidth);
|
|
1255
|
-
if (modelLine) {
|
|
1256
|
-
lines.push(modelLine);
|
|
1257
|
-
}
|
|
1258
|
-
const toggleLine = this.buildToggleLine();
|
|
1259
|
-
if (toggleLine) {
|
|
1260
|
-
lines.push(toggleLine);
|
|
1261
|
-
}
|
|
1262
|
-
const shortcutLine = this.buildShortcutLine();
|
|
1263
|
-
if (shortcutLine) {
|
|
1264
|
-
lines.push(shortcutLine);
|
|
1265
|
-
}
|
|
1266
|
-
else {
|
|
1267
|
-
lines.push(`${theme.ui.muted('?')} shortcuts`);
|
|
1268
|
-
}
|
|
1269
|
-
return { lines, cursorRow: promptRow, cursorCol: promptCol };
|
|
1270
|
-
}
|
|
1271
|
-
abbreviatePath(pathValue) {
|
|
1272
|
-
const home = homedir();
|
|
1273
|
-
if (home && pathValue.startsWith(home)) {
|
|
1274
|
-
return pathValue.replace(home, '~');
|
|
1275
|
-
}
|
|
1276
|
-
return pathValue;
|
|
1277
|
-
}
|
|
1278
|
-
buildActivityLine(maxWidth) {
|
|
1279
|
-
const base = this.statusStreaming || this.statusOverride || this.statusMessage || 'Ready';
|
|
1280
|
-
const spinner = this.mode === 'streaming' ? colorizeActivity(this.activityTicker()) : theme.ui.muted('∙');
|
|
1281
|
-
const elapsed = this.mode === 'streaming' ? formatElapsed(this.streamingStart) : null;
|
|
1282
|
-
const tokenText = this.mode === 'streaming' ? formatTokenDelta(this.statusMeta.tokensUsed) : null;
|
|
1283
|
-
const parts = [
|
|
1284
|
-
`${spinner} ${base}`,
|
|
1285
|
-
elapsed ? theme.ui.muted(elapsed) : null,
|
|
1286
|
-
tokenText ? theme.ui.muted(tokenText) : null,
|
|
1287
|
-
].filter(Boolean);
|
|
1288
|
-
if (!parts.length) {
|
|
1359
|
+
composeStatusLabel() {
|
|
1360
|
+
const statuses = [this.activityMessage, this.statusStreaming, this.statusOverride, this.statusMessage].filter((value, index, arr) => Boolean(value) && arr.indexOf(value) === index);
|
|
1361
|
+
const text = statuses.length > 0 ? statuses.join(' / ') : 'Ready for prompts';
|
|
1362
|
+
if (!text.trim()) {
|
|
1289
1363
|
return null;
|
|
1290
1364
|
}
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
segments.push(`${theme.ui.muted('session')} ${theme.ui.muted(this.statusMeta.sessionLabel)}`);
|
|
1319
|
-
}
|
|
1320
|
-
const ctxTarget = this.renderedContextPercent ?? this.statusMeta.contextPercent;
|
|
1321
|
-
const contextBar = formatTinyProgressBar(ctxTarget, 10);
|
|
1322
|
-
if (contextBar) {
|
|
1323
|
-
segments.push(`${theme.ui.muted('ctx')} ${theme.ui.muted(contextBar)}`);
|
|
1324
|
-
}
|
|
1325
|
-
if (segments.length === 0) {
|
|
1326
|
-
return null;
|
|
1365
|
+
const normalized = text.toLowerCase();
|
|
1366
|
+
const tone = normalized.includes('ready') ? 'success' : 'info';
|
|
1367
|
+
return { text, tone };
|
|
1368
|
+
}
|
|
1369
|
+
formatMetaSegment(label, value, tone) {
|
|
1370
|
+
const colorizer = tone === 'success'
|
|
1371
|
+
? theme.success
|
|
1372
|
+
: tone === 'warn'
|
|
1373
|
+
? theme.warning
|
|
1374
|
+
: tone === 'error'
|
|
1375
|
+
? theme.error
|
|
1376
|
+
: tone === 'muted'
|
|
1377
|
+
? theme.ui.muted
|
|
1378
|
+
: theme.info;
|
|
1379
|
+
return `${theme.ui.muted(label)} ${colorizer(value)}`;
|
|
1380
|
+
}
|
|
1381
|
+
applyTone(text, tone) {
|
|
1382
|
+
switch (tone) {
|
|
1383
|
+
case 'success':
|
|
1384
|
+
return theme.success(text);
|
|
1385
|
+
case 'warn':
|
|
1386
|
+
return theme.warning(text);
|
|
1387
|
+
case 'error':
|
|
1388
|
+
return theme.error(text);
|
|
1389
|
+
case 'info':
|
|
1390
|
+
default:
|
|
1391
|
+
return theme.info(text);
|
|
1327
1392
|
}
|
|
1328
|
-
return this.truncateLine(segments.join(theme.ui.muted(' • ')), maxWidth);
|
|
1329
1393
|
}
|
|
1330
|
-
|
|
1394
|
+
wrapSegments(segments, maxWidth) {
|
|
1331
1395
|
const lines = [];
|
|
1332
|
-
const
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
const shortcutLine = this.buildShortcutLine();
|
|
1337
|
-
if (shortcutLine) {
|
|
1338
|
-
lines.push(`${theme.ui.muted('keys')} ${shortcutLine}`);
|
|
1339
|
-
}
|
|
1340
|
-
return lines;
|
|
1341
|
-
}
|
|
1342
|
-
buildToggleLine() {
|
|
1343
|
-
const toggles = [];
|
|
1344
|
-
const addToggle = (label, on, hotkey, value) => {
|
|
1345
|
-
toggles.push({ label, on, hotkey: this.formatHotkey(hotkey), value });
|
|
1346
|
-
};
|
|
1347
|
-
addToggle('Auto', this.toggleState.autoContinueEnabled, this.toggleState.autoContinueHotkey);
|
|
1348
|
-
addToggle('Verify', this.toggleState.verificationEnabled, this.toggleState.verificationHotkey);
|
|
1349
|
-
const approvalMode = this.toggleState.criticalApprovalMode || 'auto';
|
|
1350
|
-
const approvalActive = approvalMode !== 'auto';
|
|
1351
|
-
addToggle('Approvals', approvalActive, this.toggleState.criticalApprovalHotkey, approvalMode === 'auto' ? 'auto' : 'ask');
|
|
1352
|
-
const thinkingLabel = (this.toggleState.thinkingModeLabel || 'off').trim();
|
|
1353
|
-
const thinkingActive = thinkingLabel.toLowerCase() !== 'off';
|
|
1354
|
-
addToggle('Thinking', thinkingActive, this.toggleState.thinkingHotkey, thinkingLabel);
|
|
1355
|
-
const buildLine = (includeHotkeys) => {
|
|
1356
|
-
return toggles
|
|
1357
|
-
.map(toggle => {
|
|
1358
|
-
const stateText = toggle.on ? theme.success(toggle.value || 'on') : theme.ui.muted(toggle.value || 'off');
|
|
1359
|
-
const hotkeyText = includeHotkeys && toggle.hotkey ? theme.ui.muted(` [${toggle.hotkey}]`) : '';
|
|
1360
|
-
return `${theme.ui.muted(`${toggle.label}:`)} ${stateText}${hotkeyText}`;
|
|
1361
|
-
})
|
|
1362
|
-
.join(theme.ui.muted(' '));
|
|
1363
|
-
};
|
|
1364
|
-
const maxWidth = this.safeWidth();
|
|
1365
|
-
let line = buildLine(true);
|
|
1366
|
-
// Record which hotkeys are actually shown so the shortcut line can avoid duplicates
|
|
1367
|
-
this.hotkeysInToggleLine = new Set(toggles
|
|
1368
|
-
.map(toggle => (toggle.hotkey ? toggle.hotkey : null))
|
|
1369
|
-
.filter((key) => Boolean(key)));
|
|
1370
|
-
// If the line is too wide, drop hotkey hints to preserve all toggle labels
|
|
1371
|
-
if (this.visibleLength(line) > maxWidth) {
|
|
1372
|
-
this.hotkeysInToggleLine.clear();
|
|
1373
|
-
line = buildLine(false);
|
|
1374
|
-
}
|
|
1375
|
-
return line.trim() ? line : null;
|
|
1376
|
-
}
|
|
1377
|
-
buildShortcutLine() {
|
|
1378
|
-
const parts = [];
|
|
1379
|
-
const addHotkey = (label, combo) => {
|
|
1380
|
-
const normalized = this.formatHotkey(combo);
|
|
1396
|
+
const separator = theme.ui.muted(' | ');
|
|
1397
|
+
let current = '';
|
|
1398
|
+
for (const segment of segments) {
|
|
1399
|
+
const normalized = segment.trim();
|
|
1381
1400
|
if (!normalized)
|
|
1382
|
-
|
|
1383
|
-
if (
|
|
1384
|
-
|
|
1401
|
+
continue;
|
|
1402
|
+
if (!current) {
|
|
1403
|
+
current = this.truncateLine(normalized, maxWidth);
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
const candidate = `${current}${separator}${normalized}`;
|
|
1407
|
+
if (this.visibleLength(candidate) <= maxWidth) {
|
|
1408
|
+
current = candidate;
|
|
1409
|
+
}
|
|
1410
|
+
else {
|
|
1411
|
+
lines.push(this.truncateLine(current, maxWidth));
|
|
1412
|
+
current = this.truncateLine(normalized, maxWidth);
|
|
1385
1413
|
}
|
|
1386
|
-
parts.push(`${theme.info(normalized)} ${theme.ui.muted(label)}`);
|
|
1387
|
-
};
|
|
1388
|
-
// Core controls
|
|
1389
|
-
addHotkey('interrupt', 'Ctrl+C');
|
|
1390
|
-
addHotkey('clear input', 'Ctrl+U');
|
|
1391
|
-
// Feature toggles (only if hotkeys are defined)
|
|
1392
|
-
addHotkey('auto-run', this.toggleState.autoContinueHotkey);
|
|
1393
|
-
addHotkey('verify', this.toggleState.verificationHotkey);
|
|
1394
|
-
addHotkey('thinking', this.toggleState.thinkingHotkey);
|
|
1395
|
-
if (parts.length === 0) {
|
|
1396
|
-
return null;
|
|
1397
1414
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1415
|
+
if (current) {
|
|
1416
|
+
lines.push(this.truncateLine(current, maxWidth));
|
|
1417
|
+
}
|
|
1418
|
+
return lines;
|
|
1400
1419
|
}
|
|
1401
|
-
|
|
1402
|
-
const prompt = theme.primary('› ');
|
|
1403
|
-
const promptWidth = this.visibleLength(prompt);
|
|
1404
|
-
const usableWidth = Math.max(8, maxWidth - promptWidth);
|
|
1420
|
+
buildInputLine() {
|
|
1405
1421
|
if (this.collapsedPaste) {
|
|
1406
1422
|
const summary = `[pasted ${this.collapsedPaste.lines} lines, ${this.collapsedPaste.chars} chars] (Ctrl+L insert, Backspace discard)`;
|
|
1407
|
-
|
|
1408
|
-
const cursorCol = Math.min(maxWidth, this.visibleLength(line) + 1);
|
|
1409
|
-
return { lines: [this.truncateLine(line, maxWidth)], cursorRow: 0, cursorCol };
|
|
1423
|
+
return this.truncateLine(`${theme.primary('> ')}${theme.ui.muted(summary)}`, this.safeWidth());
|
|
1410
1424
|
}
|
|
1425
|
+
// Claude Code uses simple '>' prompt
|
|
1426
|
+
const prompt = theme.primary('> ');
|
|
1427
|
+
const promptWidth = this.visibleLength(prompt);
|
|
1428
|
+
const maxWidth = this.safeWidth();
|
|
1429
|
+
const continuationIndent = ' '; // 2 spaces for continuation lines
|
|
1430
|
+
const continuationWidth = continuationIndent.length;
|
|
1431
|
+
// Handle multi-line input - split buffer on newlines first
|
|
1411
1432
|
const normalized = this.buffer.replace(/\r/g, '\n');
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
let
|
|
1416
|
-
|
|
1417
|
-
let cursorRow = 0;
|
|
1433
|
+
const bufferLines = normalized.split('\n');
|
|
1434
|
+
// Wrap each logical line to fit terminal width, expanding vertically
|
|
1435
|
+
const result = [];
|
|
1436
|
+
let totalChars = 0;
|
|
1437
|
+
let cursorLine = 0;
|
|
1418
1438
|
let cursorCol = 0;
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1439
|
+
let foundCursor = false;
|
|
1440
|
+
for (let lineIndex = 0; lineIndex < bufferLines.length; lineIndex++) {
|
|
1441
|
+
const line = bufferLines[lineIndex] ?? '';
|
|
1442
|
+
const isFirstLogicalLine = lineIndex === 0;
|
|
1443
|
+
const lineStartChar = totalChars;
|
|
1444
|
+
// Determine available width for this line
|
|
1445
|
+
const firstLineWidth = maxWidth - promptWidth;
|
|
1446
|
+
const contLineWidth = maxWidth - continuationWidth;
|
|
1447
|
+
// Wrap this logical line into display lines
|
|
1448
|
+
let remaining = line;
|
|
1449
|
+
let isFirstDisplayLine = true;
|
|
1450
|
+
while (remaining.length > 0 || isFirstDisplayLine) {
|
|
1451
|
+
const availableWidth = (isFirstLogicalLine && isFirstDisplayLine) ? firstLineWidth : contLineWidth;
|
|
1452
|
+
const chunk = remaining.slice(0, availableWidth);
|
|
1453
|
+
remaining = remaining.slice(availableWidth);
|
|
1454
|
+
// Build the display line
|
|
1455
|
+
let displayLine;
|
|
1456
|
+
if (isFirstLogicalLine && isFirstDisplayLine) {
|
|
1457
|
+
displayLine = `${prompt}${chunk}`;
|
|
1458
|
+
}
|
|
1459
|
+
else {
|
|
1460
|
+
displayLine = `${continuationIndent}${chunk}`;
|
|
1461
|
+
}
|
|
1462
|
+
// Track cursor position
|
|
1463
|
+
if (!foundCursor) {
|
|
1464
|
+
const chunkStart = lineStartChar + (line.length - remaining.length - chunk.length);
|
|
1465
|
+
const chunkEnd = chunkStart + chunk.length;
|
|
1466
|
+
if (this.cursor >= chunkStart && this.cursor <= chunkEnd) {
|
|
1467
|
+
cursorLine = result.length;
|
|
1468
|
+
const offsetInChunk = this.cursor - chunkStart;
|
|
1469
|
+
cursorCol = ((isFirstLogicalLine && isFirstDisplayLine) ? promptWidth : continuationWidth) + offsetInChunk;
|
|
1470
|
+
foundCursor = true;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
result.push(displayLine);
|
|
1474
|
+
isFirstDisplayLine = false;
|
|
1475
|
+
// If nothing left and this was an empty line, we already added it
|
|
1476
|
+
if (remaining.length === 0 && chunk.length === 0)
|
|
1477
|
+
break;
|
|
1440
1478
|
}
|
|
1441
|
-
|
|
1479
|
+
totalChars += line.length + 1; // +1 for the newline separator
|
|
1442
1480
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1481
|
+
// Handle cursor at very end
|
|
1482
|
+
if (!foundCursor) {
|
|
1483
|
+
cursorLine = Math.max(0, result.length - 1);
|
|
1484
|
+
const lastLine = result[cursorLine] ?? '';
|
|
1485
|
+
cursorCol = this.visibleLength(lastLine);
|
|
1445
1486
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1487
|
+
this.cursorVisibleRow = cursorLine;
|
|
1488
|
+
this.cursorVisibleColumn = cursorCol + 1;
|
|
1489
|
+
return result.join('\n');
|
|
1490
|
+
}
|
|
1491
|
+
buildInputWindow(available) {
|
|
1492
|
+
if (available <= 0) {
|
|
1493
|
+
return { text: '', cursor: 0 };
|
|
1449
1494
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1495
|
+
if (this.collapsedPaste) {
|
|
1496
|
+
return { text: '', cursor: 0 };
|
|
1497
|
+
}
|
|
1498
|
+
const normalized = this.buffer.replace(/\r/g, '\n');
|
|
1499
|
+
const cursorIndex = Math.min(this.cursor, normalized.length);
|
|
1500
|
+
let offset = this.inputRenderOffset;
|
|
1501
|
+
if (cursorIndex < offset) {
|
|
1502
|
+
offset = cursorIndex;
|
|
1503
|
+
}
|
|
1504
|
+
const overflow = cursorIndex - offset - available + 1;
|
|
1505
|
+
if (overflow > 0) {
|
|
1506
|
+
offset += overflow;
|
|
1507
|
+
}
|
|
1508
|
+
const maxOffset = Math.max(0, normalized.length - available);
|
|
1509
|
+
if (offset > maxOffset) {
|
|
1510
|
+
offset = maxOffset;
|
|
1511
|
+
}
|
|
1512
|
+
this.inputRenderOffset = offset;
|
|
1513
|
+
const window = normalized.slice(offset, offset + available);
|
|
1514
|
+
const display = window.split('').map(char => (char === '\n' ? NEWLINE_PLACEHOLDER : char)).join('');
|
|
1515
|
+
const cursorInWindow = Math.min(display.length, Math.max(0, cursorIndex - offset));
|
|
1516
|
+
const before = display.slice(0, cursorInWindow);
|
|
1517
|
+
const at = display.charAt(cursorInWindow) || ' ';
|
|
1518
|
+
const after = display.slice(cursorInWindow + 1);
|
|
1468
1519
|
return {
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
cursorCol: cursorColumn,
|
|
1520
|
+
text: `${before}${ESC.REVERSE}${at}${ESC.RESET}${after}`,
|
|
1521
|
+
cursor: cursorInWindow,
|
|
1472
1522
|
};
|
|
1473
1523
|
}
|
|
1474
1524
|
expandCollapsedPaste() {
|
|
@@ -1488,6 +1538,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1488
1538
|
if (options.resetBuffer) {
|
|
1489
1539
|
this.buffer = '';
|
|
1490
1540
|
this.cursor = 0;
|
|
1541
|
+
this.inputRenderOffset = 0;
|
|
1491
1542
|
this.resetSuggestions();
|
|
1492
1543
|
this.renderPrompt();
|
|
1493
1544
|
this.emitInputChange();
|
|
@@ -1566,17 +1617,6 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1566
1617
|
}
|
|
1567
1618
|
return result;
|
|
1568
1619
|
}
|
|
1569
|
-
clearOverlayRows(rows, startRow) {
|
|
1570
|
-
const totalRows = this.rows || 24;
|
|
1571
|
-
const limit = Math.max(0, Math.min(rows, totalRows));
|
|
1572
|
-
for (let idx = 0; idx < limit; idx++) {
|
|
1573
|
-
const row = startRow + idx;
|
|
1574
|
-
if (row < 1 || row > totalRows)
|
|
1575
|
-
continue;
|
|
1576
|
-
this.write(ESC.TO(row, 1));
|
|
1577
|
-
this.write(ESC.CLEAR_LINE);
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
1620
|
getBuffer() {
|
|
1581
1621
|
return this.buffer;
|
|
1582
1622
|
}
|
|
@@ -1586,6 +1626,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1586
1626
|
setBuffer(text, cursorPos) {
|
|
1587
1627
|
this.buffer = text;
|
|
1588
1628
|
this.cursor = cursorPos ?? text.length;
|
|
1629
|
+
this.inputRenderOffset = 0;
|
|
1589
1630
|
this.updateSuggestions();
|
|
1590
1631
|
this.renderPrompt();
|
|
1591
1632
|
this.emitInputChange();
|
|
@@ -1594,6 +1635,7 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1594
1635
|
this.cancelPlainPasteCapture();
|
|
1595
1636
|
this.buffer = '';
|
|
1596
1637
|
this.cursor = 0;
|
|
1638
|
+
this.inputRenderOffset = 0;
|
|
1597
1639
|
this.suggestions = [];
|
|
1598
1640
|
this.suggestionIndex = -1;
|
|
1599
1641
|
this.renderPrompt();
|
|
@@ -1602,6 +1644,31 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1602
1644
|
setModeStatus(status) {
|
|
1603
1645
|
this.updateStatus(status);
|
|
1604
1646
|
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Show a compacting status with animated spinner (Claude Code style)
|
|
1649
|
+
* Uses ✻ character with animation to indicate context compaction in progress
|
|
1650
|
+
*/
|
|
1651
|
+
showCompactingStatus(message) {
|
|
1652
|
+
this.statusMessage = message;
|
|
1653
|
+
if (!this.spinnerInterval) {
|
|
1654
|
+
this.spinnerInterval = setInterval(() => {
|
|
1655
|
+
this.spinnerFrame++;
|
|
1656
|
+
this.renderPrompt();
|
|
1657
|
+
}, 120);
|
|
1658
|
+
}
|
|
1659
|
+
this.renderPrompt();
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Hide the compacting status and stop spinner animation
|
|
1663
|
+
*/
|
|
1664
|
+
hideCompactingStatus() {
|
|
1665
|
+
if (this.spinnerInterval) {
|
|
1666
|
+
clearInterval(this.spinnerInterval);
|
|
1667
|
+
this.spinnerInterval = null;
|
|
1668
|
+
}
|
|
1669
|
+
this.statusMessage = null;
|
|
1670
|
+
this.renderPrompt();
|
|
1671
|
+
}
|
|
1605
1672
|
emitPrompt(content) {
|
|
1606
1673
|
this.pushPromptEvent(content);
|
|
1607
1674
|
}
|
|
@@ -1641,31 +1708,34 @@ export class UnifiedUIRenderer extends EventEmitter {
|
|
|
1641
1708
|
this.lastPromptEvent = { text: normalized, at: now };
|
|
1642
1709
|
this.addEvent('prompt', normalized);
|
|
1643
1710
|
}
|
|
1644
|
-
clearPromptArea() {
|
|
1645
|
-
|
|
1646
|
-
|
|
1711
|
+
clearPromptArea(insertNewline = false) {
|
|
1712
|
+
if (!this.isPromptActive || this.promptHeight <= 0) {
|
|
1713
|
+
if (insertNewline && !this.lastOutputEndedWithNewline) {
|
|
1714
|
+
this.write('\n');
|
|
1715
|
+
this.lastOutputEndedWithNewline = true;
|
|
1716
|
+
}
|
|
1647
1717
|
return;
|
|
1648
|
-
this.updateTerminalSize();
|
|
1649
|
-
const totalRows = this.rows || 24;
|
|
1650
|
-
const startRow = Math.max(1, Math.max(1, totalRows - this.overlayBottomPadding) - height + 1);
|
|
1651
|
-
this.clearOverlayRows(height, startRow);
|
|
1652
|
-
// Keep the padding row clean as well
|
|
1653
|
-
const paddingRow = startRow + height;
|
|
1654
|
-
if (this.overlayBottomPadding > 0 && paddingRow <= totalRows) {
|
|
1655
|
-
this.write(ESC.TO(paddingRow, 1));
|
|
1656
|
-
this.write(ESC.CLEAR_LINE);
|
|
1657
1718
|
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1719
|
+
if (this.promptHeight > 1) {
|
|
1720
|
+
readline.moveCursor(this.output, 0, -(this.promptHeight - 1));
|
|
1721
|
+
}
|
|
1722
|
+
for (let i = 0; i < this.promptHeight; i++) {
|
|
1723
|
+
readline.cursorTo(this.output, 0);
|
|
1724
|
+
readline.clearLine(this.output, 0);
|
|
1725
|
+
if (i < this.promptHeight - 1) {
|
|
1726
|
+
readline.moveCursor(this.output, 0, 1);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
readline.cursorTo(this.output, 0);
|
|
1730
|
+
if (insertNewline) {
|
|
1731
|
+
this.write('\n');
|
|
1732
|
+
this.lastOutputEndedWithNewline = true;
|
|
1733
|
+
}
|
|
1664
1734
|
this.promptHeight = 0;
|
|
1735
|
+
this.isPromptActive = false;
|
|
1665
1736
|
}
|
|
1666
1737
|
updateTerminalSize() {
|
|
1667
1738
|
if (this.output.isTTY) {
|
|
1668
|
-
this.rows = this.output.rows || 24;
|
|
1669
1739
|
this.cols = this.output.columns || 80;
|
|
1670
1740
|
}
|
|
1671
1741
|
}
|