deepagentsdk 0.9.2
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/LICENSE +21 -0
- package/README.md +159 -0
- package/package.json +95 -0
- package/src/agent.ts +1230 -0
- package/src/backends/composite.ts +273 -0
- package/src/backends/filesystem.ts +692 -0
- package/src/backends/index.ts +22 -0
- package/src/backends/local-sandbox.ts +175 -0
- package/src/backends/persistent.ts +593 -0
- package/src/backends/sandbox.ts +510 -0
- package/src/backends/state.ts +244 -0
- package/src/backends/utils.ts +287 -0
- package/src/checkpointer/file-saver.ts +98 -0
- package/src/checkpointer/index.ts +5 -0
- package/src/checkpointer/kv-saver.ts +82 -0
- package/src/checkpointer/memory-saver.ts +82 -0
- package/src/checkpointer/types.ts +125 -0
- package/src/cli/components/ApiKeyInput.tsx +300 -0
- package/src/cli/components/FilePreview.tsx +237 -0
- package/src/cli/components/Input.tsx +277 -0
- package/src/cli/components/Message.tsx +93 -0
- package/src/cli/components/ModelSelection.tsx +338 -0
- package/src/cli/components/SlashMenu.tsx +101 -0
- package/src/cli/components/StatusBar.tsx +89 -0
- package/src/cli/components/Subagent.tsx +91 -0
- package/src/cli/components/TodoList.tsx +133 -0
- package/src/cli/components/ToolApproval.tsx +70 -0
- package/src/cli/components/ToolCall.tsx +144 -0
- package/src/cli/components/ToolCallSummary.tsx +175 -0
- package/src/cli/components/Welcome.tsx +75 -0
- package/src/cli/components/index.ts +24 -0
- package/src/cli/hooks/index.ts +12 -0
- package/src/cli/hooks/useAgent.ts +933 -0
- package/src/cli/index.tsx +1066 -0
- package/src/cli/theme.ts +205 -0
- package/src/cli/utils/model-list.ts +365 -0
- package/src/constants/errors.ts +29 -0
- package/src/constants/limits.ts +195 -0
- package/src/index.ts +176 -0
- package/src/middleware/agent-memory.ts +330 -0
- package/src/prompts.ts +196 -0
- package/src/skills/index.ts +2 -0
- package/src/skills/load.ts +191 -0
- package/src/skills/types.ts +53 -0
- package/src/tools/execute.ts +167 -0
- package/src/tools/filesystem.ts +418 -0
- package/src/tools/index.ts +39 -0
- package/src/tools/subagent.ts +443 -0
- package/src/tools/todos.ts +101 -0
- package/src/tools/web.ts +567 -0
- package/src/types/backend.ts +177 -0
- package/src/types/core.ts +220 -0
- package/src/types/events.ts +429 -0
- package/src/types/index.ts +94 -0
- package/src/types/structured-output.ts +43 -0
- package/src/types/subagent.ts +96 -0
- package/src/types.ts +22 -0
- package/src/utils/approval.ts +213 -0
- package/src/utils/events.ts +416 -0
- package/src/utils/eviction.ts +181 -0
- package/src/utils/index.ts +34 -0
- package/src/utils/model-parser.ts +38 -0
- package/src/utils/patch-tool-calls.ts +233 -0
- package/src/utils/project-detection.ts +32 -0
- package/src/utils/summarization.ts +254 -0
|
@@ -0,0 +1,1066 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Deep Agent CLI - Interactive terminal interface using Ink.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* ANTHROPIC_API_KEY=xxx bunx deep-agent-ink
|
|
7
|
+
* ANTHROPIC_API_KEY=xxx bun src/cli-ink/index.tsx
|
|
8
|
+
*
|
|
9
|
+
* Or with options:
|
|
10
|
+
* ANTHROPIC_API_KEY=xxx bunx deep-agent-ink --model anthropic/claude-sonnet-4-20250514
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
14
|
+
import { render, useApp, useInput, Box, Text, Static } from "ink";
|
|
15
|
+
import { LocalSandbox } from "../backends/local-sandbox.js";
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_EVICTION_TOKEN_LIMIT,
|
|
18
|
+
DEFAULT_SUMMARIZATION_THRESHOLD,
|
|
19
|
+
DEFAULT_KEEP_MESSAGES,
|
|
20
|
+
CONTEXT_WINDOW,
|
|
21
|
+
} from "../constants/limits";
|
|
22
|
+
import { FileSaver } from "../checkpointer/file-saver.js";
|
|
23
|
+
import { useAgent, type AgentEventLog } from "./hooks/useAgent.js";
|
|
24
|
+
import {
|
|
25
|
+
Welcome,
|
|
26
|
+
WelcomeHint,
|
|
27
|
+
Input,
|
|
28
|
+
SlashMenuPanel,
|
|
29
|
+
Message,
|
|
30
|
+
StreamingMessage,
|
|
31
|
+
TodoList,
|
|
32
|
+
FilePreview,
|
|
33
|
+
FileWritten,
|
|
34
|
+
FileEdited,
|
|
35
|
+
FileRead,
|
|
36
|
+
LsResult,
|
|
37
|
+
GlobResult,
|
|
38
|
+
GrepResult,
|
|
39
|
+
FileList,
|
|
40
|
+
ToolCall,
|
|
41
|
+
StepIndicator,
|
|
42
|
+
ThinkingIndicator,
|
|
43
|
+
DoneIndicator,
|
|
44
|
+
ErrorDisplay,
|
|
45
|
+
SubagentStart,
|
|
46
|
+
SubagentFinish,
|
|
47
|
+
StatusBar,
|
|
48
|
+
ModelSelectionPanel,
|
|
49
|
+
ApiKeyInputPanel,
|
|
50
|
+
ToolApproval,
|
|
51
|
+
type MessageData,
|
|
52
|
+
} from "./components/index.js";
|
|
53
|
+
import { parseCommand, colors, SLASH_COMMANDS } from "./theme.js";
|
|
54
|
+
import type { FileInfo } from "../types.js";
|
|
55
|
+
import { estimateMessagesTokens } from "../utils/summarization.js";
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// CLI Arguments
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
interface CLIOptions {
|
|
62
|
+
model?: string;
|
|
63
|
+
maxSteps?: number;
|
|
64
|
+
systemPrompt?: string;
|
|
65
|
+
workDir?: string;
|
|
66
|
+
session?: string;
|
|
67
|
+
// New feature flags
|
|
68
|
+
enablePromptCaching?: boolean;
|
|
69
|
+
toolResultEvictionLimit?: number;
|
|
70
|
+
enableSummarization?: boolean;
|
|
71
|
+
summarizationThreshold?: number;
|
|
72
|
+
summarizationKeepMessages?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Default values for features (enabled by default)
|
|
76
|
+
const DEFAULT_PROMPT_CACHING = true;
|
|
77
|
+
const DEFAULT_EVICTION_LIMIT = DEFAULT_EVICTION_TOKEN_LIMIT;
|
|
78
|
+
const DEFAULT_SUMMARIZATION = true;
|
|
79
|
+
const DEFAULT_SUMMARIZATION_THRESHOLD_VALUE = DEFAULT_SUMMARIZATION_THRESHOLD;
|
|
80
|
+
const DEFAULT_SUMMARIZATION_KEEP = DEFAULT_KEEP_MESSAGES;
|
|
81
|
+
|
|
82
|
+
function parseArgs(): CLIOptions {
|
|
83
|
+
const args = process.argv.slice(2);
|
|
84
|
+
// Start with defaults enabled
|
|
85
|
+
const options: CLIOptions = {
|
|
86
|
+
enablePromptCaching: DEFAULT_PROMPT_CACHING,
|
|
87
|
+
toolResultEvictionLimit: DEFAULT_EVICTION_LIMIT,
|
|
88
|
+
enableSummarization: DEFAULT_SUMMARIZATION,
|
|
89
|
+
summarizationThreshold: DEFAULT_SUMMARIZATION_THRESHOLD_VALUE,
|
|
90
|
+
summarizationKeepMessages: DEFAULT_SUMMARIZATION_KEEP,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < args.length; i++) {
|
|
94
|
+
const arg = args[i];
|
|
95
|
+
|
|
96
|
+
if (arg === "--model" || arg === "-m") {
|
|
97
|
+
options.model = args[++i];
|
|
98
|
+
} else if (arg === "--max-steps" || arg === "-s") {
|
|
99
|
+
const val = args[++i];
|
|
100
|
+
if (val) options.maxSteps = parseInt(val, 10);
|
|
101
|
+
} else if (arg === "--prompt" || arg === "-p") {
|
|
102
|
+
options.systemPrompt = args[++i];
|
|
103
|
+
} else if (arg === "--dir" || arg === "-d") {
|
|
104
|
+
options.workDir = args[++i];
|
|
105
|
+
} else if (arg === "--cache" || arg === "--prompt-caching") {
|
|
106
|
+
options.enablePromptCaching = true;
|
|
107
|
+
} else if (arg === "--no-cache" || arg === "--no-prompt-caching") {
|
|
108
|
+
options.enablePromptCaching = false;
|
|
109
|
+
} else if (arg === "--eviction-limit" || arg === "-e") {
|
|
110
|
+
const val = args[++i];
|
|
111
|
+
if (val) options.toolResultEvictionLimit = parseInt(val, 10);
|
|
112
|
+
} else if (arg === "--no-eviction") {
|
|
113
|
+
options.toolResultEvictionLimit = 0;
|
|
114
|
+
} else if (arg === "--summarize" || arg === "--auto-summarize") {
|
|
115
|
+
options.enableSummarization = true;
|
|
116
|
+
} else if (arg === "--no-summarize" || arg === "--no-auto-summarize") {
|
|
117
|
+
options.enableSummarization = false;
|
|
118
|
+
} else if (arg === "--summarize-threshold") {
|
|
119
|
+
const val = args[++i];
|
|
120
|
+
if (val) {
|
|
121
|
+
options.summarizationThreshold = parseInt(val, 10);
|
|
122
|
+
options.enableSummarization = true;
|
|
123
|
+
}
|
|
124
|
+
} else if (arg === "--summarize-keep") {
|
|
125
|
+
const val = args[++i];
|
|
126
|
+
if (val) {
|
|
127
|
+
options.summarizationKeepMessages = parseInt(val, 10);
|
|
128
|
+
options.enableSummarization = true;
|
|
129
|
+
}
|
|
130
|
+
} else if (arg === "--session") {
|
|
131
|
+
const val = args[++i];
|
|
132
|
+
if (val) options.session = val;
|
|
133
|
+
} else if (arg && arg.startsWith("--session=")) {
|
|
134
|
+
const sessionVal = arg.split("=")[1];
|
|
135
|
+
if (sessionVal) options.session = sessionVal;
|
|
136
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
137
|
+
printHelp();
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return options;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const DEEP_AGENTS_ASCII = `
|
|
146
|
+
█████╗ ██╗ ███████╗ ██████╗ ██╗ ██╗
|
|
147
|
+
██╔══██╗ ██║ ██╔════╝ ██╔══██╗ ██║ ██╔╝
|
|
148
|
+
███████║ ██║█████╗███████╗ ██║ ██║ █████╔╝
|
|
149
|
+
██╔══██║ ██║╚════╝╚════██║ ██║ ██║ ██╔═██╗
|
|
150
|
+
██║ ██║ ██║ ███████║ ██████╔╝ ██║ ██╗
|
|
151
|
+
╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝
|
|
152
|
+
|
|
153
|
+
██████╗ ███████╗ ███████╗ ██████╗
|
|
154
|
+
██╔══██╗ ██╔════╝ ██╔════╝ ██╔══██╗
|
|
155
|
+
██║ ██║ █████╗ █████╗ ██████╔╝
|
|
156
|
+
██║ ██║ ██╔══╝ ██╔══╝ ██╔═══╝
|
|
157
|
+
██████╔╝ ███████╗ ███████╗ ██║
|
|
158
|
+
╚═════╝ ╚══════╝ ╚══════╝ ╚═╝
|
|
159
|
+
|
|
160
|
+
█████╗ ██████╗ ███████╗ ███╗ ██╗ ████████╗
|
|
161
|
+
██╔══██╗ ██╔════╝ ██╔════╝ ████╗ ██║ ╚══██╔══╝
|
|
162
|
+
███████║ ██║ ███╗ █████╗ ██╔██╗ ██║ ██║
|
|
163
|
+
██╔══██║ ██║ ██║ ██╔══╝ ██║╚██╗██║ ██║
|
|
164
|
+
██║ ██║ ╚██████╔╝ ███████╗ ██║ ╚████║ ██║
|
|
165
|
+
╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═══╝ ╚═╝
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
function printHelp(): void {
|
|
170
|
+
console.log(`
|
|
171
|
+
${DEEP_AGENTS_ASCII}
|
|
172
|
+
|
|
173
|
+
Usage:
|
|
174
|
+
bun src/cli-ink/index.tsx [options]
|
|
175
|
+
|
|
176
|
+
Options:
|
|
177
|
+
--model, -m <model> Model to use (default: anthropic/claude-haiku-4-5-20251001)
|
|
178
|
+
--max-steps, -s <number> Maximum steps per generation (default: 100)
|
|
179
|
+
--prompt, -p <prompt> Custom system prompt
|
|
180
|
+
--dir, -d <directory> Working directory for file operations (default: current dir)
|
|
181
|
+
--help, -h Show this help
|
|
182
|
+
|
|
183
|
+
Performance & Memory (all enabled by default):
|
|
184
|
+
--no-cache Disable prompt caching (enabled by default for Anthropic)
|
|
185
|
+
--no-eviction Disable tool result eviction (enabled by default: 20k tokens)
|
|
186
|
+
--eviction-limit, -e <n> Set custom eviction token limit
|
|
187
|
+
--no-summarize Disable auto-summarization (enabled by default)
|
|
188
|
+
--summarize-threshold <n> Token threshold to trigger summarization (default: 170000)
|
|
189
|
+
--summarize-keep <n> Number of recent messages to keep intact (default: 6)
|
|
190
|
+
|
|
191
|
+
Runtime Commands:
|
|
192
|
+
/cache on|off Toggle prompt caching
|
|
193
|
+
/eviction on|off Toggle tool result eviction
|
|
194
|
+
/summarize on|off Toggle auto-summarization
|
|
195
|
+
/features Show current feature status
|
|
196
|
+
|
|
197
|
+
API Keys:
|
|
198
|
+
The CLI automatically loads API keys from:
|
|
199
|
+
1. Environment variables (ANTHROPIC_API_KEY, OPENAI_API_KEY, TAVILY_API_KEY)
|
|
200
|
+
2. .env or .env.local file in the working directory
|
|
201
|
+
|
|
202
|
+
Example .env file:
|
|
203
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
204
|
+
OPENAI_API_KEY=sk-...
|
|
205
|
+
TAVILY_API_KEY=tvly-... # For web_search tool
|
|
206
|
+
|
|
207
|
+
Examples:
|
|
208
|
+
bun src/cli-ink/index.tsx # uses .env file
|
|
209
|
+
bun src/cli-ink/index.tsx --dir ./my-project # loads .env from ./my-project
|
|
210
|
+
ANTHROPIC_API_KEY=xxx bun src/cli-ink/index.tsx # env var takes precedence
|
|
211
|
+
bun src/cli-ink/index.tsx --model anthropic/claude-sonnet-4-20250514
|
|
212
|
+
`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ============================================================================
|
|
216
|
+
// Main App Component
|
|
217
|
+
// ============================================================================
|
|
218
|
+
|
|
219
|
+
interface AppProps {
|
|
220
|
+
options: CLIOptions;
|
|
221
|
+
backend: LocalSandbox;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
type PanelView = "none" | "help" | "todos" | "files" | "file-content" | "apikey-input" | "features" | "tokens" | "models";
|
|
225
|
+
|
|
226
|
+
interface PanelState {
|
|
227
|
+
view: PanelView;
|
|
228
|
+
fileContent?: string;
|
|
229
|
+
filePath?: string;
|
|
230
|
+
files?: FileInfo[];
|
|
231
|
+
tokenCount?: number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function App({ options, backend }: AppProps): React.ReactElement {
|
|
235
|
+
const { exit } = useApp();
|
|
236
|
+
|
|
237
|
+
// Build summarization config if enabled
|
|
238
|
+
const summarizationConfig = options.enableSummarization
|
|
239
|
+
? {
|
|
240
|
+
enabled: true,
|
|
241
|
+
tokenThreshold: options.summarizationThreshold,
|
|
242
|
+
keepMessages: options.summarizationKeepMessages,
|
|
243
|
+
}
|
|
244
|
+
: undefined;
|
|
245
|
+
|
|
246
|
+
// Create checkpointer if session is provided
|
|
247
|
+
const checkpointer = options.session
|
|
248
|
+
? new FileSaver({ dir: "./.checkpoints" })
|
|
249
|
+
: undefined;
|
|
250
|
+
|
|
251
|
+
// Agent hook with new feature options
|
|
252
|
+
const agent = useAgent({
|
|
253
|
+
model: options.model || "anthropic/claude-haiku-4-5-20251001",
|
|
254
|
+
maxSteps: options.maxSteps || 100,
|
|
255
|
+
systemPrompt: options.systemPrompt,
|
|
256
|
+
backend,
|
|
257
|
+
enablePromptCaching: options.enablePromptCaching,
|
|
258
|
+
toolResultEvictionLimit: options.toolResultEvictionLimit,
|
|
259
|
+
summarization: summarizationConfig,
|
|
260
|
+
sessionId: options.session,
|
|
261
|
+
checkpointer,
|
|
262
|
+
// Default interruptOn config for CLI - safe defaults
|
|
263
|
+
interruptOn: {
|
|
264
|
+
execute: true,
|
|
265
|
+
write_file: true,
|
|
266
|
+
edit_file: true,
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// UI state
|
|
271
|
+
const [messages, setMessages] = useState<MessageData[]>([]);
|
|
272
|
+
const [showWelcome, setShowWelcome] = useState(true);
|
|
273
|
+
const [panel, setPanel] = useState<PanelState>({ view: "none" });
|
|
274
|
+
|
|
275
|
+
// Handle Ctrl+C to abort generation
|
|
276
|
+
useInput((input, key) => {
|
|
277
|
+
if (key.ctrl && input === "c") {
|
|
278
|
+
if (agent.status !== "idle") {
|
|
279
|
+
agent.abort();
|
|
280
|
+
} else {
|
|
281
|
+
exit();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (key.ctrl && input === "d") {
|
|
285
|
+
exit();
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Handle input submission
|
|
290
|
+
const handleSubmit = useCallback(
|
|
291
|
+
async (input: string) => {
|
|
292
|
+
const trimmed = input.trim();
|
|
293
|
+
if (!trimmed) return;
|
|
294
|
+
|
|
295
|
+
// Hide welcome on first input
|
|
296
|
+
if (showWelcome) {
|
|
297
|
+
setShowWelcome(false);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check for commands
|
|
301
|
+
const { isCommand, command, args } = parseCommand(trimmed);
|
|
302
|
+
|
|
303
|
+
if (isCommand) {
|
|
304
|
+
await handleCommand(command, args);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Clear any panel
|
|
309
|
+
setPanel({ view: "none" });
|
|
310
|
+
|
|
311
|
+
// Send to agent - user message is added to events by useAgent hook
|
|
312
|
+
// Events serve as the conversation history with proper formatting
|
|
313
|
+
await agent.sendPrompt(trimmed);
|
|
314
|
+
},
|
|
315
|
+
[showWelcome, agent]
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// Handle slash commands
|
|
319
|
+
const handleCommand = async (command?: string, args?: string) => {
|
|
320
|
+
// Show slash menu if just "/"
|
|
321
|
+
if (!command || command === "") {
|
|
322
|
+
setPanel({ view: "help" });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
switch (command) {
|
|
327
|
+
case "todos":
|
|
328
|
+
case "todo":
|
|
329
|
+
case "t":
|
|
330
|
+
setPanel({ view: "todos" });
|
|
331
|
+
break;
|
|
332
|
+
|
|
333
|
+
case "files":
|
|
334
|
+
case "file":
|
|
335
|
+
case "f":
|
|
336
|
+
try {
|
|
337
|
+
const files = await backend.lsInfo(".");
|
|
338
|
+
setPanel({ view: "files", files });
|
|
339
|
+
} catch (err) {
|
|
340
|
+
// Handle error
|
|
341
|
+
}
|
|
342
|
+
break;
|
|
343
|
+
|
|
344
|
+
case "read":
|
|
345
|
+
case "r":
|
|
346
|
+
if (!args) {
|
|
347
|
+
// Show usage
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
const content = await backend.read(args);
|
|
352
|
+
setPanel({ view: "file-content", filePath: args, fileContent: content });
|
|
353
|
+
} catch (err) {
|
|
354
|
+
// Handle error
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
|
|
358
|
+
case "apikey":
|
|
359
|
+
case "key":
|
|
360
|
+
case "api":
|
|
361
|
+
// Always show interactive API key panel
|
|
362
|
+
setPanel({ view: "apikey-input" });
|
|
363
|
+
break;
|
|
364
|
+
|
|
365
|
+
case "model":
|
|
366
|
+
if (args) {
|
|
367
|
+
agent.setModel(args.trim());
|
|
368
|
+
} else {
|
|
369
|
+
// Show available models if no args provided
|
|
370
|
+
setPanel({ view: "models" });
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
|
|
374
|
+
case "features":
|
|
375
|
+
case "feat":
|
|
376
|
+
setPanel({ view: "features" });
|
|
377
|
+
break;
|
|
378
|
+
|
|
379
|
+
case "tokens":
|
|
380
|
+
case "tok":
|
|
381
|
+
const tokenCount = estimateMessagesTokens(agent.messages);
|
|
382
|
+
setPanel({ view: "tokens", tokenCount });
|
|
383
|
+
break;
|
|
384
|
+
|
|
385
|
+
case "sessions":
|
|
386
|
+
case "session-list":
|
|
387
|
+
if (checkpointer) {
|
|
388
|
+
const sessions = await checkpointer.list();
|
|
389
|
+
if (sessions.length > 0) {
|
|
390
|
+
// Show sessions list
|
|
391
|
+
const sessionList = sessions.map(s => ` - ${s}`).join('\n');
|
|
392
|
+
setMessages((prev) => [
|
|
393
|
+
...prev,
|
|
394
|
+
{
|
|
395
|
+
id: `session-list-${Date.now()}`,
|
|
396
|
+
role: "assistant",
|
|
397
|
+
content: `Saved sessions:\n${sessionList}`,
|
|
398
|
+
timestamp: new Date(),
|
|
399
|
+
},
|
|
400
|
+
]);
|
|
401
|
+
} else {
|
|
402
|
+
setMessages((prev) => [
|
|
403
|
+
...prev,
|
|
404
|
+
{
|
|
405
|
+
id: `session-list-empty-${Date.now()}`,
|
|
406
|
+
role: "assistant",
|
|
407
|
+
content: "No saved sessions",
|
|
408
|
+
timestamp: new Date(),
|
|
409
|
+
},
|
|
410
|
+
]);
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
setMessages((prev) => [
|
|
414
|
+
...prev,
|
|
415
|
+
{
|
|
416
|
+
id: `session-error-${Date.now()}`,
|
|
417
|
+
role: "assistant",
|
|
418
|
+
content: "Checkpointing not enabled. Use --session to enable.",
|
|
419
|
+
timestamp: new Date(),
|
|
420
|
+
},
|
|
421
|
+
]);
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
|
|
425
|
+
case "session":
|
|
426
|
+
if (!args) {
|
|
427
|
+
setMessages((prev) => [
|
|
428
|
+
...prev,
|
|
429
|
+
{
|
|
430
|
+
id: `session-usage-${Date.now()}`,
|
|
431
|
+
role: "assistant",
|
|
432
|
+
content: "Usage: /session clear",
|
|
433
|
+
timestamp: new Date(),
|
|
434
|
+
},
|
|
435
|
+
]);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (args.trim() === "clear" && options.session && checkpointer) {
|
|
439
|
+
await checkpointer.delete(options.session);
|
|
440
|
+
setMessages([]);
|
|
441
|
+
agent.clear();
|
|
442
|
+
setShowWelcome(true);
|
|
443
|
+
setPanel({ view: "none" });
|
|
444
|
+
setMessages((prev) => [
|
|
445
|
+
...prev,
|
|
446
|
+
{
|
|
447
|
+
id: `session-cleared-${Date.now()}`,
|
|
448
|
+
role: "assistant",
|
|
449
|
+
content: "Session cleared.",
|
|
450
|
+
timestamp: new Date(),
|
|
451
|
+
},
|
|
452
|
+
]);
|
|
453
|
+
}
|
|
454
|
+
break;
|
|
455
|
+
|
|
456
|
+
case "clear":
|
|
457
|
+
case "c":
|
|
458
|
+
setMessages([]);
|
|
459
|
+
agent.clear();
|
|
460
|
+
setShowWelcome(true);
|
|
461
|
+
setPanel({ view: "none" });
|
|
462
|
+
break;
|
|
463
|
+
|
|
464
|
+
case "cache":
|
|
465
|
+
if (args === "on" || args === "true" || args === "1") {
|
|
466
|
+
agent.setPromptCaching(true);
|
|
467
|
+
} else if (args === "off" || args === "false" || args === "0") {
|
|
468
|
+
agent.setPromptCaching(false);
|
|
469
|
+
} else {
|
|
470
|
+
// Toggle if no arg
|
|
471
|
+
agent.setPromptCaching(!agent.features.promptCaching);
|
|
472
|
+
}
|
|
473
|
+
setPanel({ view: "features" });
|
|
474
|
+
break;
|
|
475
|
+
|
|
476
|
+
case "eviction":
|
|
477
|
+
case "evict":
|
|
478
|
+
if (args === "on" || args === "true" || args === "1") {
|
|
479
|
+
agent.setEviction(true);
|
|
480
|
+
} else if (args === "off" || args === "false" || args === "0") {
|
|
481
|
+
agent.setEviction(false);
|
|
482
|
+
} else {
|
|
483
|
+
// Toggle if no arg
|
|
484
|
+
agent.setEviction(!agent.features.eviction);
|
|
485
|
+
}
|
|
486
|
+
setPanel({ view: "features" });
|
|
487
|
+
break;
|
|
488
|
+
|
|
489
|
+
case "summarize":
|
|
490
|
+
case "sum":
|
|
491
|
+
if (args === "on" || args === "true" || args === "1") {
|
|
492
|
+
agent.setSummarization(true);
|
|
493
|
+
} else if (args === "off" || args === "false" || args === "0") {
|
|
494
|
+
agent.setSummarization(false);
|
|
495
|
+
} else {
|
|
496
|
+
// Toggle if no arg
|
|
497
|
+
agent.setSummarization(!agent.features.summarization);
|
|
498
|
+
}
|
|
499
|
+
setPanel({ view: "features" });
|
|
500
|
+
break;
|
|
501
|
+
|
|
502
|
+
case "approve":
|
|
503
|
+
const newValue = !agent.autoApproveEnabled;
|
|
504
|
+
agent.setAutoApprove(newValue);
|
|
505
|
+
// Show a brief message (could be improved with a toast/notification)
|
|
506
|
+
return;
|
|
507
|
+
|
|
508
|
+
case "help":
|
|
509
|
+
case "h":
|
|
510
|
+
case "?":
|
|
511
|
+
setPanel({ view: "help" });
|
|
512
|
+
break;
|
|
513
|
+
|
|
514
|
+
case "quit":
|
|
515
|
+
case "exit":
|
|
516
|
+
case "q":
|
|
517
|
+
exit();
|
|
518
|
+
break;
|
|
519
|
+
|
|
520
|
+
case "state":
|
|
521
|
+
// Debug command
|
|
522
|
+
console.log(JSON.stringify(agent.state, null, 2));
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const isGenerating = agent.status !== "idle" && agent.status !== "done" && agent.status !== "error";
|
|
528
|
+
|
|
529
|
+
// Disable input when in interactive panels that capture keyboard input
|
|
530
|
+
const isInteractivePanel = panel.view === "apikey-input" || panel.view === "models";
|
|
531
|
+
const isInputDisabled = isGenerating || isInteractivePanel;
|
|
532
|
+
|
|
533
|
+
return (
|
|
534
|
+
<Box flexDirection="column" padding={1}>
|
|
535
|
+
{/* Welcome banner */}
|
|
536
|
+
{showWelcome && (
|
|
537
|
+
<>
|
|
538
|
+
<Welcome model={agent.currentModel} workDir={options.workDir || process.cwd()} />
|
|
539
|
+
<WelcomeHint />
|
|
540
|
+
</>
|
|
541
|
+
)}
|
|
542
|
+
|
|
543
|
+
{/* Panel views */}
|
|
544
|
+
{panel.view === "help" && <SlashMenuPanel />}
|
|
545
|
+
{panel.view === "todos" && <TodoList todos={agent.state.todos} />}
|
|
546
|
+
{panel.view === "files" && panel.files && <FileList files={panel.files} />}
|
|
547
|
+
{panel.view === "file-content" && panel.filePath && panel.fileContent && (
|
|
548
|
+
<FilePreview path={panel.filePath} content={panel.fileContent} />
|
|
549
|
+
)}
|
|
550
|
+
{panel.view === "apikey-input" && (
|
|
551
|
+
<ApiKeyInputPanel
|
|
552
|
+
onKeySaved={() => {
|
|
553
|
+
// Key saved, returns to provider selection automatically
|
|
554
|
+
}}
|
|
555
|
+
onClose={() => setPanel({ view: "none" })}
|
|
556
|
+
/>
|
|
557
|
+
)}
|
|
558
|
+
{panel.view === "features" && <FeaturesPanel features={agent.features} options={options} />}
|
|
559
|
+
{panel.view === "tokens" && <TokensPanel tokenCount={panel.tokenCount || 0} messageCount={agent.messages.length} />}
|
|
560
|
+
{panel.view === "models" && (
|
|
561
|
+
<ModelSelectionPanel
|
|
562
|
+
currentModel={agent.currentModel}
|
|
563
|
+
onModelSelect={(modelId) => {
|
|
564
|
+
agent.setModel(modelId);
|
|
565
|
+
}}
|
|
566
|
+
onClose={() => setPanel({ view: "none" })}
|
|
567
|
+
/>
|
|
568
|
+
)}
|
|
569
|
+
|
|
570
|
+
{/* Agent events in chronological order (includes text-segments) */}
|
|
571
|
+
{/* Always show events - they persist after generation completes */}
|
|
572
|
+
{agent.events.length > 0 && (
|
|
573
|
+
<Box flexDirection="column">
|
|
574
|
+
{agent.events.map((event) => (
|
|
575
|
+
<EventRenderer key={event.id} event={event} />
|
|
576
|
+
))}
|
|
577
|
+
</Box>
|
|
578
|
+
)}
|
|
579
|
+
|
|
580
|
+
{/* Current generation indicators */}
|
|
581
|
+
{isGenerating && (
|
|
582
|
+
<Box flexDirection="column">
|
|
583
|
+
{/* Currently streaming text (not yet flushed to a text-segment) */}
|
|
584
|
+
{agent.streamingText && (
|
|
585
|
+
<Box marginY={1}>
|
|
586
|
+
<Box>
|
|
587
|
+
<Text color={colors.success}>{"● "}</Text>
|
|
588
|
+
<Text>
|
|
589
|
+
{agent.streamingText}
|
|
590
|
+
<Text color={colors.muted}>▌</Text>
|
|
591
|
+
</Text>
|
|
592
|
+
</Box>
|
|
593
|
+
</Box>
|
|
594
|
+
)}
|
|
595
|
+
|
|
596
|
+
{/* Loading indicator when thinking or executing tools */}
|
|
597
|
+
{(agent.status === "thinking" || agent.status === "tool-call") && !agent.streamingText && (
|
|
598
|
+
<Box marginY={1}>
|
|
599
|
+
<ThinkingIndicator />
|
|
600
|
+
</Box>
|
|
601
|
+
)}
|
|
602
|
+
</Box>
|
|
603
|
+
)}
|
|
604
|
+
|
|
605
|
+
{/* Error display */}
|
|
606
|
+
{agent.error && <ErrorDisplay error={agent.error} />}
|
|
607
|
+
|
|
608
|
+
{/* Approval UI - show when pending and not in auto-approve mode */}
|
|
609
|
+
{agent.pendingApproval && !agent.autoApproveEnabled && (
|
|
610
|
+
<ToolApproval
|
|
611
|
+
toolName={agent.pendingApproval.toolName}
|
|
612
|
+
args={agent.pendingApproval.args}
|
|
613
|
+
onApprove={() => agent.respondToApproval(true)}
|
|
614
|
+
onDeny={() => agent.respondToApproval(false)}
|
|
615
|
+
onApproveAll={() => {
|
|
616
|
+
agent.setAutoApprove(true);
|
|
617
|
+
agent.respondToApproval(true);
|
|
618
|
+
}}
|
|
619
|
+
/>
|
|
620
|
+
)}
|
|
621
|
+
|
|
622
|
+
{/* Input - hidden when interactive panels are active */}
|
|
623
|
+
{!isInteractivePanel && (
|
|
624
|
+
<Box marginTop={1}>
|
|
625
|
+
<Input onSubmit={handleSubmit} disabled={isGenerating} />
|
|
626
|
+
</Box>
|
|
627
|
+
)}
|
|
628
|
+
|
|
629
|
+
{/* Compact status bar at bottom */}
|
|
630
|
+
<StatusBar
|
|
631
|
+
workDir={options.workDir || process.cwd()}
|
|
632
|
+
model={agent.currentModel}
|
|
633
|
+
status={agent.status}
|
|
634
|
+
features={agent.features}
|
|
635
|
+
autoApproveEnabled={agent.autoApproveEnabled}
|
|
636
|
+
sessionId={options.session}
|
|
637
|
+
/>
|
|
638
|
+
</Box>
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ============================================================================
|
|
643
|
+
// Event Renderer
|
|
644
|
+
// ============================================================================
|
|
645
|
+
|
|
646
|
+
interface EventRendererProps {
|
|
647
|
+
event: AgentEventLog;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Tools that have their own specific events - don't show generic tool-call for these
|
|
651
|
+
const TOOLS_WITH_SPECIFIC_EVENTS = new Set([
|
|
652
|
+
"read_file",
|
|
653
|
+
"ls",
|
|
654
|
+
"glob",
|
|
655
|
+
"grep",
|
|
656
|
+
"write_file",
|
|
657
|
+
"edit_file",
|
|
658
|
+
"write_todos",
|
|
659
|
+
"web_search",
|
|
660
|
+
"http_request",
|
|
661
|
+
"fetch_url",
|
|
662
|
+
]);
|
|
663
|
+
|
|
664
|
+
function EventRenderer({ event }: EventRendererProps): React.ReactElement | null {
|
|
665
|
+
const e = event.event;
|
|
666
|
+
|
|
667
|
+
switch (e.type) {
|
|
668
|
+
case "user-message":
|
|
669
|
+
// Render user message in history
|
|
670
|
+
return (
|
|
671
|
+
<Box marginBottom={1}>
|
|
672
|
+
<Text color={colors.muted} bold>{"> "}</Text>
|
|
673
|
+
<Text bold>{e.content}</Text>
|
|
674
|
+
</Box>
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
case "text-segment":
|
|
678
|
+
// Render accumulated text segment
|
|
679
|
+
if (!e.text.trim()) return null;
|
|
680
|
+
return (
|
|
681
|
+
<Box marginY={1}>
|
|
682
|
+
<Box>
|
|
683
|
+
<Text color={colors.success}>{"● "}</Text>
|
|
684
|
+
<Text>{e.text}</Text>
|
|
685
|
+
</Box>
|
|
686
|
+
</Box>
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
case "step-start":
|
|
690
|
+
return (
|
|
691
|
+
<Box marginTop={1}>
|
|
692
|
+
<Text color={colors.muted}>─── step {e.stepNumber} ───</Text>
|
|
693
|
+
</Box>
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
case "tool-call":
|
|
697
|
+
// Skip generic tool-call display for tools that have specific events
|
|
698
|
+
if (TOOLS_WITH_SPECIFIC_EVENTS.has(e.toolName)) {
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
return <ToolCall toolName={e.toolName} isExecuting={true} />;
|
|
702
|
+
|
|
703
|
+
case "todos-changed":
|
|
704
|
+
return (
|
|
705
|
+
<Box>
|
|
706
|
+
<Text color={colors.info}>📋 Todos: </Text>
|
|
707
|
+
<Text dimColor>
|
|
708
|
+
{e.todos.filter((t) => t.status === "completed").length}/{e.todos.length} completed
|
|
709
|
+
</Text>
|
|
710
|
+
</Box>
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
case "file-write-start":
|
|
714
|
+
return <FilePreview path={e.path} content={e.content} isWrite={true} maxLines={10} />;
|
|
715
|
+
|
|
716
|
+
case "file-written":
|
|
717
|
+
return <FileWritten path={e.path} />;
|
|
718
|
+
|
|
719
|
+
case "file-edited":
|
|
720
|
+
return <FileEdited path={e.path} occurrences={e.occurrences} />;
|
|
721
|
+
|
|
722
|
+
case "file-read":
|
|
723
|
+
return <FileRead path={e.path} lines={e.lines} />;
|
|
724
|
+
|
|
725
|
+
case "ls":
|
|
726
|
+
return <LsResult path={e.path} count={e.count} />;
|
|
727
|
+
|
|
728
|
+
case "glob":
|
|
729
|
+
return <GlobResult pattern={e.pattern} count={e.count} />;
|
|
730
|
+
|
|
731
|
+
case "grep":
|
|
732
|
+
return <GrepResult pattern={e.pattern} count={e.count} />;
|
|
733
|
+
|
|
734
|
+
case "web-search-start":
|
|
735
|
+
return (
|
|
736
|
+
<Box>
|
|
737
|
+
<Text color={colors.info}>🔍 </Text>
|
|
738
|
+
<Text>Searching web: </Text>
|
|
739
|
+
<Text color={colors.muted}>{e.query}</Text>
|
|
740
|
+
</Box>
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
case "web-search-finish":
|
|
744
|
+
return (
|
|
745
|
+
<Box>
|
|
746
|
+
<Text color={colors.success}>✓ </Text>
|
|
747
|
+
<Text>Found </Text>
|
|
748
|
+
<Text color={colors.info}>{e.resultCount}</Text>
|
|
749
|
+
<Text> results</Text>
|
|
750
|
+
</Box>
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
case "http-request-start":
|
|
754
|
+
return (
|
|
755
|
+
<Box>
|
|
756
|
+
<Text color={colors.info}>🌐 </Text>
|
|
757
|
+
<Text>{e.method} </Text>
|
|
758
|
+
<Text color={colors.muted}>{e.url}</Text>
|
|
759
|
+
</Box>
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
case "http-request-finish":
|
|
763
|
+
return (
|
|
764
|
+
<Box>
|
|
765
|
+
<Text color={e.statusCode >= 200 && e.statusCode < 300 ? colors.success : colors.error}>
|
|
766
|
+
{e.statusCode >= 200 && e.statusCode < 300 ? "✓" : "✗"}{" "}
|
|
767
|
+
</Text>
|
|
768
|
+
<Text>Status: </Text>
|
|
769
|
+
<Text color={e.statusCode >= 200 && e.statusCode < 300 ? colors.success : colors.error}>
|
|
770
|
+
{e.statusCode}
|
|
771
|
+
</Text>
|
|
772
|
+
</Box>
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
case "fetch-url-start":
|
|
776
|
+
return (
|
|
777
|
+
<Box>
|
|
778
|
+
<Text color={colors.info}>📄 </Text>
|
|
779
|
+
<Text>Fetching: </Text>
|
|
780
|
+
<Text color={colors.muted}>{e.url}</Text>
|
|
781
|
+
</Box>
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
case "fetch-url-finish":
|
|
785
|
+
return (
|
|
786
|
+
<Box>
|
|
787
|
+
<Text color={e.success ? colors.success : colors.error}>
|
|
788
|
+
{e.success ? "✓" : "✗"}{" "}
|
|
789
|
+
</Text>
|
|
790
|
+
<Text>{e.success ? "Content fetched" : "Failed to fetch"}</Text>
|
|
791
|
+
</Box>
|
|
792
|
+
);
|
|
793
|
+
|
|
794
|
+
case "subagent-start":
|
|
795
|
+
return <SubagentStart name={e.name} task={e.task} />;
|
|
796
|
+
|
|
797
|
+
case "subagent-finish":
|
|
798
|
+
return <SubagentFinish name={e.name} />;
|
|
799
|
+
|
|
800
|
+
case "done":
|
|
801
|
+
return (
|
|
802
|
+
<DoneIndicator
|
|
803
|
+
todosCompleted={e.state.todos.filter((t) => t.status === "completed").length}
|
|
804
|
+
todosTotal={e.state.todos.length}
|
|
805
|
+
filesCount={Object.keys(e.state.files).length}
|
|
806
|
+
/>
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
case "error":
|
|
810
|
+
return <ErrorDisplay error={e.error} />;
|
|
811
|
+
|
|
812
|
+
default:
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ============================================================================
|
|
818
|
+
// Features Panel
|
|
819
|
+
// ============================================================================
|
|
820
|
+
|
|
821
|
+
interface FeaturesPanelProps {
|
|
822
|
+
features: {
|
|
823
|
+
promptCaching: boolean;
|
|
824
|
+
eviction: boolean;
|
|
825
|
+
summarization: boolean;
|
|
826
|
+
};
|
|
827
|
+
options: CLIOptions;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function FeaturesPanel({ features, options }: FeaturesPanelProps): React.ReactElement {
|
|
831
|
+
return (
|
|
832
|
+
<Box
|
|
833
|
+
flexDirection="column"
|
|
834
|
+
borderStyle="single"
|
|
835
|
+
borderColor={colors.muted}
|
|
836
|
+
paddingX={2}
|
|
837
|
+
paddingY={1}
|
|
838
|
+
marginY={1}
|
|
839
|
+
>
|
|
840
|
+
<Text bold color={colors.info}>
|
|
841
|
+
⚙️ Feature Status
|
|
842
|
+
</Text>
|
|
843
|
+
<Box height={1} />
|
|
844
|
+
<Box>
|
|
845
|
+
{features.promptCaching ? (
|
|
846
|
+
<>
|
|
847
|
+
<Text color={colors.success}>✓ </Text>
|
|
848
|
+
<Text>Prompt Caching: </Text>
|
|
849
|
+
<Text color={colors.success}>enabled</Text>
|
|
850
|
+
</>
|
|
851
|
+
) : (
|
|
852
|
+
<>
|
|
853
|
+
<Text dimColor>✗ </Text>
|
|
854
|
+
<Text>Prompt Caching: </Text>
|
|
855
|
+
<Text dimColor>disabled</Text>
|
|
856
|
+
</>
|
|
857
|
+
)}
|
|
858
|
+
</Box>
|
|
859
|
+
<Box>
|
|
860
|
+
{features.eviction ? (
|
|
861
|
+
<>
|
|
862
|
+
<Text color={colors.success}>✓ </Text>
|
|
863
|
+
<Text>Tool Eviction: </Text>
|
|
864
|
+
<Text color={colors.success}>enabled ({options.toolResultEvictionLimit} tokens)</Text>
|
|
865
|
+
</>
|
|
866
|
+
) : (
|
|
867
|
+
<>
|
|
868
|
+
<Text dimColor>✗ </Text>
|
|
869
|
+
<Text>Tool Eviction: </Text>
|
|
870
|
+
<Text dimColor>disabled</Text>
|
|
871
|
+
</>
|
|
872
|
+
)}
|
|
873
|
+
</Box>
|
|
874
|
+
<Box>
|
|
875
|
+
{features.summarization ? (
|
|
876
|
+
<>
|
|
877
|
+
<Text color={colors.success}>✓ </Text>
|
|
878
|
+
<Text>Auto-Summarization: </Text>
|
|
879
|
+
<Text color={colors.success}>
|
|
880
|
+
enabled ({options.summarizationThreshold || DEFAULT_SUMMARIZATION_THRESHOLD} tokens, keep {options.summarizationKeepMessages || DEFAULT_KEEP_MESSAGES} msgs)
|
|
881
|
+
</Text>
|
|
882
|
+
</>
|
|
883
|
+
) : (
|
|
884
|
+
<>
|
|
885
|
+
<Text dimColor>✗ </Text>
|
|
886
|
+
<Text>Auto-Summarization: </Text>
|
|
887
|
+
<Text dimColor>disabled</Text>
|
|
888
|
+
</>
|
|
889
|
+
)}
|
|
890
|
+
</Box>
|
|
891
|
+
<Box height={1} />
|
|
892
|
+
<Text dimColor>Enable with: --cache --eviction-limit {DEFAULT_EVICTION_LIMIT} --summarize</Text>
|
|
893
|
+
</Box>
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// ============================================================================
|
|
898
|
+
// Tokens Panel
|
|
899
|
+
// ============================================================================
|
|
900
|
+
|
|
901
|
+
interface TokensPanelProps {
|
|
902
|
+
tokenCount: number;
|
|
903
|
+
messageCount: number;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function TokensPanel({ tokenCount, messageCount }: TokensPanelProps): React.ReactElement {
|
|
907
|
+
const formatNumber = (n: number) => n.toLocaleString();
|
|
908
|
+
|
|
909
|
+
// Estimate percentage of typical context window
|
|
910
|
+
const percentage = Math.round((tokenCount / CONTEXT_WINDOW) * 100);
|
|
911
|
+
|
|
912
|
+
// Color based on usage
|
|
913
|
+
let usageColor: string = colors.success;
|
|
914
|
+
if (percentage > 80) {
|
|
915
|
+
usageColor = colors.error;
|
|
916
|
+
} else if (percentage > 50) {
|
|
917
|
+
usageColor = colors.warning;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
return (
|
|
921
|
+
<Box
|
|
922
|
+
flexDirection="column"
|
|
923
|
+
borderStyle="single"
|
|
924
|
+
borderColor={colors.muted}
|
|
925
|
+
paddingX={2}
|
|
926
|
+
paddingY={1}
|
|
927
|
+
marginY={1}
|
|
928
|
+
>
|
|
929
|
+
<Text bold color={colors.info}>
|
|
930
|
+
📊 Token Usage
|
|
931
|
+
</Text>
|
|
932
|
+
<Box height={1} />
|
|
933
|
+
<Box>
|
|
934
|
+
<Text>Messages: </Text>
|
|
935
|
+
<Text color={colors.primary}>{messageCount}</Text>
|
|
936
|
+
</Box>
|
|
937
|
+
<Box>
|
|
938
|
+
<Text>Estimated Tokens: </Text>
|
|
939
|
+
<Text color={usageColor}>{formatNumber(tokenCount)}</Text>
|
|
940
|
+
</Box>
|
|
941
|
+
<Box>
|
|
942
|
+
<Text>Context Usage: </Text>
|
|
943
|
+
<Text color={usageColor}>{percentage}%</Text>
|
|
944
|
+
<Text dimColor> (of ~200k)</Text>
|
|
945
|
+
</Box>
|
|
946
|
+
<Box height={1} />
|
|
947
|
+
{percentage > 50 && (
|
|
948
|
+
<Text color={colors.warning}>
|
|
949
|
+
⚠️ Consider enabling --summarize to manage context
|
|
950
|
+
</Text>
|
|
951
|
+
)}
|
|
952
|
+
</Box>
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// ============================================================================
|
|
957
|
+
// Environment Variable Loading
|
|
958
|
+
// ============================================================================
|
|
959
|
+
|
|
960
|
+
interface EnvLoadResult {
|
|
961
|
+
loaded: boolean;
|
|
962
|
+
path?: string;
|
|
963
|
+
keysFound: string[];
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Load environment variables from .env file in the working directory.
|
|
968
|
+
* Bun automatically loads .env from cwd, but we want to also check the
|
|
969
|
+
* specified working directory if different.
|
|
970
|
+
*/
|
|
971
|
+
async function loadEnvFile(workDir: string): Promise<EnvLoadResult> {
|
|
972
|
+
const envPaths = [
|
|
973
|
+
`${workDir}/.env`,
|
|
974
|
+
`${workDir}/.env.local`,
|
|
975
|
+
];
|
|
976
|
+
|
|
977
|
+
const keysToCheck = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY'];
|
|
978
|
+
const result: EnvLoadResult = { loaded: false, keysFound: [] };
|
|
979
|
+
|
|
980
|
+
for (const envPath of envPaths) {
|
|
981
|
+
try {
|
|
982
|
+
const file = Bun.file(envPath);
|
|
983
|
+
const exists = await file.exists();
|
|
984
|
+
|
|
985
|
+
if (exists) {
|
|
986
|
+
const content = await file.text();
|
|
987
|
+
const lines = content.split('\n');
|
|
988
|
+
|
|
989
|
+
for (const line of lines) {
|
|
990
|
+
const trimmed = line.trim();
|
|
991
|
+
// Skip comments and empty lines
|
|
992
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
993
|
+
|
|
994
|
+
// Parse KEY=VALUE format
|
|
995
|
+
const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
996
|
+
if (match) {
|
|
997
|
+
const key = match[1];
|
|
998
|
+
const rawValue = match[2];
|
|
999
|
+
if (!key || rawValue === undefined) continue;
|
|
1000
|
+
|
|
1001
|
+
// Remove quotes if present
|
|
1002
|
+
let value = rawValue.trim();
|
|
1003
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
1004
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
1005
|
+
value = value.slice(1, -1);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Only set if not already set (env vars take precedence)
|
|
1009
|
+
if (!process.env[key] && value) {
|
|
1010
|
+
process.env[key] = value;
|
|
1011
|
+
if (keysToCheck.includes(key)) {
|
|
1012
|
+
result.keysFound.push(key);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
result.loaded = true;
|
|
1019
|
+
result.path = envPath;
|
|
1020
|
+
break; // Stop after first .env file found
|
|
1021
|
+
}
|
|
1022
|
+
} catch {
|
|
1023
|
+
// File doesn't exist or can't be read, continue
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Check which keys are now available (from env or .env file)
|
|
1028
|
+
for (const key of keysToCheck) {
|
|
1029
|
+
if (process.env[key] && !result.keysFound.includes(key)) {
|
|
1030
|
+
// Key was already in environment
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return result;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// ============================================================================
|
|
1038
|
+
// Main Entry Point
|
|
1039
|
+
// ============================================================================
|
|
1040
|
+
|
|
1041
|
+
async function main() {
|
|
1042
|
+
const options = parseArgs();
|
|
1043
|
+
const workDir = options.workDir || process.cwd();
|
|
1044
|
+
|
|
1045
|
+
// Load .env file from working directory
|
|
1046
|
+
const envResult = await loadEnvFile(workDir);
|
|
1047
|
+
|
|
1048
|
+
// Show env loading info
|
|
1049
|
+
if (envResult.loaded && envResult.keysFound.length > 0) {
|
|
1050
|
+
console.log(`\x1b[32m✓\x1b[0m Loaded API keys from ${envResult.path}: ${envResult.keysFound.join(', ')}`);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// Warn if no API keys found
|
|
1054
|
+
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
|
|
1055
|
+
console.log(`\x1b[33m⚠\x1b[0m No API keys found. Set ANTHROPIC_API_KEY or OPENAI_API_KEY in environment or .env file.`);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const backend = new LocalSandbox({
|
|
1059
|
+
cwd: workDir,
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
render(<App options={options} backend={backend} />);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
main();
|
|
1066
|
+
|