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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/package.json +95 -0
  4. package/src/agent.ts +1230 -0
  5. package/src/backends/composite.ts +273 -0
  6. package/src/backends/filesystem.ts +692 -0
  7. package/src/backends/index.ts +22 -0
  8. package/src/backends/local-sandbox.ts +175 -0
  9. package/src/backends/persistent.ts +593 -0
  10. package/src/backends/sandbox.ts +510 -0
  11. package/src/backends/state.ts +244 -0
  12. package/src/backends/utils.ts +287 -0
  13. package/src/checkpointer/file-saver.ts +98 -0
  14. package/src/checkpointer/index.ts +5 -0
  15. package/src/checkpointer/kv-saver.ts +82 -0
  16. package/src/checkpointer/memory-saver.ts +82 -0
  17. package/src/checkpointer/types.ts +125 -0
  18. package/src/cli/components/ApiKeyInput.tsx +300 -0
  19. package/src/cli/components/FilePreview.tsx +237 -0
  20. package/src/cli/components/Input.tsx +277 -0
  21. package/src/cli/components/Message.tsx +93 -0
  22. package/src/cli/components/ModelSelection.tsx +338 -0
  23. package/src/cli/components/SlashMenu.tsx +101 -0
  24. package/src/cli/components/StatusBar.tsx +89 -0
  25. package/src/cli/components/Subagent.tsx +91 -0
  26. package/src/cli/components/TodoList.tsx +133 -0
  27. package/src/cli/components/ToolApproval.tsx +70 -0
  28. package/src/cli/components/ToolCall.tsx +144 -0
  29. package/src/cli/components/ToolCallSummary.tsx +175 -0
  30. package/src/cli/components/Welcome.tsx +75 -0
  31. package/src/cli/components/index.ts +24 -0
  32. package/src/cli/hooks/index.ts +12 -0
  33. package/src/cli/hooks/useAgent.ts +933 -0
  34. package/src/cli/index.tsx +1066 -0
  35. package/src/cli/theme.ts +205 -0
  36. package/src/cli/utils/model-list.ts +365 -0
  37. package/src/constants/errors.ts +29 -0
  38. package/src/constants/limits.ts +195 -0
  39. package/src/index.ts +176 -0
  40. package/src/middleware/agent-memory.ts +330 -0
  41. package/src/prompts.ts +196 -0
  42. package/src/skills/index.ts +2 -0
  43. package/src/skills/load.ts +191 -0
  44. package/src/skills/types.ts +53 -0
  45. package/src/tools/execute.ts +167 -0
  46. package/src/tools/filesystem.ts +418 -0
  47. package/src/tools/index.ts +39 -0
  48. package/src/tools/subagent.ts +443 -0
  49. package/src/tools/todos.ts +101 -0
  50. package/src/tools/web.ts +567 -0
  51. package/src/types/backend.ts +177 -0
  52. package/src/types/core.ts +220 -0
  53. package/src/types/events.ts +429 -0
  54. package/src/types/index.ts +94 -0
  55. package/src/types/structured-output.ts +43 -0
  56. package/src/types/subagent.ts +96 -0
  57. package/src/types.ts +22 -0
  58. package/src/utils/approval.ts +213 -0
  59. package/src/utils/events.ts +416 -0
  60. package/src/utils/eviction.ts +181 -0
  61. package/src/utils/index.ts +34 -0
  62. package/src/utils/model-parser.ts +38 -0
  63. package/src/utils/patch-tool-calls.ts +233 -0
  64. package/src/utils/project-detection.ts +32 -0
  65. 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
+