@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
|
@@ -1,537 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Main Interactive TUI Component
|
|
3
|
-
* Orchestrates all sub-components and manages state and agent loop
|
|
4
|
-
*
|
|
5
|
-
* Uses:
|
|
6
|
-
* - MessageStore: Centralized message state management
|
|
7
|
-
* - InputContext: Centralized keyboard input handling
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
11
|
-
import { Box, Text, useApp, useStdout } from "ink";
|
|
12
|
-
import type { ExtendedThinkingConfig } from "../../../../types/index.js";
|
|
13
|
-
import { agentLoop } from "../../../../core/agent-loop.js";
|
|
14
|
-
import { getGitStatus } from "../../../../core/git-status.js";
|
|
15
|
-
import { createStreamHighlighter } from "../../../../core/stream-highlighter.js";
|
|
16
|
-
import { calculateContextInfo } from "../shared/status-line.js";
|
|
17
|
-
import { spinnerFrames } from "./spinner.js";
|
|
18
|
-
import { MessageArea } from "./MessageArea.js";
|
|
19
|
-
import { InputField, setGlobalInput } from "./InputField.js";
|
|
20
|
-
import { handleCommand } from "./commands.js";
|
|
21
|
-
import { useNativeInput, KeyEvents } from "./useNativeInput.js";
|
|
22
|
-
import { InputProvider } from "./InputContext.js";
|
|
23
|
-
import { MessageStoreProvider, useMessageStore } from "./MessageStore.js";
|
|
24
|
-
import type { InteractiveTUIProps, MessageSubType } from "./types.js";
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Estimate token count from text
|
|
28
|
-
* Uses ~4 characters per token as rough approximation
|
|
29
|
-
*/
|
|
30
|
-
function estimateTokens(text: string): number {
|
|
31
|
-
if (!text) return 0;
|
|
32
|
-
return Math.ceil(text.length / 4);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Estimate total tokens from messages
|
|
37
|
-
*/
|
|
38
|
-
function estimateMessagesTokens(messages: import("../../../../types/index.js").Message[]): number {
|
|
39
|
-
let total = 0;
|
|
40
|
-
for (const msg of messages) {
|
|
41
|
-
if (typeof msg.content === "string") {
|
|
42
|
-
total += estimateTokens(msg.content);
|
|
43
|
-
} else if (Array.isArray(msg.content)) {
|
|
44
|
-
for (const block of msg.content) {
|
|
45
|
-
if (block.type === "text") {
|
|
46
|
-
total += estimateTokens(block.text);
|
|
47
|
-
} else if (block.type === "tool_use") {
|
|
48
|
-
total += estimateTokens(JSON.stringify(block.input));
|
|
49
|
-
} else if (block.type === "tool_result") {
|
|
50
|
-
if (typeof block.content === "string") {
|
|
51
|
-
total += estimateTokens(block.content);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return total;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Inner component that uses MessageStore
|
|
62
|
-
*/
|
|
63
|
-
function InteractiveTUIInner({
|
|
64
|
-
apiKey,
|
|
65
|
-
model: initialModel,
|
|
66
|
-
permissionMode,
|
|
67
|
-
maxTokens,
|
|
68
|
-
systemPrompt: initialSystemPrompt,
|
|
69
|
-
tools,
|
|
70
|
-
hookManager,
|
|
71
|
-
sessionStore,
|
|
72
|
-
sessionId,
|
|
73
|
-
setSessionId,
|
|
74
|
-
workingDirectory,
|
|
75
|
-
onExit,
|
|
76
|
-
}: InteractiveTUIProps) {
|
|
77
|
-
// Message store
|
|
78
|
-
const {
|
|
79
|
-
messages,
|
|
80
|
-
apiMessages,
|
|
81
|
-
addMessage,
|
|
82
|
-
addApiMessages,
|
|
83
|
-
addSystem,
|
|
84
|
-
tokenCount,
|
|
85
|
-
setTokenCount,
|
|
86
|
-
} = useMessageStore();
|
|
87
|
-
|
|
88
|
-
// UI state - use refs for immediate input display updates
|
|
89
|
-
const inputRef = useRef("");
|
|
90
|
-
const cursorRef = useRef(0);
|
|
91
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
92
|
-
|
|
93
|
-
// Update input and sync to global state for InputField
|
|
94
|
-
// NOTE: Use inputRef.current and cursorRef.current directly in callbacks
|
|
95
|
-
// to avoid stale closure values. Don't use inputValue/cursorPos variables.
|
|
96
|
-
const setInputValue = useCallback((value: string) => {
|
|
97
|
-
inputRef.current = value;
|
|
98
|
-
setGlobalInput(value, cursorRef.current);
|
|
99
|
-
}, []);
|
|
100
|
-
|
|
101
|
-
const setCursorPos = useCallback((pos: number | ((prev: number) => number)) => {
|
|
102
|
-
const newPos = typeof pos === "function" ? pos(cursorRef.current) : pos;
|
|
103
|
-
cursorRef.current = newPos;
|
|
104
|
-
setGlobalInput(inputRef.current, newPos);
|
|
105
|
-
}, []);
|
|
106
|
-
|
|
107
|
-
const [totalCost, setTotalCost] = useState(0);
|
|
108
|
-
const [spinnerFrame, setSpinnerFrame] = useState("⠋");
|
|
109
|
-
const [model, setModel] = useState(initialModel);
|
|
110
|
-
const [systemPrompt] = useState(initialSystemPrompt);
|
|
111
|
-
const [streamingText, setStreamingText] = useState("");
|
|
112
|
-
const [scrollOffset, setScrollOffset] = useState(0);
|
|
113
|
-
const [sessionSelectMode, setSessionSelectMode] = useState(false);
|
|
114
|
-
const [selectableSessions, setSelectableSessions] = useState<Array<{ id: string; messageCount: number; metadata?: Record<string, unknown> }>>([]);
|
|
115
|
-
const [helpMode, setHelpMode] = useState(false);
|
|
116
|
-
const [helpSection, setHelpSection] = useState(0);
|
|
117
|
-
|
|
118
|
-
// Input history
|
|
119
|
-
const [inputHistory, setInputHistory] = useState<string[]>([]);
|
|
120
|
-
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
121
|
-
const [savedInput, setSavedInput] = useState("");
|
|
122
|
-
|
|
123
|
-
const { exit } = useApp();
|
|
124
|
-
const { stdout } = useStdout();
|
|
125
|
-
const frameRef = useRef(0);
|
|
126
|
-
const isProcessingRef = useRef(false);
|
|
127
|
-
const highlighterRef = useRef(createStreamHighlighter());
|
|
128
|
-
|
|
129
|
-
// Calculate terminal layout
|
|
130
|
-
const terminalHeight = stdout.rows || 24;
|
|
131
|
-
const inputHeight = 3;
|
|
132
|
-
const statusHeight = 3;
|
|
133
|
-
const messageHeight = terminalHeight - inputHeight - statusHeight;
|
|
134
|
-
|
|
135
|
-
// Calculate context warning
|
|
136
|
-
const contextInfo = calculateContextInfo(tokenCount, model);
|
|
137
|
-
const contextWarning = contextInfo.isCritical
|
|
138
|
-
? "Context critical! Use /compact or start new conversation"
|
|
139
|
-
: contextInfo.isLow
|
|
140
|
-
? `Context low: ${contextInfo.percentRemaining.toFixed(0)}% remaining`
|
|
141
|
-
: null;
|
|
142
|
-
|
|
143
|
-
// Auto-scroll to bottom when new messages arrive
|
|
144
|
-
useEffect(() => {
|
|
145
|
-
setScrollOffset(0);
|
|
146
|
-
}, [messages.length]);
|
|
147
|
-
|
|
148
|
-
// Spinner animation
|
|
149
|
-
useEffect(() => {
|
|
150
|
-
if (!isLoading) return;
|
|
151
|
-
|
|
152
|
-
const interval = setInterval(() => {
|
|
153
|
-
frameRef.current = (frameRef.current + 1) % spinnerFrames.length;
|
|
154
|
-
const frame = spinnerFrames[frameRef.current];
|
|
155
|
-
if (frame) setSpinnerFrame(frame);
|
|
156
|
-
}, 80);
|
|
157
|
-
|
|
158
|
-
return () => clearInterval(interval);
|
|
159
|
-
}, [isLoading]);
|
|
160
|
-
|
|
161
|
-
// Process a message
|
|
162
|
-
const processMessage = useCallback(async (input: string, messageAlreadyAdded = false) => {
|
|
163
|
-
if (isProcessingRef.current) return;
|
|
164
|
-
isProcessingRef.current = true;
|
|
165
|
-
|
|
166
|
-
// Add user message to UI if not already added
|
|
167
|
-
if (!messageAlreadyAdded) {
|
|
168
|
-
addMessage({ role: "user", content: input });
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
setIsLoading(true);
|
|
172
|
-
setStreamingText("");
|
|
173
|
-
highlighterRef.current = createStreamHighlighter();
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
// Execute UserPromptSubmit hook
|
|
177
|
-
const hookResult = await hookManager.execute("UserPromptSubmit", {
|
|
178
|
-
prompt: input,
|
|
179
|
-
session_id: sessionId,
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
if (hookResult.decision === "deny" || hookResult.decision === "block") {
|
|
183
|
-
addSystem(`Input blocked: ${hookResult.reason || "Security policy"}`);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const processedInput = (hookResult.modified_input?.prompt as string) ?? input;
|
|
188
|
-
|
|
189
|
-
// Build messages for API
|
|
190
|
-
const newUserMsg = {
|
|
191
|
-
role: "user" as const,
|
|
192
|
-
content: [{ type: "text" as const, text: processedInput }],
|
|
193
|
-
};
|
|
194
|
-
const messagesForApi = [...apiMessages, newUserMsg];
|
|
195
|
-
|
|
196
|
-
// Get git status
|
|
197
|
-
const gitStatus = await getGitStatus(workingDirectory);
|
|
198
|
-
|
|
199
|
-
// Run agent loop
|
|
200
|
-
const result = await agentLoop(messagesForApi, {
|
|
201
|
-
apiKey,
|
|
202
|
-
model,
|
|
203
|
-
maxTokens,
|
|
204
|
-
systemPrompt,
|
|
205
|
-
tools,
|
|
206
|
-
permissionMode,
|
|
207
|
-
workingDirectory,
|
|
208
|
-
gitStatus,
|
|
209
|
-
extendedThinking: undefined,
|
|
210
|
-
hookManager,
|
|
211
|
-
sessionId,
|
|
212
|
-
onText: (text) => {
|
|
213
|
-
setStreamingText((prev) => prev + text);
|
|
214
|
-
},
|
|
215
|
-
onThinking: () => {
|
|
216
|
-
// Could show thinking in UI
|
|
217
|
-
},
|
|
218
|
-
onToolUse: (toolUse) => {
|
|
219
|
-
addSystem(`[Using: ${toolUse.name}]`, "tool_call", toolUse.name);
|
|
220
|
-
},
|
|
221
|
-
onToolResult: (toolResult) => {
|
|
222
|
-
if (toolResult.result.is_error) {
|
|
223
|
-
addSystem(`[Tool ${toolResult.id}: Error]`, "tool_result", undefined, true);
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
onMetrics: async (metrics) => {
|
|
227
|
-
const apiTokens = metrics.usage.input_tokens + metrics.usage.output_tokens;
|
|
228
|
-
if (apiTokens > 0) {
|
|
229
|
-
setTokenCount(apiTokens);
|
|
230
|
-
}
|
|
231
|
-
await sessionStore.saveMetrics(metrics);
|
|
232
|
-
},
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// Update API messages (MessageStore will convert to UI messages)
|
|
236
|
-
// Note: result.messages already includes newUserMsg, so we don't add it again
|
|
237
|
-
// Only add the NEW messages from the result (skip ones we already have)
|
|
238
|
-
addApiMessages(result.messages.slice(apiMessages.length));
|
|
239
|
-
setTotalCost((prev) => prev + result.totalCost);
|
|
240
|
-
|
|
241
|
-
// Estimate tokens from final messages
|
|
242
|
-
const estimatedTokens = estimateMessagesTokens(result.messages);
|
|
243
|
-
setTokenCount(estimatedTokens);
|
|
244
|
-
|
|
245
|
-
// Save to session
|
|
246
|
-
const lastUserMsg = result.messages[result.messages.length - 2];
|
|
247
|
-
const lastAssistantMsg = result.messages[result.messages.length - 1];
|
|
248
|
-
if (lastUserMsg) await sessionStore.saveMessage(lastUserMsg);
|
|
249
|
-
if (lastAssistantMsg) await sessionStore.saveMessage(lastAssistantMsg);
|
|
250
|
-
|
|
251
|
-
} catch (error) {
|
|
252
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
253
|
-
addSystem(`Error: ${errorMessage}`, "error");
|
|
254
|
-
} finally {
|
|
255
|
-
setIsLoading(false);
|
|
256
|
-
isProcessingRef.current = false;
|
|
257
|
-
setStreamingText("");
|
|
258
|
-
}
|
|
259
|
-
}, [apiMessages, apiKey, model, maxTokens, systemPrompt, tools, permissionMode, workingDirectory, hookManager, sessionId, sessionStore, addMessage, addSystem, addApiMessages, setTokenCount]);
|
|
260
|
-
|
|
261
|
-
// Handle commands
|
|
262
|
-
const handleCommandWrapper = useCallback(async (cmd: string) => {
|
|
263
|
-
// Import command context dynamically to avoid circular deps
|
|
264
|
-
const { setMessages: setExternalMessages, processedCountRef } = {
|
|
265
|
-
setMessages: () => {}, // MessageStore handles this now
|
|
266
|
-
processedCountRef: { current: apiMessages.length },
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
await handleCommand(cmd, {
|
|
270
|
-
sessionId,
|
|
271
|
-
setSessionId,
|
|
272
|
-
model,
|
|
273
|
-
setModel,
|
|
274
|
-
apiMessages,
|
|
275
|
-
setApiMessages: (msgs) => addApiMessages(msgs.slice(apiMessages.length)),
|
|
276
|
-
setMessages: setExternalMessages,
|
|
277
|
-
processedCountRef,
|
|
278
|
-
totalCost,
|
|
279
|
-
setTotalCost,
|
|
280
|
-
totalTokens: tokenCount,
|
|
281
|
-
setTotalTokens: setTokenCount,
|
|
282
|
-
permissionMode,
|
|
283
|
-
tools,
|
|
284
|
-
workingDirectory,
|
|
285
|
-
sessionStore,
|
|
286
|
-
addSystemMessage: (content: string, subType?: MessageSubType, toolName?: string, isError?: boolean) => {
|
|
287
|
-
addSystem(content, subType, toolName, isError);
|
|
288
|
-
},
|
|
289
|
-
messagesLength: messages.length,
|
|
290
|
-
onExit,
|
|
291
|
-
exit,
|
|
292
|
-
sessionSelectMode,
|
|
293
|
-
setSessionSelectMode,
|
|
294
|
-
setSelectableSessions,
|
|
295
|
-
helpMode,
|
|
296
|
-
setHelpMode,
|
|
297
|
-
helpSection,
|
|
298
|
-
setHelpSection,
|
|
299
|
-
});
|
|
300
|
-
}, [sessionId, setSessionId, model, apiMessages, addApiMessages, totalCost, tokenCount, setTokenCount, permissionMode, tools, workingDirectory, sessionStore, addSystem, messages.length, onExit, exit, sessionSelectMode, helpMode, helpSection]);
|
|
301
|
-
|
|
302
|
-
// Handle input with native terminal input
|
|
303
|
-
useNativeInput({
|
|
304
|
-
isActive: true,
|
|
305
|
-
onKey: (event) => {
|
|
306
|
-
// Scroll handling
|
|
307
|
-
if (KeyEvents.isPageUp(event)) {
|
|
308
|
-
setScrollOffset((prev) => prev + 5);
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (KeyEvents.isPageDown(event)) {
|
|
313
|
-
setScrollOffset((prev) => Math.max(0, prev - 5));
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (KeyEvents.isShiftUp(event)) {
|
|
318
|
-
setScrollOffset((prev) => prev + 1);
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (KeyEvents.isShiftDown(event)) {
|
|
323
|
-
setScrollOffset((prev) => Math.max(0, prev - 1));
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Ctrl+C to exit
|
|
328
|
-
if (KeyEvents.isCtrlC(event)) {
|
|
329
|
-
onExit();
|
|
330
|
-
exit();
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (isLoading) return;
|
|
335
|
-
|
|
336
|
-
// Help mode navigation
|
|
337
|
-
if (helpMode) {
|
|
338
|
-
const HELP_SECTIONS_COUNT = 5;
|
|
339
|
-
|
|
340
|
-
if (event.code === "escape" || event.code === "q") {
|
|
341
|
-
setHelpMode(false);
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (event.code === "tab" || KeyEvents.isRight(event)) {
|
|
346
|
-
setHelpSection((prev) => (prev + 1) % HELP_SECTIONS_COUNT);
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (KeyEvents.isLeft(event)) {
|
|
351
|
-
setHelpSection((prev) => (prev - 1 + HELP_SECTIONS_COUNT) % HELP_SECTIONS_COUNT);
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Session selection mode
|
|
359
|
-
if (sessionSelectMode) {
|
|
360
|
-
const num = parseInt(event.code, 10);
|
|
361
|
-
if (!isNaN(num) && num >= 1 && num <= selectableSessions.length) {
|
|
362
|
-
const selectedSession = selectableSessions[num - 1];
|
|
363
|
-
if (selectedSession) {
|
|
364
|
-
setSessionSelectMode(false);
|
|
365
|
-
setSelectableSessions([]);
|
|
366
|
-
handleCommandWrapper(`/resume ${selectedSession.id}`);
|
|
367
|
-
}
|
|
368
|
-
} else if (KeyEvents.isEnter(event) || (event.code && isNaN(num))) {
|
|
369
|
-
setSessionSelectMode(false);
|
|
370
|
-
setSelectableSessions([]);
|
|
371
|
-
addSystem("Session selection cancelled.");
|
|
372
|
-
}
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Submit on Enter
|
|
377
|
-
if (KeyEvents.isEnter(event)) {
|
|
378
|
-
// Prevent duplicate submissions while processing
|
|
379
|
-
if (isProcessingRef.current) return;
|
|
380
|
-
|
|
381
|
-
const currentInput = inputRef.current;
|
|
382
|
-
if (currentInput.trim()) {
|
|
383
|
-
// Capture value BEFORE clearing
|
|
384
|
-
const valueToSubmit = currentInput;
|
|
385
|
-
|
|
386
|
-
// Clear input IMMEDIATELY to prevent duplicate submissions
|
|
387
|
-
// This must happen before any async operations
|
|
388
|
-
inputRef.current = "";
|
|
389
|
-
setGlobalInput("", 0);
|
|
390
|
-
|
|
391
|
-
// Add user message to UI
|
|
392
|
-
addMessage({ role: "user", content: valueToSubmit });
|
|
393
|
-
|
|
394
|
-
// Clear cursor and history state
|
|
395
|
-
setCursorPos(0);
|
|
396
|
-
setHistoryIndex(-1);
|
|
397
|
-
setSavedInput("");
|
|
398
|
-
|
|
399
|
-
// Update history
|
|
400
|
-
if (!valueToSubmit.startsWith("/") && valueToSubmit !== inputHistory[0]) {
|
|
401
|
-
setInputHistory((prev) => [valueToSubmit, ...prev].slice(0, 100));
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Process after UI updates
|
|
405
|
-
setTimeout(() => {
|
|
406
|
-
if (valueToSubmit.startsWith("/")) {
|
|
407
|
-
handleCommandWrapper(valueToSubmit);
|
|
408
|
-
} else {
|
|
409
|
-
processMessage(valueToSubmit, true); // true = message already added
|
|
410
|
-
}
|
|
411
|
-
}, 50);
|
|
412
|
-
}
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// History navigation
|
|
417
|
-
if (KeyEvents.isUp(event)) {
|
|
418
|
-
if (inputHistory.length > 0) {
|
|
419
|
-
if (historyIndex === -1) {
|
|
420
|
-
setSavedInput(inputRef.current);
|
|
421
|
-
}
|
|
422
|
-
const newIndex = Math.min(historyIndex + 1, inputHistory.length - 1);
|
|
423
|
-
setHistoryIndex(newIndex);
|
|
424
|
-
setInputValue(inputHistory[newIndex] ?? "");
|
|
425
|
-
setCursorPos((inputHistory[newIndex] ?? "").length);
|
|
426
|
-
}
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (KeyEvents.isDown(event)) {
|
|
431
|
-
if (historyIndex > 0) {
|
|
432
|
-
const newIndex = historyIndex - 1;
|
|
433
|
-
setHistoryIndex(newIndex);
|
|
434
|
-
setInputValue(inputHistory[newIndex] ?? "");
|
|
435
|
-
setCursorPos((inputHistory[newIndex] ?? "").length);
|
|
436
|
-
} else if (historyIndex === 0) {
|
|
437
|
-
setHistoryIndex(-1);
|
|
438
|
-
setInputValue(savedInput);
|
|
439
|
-
setCursorPos(savedInput.length);
|
|
440
|
-
}
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Text editing
|
|
445
|
-
if (KeyEvents.isBackspace(event)) {
|
|
446
|
-
const currentInput = inputRef.current;
|
|
447
|
-
const currentCursor = cursorRef.current;
|
|
448
|
-
if (currentCursor > 0) {
|
|
449
|
-
const newVal = currentInput.slice(0, currentCursor - 1) + currentInput.slice(currentCursor);
|
|
450
|
-
setInputValue(newVal);
|
|
451
|
-
setCursorPos((p) => p - 1);
|
|
452
|
-
}
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (KeyEvents.isDelete(event)) {
|
|
457
|
-
const currentInput = inputRef.current;
|
|
458
|
-
const currentCursor = cursorRef.current;
|
|
459
|
-
if (currentCursor < currentInput.length) {
|
|
460
|
-
const newVal = currentInput.slice(0, currentCursor) + currentInput.slice(currentCursor + 1);
|
|
461
|
-
setInputValue(newVal);
|
|
462
|
-
}
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (KeyEvents.isLeft(event)) {
|
|
467
|
-
setCursorPos((p) => Math.max(0, p - 1));
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (KeyEvents.isRight(event)) {
|
|
472
|
-
setCursorPos((p) => Math.min(inputRef.current.length, p + 1));
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (KeyEvents.isHome(event) || KeyEvents.isCtrlA(event)) {
|
|
477
|
-
setCursorPos(0);
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (KeyEvents.isEnd(event) || KeyEvents.isCtrlE(event)) {
|
|
482
|
-
setCursorPos(inputRef.current.length);
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Regular character
|
|
487
|
-
if (KeyEvents.isPrintable(event)) {
|
|
488
|
-
if (historyIndex !== -1) {
|
|
489
|
-
setHistoryIndex(-1);
|
|
490
|
-
setSavedInput("");
|
|
491
|
-
}
|
|
492
|
-
const currentInput = inputRef.current;
|
|
493
|
-
const currentCursor = cursorRef.current;
|
|
494
|
-
setInputValue(currentInput.slice(0, currentCursor) + event.code + currentInput.slice(currentCursor));
|
|
495
|
-
setCursorPos((p) => p + 1);
|
|
496
|
-
}
|
|
497
|
-
},
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
return (
|
|
501
|
-
<InputProvider initialBlocked={isLoading}>
|
|
502
|
-
<Box flexDirection="column" width="100%">
|
|
503
|
-
<MessageArea
|
|
504
|
-
messages={messages}
|
|
505
|
-
isLoading={isLoading}
|
|
506
|
-
spinnerFrame={spinnerFrame}
|
|
507
|
-
height={messageHeight}
|
|
508
|
-
scrollOffset={scrollOffset}
|
|
509
|
-
contextWarning={contextWarning}
|
|
510
|
-
streamingText={streamingText}
|
|
511
|
-
/>
|
|
512
|
-
|
|
513
|
-
<Text dimColor>
|
|
514
|
-
{isLoading ? spinnerFrame : ""} Context: {tokenCount} tokens | {permissionMode}
|
|
515
|
-
</Text>
|
|
516
|
-
|
|
517
|
-
<InputField
|
|
518
|
-
placeholder="Type your message... (/help for commands)"
|
|
519
|
-
isActive={!isLoading}
|
|
520
|
-
/>
|
|
521
|
-
</Box>
|
|
522
|
-
</InputProvider>
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Main Interactive TUI Component with providers
|
|
528
|
-
*/
|
|
529
|
-
function InteractiveTUI(props: InteractiveTUIProps) {
|
|
530
|
-
return (
|
|
531
|
-
<MessageStoreProvider initialMessages={props.initialMessages}>
|
|
532
|
-
<InteractiveTUIInner {...props} />
|
|
533
|
-
</MessageStoreProvider>
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
export default InteractiveTUI;
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/** @jsx React.createElement */
|
|
2
|
-
/** @jsxFrag React.Fragment */
|
|
3
|
-
/**
|
|
4
|
-
* Message Area Component - Simple plain text display
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import React from "react";
|
|
8
|
-
import { Text } from "ink";
|
|
9
|
-
import type { MessageAreaProps, UIMessage, MessageSubType } from "./types.js";
|
|
10
|
-
import { VERSION } from "../shared/status-line.js";
|
|
11
|
-
|
|
12
|
-
function getMessageColor(role: UIMessage["role"], subType?: MessageSubType, isError?: boolean): string {
|
|
13
|
-
if (role === "system") {
|
|
14
|
-
if (isError || subType === "error") return "red";
|
|
15
|
-
if (subType === "tool_call") return "blue";
|
|
16
|
-
if (subType === "tool_result") return "green";
|
|
17
|
-
if (subType === "hook") return "yellow";
|
|
18
|
-
if (subType === "thinking") return "gray";
|
|
19
|
-
if (subType === "info") return "cyan";
|
|
20
|
-
return "yellow";
|
|
21
|
-
}
|
|
22
|
-
switch (role) {
|
|
23
|
-
case "user": return "cyan";
|
|
24
|
-
case "assistant": return "magenta";
|
|
25
|
-
default: return "white";
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getMessageLabel(message: UIMessage): string {
|
|
30
|
-
const { role, subType, toolName, isError } = message;
|
|
31
|
-
|
|
32
|
-
if (role === "system" && subType) {
|
|
33
|
-
switch (subType) {
|
|
34
|
-
case "tool_call": return toolName ? `[${toolName}]` : "[Tool]";
|
|
35
|
-
case "tool_result": return isError ? "[Error]" : "[Result]";
|
|
36
|
-
case "hook": return "[Hook]";
|
|
37
|
-
case "error": return "[Error]";
|
|
38
|
-
case "thinking": return "[Thinking]";
|
|
39
|
-
case "info": return "[Info]";
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
switch (role) {
|
|
44
|
-
case "user": return "You:";
|
|
45
|
-
case "system": return "[System]";
|
|
46
|
-
case "assistant": return "Claude:";
|
|
47
|
-
default: return "";
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function MessageArea({
|
|
52
|
-
messages,
|
|
53
|
-
isLoading,
|
|
54
|
-
spinnerFrame,
|
|
55
|
-
height,
|
|
56
|
-
scrollOffset = 0,
|
|
57
|
-
contextWarning,
|
|
58
|
-
streamingText,
|
|
59
|
-
}: MessageAreaProps) {
|
|
60
|
-
const totalMessages = messages.length;
|
|
61
|
-
const maxVisibleMessages = 50;
|
|
62
|
-
const endIdx = totalMessages - scrollOffset;
|
|
63
|
-
const startIdx = Math.max(0, endIdx - maxVisibleMessages);
|
|
64
|
-
const visibleMessages = messages.slice(startIdx, endIdx);
|
|
65
|
-
|
|
66
|
-
const isEmpty = visibleMessages.length === 0 && !isLoading && !streamingText;
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<>
|
|
70
|
-
{contextWarning && (
|
|
71
|
-
<Text color="yellow" bold>Warning: {contextWarning}{"\n"}</Text>
|
|
72
|
-
)}
|
|
73
|
-
|
|
74
|
-
{isEmpty && (
|
|
75
|
-
<Text dimColor>Welcome to Coder v{VERSION}. Type your message or /help for commands.{"\n"}</Text>
|
|
76
|
-
)}
|
|
77
|
-
|
|
78
|
-
{visibleMessages.map((msg) => {
|
|
79
|
-
const displayContent = msg.content.length > 2000
|
|
80
|
-
? msg.content.slice(0, 2000) + "..."
|
|
81
|
-
: msg.content;
|
|
82
|
-
const color = getMessageColor(msg.role, msg.subType, msg.isError);
|
|
83
|
-
const label = getMessageLabel(msg);
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<Text key={msg.id}>
|
|
87
|
-
<Text bold color={color}>{label} </Text>
|
|
88
|
-
<Text dimColor={msg.role === "system"}>{displayContent}{"\n"}</Text>
|
|
89
|
-
</Text>
|
|
90
|
-
);
|
|
91
|
-
})}
|
|
92
|
-
|
|
93
|
-
{streamingText && (
|
|
94
|
-
<Text>
|
|
95
|
-
<Text bold color="magenta">Claude: </Text>
|
|
96
|
-
<Text dimColor>{streamingText.length > 500 ? "..." + streamingText.slice(-500) : streamingText}{"\n"}</Text>
|
|
97
|
-
</Text>
|
|
98
|
-
)}
|
|
99
|
-
|
|
100
|
-
{isLoading && !streamingText && (
|
|
101
|
-
<Text color="cyan">{spinnerFrame} Processing...{"\n"}</Text>
|
|
102
|
-
)}
|
|
103
|
-
</>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export default MessageArea;
|