@ebowwa/coder 0.7.64 → 0.7.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +36168 -32
- package/dist/interfaces/ui/terminal/cli/index.js +34253 -158
- package/dist/interfaces/ui/terminal/native/README.md +53 -0
- package/dist/interfaces/ui/terminal/native/claude_code_native.darwin-x64.node +0 -0
- package/dist/interfaces/ui/terminal/native/claude_code_native.dylib +0 -0
- package/dist/interfaces/ui/terminal/native/index.d.ts +0 -0
- package/dist/interfaces/ui/terminal/native/index.darwin-arm64.node +0 -0
- package/dist/interfaces/ui/terminal/native/index.js +43 -0
- package/dist/interfaces/ui/terminal/native/index.node +0 -0
- package/dist/interfaces/ui/terminal/native/package.json +34 -0
- package/dist/native/README.md +53 -0
- package/dist/native/claude_code_native.darwin-x64.node +0 -0
- package/dist/native/claude_code_native.dylib +0 -0
- package/dist/native/index.d.ts +0 -480
- package/dist/native/index.darwin-arm64.node +0 -0
- package/dist/native/index.js +43 -1625
- package/dist/native/index.node +0 -0
- package/dist/native/package.json +34 -0
- package/native/index.darwin-arm64.node +0 -0
- package/native/index.js +33 -19
- package/package.json +3 -2
- package/packages/src/core/agent-loop/__tests__/compaction.test.ts +17 -14
- package/packages/src/core/agent-loop/compaction.ts +6 -2
- package/packages/src/core/agent-loop/index.ts +2 -0
- package/packages/src/core/agent-loop/loop-state.ts +1 -1
- package/packages/src/core/agent-loop/turn-executor.ts +4 -0
- package/packages/src/core/agent-loop/types.ts +4 -0
- package/packages/src/core/api-client-impl.ts +283 -173
- package/packages/src/core/cognitive-security/hooks.ts +2 -1
- package/packages/src/core/config/todo +7 -0
- package/packages/src/core/context/__tests__/integration.test.ts +334 -0
- package/packages/src/core/context/compaction.ts +170 -0
- package/packages/src/core/context/constants.ts +58 -0
- package/packages/src/core/context/extraction.ts +85 -0
- package/packages/src/core/context/index.ts +66 -0
- package/packages/src/core/context/summarization.ts +251 -0
- package/packages/src/core/context/token-estimation.ts +98 -0
- package/packages/src/core/context/types.ts +59 -0
- package/packages/src/core/models.ts +81 -4
- package/packages/src/core/normalizers/todo +5 -1
- package/packages/src/core/providers/README.md +230 -0
- package/packages/src/core/providers/__tests__/providers.test.ts +135 -0
- package/packages/src/core/providers/index.ts +419 -0
- package/packages/src/core/providers/types.ts +132 -0
- package/packages/src/core/retry.ts +10 -0
- package/packages/src/ecosystem/tools/index.ts +174 -0
- package/packages/src/index.ts +23 -2
- package/packages/src/interfaces/ui/index.ts +17 -20
- package/packages/src/interfaces/ui/spinner.ts +2 -2
- package/packages/src/interfaces/ui/terminal/bridge/index.ts +370 -0
- package/packages/src/interfaces/ui/terminal/bridge/ipc.ts +829 -0
- package/packages/src/interfaces/ui/terminal/bridge/screen-export.ts +968 -0
- package/packages/src/interfaces/ui/terminal/bridge/types.ts +226 -0
- package/packages/src/interfaces/ui/terminal/bridge/useBridge.ts +210 -0
- package/packages/src/interfaces/ui/terminal/cli/bootstrap.ts +132 -0
- package/packages/src/interfaces/ui/terminal/cli/index.ts +200 -13
- package/packages/src/interfaces/ui/terminal/cli/interactive/index.ts +110 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/input-handler.ts +393 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/interactive-runner.ts +820 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/message-store.ts +299 -0
- package/packages/src/interfaces/ui/terminal/cli/interactive/types.ts +274 -0
- package/packages/src/interfaces/ui/terminal/shared/index.ts +13 -0
- package/packages/src/interfaces/ui/terminal/shared/query.ts +9 -3
- package/packages/src/interfaces/ui/terminal/shared/setup.ts +5 -1
- package/packages/src/interfaces/ui/terminal/shared/spinner-frames.ts +73 -0
- package/packages/src/interfaces/ui/terminal/shared/status-line.ts +10 -2
- package/packages/src/native/index.ts +404 -27
- package/packages/src/native/tui_v2_types.ts +39 -0
- package/packages/src/teammates/coordination.test.ts +279 -0
- package/packages/src/teammates/coordination.ts +646 -0
- package/packages/src/teammates/index.ts +95 -25
- package/packages/src/teammates/integration.test.ts +272 -0
- package/packages/src/teammates/runner.test.ts +235 -0
- package/packages/src/teammates/runner.ts +750 -0
- package/packages/src/teammates/schemas.ts +673 -0
- package/packages/src/types/index.ts +1 -0
- package/packages/src/core/context-compaction.ts +0 -578
- package/packages/src/interfaces/ui/Screenshot 2026-03-02 at 9.23.10/342/200/257PM.png +0 -0
- package/packages/src/interfaces/ui/Screenshot 2026-03-03 at 10.55.11/342/200/257AM.png +0 -0
- package/packages/src/interfaces/ui/terminal/tui/HelpPanel.tsx +0 -262
- package/packages/src/interfaces/ui/terminal/tui/InputContext.tsx +0 -232
- package/packages/src/interfaces/ui/terminal/tui/InputField.tsx +0 -62
- package/packages/src/interfaces/ui/terminal/tui/InteractiveTUI.tsx +0 -537
- package/packages/src/interfaces/ui/terminal/tui/MessageArea.tsx +0 -107
- package/packages/src/interfaces/ui/terminal/tui/MessageStore.tsx +0 -240
- package/packages/src/interfaces/ui/terminal/tui/StatusBar.tsx +0 -54
- package/packages/src/interfaces/ui/terminal/tui/commands.ts +0 -438
- package/packages/src/interfaces/ui/terminal/tui/components/InteractiveElements.tsx +0 -584
- package/packages/src/interfaces/ui/terminal/tui/components/MultilineInput.tsx +0 -614
- package/packages/src/interfaces/ui/terminal/tui/components/PaneManager.tsx +0 -333
- package/packages/src/interfaces/ui/terminal/tui/components/Sidebar.tsx +0 -604
- package/packages/src/interfaces/ui/terminal/tui/components/index.ts +0 -118
- package/packages/src/interfaces/ui/terminal/tui/console.ts +0 -49
- package/packages/src/interfaces/ui/terminal/tui/index.ts +0 -90
- package/packages/src/interfaces/ui/terminal/tui/run.tsx +0 -42
- package/packages/src/interfaces/ui/terminal/tui/spinner.ts +0 -69
- package/packages/src/interfaces/ui/terminal/tui/tui-app.tsx +0 -390
- package/packages/src/interfaces/ui/terminal/tui/tui-footer.ts +0 -422
- package/packages/src/interfaces/ui/terminal/tui/types.ts +0 -186
- package/packages/src/interfaces/ui/terminal/tui/useInputHandler.ts +0 -104
- package/packages/src/interfaces/ui/terminal/tui/useNativeInput.ts +0 -239
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive Runner - Main Interactive CLI Loop
|
|
3
|
+
*
|
|
4
|
+
* Non-React implementation of the interactive CLI mode.
|
|
5
|
+
* Extracts the core patterns from v1 TUI without the Ink dependency.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Native terminal input handling
|
|
9
|
+
* - Message state management via MessageStore
|
|
10
|
+
* - Agent loop integration for AI responses
|
|
11
|
+
* - Command handling
|
|
12
|
+
* - History navigation
|
|
13
|
+
* - Loading states with spinner
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import process from "node:process";
|
|
17
|
+
import type { Message as ApiMessage } from "../../../../../types/index.js";
|
|
18
|
+
import { agentLoop } from "../../../../../core/agent-loop.js";
|
|
19
|
+
import { getGitStatus } from "../../../../../core/git-status.js";
|
|
20
|
+
import { createStreamHighlighter } from "../../../../../core/stream-highlighter.js";
|
|
21
|
+
import { NativeRenderer } from "../../../../../native/index.js";
|
|
22
|
+
import type { InputEvent, NativeRendererType } from "../../../../../native/index.js";
|
|
23
|
+
import { spinnerFrames } from "../../shared/spinner-frames.js";
|
|
24
|
+
import { MessageStoreImpl } from "./message-store.js";
|
|
25
|
+
import { InputManagerImpl, KeyEvents, inputEventToNativeKeyEvent } from "./input-handler.js";
|
|
26
|
+
import type {
|
|
27
|
+
InteractiveRunnerProps,
|
|
28
|
+
InteractiveState,
|
|
29
|
+
NativeKeyEvent,
|
|
30
|
+
} from "./types.js";
|
|
31
|
+
import { InputPriority } from "./types.js";
|
|
32
|
+
|
|
33
|
+
// ============================================
|
|
34
|
+
// STATE MANAGEMENT
|
|
35
|
+
// ============================================
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create initial interactive state
|
|
39
|
+
*/
|
|
40
|
+
function createInitialState(): InteractiveState {
|
|
41
|
+
return {
|
|
42
|
+
isLoading: false,
|
|
43
|
+
inputValue: "",
|
|
44
|
+
cursorPos: 0,
|
|
45
|
+
scrollOffset: 0,
|
|
46
|
+
totalCost: 0,
|
|
47
|
+
spinnerFrame: spinnerFrames[0] ?? "⠋",
|
|
48
|
+
streamingText: "",
|
|
49
|
+
inputHistory: [],
|
|
50
|
+
historyIndex: -1,
|
|
51
|
+
sessionSelectMode: false,
|
|
52
|
+
selectableSessions: [],
|
|
53
|
+
helpMode: false,
|
|
54
|
+
helpSection: 0,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============================================
|
|
59
|
+
// INTERACTIVE RUNNER CLASS
|
|
60
|
+
// ============================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Main interactive runner class
|
|
64
|
+
*
|
|
65
|
+
* Usage:
|
|
66
|
+
* ```ts
|
|
67
|
+
* const runner = new InteractiveRunner(props);
|
|
68
|
+
* await runner.start();
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export class InteractiveRunner {
|
|
72
|
+
private props: InteractiveRunnerProps;
|
|
73
|
+
private messageStore: MessageStoreImpl;
|
|
74
|
+
private inputManager: InputManagerImpl;
|
|
75
|
+
private state: InteractiveState;
|
|
76
|
+
private renderer: NativeRendererType | null = null;
|
|
77
|
+
private spinnerInterval: Timer | null = null;
|
|
78
|
+
private frameIndex = 0;
|
|
79
|
+
private isProcessing = false;
|
|
80
|
+
private savedInput = "";
|
|
81
|
+
private shouldExit = false;
|
|
82
|
+
|
|
83
|
+
constructor(props: InteractiveRunnerProps) {
|
|
84
|
+
this.props = props;
|
|
85
|
+
this.messageStore = new MessageStoreImpl();
|
|
86
|
+
this.inputManager = new InputManagerImpl();
|
|
87
|
+
this.state = createInitialState();
|
|
88
|
+
|
|
89
|
+
// Initialize messages from props
|
|
90
|
+
if (props.initialMessages.length > 0) {
|
|
91
|
+
this.messageStore.addApiMessages(props.initialMessages);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Start the interactive loop
|
|
97
|
+
*/
|
|
98
|
+
async start(): Promise<void> {
|
|
99
|
+
// Check if stdin is a TTY
|
|
100
|
+
const isInteractive = process.stdin.isTTY;
|
|
101
|
+
const forceInteractive = process.env.CLAUDE_FORCE_INTERACTIVE === "true";
|
|
102
|
+
|
|
103
|
+
// Debug output
|
|
104
|
+
if (process.env.CODER_DEBUG === "1") {
|
|
105
|
+
console.error("[InteractiveRunner] isInteractive:", isInteractive);
|
|
106
|
+
console.error("[InteractiveRunner] forceInteractive:", forceInteractive);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!isInteractive && !forceInteractive) {
|
|
110
|
+
console.error("Error: Interactive mode requires a TTY. Use -q for single query mode.");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Try to use native TUI renderer, fall back to simple mode if not available
|
|
115
|
+
if (isInteractive) {
|
|
116
|
+
try {
|
|
117
|
+
if (process.env.CODER_DEBUG === "1") {
|
|
118
|
+
console.error("[InteractiveRunner] Creating NativeRenderer...");
|
|
119
|
+
}
|
|
120
|
+
this.renderer = new NativeRenderer();
|
|
121
|
+
if (process.env.CODER_DEBUG === "1") {
|
|
122
|
+
console.error("[InteractiveRunner] NativeRenderer created, initializing...");
|
|
123
|
+
}
|
|
124
|
+
this.renderer.init();
|
|
125
|
+
if (process.env.CODER_DEBUG === "1") {
|
|
126
|
+
console.error("[InteractiveRunner] NativeRenderer initialized successfully");
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
// Native renderer not available, fall back to simple mode
|
|
130
|
+
if (process.env.CODER_DEBUG === "1") {
|
|
131
|
+
console.error("[InteractiveRunner] NativeRenderer failed:", err);
|
|
132
|
+
}
|
|
133
|
+
await this._runSimpleMode();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
// Non-interactive terminal, use simple mode
|
|
138
|
+
await this._runSimpleMode();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Show startup message
|
|
143
|
+
this._showStartupMessage();
|
|
144
|
+
|
|
145
|
+
// Register input handlers
|
|
146
|
+
this._registerInputHandlers();
|
|
147
|
+
|
|
148
|
+
// Start spinner animation (when loading)
|
|
149
|
+
// Note: Spinner is controlled by state.isLoading
|
|
150
|
+
|
|
151
|
+
// Main event loop
|
|
152
|
+
while (!this.shouldExit) {
|
|
153
|
+
// Poll for input (non-blocking, 16ms timeout for ~60fps)
|
|
154
|
+
const event = this.renderer.pollInput(16);
|
|
155
|
+
|
|
156
|
+
// Dispatch to input manager if it's a key event
|
|
157
|
+
// Note: NAPI-RS converts snake_case to camelCase, so use eventType (not event_type)
|
|
158
|
+
if (event.eventType === "key") {
|
|
159
|
+
// Convert InputEvent (from native module) to NativeKeyEvent (for input handlers)
|
|
160
|
+
const nativeEvent = inputEventToNativeKeyEvent(event);
|
|
161
|
+
if (nativeEvent) {
|
|
162
|
+
this.inputManager.dispatch(nativeEvent);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Render current state
|
|
167
|
+
this._render();
|
|
168
|
+
|
|
169
|
+
// Small yield to prevent CPU spinning
|
|
170
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Cleanup
|
|
174
|
+
this._cleanup();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Stop the interactive loop
|
|
179
|
+
*/
|
|
180
|
+
stop(): void {
|
|
181
|
+
this.shouldExit = true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ============================================
|
|
185
|
+
// PRIVATE METHODS
|
|
186
|
+
// ============================================
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Show startup message
|
|
190
|
+
*/
|
|
191
|
+
private _showStartupMessage(): void {
|
|
192
|
+
console.log(`\x1b[90mSession: ${this.props.sessionId}\x1b[0m`);
|
|
193
|
+
console.log(`\x1b[90mModel: ${this.props.model}\x1b[0m`);
|
|
194
|
+
if (this.props.teammateRunner) {
|
|
195
|
+
console.log(`\x1b[90mTeammate Mode: Active\x1b[0m`);
|
|
196
|
+
}
|
|
197
|
+
console.log(`\x1b[90mType your message, ? for help, or /help for commands.\x1b[0m\n`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Register input handlers
|
|
202
|
+
*/
|
|
203
|
+
private _registerInputHandlers(): void {
|
|
204
|
+
// System handler (Ctrl+C, etc.) - highest priority
|
|
205
|
+
this.inputManager.register({
|
|
206
|
+
id: "system",
|
|
207
|
+
priority: InputPriority.SYSTEM,
|
|
208
|
+
handler: (event) => {
|
|
209
|
+
if (KeyEvents.isCtrlC(event)) {
|
|
210
|
+
this._handleExit();
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
if (KeyEvents.isCtrlD(event)) {
|
|
214
|
+
this._handleExit();
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Main input handler
|
|
222
|
+
this.inputManager.register({
|
|
223
|
+
id: "main-input",
|
|
224
|
+
priority: InputPriority.INPUT,
|
|
225
|
+
handler: (event) => this._handleMainInput(event),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Focus on main input
|
|
229
|
+
this.inputManager.focus("main-input");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Handle main input events
|
|
234
|
+
*/
|
|
235
|
+
private _handleMainInput(event: NativeKeyEvent): boolean {
|
|
236
|
+
// Block input when loading
|
|
237
|
+
if (this.state.isLoading) {
|
|
238
|
+
// Allow Ctrl+C even when loading
|
|
239
|
+
if (KeyEvents.isCtrlC(event)) {
|
|
240
|
+
this._handleExit();
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Handle special modes
|
|
247
|
+
if (this.state.helpMode) {
|
|
248
|
+
return this._handleHelpModeInput(event);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (this.state.sessionSelectMode) {
|
|
252
|
+
return this._handleSessionSelectInput(event);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Regular input handling
|
|
256
|
+
const { inputValue, cursorPos } = this.state;
|
|
257
|
+
|
|
258
|
+
// Enter - submit
|
|
259
|
+
if (KeyEvents.isEnter(event)) {
|
|
260
|
+
if (inputValue.trim()) {
|
|
261
|
+
this._submitInput();
|
|
262
|
+
}
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Escape - clear input or exit help
|
|
267
|
+
if (KeyEvents.isEscape(event)) {
|
|
268
|
+
this.state = { ...this.state, inputValue: "", cursorPos: 0 };
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// History navigation
|
|
273
|
+
if (KeyEvents.isUp(event)) {
|
|
274
|
+
return this._handleHistoryUp();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (KeyEvents.isDown(event)) {
|
|
278
|
+
return this._handleHistoryDown();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Cursor movement
|
|
282
|
+
if (KeyEvents.isLeft(event)) {
|
|
283
|
+
this.state = { ...this.state, cursorPos: Math.max(0, cursorPos - 1) };
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (KeyEvents.isRight(event)) {
|
|
288
|
+
this.state = { ...this.state, cursorPos: Math.min(inputValue.length, cursorPos + 1) };
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (KeyEvents.isHome(event) || KeyEvents.isCtrlA(event)) {
|
|
293
|
+
this.state = { ...this.state, cursorPos: 0 };
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (KeyEvents.isEnd(event) || KeyEvents.isCtrlE(event)) {
|
|
298
|
+
this.state = { ...this.state, cursorPos: inputValue.length };
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Backspace
|
|
303
|
+
if (KeyEvents.isBackspace(event)) {
|
|
304
|
+
if (cursorPos > 0) {
|
|
305
|
+
const newVal = inputValue.slice(0, cursorPos - 1) + inputValue.slice(cursorPos);
|
|
306
|
+
this.state = { ...this.state, inputValue: newVal, cursorPos: cursorPos - 1 };
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Delete
|
|
312
|
+
if (KeyEvents.isDelete(event)) {
|
|
313
|
+
if (cursorPos < inputValue.length) {
|
|
314
|
+
const newVal = inputValue.slice(0, cursorPos) + inputValue.slice(cursorPos + 1);
|
|
315
|
+
this.state = { ...this.state, inputValue: newVal };
|
|
316
|
+
}
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Printable character
|
|
321
|
+
if (KeyEvents.isPrintable(event)) {
|
|
322
|
+
const char = KeyEvents.getChar(event);
|
|
323
|
+
const newVal = inputValue.slice(0, cursorPos) + char + inputValue.slice(cursorPos);
|
|
324
|
+
this.state = {
|
|
325
|
+
...this.state,
|
|
326
|
+
inputValue: newVal,
|
|
327
|
+
cursorPos: cursorPos + 1,
|
|
328
|
+
historyIndex: -1,
|
|
329
|
+
};
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Handle history up
|
|
338
|
+
*/
|
|
339
|
+
private _handleHistoryUp(): boolean {
|
|
340
|
+
const { inputHistory, historyIndex } = this.state;
|
|
341
|
+
if (inputHistory.length === 0) return false;
|
|
342
|
+
|
|
343
|
+
if (historyIndex === -1) {
|
|
344
|
+
this.savedInput = this.state.inputValue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const newIndex = Math.min(historyIndex + 1, inputHistory.length - 1);
|
|
348
|
+
const newInput = inputHistory[newIndex] ?? "";
|
|
349
|
+
this.state = {
|
|
350
|
+
...this.state,
|
|
351
|
+
historyIndex: newIndex,
|
|
352
|
+
inputValue: newInput,
|
|
353
|
+
cursorPos: newInput.length,
|
|
354
|
+
};
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Handle history down
|
|
360
|
+
*/
|
|
361
|
+
private _handleHistoryDown(): boolean {
|
|
362
|
+
const { inputHistory, historyIndex } = this.state;
|
|
363
|
+
if (historyIndex === -1) return false;
|
|
364
|
+
|
|
365
|
+
if (historyIndex > 0) {
|
|
366
|
+
const newIndex = historyIndex - 1;
|
|
367
|
+
const newInput = inputHistory[newIndex] ?? "";
|
|
368
|
+
this.state = {
|
|
369
|
+
...this.state,
|
|
370
|
+
historyIndex: newIndex,
|
|
371
|
+
inputValue: newInput,
|
|
372
|
+
cursorPos: newInput.length,
|
|
373
|
+
};
|
|
374
|
+
} else {
|
|
375
|
+
this.state = {
|
|
376
|
+
...this.state,
|
|
377
|
+
historyIndex: -1,
|
|
378
|
+
inputValue: this.savedInput,
|
|
379
|
+
cursorPos: this.savedInput.length,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Handle help mode input
|
|
387
|
+
*/
|
|
388
|
+
private _handleHelpModeInput(event: NativeKeyEvent): boolean {
|
|
389
|
+
const HELP_SECTIONS_COUNT = 5;
|
|
390
|
+
|
|
391
|
+
if (KeyEvents.isEscape(event) || event.code === "q") {
|
|
392
|
+
this.state = { ...this.state, helpMode: false };
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (event.code === "tab" || KeyEvents.isRight(event)) {
|
|
397
|
+
this.state = {
|
|
398
|
+
...this.state,
|
|
399
|
+
helpSection: (this.state.helpSection + 1) % HELP_SECTIONS_COUNT,
|
|
400
|
+
};
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (KeyEvents.isLeft(event)) {
|
|
405
|
+
this.state = {
|
|
406
|
+
...this.state,
|
|
407
|
+
helpSection: (this.state.helpSection - 1 + HELP_SECTIONS_COUNT) % HELP_SECTIONS_COUNT,
|
|
408
|
+
};
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Handle session select input
|
|
417
|
+
*/
|
|
418
|
+
private _handleSessionSelectInput(event: NativeKeyEvent): boolean {
|
|
419
|
+
const num = parseInt(event.code ?? "", 10);
|
|
420
|
+
if (!isNaN(num) && num >= 1 && num <= this.state.selectableSessions.length) {
|
|
421
|
+
const selected = this.state.selectableSessions[num - 1];
|
|
422
|
+
if (selected) {
|
|
423
|
+
this.state = {
|
|
424
|
+
...this.state,
|
|
425
|
+
sessionSelectMode: false,
|
|
426
|
+
selectableSessions: [],
|
|
427
|
+
};
|
|
428
|
+
// Handle session resume
|
|
429
|
+
this._handleCommand(`/resume ${selected.id}`);
|
|
430
|
+
}
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (KeyEvents.isEnter(event) || KeyEvents.isEscape(event)) {
|
|
435
|
+
this.state = {
|
|
436
|
+
...this.state,
|
|
437
|
+
sessionSelectMode: false,
|
|
438
|
+
selectableSessions: [],
|
|
439
|
+
};
|
|
440
|
+
this.messageStore.addSystem("Session selection cancelled.");
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Submit the current input
|
|
449
|
+
*/
|
|
450
|
+
private async _submitInput(): Promise<void> {
|
|
451
|
+
if (this.isProcessing) return;
|
|
452
|
+
|
|
453
|
+
const input = this.state.inputValue.trim();
|
|
454
|
+
if (!input) return;
|
|
455
|
+
|
|
456
|
+
// Clear input immediately
|
|
457
|
+
this.state = { ...this.state, inputValue: "", cursorPos: 0 };
|
|
458
|
+
|
|
459
|
+
// Add to history (skip commands and duplicates)
|
|
460
|
+
if (!input.startsWith("/") && input !== this.state.inputHistory[0]) {
|
|
461
|
+
this.state = {
|
|
462
|
+
...this.state,
|
|
463
|
+
inputHistory: [input, ...this.state.inputHistory].slice(0, 100),
|
|
464
|
+
historyIndex: -1,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Handle commands
|
|
469
|
+
if (input.startsWith("/")) {
|
|
470
|
+
this._handleCommand(input);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Process as message
|
|
475
|
+
await this._processMessage(input);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Handle a command
|
|
480
|
+
*/
|
|
481
|
+
private _handleCommand(cmd: string): void {
|
|
482
|
+
// Import command handling logic
|
|
483
|
+
// For now, handle basic commands inline
|
|
484
|
+
const command = cmd.toLowerCase().trim();
|
|
485
|
+
|
|
486
|
+
if (command === "/exit" || command === "/quit") {
|
|
487
|
+
this._handleExit();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (command === "/clear") {
|
|
492
|
+
this.messageStore.clear();
|
|
493
|
+
this.messageStore.addSystem("Messages cleared.");
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (command === "/help" || command === "?") {
|
|
498
|
+
this.state = { ...this.state, helpMode: true, helpSection: 0 };
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (command === "/cost") {
|
|
503
|
+
this.messageStore.addSystem(`Total cost: $${this.state.totalCost.toFixed(4)}`);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (command === "/status") {
|
|
508
|
+
this.messageStore.addSystem(
|
|
509
|
+
`Session: ${this.props.sessionId}\n` +
|
|
510
|
+
`Model: ${this.props.model}\n` +
|
|
511
|
+
`Messages: ${this.messageStore.messages.length}\n` +
|
|
512
|
+
`Tokens: ${this.messageStore.tokenCount}`
|
|
513
|
+
);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Unknown command
|
|
518
|
+
this.messageStore.addSystem(`Unknown command: ${cmd}. Type /help for available commands.`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Process a message through the agent loop
|
|
523
|
+
*/
|
|
524
|
+
private async _processMessage(input: string): Promise<void> {
|
|
525
|
+
this.isProcessing = true;
|
|
526
|
+
this.state = { ...this.state, isLoading: true, streamingText: "" };
|
|
527
|
+
|
|
528
|
+
// Start spinner animation
|
|
529
|
+
this._startSpinner();
|
|
530
|
+
|
|
531
|
+
// Add user message
|
|
532
|
+
this.messageStore.addMessage({ role: "user", content: input });
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
// Execute hooks
|
|
536
|
+
const hookResult = await this.props.hookManager.execute("UserPromptSubmit", {
|
|
537
|
+
prompt: input,
|
|
538
|
+
session_id: this.props.sessionId,
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
if (hookResult.decision === "deny" || hookResult.decision === "block") {
|
|
542
|
+
this.messageStore.addSystem(`Input blocked: ${hookResult.reason || "Security policy"}`);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const processedInput = (hookResult.modified_input?.prompt as string) ?? input;
|
|
547
|
+
|
|
548
|
+
// Build messages for API
|
|
549
|
+
const newUserMsg: ApiMessage = {
|
|
550
|
+
role: "user",
|
|
551
|
+
content: [{ type: "text", text: processedInput }],
|
|
552
|
+
};
|
|
553
|
+
const messagesForApi = [...this.messageStore.apiMessages, newUserMsg];
|
|
554
|
+
|
|
555
|
+
// Get git status
|
|
556
|
+
const gitStatus = await getGitStatus(this.props.workingDirectory);
|
|
557
|
+
|
|
558
|
+
// Create stream highlighter
|
|
559
|
+
const highlighter = createStreamHighlighter();
|
|
560
|
+
let streamingText = "";
|
|
561
|
+
|
|
562
|
+
// Run agent loop
|
|
563
|
+
const result = await agentLoop(messagesForApi, {
|
|
564
|
+
apiKey: this.props.apiKey,
|
|
565
|
+
model: this.props.model,
|
|
566
|
+
maxTokens: this.props.maxTokens,
|
|
567
|
+
systemPrompt: this.props.systemPrompt,
|
|
568
|
+
tools: this.props.tools,
|
|
569
|
+
permissionMode: this.props.permissionMode,
|
|
570
|
+
workingDirectory: this.props.workingDirectory,
|
|
571
|
+
gitStatus,
|
|
572
|
+
hookManager: this.props.hookManager,
|
|
573
|
+
sessionId: this.props.sessionId,
|
|
574
|
+
onText: (text) => {
|
|
575
|
+
streamingText += text;
|
|
576
|
+
this.state = { ...this.state, streamingText };
|
|
577
|
+
},
|
|
578
|
+
onToolUse: (toolUse) => {
|
|
579
|
+
this.messageStore.addSystem(`[Using: ${toolUse.name}]`, "tool_call", toolUse.name);
|
|
580
|
+
},
|
|
581
|
+
onToolResult: (toolResult) => {
|
|
582
|
+
if (toolResult.result.is_error) {
|
|
583
|
+
this.messageStore.addSystem(`[Tool Error]`, "tool_result", undefined, true);
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
onMetrics: async (metrics) => {
|
|
587
|
+
const apiTokens = metrics.usage.input_tokens + metrics.usage.output_tokens;
|
|
588
|
+
if (apiTokens > 0) {
|
|
589
|
+
this.messageStore.setTokenCount(apiTokens);
|
|
590
|
+
}
|
|
591
|
+
await this.props.sessionStore.saveMetrics(metrics);
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// Add API messages (skipping user message already added)
|
|
596
|
+
this.messageStore.addApiMessages(result.messages.slice(this.messageStore.apiMessages.length));
|
|
597
|
+
this.state = { ...this.state, totalCost: this.state.totalCost + result.totalCost };
|
|
598
|
+
|
|
599
|
+
// Save to session
|
|
600
|
+
const lastUserMsg = result.messages[result.messages.length - 2];
|
|
601
|
+
const lastAssistantMsg = result.messages[result.messages.length - 1];
|
|
602
|
+
if (lastUserMsg) await this.props.sessionStore.saveMessage(lastUserMsg);
|
|
603
|
+
if (lastAssistantMsg) await this.props.sessionStore.saveMessage(lastAssistantMsg);
|
|
604
|
+
|
|
605
|
+
} catch (error) {
|
|
606
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
607
|
+
this.messageStore.addSystem(`Error: ${errorMessage}`, "error");
|
|
608
|
+
} finally {
|
|
609
|
+
this._stopSpinner();
|
|
610
|
+
this.state = { ...this.state, isLoading: false, streamingText: "" };
|
|
611
|
+
this.isProcessing = false;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Start spinner animation
|
|
617
|
+
*/
|
|
618
|
+
private _startSpinner(): void {
|
|
619
|
+
this._stopSpinner();
|
|
620
|
+
this.spinnerInterval = setInterval(() => {
|
|
621
|
+
this.frameIndex = (this.frameIndex + 1) % spinnerFrames.length;
|
|
622
|
+
const frame = spinnerFrames[this.frameIndex];
|
|
623
|
+
if (frame) {
|
|
624
|
+
this.state = { ...this.state, spinnerFrame: frame };
|
|
625
|
+
}
|
|
626
|
+
}, 80);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Stop spinner animation
|
|
631
|
+
*/
|
|
632
|
+
private _stopSpinner(): void {
|
|
633
|
+
if (this.spinnerInterval) {
|
|
634
|
+
clearInterval(this.spinnerInterval);
|
|
635
|
+
this.spinnerInterval = null;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Handle exit
|
|
641
|
+
*/
|
|
642
|
+
private async _handleExit(): Promise<void> {
|
|
643
|
+
this.shouldExit = true;
|
|
644
|
+
await this.props.onExit?.();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Render current state
|
|
649
|
+
*/
|
|
650
|
+
private _render(): void {
|
|
651
|
+
if (!this.renderer) return;
|
|
652
|
+
|
|
653
|
+
// Build render state
|
|
654
|
+
const renderState = this._buildRenderState();
|
|
655
|
+
this.renderer.render(renderState);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Build render state for the renderer
|
|
660
|
+
* Returns a RenderState object matching the NativeRenderer.render() interface
|
|
661
|
+
*/
|
|
662
|
+
private _buildRenderState() {
|
|
663
|
+
const { messages } = this.messageStore;
|
|
664
|
+
const { isLoading, inputValue, cursorPos, streamingText, helpMode, helpSection, sessionSelectMode, selectableSessions } = this.state;
|
|
665
|
+
|
|
666
|
+
// Convert MessageStore messages to RenderMessage format
|
|
667
|
+
const renderMessages = messages.map(msg => ({
|
|
668
|
+
role: msg.role,
|
|
669
|
+
content: msg.content,
|
|
670
|
+
}));
|
|
671
|
+
|
|
672
|
+
// Build status text
|
|
673
|
+
const statusText = `Session: ${this.props.sessionId} | Model: ${this.props.model}`;
|
|
674
|
+
|
|
675
|
+
// Build help text if in help mode
|
|
676
|
+
const helpText = helpMode ? this._getHelpText(helpSection) : "";
|
|
677
|
+
|
|
678
|
+
// Build search results if in session select mode
|
|
679
|
+
const searchResults = sessionSelectMode ? selectableSessions.map(s => ({
|
|
680
|
+
filePath: s.id,
|
|
681
|
+
lineNumber: 0,
|
|
682
|
+
content: `${s.messageCount} messages`,
|
|
683
|
+
})) : [];
|
|
684
|
+
|
|
685
|
+
// NativeRenderer expects camelCase field names (NAPI-RS converts snake_case to camelCase)
|
|
686
|
+
return {
|
|
687
|
+
messages: renderMessages,
|
|
688
|
+
inputValue: inputValue,
|
|
689
|
+
cursorPos: cursorPos,
|
|
690
|
+
statusText: statusText,
|
|
691
|
+
isLoading: isLoading,
|
|
692
|
+
streamingText: streamingText,
|
|
693
|
+
model: this.props.model,
|
|
694
|
+
showHelp: helpMode,
|
|
695
|
+
helpText: helpText,
|
|
696
|
+
searchMode: sessionSelectMode,
|
|
697
|
+
searchQuery: "",
|
|
698
|
+
searchResults: searchResults,
|
|
699
|
+
searchSelected: 0,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Get help text for a given section
|
|
705
|
+
*/
|
|
706
|
+
private _getHelpText(section: number): string {
|
|
707
|
+
const sections = [
|
|
708
|
+
`Commands:
|
|
709
|
+
/help, ? Show this help
|
|
710
|
+
/exit, /quit Exit the CLI
|
|
711
|
+
/clear Clear messages
|
|
712
|
+
/cost Show total cost
|
|
713
|
+
/status Show session status`,
|
|
714
|
+
`Keyboard Shortcuts:
|
|
715
|
+
Enter Send message
|
|
716
|
+
Escape Clear input
|
|
717
|
+
Up/Down History navigation
|
|
718
|
+
Ctrl+C Exit`,
|
|
719
|
+
`Model: ${this.props.model}
|
|
720
|
+
Max tokens: ${this.props.maxTokens}
|
|
721
|
+
Permission mode: ${this.props.permissionMode}`,
|
|
722
|
+
];
|
|
723
|
+
return sections[section] || sections[0] || "";
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Simple input mode fallback (without native TUI)
|
|
728
|
+
* Uses basic readline for input
|
|
729
|
+
*/
|
|
730
|
+
private async _runSimpleMode(): Promise<void> {
|
|
731
|
+
const readline = await import("node:readline");
|
|
732
|
+
const rl = readline.createInterface({
|
|
733
|
+
input: process.stdin,
|
|
734
|
+
output: process.stdout,
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
console.log("\n\x1b[90mRunning in simple mode (no TUI)\x1b[0m");
|
|
738
|
+
console.log("\x1b[90mType /exit to quit\x1b[0m\n");
|
|
739
|
+
|
|
740
|
+
const prompt = (): void => {
|
|
741
|
+
rl.question("> ", async (input) => {
|
|
742
|
+
const trimmed = input.trim();
|
|
743
|
+
|
|
744
|
+
if (trimmed === "/exit" || trimmed === "/quit") {
|
|
745
|
+
rl.close();
|
|
746
|
+
console.log("\n\x1b[90mGoodbye!\x1b[0m");
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (trimmed === "/clear") {
|
|
751
|
+
console.clear();
|
|
752
|
+
prompt();
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (trimmed === "/help" || trimmed === "?") {
|
|
757
|
+
console.log(`
|
|
758
|
+
Commands:
|
|
759
|
+
/help, ? Show this help
|
|
760
|
+
/exit Exit the CLI
|
|
761
|
+
/clear Clear screen
|
|
762
|
+
/cost Show total cost
|
|
763
|
+
/status Show session status
|
|
764
|
+
`);
|
|
765
|
+
prompt();
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (trimmed === "/cost") {
|
|
770
|
+
console.log(`Total cost: $${this.state.totalCost.toFixed(4)}`);
|
|
771
|
+
prompt();
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (trimmed === "/status") {
|
|
776
|
+
console.log(`
|
|
777
|
+
Session: ${this.props.sessionId}
|
|
778
|
+
Model: ${this.props.model}
|
|
779
|
+
Messages: ${this.messageStore.messages.length}
|
|
780
|
+
Tokens: ${this.messageStore.tokenCount}
|
|
781
|
+
`);
|
|
782
|
+
prompt();
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (trimmed) {
|
|
787
|
+
await this._processMessage(trimmed);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
prompt();
|
|
791
|
+
});
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
prompt();
|
|
795
|
+
|
|
796
|
+
return new Promise((resolve) => {
|
|
797
|
+
rl.on("close", () => {
|
|
798
|
+
resolve();
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Cleanup resources
|
|
805
|
+
*/
|
|
806
|
+
private _cleanup(): void {
|
|
807
|
+
this._stopSpinner();
|
|
808
|
+
if (this.renderer) {
|
|
809
|
+
this.renderer.cleanup();
|
|
810
|
+
this.renderer = null;
|
|
811
|
+
}
|
|
812
|
+
console.log("\n\x1b[90mGoodbye!\x1b[0m");
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// ============================================
|
|
817
|
+
// EXPORTS
|
|
818
|
+
// ============================================
|
|
819
|
+
|
|
820
|
+
export type { InteractiveRunnerProps, InteractiveState } from "./types.js";
|