brownian-code 2026.2.10
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 +97 -0
- package/bin/brownian +25 -0
- package/env.example +21 -0
- package/package.json +87 -0
- package/src/agent/agent.test.ts +414 -0
- package/src/agent/agent.ts +385 -0
- package/src/agent/index.ts +27 -0
- package/src/agent/prompts.ts +271 -0
- package/src/agent/scratchpad.test.ts +482 -0
- package/src/agent/scratchpad.ts +526 -0
- package/src/agent/token-counter.test.ts +59 -0
- package/src/agent/token-counter.ts +33 -0
- package/src/agent/types.ts +137 -0
- package/src/cli.tsx +385 -0
- package/src/commands/builtin.test.ts +271 -0
- package/src/commands/builtin.ts +200 -0
- package/src/commands/registry.test.ts +188 -0
- package/src/commands/registry.ts +111 -0
- package/src/commands/types.ts +64 -0
- package/src/components/AgentEventView.tsx +487 -0
- package/src/components/AnswerBox.tsx +81 -0
- package/src/components/ApiKeyPrompt.tsx +75 -0
- package/src/components/CommandMenu.test.tsx +64 -0
- package/src/components/CommandMenu.tsx +38 -0
- package/src/components/CursorText.tsx +43 -0
- package/src/components/DebugPanel.tsx +48 -0
- package/src/components/ErrorBox.test.tsx +58 -0
- package/src/components/ErrorBox.tsx +26 -0
- package/src/components/HelpView.test.tsx +70 -0
- package/src/components/HelpView.tsx +61 -0
- package/src/components/HistoryItemView.tsx +108 -0
- package/src/components/Input.tsx +193 -0
- package/src/components/Intro.test.tsx +59 -0
- package/src/components/Intro.tsx +35 -0
- package/src/components/ModelSelector.tsx +288 -0
- package/src/components/StatusBar.test.tsx +78 -0
- package/src/components/StatusBar.tsx +56 -0
- package/src/components/WorkingIndicator.tsx +133 -0
- package/src/components/index.ts +23 -0
- package/src/e2e/agent-flow.test.ts +378 -0
- package/src/evals/components/EvalApp.tsx +206 -0
- package/src/evals/components/EvalCurrentQuestion.tsx +42 -0
- package/src/evals/components/EvalProgress.tsx +33 -0
- package/src/evals/components/EvalRecentResults.tsx +63 -0
- package/src/evals/components/EvalStats.tsx +49 -0
- package/src/evals/components/index.ts +5 -0
- package/src/evals/dataset/crypto_agent.csv +16 -0
- package/src/evals/run.ts +355 -0
- package/src/gateway/channels/whatsapp/auth-store.ts +15 -0
- package/src/gateway/channels/whatsapp/inbound.ts +86 -0
- package/src/gateway/channels/whatsapp/login.ts +28 -0
- package/src/gateway/channels/whatsapp/outbound.ts +27 -0
- package/src/gateway/channels/whatsapp/session.ts +69 -0
- package/src/gateway/config.ts +81 -0
- package/src/gateway/index.ts +62 -0
- package/src/hooks/useAgentRunner.ts +317 -0
- package/src/hooks/useDebugLogs.ts +22 -0
- package/src/hooks/useInputHistory.ts +106 -0
- package/src/hooks/useModelSelection.ts +249 -0
- package/src/hooks/useTextBuffer.test.ts +121 -0
- package/src/hooks/useTextBuffer.ts +97 -0
- package/src/index.tsx +74 -0
- package/src/mcp/cache.ts +205 -0
- package/src/mcp/client.test.ts +126 -0
- package/src/mcp/client.ts +145 -0
- package/src/mcp/index.ts +2 -0
- package/src/model/llm.test.ts +158 -0
- package/src/model/llm.ts +233 -0
- package/src/providers.ts +94 -0
- package/src/skills/index.ts +17 -0
- package/src/skills/loader.ts +73 -0
- package/src/skills/registry.ts +125 -0
- package/src/skills/types.ts +31 -0
- package/src/test-utils/mocks.ts +110 -0
- package/src/theme.ts +21 -0
- package/src/tools/browser/browser.ts +357 -0
- package/src/tools/browser/index.ts +1 -0
- package/src/tools/crypto/hive-tools.ts +171 -0
- package/src/tools/crypto/index.ts +1 -0
- package/src/tools/descriptions/browser.ts +105 -0
- package/src/tools/descriptions/crypto-search.ts +58 -0
- package/src/tools/descriptions/index.ts +8 -0
- package/src/tools/descriptions/web-fetch.ts +44 -0
- package/src/tools/descriptions/web-search.ts +26 -0
- package/src/tools/fetch/cache.ts +95 -0
- package/src/tools/fetch/external-content.ts +200 -0
- package/src/tools/fetch/index.ts +1 -0
- package/src/tools/fetch/web-fetch-utils.ts +122 -0
- package/src/tools/fetch/web-fetch.ts +371 -0
- package/src/tools/index.ts +12 -0
- package/src/tools/registry.ts +130 -0
- package/src/tools/search/exa.ts +43 -0
- package/src/tools/search/index.ts +2 -0
- package/src/tools/search/tavily.ts +35 -0
- package/src/tools/skill.ts +62 -0
- package/src/tools/types.ts +53 -0
- package/src/utils/ai-message.ts +26 -0
- package/src/utils/config.ts +54 -0
- package/src/utils/cost-calculator.test.ts +101 -0
- package/src/utils/cost-calculator.ts +74 -0
- package/src/utils/env.ts +101 -0
- package/src/utils/error-classifier.test.ts +146 -0
- package/src/utils/error-classifier.ts +91 -0
- package/src/utils/in-memory-chat-history.test.ts +291 -0
- package/src/utils/in-memory-chat-history.ts +224 -0
- package/src/utils/index.ts +19 -0
- package/src/utils/input-key-handlers.test.ts +155 -0
- package/src/utils/input-key-handlers.ts +64 -0
- package/src/utils/logger.ts +67 -0
- package/src/utils/long-term-chat-history.ts +138 -0
- package/src/utils/markdown-table.ts +227 -0
- package/src/utils/ollama.ts +37 -0
- package/src/utils/progress-channel.ts +84 -0
- package/src/utils/text-navigation.test.ts +222 -0
- package/src/utils/text-navigation.ts +81 -0
- package/src/utils/thinking-verbs.ts +29 -0
- package/src/utils/tokens.test.ts +163 -0
- package/src/utils/tokens.ts +67 -0
- package/src/utils/tool-description.ts +88 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent configuration
|
|
3
|
+
*/
|
|
4
|
+
export interface AgentConfig {
|
|
5
|
+
/** Model to use for LLM calls (e.g., 'gpt-5.2', 'claude-sonnet-4-20250514') */
|
|
6
|
+
model?: string;
|
|
7
|
+
/** Model provider (e.g., 'openai', 'anthropic', 'google', 'ollama') */
|
|
8
|
+
modelProvider?: string;
|
|
9
|
+
/** Maximum agent loop iterations (default: 10) */
|
|
10
|
+
maxIterations?: number;
|
|
11
|
+
/** AbortSignal for cancelling agent execution */
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Message in conversation history
|
|
17
|
+
*/
|
|
18
|
+
export interface Message {
|
|
19
|
+
role: 'user' | 'assistant' | 'tool';
|
|
20
|
+
content: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Agent Events (for real-time streaming UI)
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Agent is processing/thinking
|
|
29
|
+
*/
|
|
30
|
+
export interface ThinkingEvent {
|
|
31
|
+
type: 'thinking';
|
|
32
|
+
message: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Tool execution started
|
|
37
|
+
*/
|
|
38
|
+
export interface ToolStartEvent {
|
|
39
|
+
type: 'tool_start';
|
|
40
|
+
tool: string;
|
|
41
|
+
args: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Tool execution completed successfully
|
|
46
|
+
*/
|
|
47
|
+
export interface ToolEndEvent {
|
|
48
|
+
type: 'tool_end';
|
|
49
|
+
tool: string;
|
|
50
|
+
args: Record<string, unknown>;
|
|
51
|
+
result: string;
|
|
52
|
+
duration: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Tool execution failed
|
|
57
|
+
*/
|
|
58
|
+
export interface ToolErrorEvent {
|
|
59
|
+
type: 'tool_error';
|
|
60
|
+
tool: string;
|
|
61
|
+
error: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Mid-execution progress update from a subagent tool
|
|
66
|
+
*/
|
|
67
|
+
export interface ToolProgressEvent {
|
|
68
|
+
type: 'tool_progress';
|
|
69
|
+
tool: string;
|
|
70
|
+
message: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Tool call warning due to approaching/exceeding suggested limits
|
|
75
|
+
*/
|
|
76
|
+
export interface ToolLimitEvent {
|
|
77
|
+
type: 'tool_limit';
|
|
78
|
+
tool: string;
|
|
79
|
+
/** Warning message about tool usage limits */
|
|
80
|
+
warning?: string;
|
|
81
|
+
/** Whether the tool call was blocked (always false - we only warn, never block) */
|
|
82
|
+
blocked: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Context was cleared due to exceeding token threshold (Anthropic-style)
|
|
87
|
+
*/
|
|
88
|
+
export interface ContextClearedEvent {
|
|
89
|
+
type: 'context_cleared';
|
|
90
|
+
/** Number of tool results that were cleared from context */
|
|
91
|
+
clearedCount: number;
|
|
92
|
+
/** Number of most recent tool results that were kept */
|
|
93
|
+
keptCount: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Final answer generation started
|
|
98
|
+
*/
|
|
99
|
+
export interface AnswerStartEvent {
|
|
100
|
+
type: 'answer_start';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Token usage statistics
|
|
105
|
+
*/
|
|
106
|
+
export interface TokenUsage {
|
|
107
|
+
inputTokens: number;
|
|
108
|
+
outputTokens: number;
|
|
109
|
+
totalTokens: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Agent completed with final result
|
|
114
|
+
*/
|
|
115
|
+
export interface DoneEvent {
|
|
116
|
+
type: 'done';
|
|
117
|
+
answer: string;
|
|
118
|
+
toolCalls: Array<{ tool: string; args: Record<string, unknown>; result: string }>;
|
|
119
|
+
iterations: number;
|
|
120
|
+
totalTime: number;
|
|
121
|
+
tokenUsage?: TokenUsage;
|
|
122
|
+
tokensPerSecond?: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Union type for all agent events
|
|
127
|
+
*/
|
|
128
|
+
export type AgentEvent =
|
|
129
|
+
| ThinkingEvent
|
|
130
|
+
| ToolStartEvent
|
|
131
|
+
| ToolProgressEvent
|
|
132
|
+
| ToolEndEvent
|
|
133
|
+
| ToolErrorEvent
|
|
134
|
+
| ToolLimitEvent
|
|
135
|
+
| ContextClearedEvent
|
|
136
|
+
| AnswerStartEvent
|
|
137
|
+
| DoneEvent;
|
package/src/cli.tsx
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* CLI - Real-time agentic loop interface
|
|
4
|
+
* Shows tool calls and progress in Claude Code style
|
|
5
|
+
*/
|
|
6
|
+
import React, { useCallback, useRef, useState } from 'react';
|
|
7
|
+
import { Box, Static, Text, useApp, useInput } from 'ink';
|
|
8
|
+
import { config } from 'dotenv';
|
|
9
|
+
|
|
10
|
+
import { Input } from './components/Input.js';
|
|
11
|
+
import { Intro } from './components/Intro.js';
|
|
12
|
+
import { ProviderSelector, ModelSelector, ModelInputField } from './components/ModelSelector.js';
|
|
13
|
+
import { ApiKeyConfirm, ApiKeyInput } from './components/ApiKeyPrompt.js';
|
|
14
|
+
import { DebugPanel } from './components/DebugPanel.js';
|
|
15
|
+
import { HistoryItemView, WorkingIndicator, type HistoryItem } from './components/index.js';
|
|
16
|
+
import { StatusBar } from './components/StatusBar.js';
|
|
17
|
+
import { CommandMenu } from './components/CommandMenu.js';
|
|
18
|
+
import { ErrorBox } from './components/ErrorBox.js';
|
|
19
|
+
import { getApiKeyNameForProvider, getProviderDisplayName } from './utils/env.js';
|
|
20
|
+
import { getModelDisplayName } from './components/ModelSelector.js';
|
|
21
|
+
import { getSetting, setSetting } from './utils/config.js';
|
|
22
|
+
|
|
23
|
+
import { useModelSelection } from './hooks/useModelSelection.js';
|
|
24
|
+
import { useAgentRunner } from './hooks/useAgentRunner.js';
|
|
25
|
+
import { useInputHistory } from './hooks/useInputHistory.js';
|
|
26
|
+
import { getContextWindow } from './utils/tokens.js';
|
|
27
|
+
|
|
28
|
+
import { registerBuiltinCommands } from './commands/builtin.js';
|
|
29
|
+
import { commandRegistry, parseCommand, executeCommand } from './commands/registry.js';
|
|
30
|
+
import type { CommandContext } from './commands/types.js';
|
|
31
|
+
import type { CommandDef } from './commands/types.js';
|
|
32
|
+
|
|
33
|
+
// Load environment variables
|
|
34
|
+
config({ quiet: true });
|
|
35
|
+
|
|
36
|
+
// Register all builtin commands on startup
|
|
37
|
+
registerBuiltinCommands();
|
|
38
|
+
|
|
39
|
+
export function CLI() {
|
|
40
|
+
const { exit } = useApp();
|
|
41
|
+
|
|
42
|
+
// Debug panel state
|
|
43
|
+
const [showDebug, setShowDebug] = useState(() => getSetting('showDebug', false));
|
|
44
|
+
|
|
45
|
+
// Command autocomplete state
|
|
46
|
+
const [commandMenuVisible, setCommandMenuVisible] = useState(false);
|
|
47
|
+
const [commandMenuItems, setCommandMenuItems] = useState<CommandDef[]>([]);
|
|
48
|
+
const [commandMenuIndex, setCommandMenuIndex] = useState(0);
|
|
49
|
+
|
|
50
|
+
// Command JSX output (for commands that render components like /help)
|
|
51
|
+
const [commandOutput, setCommandOutput] = useState<React.ReactNode | null>(null);
|
|
52
|
+
|
|
53
|
+
// Ref to hold setError - avoids TDZ issue since useModelSelection needs to call
|
|
54
|
+
// setError but useAgentRunner (which provides setError) depends on useModelSelection's outputs
|
|
55
|
+
const setErrorRef = useRef<((error: string | null) => void) | null>(null);
|
|
56
|
+
|
|
57
|
+
// Model selection state and handlers
|
|
58
|
+
const {
|
|
59
|
+
selectionState,
|
|
60
|
+
provider,
|
|
61
|
+
model,
|
|
62
|
+
inMemoryChatHistoryRef,
|
|
63
|
+
startSelection,
|
|
64
|
+
cancelSelection,
|
|
65
|
+
handleProviderSelect,
|
|
66
|
+
handleModelSelect,
|
|
67
|
+
handleModelInputSubmit,
|
|
68
|
+
handleApiKeyConfirm,
|
|
69
|
+
handleApiKeySubmit,
|
|
70
|
+
isInSelectionFlow,
|
|
71
|
+
} = useModelSelection((errorMsg) => setErrorRef.current?.(errorMsg));
|
|
72
|
+
|
|
73
|
+
// Agent execution state and handlers
|
|
74
|
+
const {
|
|
75
|
+
history,
|
|
76
|
+
workingState,
|
|
77
|
+
error,
|
|
78
|
+
isProcessing,
|
|
79
|
+
cumulativeTokens,
|
|
80
|
+
cumulativeCost,
|
|
81
|
+
turnCount,
|
|
82
|
+
runQuery,
|
|
83
|
+
runCompactQuery,
|
|
84
|
+
cancelExecution,
|
|
85
|
+
setError,
|
|
86
|
+
clearHistory,
|
|
87
|
+
} = useAgentRunner({ model, modelProvider: provider, maxIterations: 10 }, inMemoryChatHistoryRef);
|
|
88
|
+
|
|
89
|
+
// Assign setError to ref so useModelSelection's callback can access it
|
|
90
|
+
setErrorRef.current = setError;
|
|
91
|
+
|
|
92
|
+
// Input history for up/down arrow navigation
|
|
93
|
+
const {
|
|
94
|
+
historyValue,
|
|
95
|
+
navigateUp,
|
|
96
|
+
navigateDown,
|
|
97
|
+
saveMessage,
|
|
98
|
+
updateAgentResponse,
|
|
99
|
+
resetNavigation,
|
|
100
|
+
} = useInputHistory();
|
|
101
|
+
|
|
102
|
+
// Toggle debug panel
|
|
103
|
+
const toggleDebug = useCallback(() => {
|
|
104
|
+
setShowDebug(prev => {
|
|
105
|
+
const next = !prev;
|
|
106
|
+
setSetting('showDebug', next);
|
|
107
|
+
return next;
|
|
108
|
+
});
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
// Build command context
|
|
112
|
+
const getCommandContext = useCallback((): CommandContext => ({
|
|
113
|
+
provider,
|
|
114
|
+
model,
|
|
115
|
+
history,
|
|
116
|
+
inMemoryChatHistoryRef,
|
|
117
|
+
setError,
|
|
118
|
+
clearHistory,
|
|
119
|
+
startSelection,
|
|
120
|
+
toggleDebug,
|
|
121
|
+
showDebug,
|
|
122
|
+
cumulativeTokens,
|
|
123
|
+
cumulativeCost,
|
|
124
|
+
turnCount,
|
|
125
|
+
runCompactQuery,
|
|
126
|
+
}), [provider, model, history, inMemoryChatHistoryRef, setError, clearHistory, startSelection, toggleDebug, showDebug, cumulativeTokens, cumulativeCost, turnCount, runCompactQuery]);
|
|
127
|
+
|
|
128
|
+
// Handle history navigation from Input component
|
|
129
|
+
const handleHistoryNavigate = useCallback((direction: 'up' | 'down') => {
|
|
130
|
+
if (direction === 'up') {
|
|
131
|
+
navigateUp();
|
|
132
|
+
} else {
|
|
133
|
+
navigateDown();
|
|
134
|
+
}
|
|
135
|
+
}, [navigateUp, navigateDown]);
|
|
136
|
+
|
|
137
|
+
// Handle slash command input changes (for autocomplete)
|
|
138
|
+
const handleSlashChange = useCallback((text: string) => {
|
|
139
|
+
if (text.startsWith('/') && text.length >= 1) {
|
|
140
|
+
const prefix = text.slice(1); // Remove the "/"
|
|
141
|
+
const matches = prefix.length === 0
|
|
142
|
+
? commandRegistry.getVisible()
|
|
143
|
+
: commandRegistry.search(prefix);
|
|
144
|
+
setCommandMenuItems(matches);
|
|
145
|
+
setCommandMenuIndex(0);
|
|
146
|
+
setCommandMenuVisible(matches.length > 0);
|
|
147
|
+
} else {
|
|
148
|
+
setCommandMenuVisible(false);
|
|
149
|
+
}
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
// Handle command menu navigation
|
|
153
|
+
const handleCommandMenuNavigate = useCallback((direction: 'up' | 'down') => {
|
|
154
|
+
setCommandMenuIndex(prev => {
|
|
155
|
+
if (direction === 'up') {
|
|
156
|
+
return prev > 0 ? prev - 1 : commandMenuItems.length - 1;
|
|
157
|
+
}
|
|
158
|
+
return prev < commandMenuItems.length - 1 ? prev + 1 : 0;
|
|
159
|
+
});
|
|
160
|
+
}, [commandMenuItems.length]);
|
|
161
|
+
|
|
162
|
+
// Get selected command from menu
|
|
163
|
+
const getSelectedCommand = useCallback((): string | null => {
|
|
164
|
+
if (!commandMenuVisible || commandMenuItems.length === 0) return null;
|
|
165
|
+
return '/' + commandMenuItems[commandMenuIndex].name;
|
|
166
|
+
}, [commandMenuVisible, commandMenuItems, commandMenuIndex]);
|
|
167
|
+
|
|
168
|
+
// Handle user input submission
|
|
169
|
+
const handleSubmit = useCallback(async (query: string) => {
|
|
170
|
+
// Clear any previous command output
|
|
171
|
+
setCommandOutput(null);
|
|
172
|
+
|
|
173
|
+
// Handle exit
|
|
174
|
+
if (query.toLowerCase() === 'exit' || query.toLowerCase() === 'quit') {
|
|
175
|
+
console.log('Goodbye!');
|
|
176
|
+
exit();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Handle slash commands
|
|
181
|
+
const parsed = parseCommand(query);
|
|
182
|
+
if (parsed) {
|
|
183
|
+
// Dismiss autocomplete menu
|
|
184
|
+
setCommandMenuVisible(false);
|
|
185
|
+
|
|
186
|
+
const cmd = commandRegistry.get(parsed.name);
|
|
187
|
+
if (!cmd) {
|
|
188
|
+
setError(`Unknown command: /${parsed.name}. Type /help for available commands.`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const result = await executeCommand(parsed.name, parsed.args, getCommandContext());
|
|
194
|
+
if (result !== undefined && result !== null) {
|
|
195
|
+
if (typeof result === 'string') {
|
|
196
|
+
// Show as a text message in history
|
|
197
|
+
setCommandOutput(
|
|
198
|
+
<Box marginBottom={1}>
|
|
199
|
+
<Text color="#a6a6a6">{result}</Text>
|
|
200
|
+
</Box>
|
|
201
|
+
);
|
|
202
|
+
} else {
|
|
203
|
+
// Render as JSX (React component)
|
|
204
|
+
setCommandOutput(result);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} catch (e) {
|
|
208
|
+
setError(`Command error: ${e instanceof Error ? e.message : String(e)}`);
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Ignore if not idle (processing or in selection flow)
|
|
214
|
+
if (isInSelectionFlow() || workingState.status !== 'idle') return;
|
|
215
|
+
|
|
216
|
+
// Save user message to history immediately and reset navigation
|
|
217
|
+
await saveMessage(query);
|
|
218
|
+
resetNavigation();
|
|
219
|
+
|
|
220
|
+
// Run query and save agent response when complete
|
|
221
|
+
const result = await runQuery(query);
|
|
222
|
+
if (result?.answer) {
|
|
223
|
+
await updateAgentResponse(result.answer);
|
|
224
|
+
}
|
|
225
|
+
}, [exit, getCommandContext, isInSelectionFlow, workingState.status, runQuery, saveMessage, updateAgentResponse, resetNavigation, setError]);
|
|
226
|
+
|
|
227
|
+
// Handle keyboard shortcuts
|
|
228
|
+
useInput((input, key) => {
|
|
229
|
+
// Escape key - cancel selection flows or running agent, or dismiss command menu
|
|
230
|
+
if (key.escape) {
|
|
231
|
+
if (commandMenuVisible) {
|
|
232
|
+
setCommandMenuVisible(false);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (isInSelectionFlow()) {
|
|
236
|
+
cancelSelection();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (isProcessing) {
|
|
240
|
+
cancelExecution();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Ctrl+D - toggle debug panel
|
|
246
|
+
if (key.ctrl && input === 'd') {
|
|
247
|
+
toggleDebug();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Ctrl+C - cancel or exit
|
|
252
|
+
if (key.ctrl && input === 'c') {
|
|
253
|
+
if (isInSelectionFlow()) {
|
|
254
|
+
cancelSelection();
|
|
255
|
+
} else if (isProcessing) {
|
|
256
|
+
cancelExecution();
|
|
257
|
+
} else {
|
|
258
|
+
console.log('\nGoodbye!');
|
|
259
|
+
exit();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Render selection screens
|
|
265
|
+
const { appState, pendingProvider, pendingModels } = selectionState;
|
|
266
|
+
|
|
267
|
+
if (appState === 'provider_select') {
|
|
268
|
+
return (
|
|
269
|
+
<Box flexDirection="column">
|
|
270
|
+
<ProviderSelector provider={provider} onSelect={handleProviderSelect} />
|
|
271
|
+
</Box>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (appState === 'model_select' && pendingProvider) {
|
|
276
|
+
return (
|
|
277
|
+
<Box flexDirection="column">
|
|
278
|
+
<ModelSelector
|
|
279
|
+
providerId={pendingProvider}
|
|
280
|
+
models={pendingModels}
|
|
281
|
+
currentModel={provider === pendingProvider ? model : undefined}
|
|
282
|
+
onSelect={handleModelSelect}
|
|
283
|
+
/>
|
|
284
|
+
</Box>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (appState === 'model_input' && pendingProvider) {
|
|
289
|
+
return (
|
|
290
|
+
<Box flexDirection="column">
|
|
291
|
+
<ModelInputField
|
|
292
|
+
providerId={pendingProvider}
|
|
293
|
+
currentModel={provider === pendingProvider ? model : undefined}
|
|
294
|
+
onSubmit={handleModelInputSubmit}
|
|
295
|
+
/>
|
|
296
|
+
</Box>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (appState === 'api_key_confirm' && pendingProvider) {
|
|
301
|
+
return (
|
|
302
|
+
<Box flexDirection="column">
|
|
303
|
+
<ApiKeyConfirm
|
|
304
|
+
providerName={getProviderDisplayName(pendingProvider)}
|
|
305
|
+
onConfirm={handleApiKeyConfirm}
|
|
306
|
+
/>
|
|
307
|
+
</Box>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (appState === 'api_key_input' && pendingProvider) {
|
|
312
|
+
const apiKeyName = getApiKeyNameForProvider(pendingProvider) || '';
|
|
313
|
+
return (
|
|
314
|
+
<Box flexDirection="column">
|
|
315
|
+
<ApiKeyInput
|
|
316
|
+
providerName={getProviderDisplayName(pendingProvider)}
|
|
317
|
+
apiKeyName={apiKeyName}
|
|
318
|
+
onSubmit={handleApiKeySubmit}
|
|
319
|
+
/>
|
|
320
|
+
</Box>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Main chat interface
|
|
325
|
+
return (
|
|
326
|
+
<Box flexDirection="column">
|
|
327
|
+
{/* Intro + completed history — written once to scrollback, never re-rendered */}
|
|
328
|
+
<Static items={[{ id: '__intro__' } as { id: string }, ...history.filter(item => item.status !== 'processing')]}>
|
|
329
|
+
{(item) => item.id === '__intro__'
|
|
330
|
+
? <Intro key="__intro__" provider={provider} model={model} />
|
|
331
|
+
: <HistoryItemView key={item.id} item={item as HistoryItem} />
|
|
332
|
+
}
|
|
333
|
+
</Static>
|
|
334
|
+
|
|
335
|
+
{/* Active (processing) item — dynamic, repainted on updates */}
|
|
336
|
+
{history.length > 0 && history[history.length - 1].status === 'processing' && (
|
|
337
|
+
<HistoryItemView key={history[history.length - 1].id} item={history[history.length - 1]} />
|
|
338
|
+
)}
|
|
339
|
+
|
|
340
|
+
{/* Command output (from slash commands like /help) */}
|
|
341
|
+
{commandOutput}
|
|
342
|
+
|
|
343
|
+
{/* Error display */}
|
|
344
|
+
{error && <ErrorBox error={error} />}
|
|
345
|
+
|
|
346
|
+
{/* Working indicator - only show when processing */}
|
|
347
|
+
{isProcessing && <WorkingIndicator state={workingState} />}
|
|
348
|
+
|
|
349
|
+
{/* Status bar */}
|
|
350
|
+
<StatusBar
|
|
351
|
+
modelDisplayName={getModelDisplayName(model)}
|
|
352
|
+
cumulativeTokens={cumulativeTokens}
|
|
353
|
+
cumulativeCost={cumulativeCost}
|
|
354
|
+
turnCount={turnCount}
|
|
355
|
+
contextPercentage={getContextWindow(model) > 0
|
|
356
|
+
? Math.round((cumulativeTokens / getContextWindow(model)) * 100)
|
|
357
|
+
: undefined}
|
|
358
|
+
/>
|
|
359
|
+
|
|
360
|
+
{/* Command autocomplete menu */}
|
|
361
|
+
{commandMenuVisible && (
|
|
362
|
+
<CommandMenu
|
|
363
|
+
commands={commandMenuItems}
|
|
364
|
+
selectedIndex={commandMenuIndex}
|
|
365
|
+
/>
|
|
366
|
+
)}
|
|
367
|
+
|
|
368
|
+
{/* Input */}
|
|
369
|
+
<Box marginTop={turnCount === 0 && !commandOutput ? 1 : 0}>
|
|
370
|
+
<Input
|
|
371
|
+
onSubmit={handleSubmit}
|
|
372
|
+
historyValue={historyValue}
|
|
373
|
+
onHistoryNavigate={handleHistoryNavigate}
|
|
374
|
+
onTextChange={handleSlashChange}
|
|
375
|
+
commandMenuVisible={commandMenuVisible}
|
|
376
|
+
onCommandMenuNavigate={handleCommandMenuNavigate}
|
|
377
|
+
getSelectedCommand={getSelectedCommand}
|
|
378
|
+
/>
|
|
379
|
+
</Box>
|
|
380
|
+
|
|
381
|
+
{/* Debug Panel */}
|
|
382
|
+
<DebugPanel maxLines={8} show={showDebug} />
|
|
383
|
+
</Box>
|
|
384
|
+
);
|
|
385
|
+
}
|