@fastino-ai/pioneer-cli 0.1.0 → 0.2.0
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/README.md +161 -22
- package/bun.lock +82 -0
- package/cache/cache.db +0 -0
- package/cache/cache.db-shm +0 -0
- package/cache/cache.db-wal +0 -0
- package/fastino-ai-pioneer-cli-0.2.0.tgz +0 -0
- package/package.json +6 -3
- package/src/agent/Agent.ts +342 -0
- package/src/agent/BudgetManager.ts +167 -0
- package/src/agent/FileResolver.ts +321 -0
- package/src/agent/LLMClient.ts +435 -0
- package/src/agent/ToolRegistry.ts +97 -0
- package/src/agent/index.ts +15 -0
- package/src/agent/types.ts +84 -0
- package/src/chat/ChatApp.tsx +701 -0
- package/src/chat/index.ts +7 -0
- package/src/config.ts +185 -3
- package/src/evolution/EvalRunner.ts +301 -0
- package/src/evolution/EvolutionEngine.ts +319 -0
- package/src/evolution/FeedbackCollector.ts +197 -0
- package/src/evolution/ModelTrainer.ts +371 -0
- package/src/evolution/index.ts +18 -0
- package/src/evolution/types.ts +110 -0
- package/src/index.tsx +101 -2
- package/src/tools/bash.ts +184 -0
- package/src/tools/filesystem.ts +444 -0
- package/src/tools/index.ts +29 -0
- package/src/tools/modal.ts +269 -0
- package/src/tools/sandbox.ts +310 -0
- package/src/tools/training.ts +443 -0
- package/src/tools/wandb.ts +348 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatApp - Main interactive chat interface using Ink
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { useState, useEffect, useCallback, useRef } from "react";
|
|
6
|
+
import { Box, Text, useApp, useInput, useStdin } from "ink";
|
|
7
|
+
import TextInput from "ink-text-input";
|
|
8
|
+
import Spinner from "ink-spinner";
|
|
9
|
+
|
|
10
|
+
import { Agent, AgentEvents } from "../agent/Agent.js";
|
|
11
|
+
import type { Message, ToolCall, ToolResult, AgentConfig } from "../agent/types.js";
|
|
12
|
+
import type { FileReference } from "../agent/FileResolver.js";
|
|
13
|
+
import { createBashTool } from "../tools/bash.js";
|
|
14
|
+
import { createFilesystemTools } from "../tools/filesystem.js";
|
|
15
|
+
import { createSandboxTool } from "../tools/sandbox.js";
|
|
16
|
+
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
// Types
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
interface ChatMessage {
|
|
22
|
+
role: "user" | "assistant" | "system" | "tool";
|
|
23
|
+
content: string;
|
|
24
|
+
timestamp: Date;
|
|
25
|
+
toolName?: string;
|
|
26
|
+
isStreaming?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ChatState {
|
|
30
|
+
messages: ChatMessage[];
|
|
31
|
+
isProcessing: boolean;
|
|
32
|
+
streamingContent: string;
|
|
33
|
+
currentToolCall: string | null;
|
|
34
|
+
fileReferences: FileReference[];
|
|
35
|
+
error: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
// Message Display Components
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const MessageBubble: React.FC<{ message: ChatMessage }> = ({ message }) => {
|
|
43
|
+
const roleColors: Record<string, string> = {
|
|
44
|
+
user: "cyan",
|
|
45
|
+
assistant: "green",
|
|
46
|
+
system: "yellow",
|
|
47
|
+
tool: "magenta",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const roleLabels: Record<string, string> = {
|
|
51
|
+
user: "You",
|
|
52
|
+
assistant: "Agent",
|
|
53
|
+
system: "System",
|
|
54
|
+
tool: `Tool${message.toolName ? `: ${message.toolName}` : ""}`,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const color = roleColors[message.role] || "white";
|
|
58
|
+
const label = roleLabels[message.role] || message.role;
|
|
59
|
+
|
|
60
|
+
// Truncate long tool outputs
|
|
61
|
+
let content = message.content;
|
|
62
|
+
if (message.role === "tool" && content.length > 1000) {
|
|
63
|
+
content = content.slice(0, 1000) + "\n... (truncated)";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
68
|
+
<Text color={color} bold>
|
|
69
|
+
{label}:
|
|
70
|
+
</Text>
|
|
71
|
+
<Box marginLeft={2}>
|
|
72
|
+
<Text wrap="wrap">{content}</Text>
|
|
73
|
+
</Box>
|
|
74
|
+
</Box>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const StreamingMessage: React.FC<{ content: string }> = ({ content }) => (
|
|
79
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
80
|
+
<Text color="green" bold>
|
|
81
|
+
Agent: <Text color="gray">(streaming...)</Text>
|
|
82
|
+
</Text>
|
|
83
|
+
<Box marginLeft={2}>
|
|
84
|
+
<Text wrap="wrap">{content || "..."}</Text>
|
|
85
|
+
</Box>
|
|
86
|
+
</Box>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const ToolCallIndicator: React.FC<{ toolName: string }> = ({ toolName }) => (
|
|
90
|
+
<Box marginBottom={1}>
|
|
91
|
+
<Text color="magenta">
|
|
92
|
+
<Spinner type="dots" /> Calling tool: {toolName}
|
|
93
|
+
</Text>
|
|
94
|
+
</Box>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const FileReferencesIndicator: React.FC<{ refs: FileReference[] }> = ({ refs }) => {
|
|
98
|
+
if (refs.length === 0) return null;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
102
|
+
<Text color="blue" bold>📎 Referenced files:</Text>
|
|
103
|
+
{refs.map((ref, i) => (
|
|
104
|
+
<Box key={i} marginLeft={2}>
|
|
105
|
+
<Text color={ref.exists ? "blue" : "red"}>
|
|
106
|
+
{ref.exists ? "✓" : "✗"} {ref.relativePath}
|
|
107
|
+
{ref.isDirectory ? "/" : ""}
|
|
108
|
+
{ref.error ? ` (${ref.error})` : ""}
|
|
109
|
+
</Text>
|
|
110
|
+
</Box>
|
|
111
|
+
))}
|
|
112
|
+
</Box>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// File autocomplete dropdown
|
|
117
|
+
const FileSuggestions: React.FC<{
|
|
118
|
+
suggestions: string[];
|
|
119
|
+
selectedIndex: number;
|
|
120
|
+
searchPath: string;
|
|
121
|
+
}> = ({ suggestions, selectedIndex, searchPath }) => {
|
|
122
|
+
if (suggestions.length === 0) return null;
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<Box flexDirection="column" borderStyle="round" borderColor="blue" paddingX={1} marginBottom={1}>
|
|
126
|
+
<Text color="blue" dimColor>
|
|
127
|
+
📁 {searchPath || "./"} <Text color="gray">(↑↓ navigate, Tab select, Esc close)</Text>
|
|
128
|
+
</Text>
|
|
129
|
+
{suggestions.slice(0, 10).map((suggestion, i) => (
|
|
130
|
+
<Box key={i}>
|
|
131
|
+
<Text
|
|
132
|
+
color={i === selectedIndex ? "black" : "white"}
|
|
133
|
+
backgroundColor={i === selectedIndex ? "blue" : undefined}
|
|
134
|
+
>
|
|
135
|
+
{" "}{suggestion.endsWith("/") ? "📁" : "📄"} {suggestion}{" "}
|
|
136
|
+
</Text>
|
|
137
|
+
</Box>
|
|
138
|
+
))}
|
|
139
|
+
{suggestions.length > 10 && (
|
|
140
|
+
<Text dimColor> ... and {suggestions.length - 10} more</Text>
|
|
141
|
+
)}
|
|
142
|
+
</Box>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Extract @path from end of input
|
|
147
|
+
function extractAtPath(input: string): { path: string; start: number } | null {
|
|
148
|
+
// Match @ followed by path characters at end of string
|
|
149
|
+
const match = input.match(/@([\w\-.\/]*)$/);
|
|
150
|
+
if (!match) return null;
|
|
151
|
+
return { path: match[1], start: input.length - match[0].length };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const StatusBar: React.FC<{ budgetSummary: string; isProcessing: boolean }> = ({
|
|
155
|
+
budgetSummary,
|
|
156
|
+
isProcessing,
|
|
157
|
+
}) => (
|
|
158
|
+
<Box
|
|
159
|
+
borderStyle="single"
|
|
160
|
+
borderColor="gray"
|
|
161
|
+
paddingX={1}
|
|
162
|
+
marginTop={1}
|
|
163
|
+
>
|
|
164
|
+
<Text dimColor>
|
|
165
|
+
{isProcessing ? "Processing... | " : ""}
|
|
166
|
+
{budgetSummary}
|
|
167
|
+
</Text>
|
|
168
|
+
</Box>
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
172
|
+
// Main Chat Component
|
|
173
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
export interface ChatAppProps {
|
|
176
|
+
config: AgentConfig;
|
|
177
|
+
initialMessage?: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export const ChatApp: React.FC<ChatAppProps> = ({ config, initialMessage }) => {
|
|
181
|
+
const { exit } = useApp();
|
|
182
|
+
const { isRawModeSupported } = useStdin();
|
|
183
|
+
|
|
184
|
+
const [state, setState] = useState<ChatState>({
|
|
185
|
+
messages: [],
|
|
186
|
+
isProcessing: false,
|
|
187
|
+
streamingContent: "",
|
|
188
|
+
currentToolCall: null,
|
|
189
|
+
fileReferences: [],
|
|
190
|
+
error: null,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const [input, setInput] = useState("");
|
|
194
|
+
const [agent, setAgent] = useState<Agent | null>(null);
|
|
195
|
+
const [budgetSummary, setBudgetSummary] = useState("Initializing...");
|
|
196
|
+
|
|
197
|
+
// Autocomplete state
|
|
198
|
+
const [suggestions, setSuggestions] = useState<string[]>([]);
|
|
199
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
200
|
+
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
201
|
+
const [currentAtPath, setCurrentAtPath] = useState<{ path: string; start: number } | null>(null);
|
|
202
|
+
|
|
203
|
+
// Streaming content refs for throttled updates
|
|
204
|
+
const streamBufferRef = useRef("");
|
|
205
|
+
const streamUpdateTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
206
|
+
|
|
207
|
+
// Initialize agent
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
const newAgent = new Agent(config, {
|
|
210
|
+
onStream: (chunk) => {
|
|
211
|
+
// Accumulate in buffer
|
|
212
|
+
streamBufferRef.current += chunk;
|
|
213
|
+
|
|
214
|
+
// Throttle UI updates to every 50ms
|
|
215
|
+
if (!streamUpdateTimerRef.current) {
|
|
216
|
+
streamUpdateTimerRef.current = setTimeout(() => {
|
|
217
|
+
setState((s) => ({
|
|
218
|
+
...s,
|
|
219
|
+
streamingContent: streamBufferRef.current,
|
|
220
|
+
}));
|
|
221
|
+
streamUpdateTimerRef.current = null;
|
|
222
|
+
}, 50);
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
onToolCall: (toolCall) => {
|
|
226
|
+
setState((s) => ({
|
|
227
|
+
...s,
|
|
228
|
+
currentToolCall: toolCall.name,
|
|
229
|
+
}));
|
|
230
|
+
},
|
|
231
|
+
onToolResult: (result) => {
|
|
232
|
+
setState((s) => ({
|
|
233
|
+
...s,
|
|
234
|
+
currentToolCall: null,
|
|
235
|
+
messages: [
|
|
236
|
+
...s.messages,
|
|
237
|
+
{
|
|
238
|
+
role: "tool",
|
|
239
|
+
content: result.output || result.error || "(no output)",
|
|
240
|
+
timestamp: new Date(),
|
|
241
|
+
toolName: s.currentToolCall || undefined,
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
}));
|
|
245
|
+
},
|
|
246
|
+
onBudgetWarning: (warnings) => {
|
|
247
|
+
setState((s) => ({
|
|
248
|
+
...s,
|
|
249
|
+
messages: [
|
|
250
|
+
...s.messages,
|
|
251
|
+
{
|
|
252
|
+
role: "system",
|
|
253
|
+
content: `⚠️ Budget warning: ${warnings.join(", ")}`,
|
|
254
|
+
timestamp: new Date(),
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
}));
|
|
258
|
+
},
|
|
259
|
+
onFileReference: (refs) => {
|
|
260
|
+
setState((s) => ({
|
|
261
|
+
...s,
|
|
262
|
+
fileReferences: refs,
|
|
263
|
+
}));
|
|
264
|
+
},
|
|
265
|
+
onError: (error) => {
|
|
266
|
+
setState((s) => ({
|
|
267
|
+
...s,
|
|
268
|
+
error: error.message,
|
|
269
|
+
isProcessing: false,
|
|
270
|
+
}));
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Register tools
|
|
275
|
+
newAgent.registerTool(createBashTool({ cwd: process.cwd() }));
|
|
276
|
+
newAgent.registerTools(createFilesystemTools());
|
|
277
|
+
newAgent.registerTool(createSandboxTool());
|
|
278
|
+
|
|
279
|
+
setAgent(newAgent);
|
|
280
|
+
|
|
281
|
+
// Update budget summary - only when agent is initialized
|
|
282
|
+
setBudgetSummary(newAgent.getBudgetStatus().summary);
|
|
283
|
+
|
|
284
|
+
return () => { };
|
|
285
|
+
}, [config]);
|
|
286
|
+
|
|
287
|
+
// Update file suggestions when input changes
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
if (!agent || state.isProcessing) {
|
|
290
|
+
// Only update if currently showing suggestions
|
|
291
|
+
if (showSuggestions) {
|
|
292
|
+
setSuggestions([]);
|
|
293
|
+
setShowSuggestions(false);
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const atPath = extractAtPath(input);
|
|
299
|
+
|
|
300
|
+
// Only update currentAtPath if it changed
|
|
301
|
+
if (JSON.stringify(atPath) !== JSON.stringify(currentAtPath)) {
|
|
302
|
+
setCurrentAtPath(atPath);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (atPath) {
|
|
306
|
+
const newSuggestions = agent.getFileSuggestions(atPath.path);
|
|
307
|
+
setSuggestions(newSuggestions);
|
|
308
|
+
setShowSuggestions(newSuggestions.length > 0);
|
|
309
|
+
setSelectedIndex(0);
|
|
310
|
+
} else if (showSuggestions) {
|
|
311
|
+
// Only clear if currently showing
|
|
312
|
+
setSuggestions([]);
|
|
313
|
+
setShowSuggestions(false);
|
|
314
|
+
}
|
|
315
|
+
}, [input, agent, state.isProcessing]);
|
|
316
|
+
|
|
317
|
+
// Handle initial message if provided
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
if (agent && initialMessage && state.messages.length === 0) {
|
|
320
|
+
handleSubmit(initialMessage);
|
|
321
|
+
}
|
|
322
|
+
}, [agent, initialMessage]);
|
|
323
|
+
|
|
324
|
+
const handleSubmit = useCallback(
|
|
325
|
+
async (value: string) => {
|
|
326
|
+
if (!agent || !value.trim() || state.isProcessing) return;
|
|
327
|
+
|
|
328
|
+
const userMessage = value.trim();
|
|
329
|
+
setInput("");
|
|
330
|
+
|
|
331
|
+
// Handle special commands
|
|
332
|
+
if (userMessage === "/exit" || userMessage === "/quit") {
|
|
333
|
+
exit();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (userMessage === "/clear") {
|
|
338
|
+
agent.clearHistory();
|
|
339
|
+
streamBufferRef.current = "";
|
|
340
|
+
setState({
|
|
341
|
+
messages: [],
|
|
342
|
+
isProcessing: false,
|
|
343
|
+
streamingContent: "",
|
|
344
|
+
currentToolCall: null,
|
|
345
|
+
fileReferences: [],
|
|
346
|
+
error: null,
|
|
347
|
+
});
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (userMessage === "/help") {
|
|
352
|
+
setState((s) => ({
|
|
353
|
+
...s,
|
|
354
|
+
messages: [
|
|
355
|
+
...s.messages,
|
|
356
|
+
{
|
|
357
|
+
role: "system",
|
|
358
|
+
content: `Available commands:
|
|
359
|
+
/clear - Clear conversation history
|
|
360
|
+
/help - Show this help
|
|
361
|
+
/exit - Exit the chat
|
|
362
|
+
/tools - List available tools
|
|
363
|
+
/model - Show/switch model
|
|
364
|
+
/budget - Show budget status
|
|
365
|
+
|
|
366
|
+
Budget adjustment:
|
|
367
|
+
/budget tokens <n> - Set max tokens (0=unlimited)
|
|
368
|
+
/budget cost <n> - Set max cost in USD
|
|
369
|
+
/budget time <n> - Set max time in seconds
|
|
370
|
+
/budget tools <n> - Set max tool calls per turn
|
|
371
|
+
/budget unlimited - Remove all limits
|
|
372
|
+
|
|
373
|
+
File references:
|
|
374
|
+
@file.ts - Include file contents
|
|
375
|
+
@src/ - Include directory listing
|
|
376
|
+
@package.json - Include any file by path`,
|
|
377
|
+
timestamp: new Date(),
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
}));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (userMessage === "/tools") {
|
|
385
|
+
const tools = agent.getTools();
|
|
386
|
+
const toolList = tools
|
|
387
|
+
.map((t) => ` • ${t.name}: ${t.description}`)
|
|
388
|
+
.join("\n");
|
|
389
|
+
setState((s) => ({
|
|
390
|
+
...s,
|
|
391
|
+
messages: [
|
|
392
|
+
...s.messages,
|
|
393
|
+
{
|
|
394
|
+
role: "system",
|
|
395
|
+
content: `Available tools:\n${toolList}`,
|
|
396
|
+
timestamp: new Date(),
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
}));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (userMessage === "/model") {
|
|
404
|
+
const info = agent.getModelInfo();
|
|
405
|
+
const modelHelp = `🤖 Current Model
|
|
406
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
407
|
+
Provider: ${info.provider}
|
|
408
|
+
Model: ${info.model}
|
|
409
|
+
|
|
410
|
+
Switch with /model <name>. Examples:
|
|
411
|
+
Anthropic (Claude 4.5):
|
|
412
|
+
claude-opus-4-5-20251101 (strongest)
|
|
413
|
+
claude-sonnet-4-5-20250929 (balanced)
|
|
414
|
+
claude-haiku-4-5-20251001 (fast/cheap)
|
|
415
|
+
OpenAI:
|
|
416
|
+
gpt-4o, gpt-4o-mini, o1, o3-mini`;
|
|
417
|
+
setState((s) => ({
|
|
418
|
+
...s,
|
|
419
|
+
messages: [...s.messages, { role: "system", content: modelHelp, timestamp: new Date() }],
|
|
420
|
+
}));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (userMessage.startsWith("/model ")) {
|
|
425
|
+
const newModel = userMessage.slice(7).trim();
|
|
426
|
+
if (newModel) {
|
|
427
|
+
agent.setModel(newModel);
|
|
428
|
+
setState((s) => ({
|
|
429
|
+
...s,
|
|
430
|
+
messages: [...s.messages, { role: "system", content: `✓ Switched to model: ${newModel}`, timestamp: new Date() }],
|
|
431
|
+
}));
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (userMessage === "/budget") {
|
|
437
|
+
const status = agent.getBudgetStatus();
|
|
438
|
+
const maxTools = agent.getMaxToolCalls();
|
|
439
|
+
const budgetInfo = `📊 Budget Status
|
|
440
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
441
|
+
${status.summary}
|
|
442
|
+
Max tool calls: ${maxTools}
|
|
443
|
+
|
|
444
|
+
⚙️ Adjust Limits
|
|
445
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
446
|
+
/budget tokens <n> Set max tokens (0 = unlimited)
|
|
447
|
+
/budget cost <n> Set max cost in USD (0 = unlimited)
|
|
448
|
+
/budget time <n> Set max time in seconds (0 = unlimited)
|
|
449
|
+
/budget tools <n> Set max tool calls per turn (0 = unlimited)
|
|
450
|
+
/budget unlimited Remove all limits`;
|
|
451
|
+
|
|
452
|
+
setState((s) => ({
|
|
453
|
+
...s,
|
|
454
|
+
messages: [
|
|
455
|
+
...s.messages,
|
|
456
|
+
{ role: "system", content: budgetInfo, timestamp: new Date() },
|
|
457
|
+
],
|
|
458
|
+
}));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Budget adjustment commands
|
|
463
|
+
if (userMessage.startsWith("/budget ")) {
|
|
464
|
+
const parts = userMessage.split(" ");
|
|
465
|
+
const subCmd = parts[1];
|
|
466
|
+
const value = parts[2];
|
|
467
|
+
|
|
468
|
+
let message = "";
|
|
469
|
+
if (subCmd === "unlimited") {
|
|
470
|
+
agent.updateBudget({ maxTokens: undefined, maxCost: undefined, maxTime: undefined, maxIterations: undefined });
|
|
471
|
+
agent.setMaxToolCalls(1000);
|
|
472
|
+
message = "✓ All limits removed";
|
|
473
|
+
} else if (subCmd === "tokens" && value) {
|
|
474
|
+
const val = parseInt(value, 10);
|
|
475
|
+
agent.updateBudget({ maxTokens: val === 0 ? undefined : val });
|
|
476
|
+
message = val === 0 ? "✓ Token limit removed" : `✓ Token limit set to ${val.toLocaleString()}`;
|
|
477
|
+
} else if (subCmd === "cost" && value) {
|
|
478
|
+
const val = parseFloat(value);
|
|
479
|
+
agent.updateBudget({ maxCost: val === 0 ? undefined : val });
|
|
480
|
+
message = val === 0 ? "✓ Cost limit removed" : `✓ Cost limit set to $${val}`;
|
|
481
|
+
} else if (subCmd === "time" && value) {
|
|
482
|
+
const val = parseInt(value, 10);
|
|
483
|
+
agent.updateBudget({ maxTime: val === 0 ? undefined : val });
|
|
484
|
+
message = val === 0 ? "✓ Time limit removed" : `✓ Time limit set to ${val}s`;
|
|
485
|
+
} else if (subCmd === "tools" && value) {
|
|
486
|
+
const val = parseInt(value, 10);
|
|
487
|
+
agent.setMaxToolCalls(val === 0 ? 1000 : val);
|
|
488
|
+
message = val === 0 ? "✓ Tool call limit removed" : `✓ Max tool calls set to ${val}`;
|
|
489
|
+
} else {
|
|
490
|
+
message = "Usage: /budget [tokens|cost|time|tools] <value> or /budget unlimited";
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
setState((s) => ({
|
|
494
|
+
...s,
|
|
495
|
+
messages: [...s.messages, { role: "system", content: message, timestamp: new Date() }],
|
|
496
|
+
}));
|
|
497
|
+
setBudgetSummary(agent.getBudgetStatus().summary);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Add user message
|
|
502
|
+
streamBufferRef.current = "";
|
|
503
|
+
if (streamUpdateTimerRef.current) {
|
|
504
|
+
clearTimeout(streamUpdateTimerRef.current);
|
|
505
|
+
streamUpdateTimerRef.current = null;
|
|
506
|
+
}
|
|
507
|
+
setState((s) => ({
|
|
508
|
+
...s,
|
|
509
|
+
messages: [
|
|
510
|
+
...s.messages,
|
|
511
|
+
{ role: "user", content: userMessage, timestamp: new Date() },
|
|
512
|
+
],
|
|
513
|
+
isProcessing: true,
|
|
514
|
+
streamingContent: "",
|
|
515
|
+
fileReferences: [],
|
|
516
|
+
error: null,
|
|
517
|
+
}));
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
const response = await agent.chat(userMessage, true);
|
|
521
|
+
|
|
522
|
+
// Clear any pending stream update
|
|
523
|
+
if (streamUpdateTimerRef.current) {
|
|
524
|
+
clearTimeout(streamUpdateTimerRef.current);
|
|
525
|
+
streamUpdateTimerRef.current = null;
|
|
526
|
+
}
|
|
527
|
+
streamBufferRef.current = "";
|
|
528
|
+
|
|
529
|
+
setState((s) => ({
|
|
530
|
+
...s,
|
|
531
|
+
messages: [
|
|
532
|
+
...s.messages,
|
|
533
|
+
{ role: "assistant", content: response, timestamp: new Date() },
|
|
534
|
+
],
|
|
535
|
+
isProcessing: false,
|
|
536
|
+
streamingContent: "",
|
|
537
|
+
}));
|
|
538
|
+
} catch (error) {
|
|
539
|
+
streamBufferRef.current = "";
|
|
540
|
+
setState((s) => ({
|
|
541
|
+
...s,
|
|
542
|
+
error: error instanceof Error ? error.message : String(error),
|
|
543
|
+
isProcessing: false,
|
|
544
|
+
}));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
setBudgetSummary(agent.getBudgetStatus().summary);
|
|
548
|
+
},
|
|
549
|
+
[agent, state.isProcessing, exit]
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
// Insert selected suggestion into input
|
|
553
|
+
const insertSuggestion = useCallback((suggestion: string) => {
|
|
554
|
+
if (!currentAtPath) return;
|
|
555
|
+
const before = input.slice(0, currentAtPath.start);
|
|
556
|
+
const newInput = before + "@" + suggestion;
|
|
557
|
+
setInput(newInput);
|
|
558
|
+
setShowSuggestions(false);
|
|
559
|
+
setSuggestions([]);
|
|
560
|
+
}, [input, currentAtPath]);
|
|
561
|
+
|
|
562
|
+
// Stop the agent
|
|
563
|
+
const handleStop = useCallback(() => {
|
|
564
|
+
if (agent && state.isProcessing) {
|
|
565
|
+
agent.stop();
|
|
566
|
+
setState((s) => ({
|
|
567
|
+
...s,
|
|
568
|
+
isProcessing: false,
|
|
569
|
+
currentToolCall: null,
|
|
570
|
+
}));
|
|
571
|
+
}
|
|
572
|
+
}, [agent, state.isProcessing]);
|
|
573
|
+
|
|
574
|
+
// Handle keyboard shortcuts
|
|
575
|
+
useInput((char, key) => {
|
|
576
|
+
if (key.ctrl && char === "c") {
|
|
577
|
+
exit();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Escape to stop agent when processing
|
|
582
|
+
if (key.escape && state.isProcessing) {
|
|
583
|
+
handleStop();
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Handle suggestion navigation when suggestions are shown
|
|
588
|
+
if (showSuggestions && suggestions.length > 0) {
|
|
589
|
+
if (key.downArrow) {
|
|
590
|
+
setSelectedIndex((i) => Math.min(i + 1, suggestions.length - 1));
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (key.upArrow) {
|
|
594
|
+
setSelectedIndex((i) => Math.max(i - 1, 0));
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (key.tab) {
|
|
598
|
+
insertSuggestion(suggestions[selectedIndex]);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (key.escape) {
|
|
602
|
+
setShowSuggestions(false);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
if (!agent) {
|
|
609
|
+
return (
|
|
610
|
+
<Box>
|
|
611
|
+
<Text color="yellow">
|
|
612
|
+
<Spinner type="dots" />
|
|
613
|
+
</Text>
|
|
614
|
+
<Text> Initializing agent...</Text>
|
|
615
|
+
</Box>
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return (
|
|
620
|
+
<Box flexDirection="column" paddingX={1}>
|
|
621
|
+
{/* Header */}
|
|
622
|
+
<Box marginBottom={1}>
|
|
623
|
+
<Text color="cyan" bold>
|
|
624
|
+
Pioneer Agent
|
|
625
|
+
</Text>
|
|
626
|
+
<Text dimColor> — /help for commands, @ for files, Ctrl+C to exit</Text>
|
|
627
|
+
</Box>
|
|
628
|
+
|
|
629
|
+
{/* Messages */}
|
|
630
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
631
|
+
{state.messages.map((msg, i) => (
|
|
632
|
+
<MessageBubble key={i} message={msg} />
|
|
633
|
+
))}
|
|
634
|
+
|
|
635
|
+
{/* Streaming content */}
|
|
636
|
+
{state.isProcessing && state.streamingContent && (
|
|
637
|
+
<StreamingMessage content={state.streamingContent} />
|
|
638
|
+
)}
|
|
639
|
+
|
|
640
|
+
{/* File references indicator */}
|
|
641
|
+
{state.fileReferences.length > 0 && (
|
|
642
|
+
<FileReferencesIndicator refs={state.fileReferences} />
|
|
643
|
+
)}
|
|
644
|
+
|
|
645
|
+
{/* Tool call indicator */}
|
|
646
|
+
{state.currentToolCall && (
|
|
647
|
+
<ToolCallIndicator toolName={state.currentToolCall} />
|
|
648
|
+
)}
|
|
649
|
+
|
|
650
|
+
{/* Error display */}
|
|
651
|
+
{state.error && (
|
|
652
|
+
<Box marginBottom={1}>
|
|
653
|
+
<Text color="red">Error: {state.error}</Text>
|
|
654
|
+
</Box>
|
|
655
|
+
)}
|
|
656
|
+
</Box>
|
|
657
|
+
|
|
658
|
+
{/* File suggestions dropdown */}
|
|
659
|
+
{showSuggestions && !state.isProcessing && (
|
|
660
|
+
<FileSuggestions
|
|
661
|
+
suggestions={suggestions}
|
|
662
|
+
selectedIndex={selectedIndex}
|
|
663
|
+
searchPath={currentAtPath?.path || ""}
|
|
664
|
+
/>
|
|
665
|
+
)}
|
|
666
|
+
|
|
667
|
+
{/* Input area */}
|
|
668
|
+
<Box marginTop={1}>
|
|
669
|
+
{state.isProcessing ? (
|
|
670
|
+
<Box>
|
|
671
|
+
<Text color="yellow">
|
|
672
|
+
<Spinner type="dots" />
|
|
673
|
+
</Text>
|
|
674
|
+
<Text> Processing... </Text>
|
|
675
|
+
<Text dimColor>(Esc to stop)</Text>
|
|
676
|
+
</Box>
|
|
677
|
+
) : (
|
|
678
|
+
<Box>
|
|
679
|
+
<Text color="cyan" bold>
|
|
680
|
+
{">"}{" "}
|
|
681
|
+
</Text>
|
|
682
|
+
{isRawModeSupported ? (
|
|
683
|
+
<TextInput
|
|
684
|
+
value={input}
|
|
685
|
+
onChange={setInput}
|
|
686
|
+
onSubmit={handleSubmit}
|
|
687
|
+
placeholder="Type a message... (@ for files)"
|
|
688
|
+
/>
|
|
689
|
+
) : (
|
|
690
|
+
<Text dimColor>Raw mode not supported in this terminal</Text>
|
|
691
|
+
)}
|
|
692
|
+
</Box>
|
|
693
|
+
)}
|
|
694
|
+
</Box>
|
|
695
|
+
|
|
696
|
+
{/* Status bar */}
|
|
697
|
+
<StatusBar budgetSummary={budgetSummary} isProcessing={state.isProcessing} />
|
|
698
|
+
</Box>
|
|
699
|
+
);
|
|
700
|
+
};
|
|
701
|
+
|