@fastino-ai/pioneer-cli 0.2.1 → 0.2.3

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 (40) hide show
  1. package/.claude/settings.local.json +7 -1
  2. package/.cursor/rules/api-documentation.mdc +14 -0
  3. package/.cursor/rules/backend-location-rule.mdc +5 -0
  4. package/Medical_NER_Dataset_1.jsonl +50 -0
  5. package/README.md +4 -1
  6. package/bun.lock +52 -0
  7. package/package.json +5 -2
  8. package/src/api.ts +551 -22
  9. package/src/chat/ChatApp.tsx +548 -263
  10. package/src/client/ToolExecutor.ts +175 -0
  11. package/src/client/WebSocketClient.ts +333 -0
  12. package/src/client/index.ts +2 -0
  13. package/src/config.ts +49 -139
  14. package/src/index.tsx +815 -107
  15. package/src/telemetry.ts +173 -0
  16. package/src/tests/config.test.ts +19 -0
  17. package/src/tools/bash.ts +1 -1
  18. package/src/tools/filesystem.ts +1 -1
  19. package/src/tools/index.ts +2 -9
  20. package/src/tools/sandbox.ts +1 -1
  21. package/src/tools/types.ts +25 -0
  22. package/src/utils/index.ts +6 -0
  23. package/fastino-ai-pioneer-cli-0.2.0.tgz +0 -0
  24. package/ner_dataset.json +0 -111
  25. package/src/agent/Agent.ts +0 -342
  26. package/src/agent/BudgetManager.ts +0 -167
  27. package/src/agent/LLMClient.ts +0 -435
  28. package/src/agent/ToolRegistry.ts +0 -97
  29. package/src/agent/index.ts +0 -15
  30. package/src/agent/types.ts +0 -84
  31. package/src/evolution/EvalRunner.ts +0 -301
  32. package/src/evolution/EvolutionEngine.ts +0 -319
  33. package/src/evolution/FeedbackCollector.ts +0 -197
  34. package/src/evolution/ModelTrainer.ts +0 -371
  35. package/src/evolution/index.ts +0 -18
  36. package/src/evolution/types.ts +0 -110
  37. package/src/tools/modal.ts +0 -269
  38. package/src/tools/training.ts +0 -443
  39. package/src/tools/wandb.ts +0 -348
  40. /package/src/{agent → utils}/FileResolver.ts +0 -0
@@ -1,18 +1,21 @@
1
1
  /**
2
2
  * ChatApp - Main interactive chat interface using Ink
3
+ * Uses Pioneer backend via WebSocket for agent, executes CLI tools locally
3
4
  */
4
5
 
5
6
  import React, { useState, useEffect, useCallback, useRef } from "react";
6
7
  import { Box, Text, useApp, useInput, useStdin } from "ink";
7
8
  import TextInput from "ink-text-input";
8
9
  import Spinner from "ink-spinner";
10
+ import Markdown from "@inkkit/ink-markdown";
9
11
 
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, executeBashStream } from "../tools/bash.js";
14
- import { createFilesystemTools } from "../tools/filesystem.js";
15
- import { createSandboxTool } from "../tools/sandbox.js";
12
+ import { WebSocketClient, type HistoryMessage } from "../client/WebSocketClient.js";
13
+ import { ToolExecutor } from "../client/ToolExecutor.js";
14
+ import type { FileReference } from "../utils/FileResolver.js";
15
+ import { FileResolver } from "../utils/FileResolver.js";
16
+ import { executeBashStream } from "../tools/bash.js";
17
+ import { trackChatSessionStart, trackChatSessionEnd } from "../telemetry.js";
18
+ import { getMleModel, setMleModel, AVAILABLE_MLE_MODELS, DEFAULT_MLE_MODEL } from "../config.js";
16
19
 
17
20
  // ─────────────────────────────────────────────────────────────────────────────
18
21
  // Types
@@ -23,23 +26,119 @@ interface ChatMessage {
23
26
  content: string;
24
27
  timestamp: Date;
25
28
  toolName?: string;
29
+ toolArgs?: Record<string, unknown>;
26
30
  isStreaming?: boolean;
31
+ isUserBash?: boolean; // For !command - show raw output
32
+ }
33
+
34
+ interface ToolCallInfo {
35
+ name: string;
36
+ args: Record<string, unknown>;
27
37
  }
28
38
 
29
39
  interface ChatState {
30
40
  messages: ChatMessage[];
31
41
  isProcessing: boolean;
32
42
  streamingContent: string;
33
- currentToolCall: string | null;
43
+ currentToolCall: ToolCallInfo | null;
34
44
  fileReferences: FileReference[];
35
45
  error: string | null;
36
46
  }
37
47
 
48
+ // ─────────────────────────────────────────────────────────────────────────────
49
+ // Tool Output Formatting
50
+ // ─────────────────────────────────────────────────────────────────────────────
51
+
52
+ interface FormattedToolOutput {
53
+ header: string;
54
+ summary: string;
55
+ showContent: boolean;
56
+ content?: string;
57
+ }
58
+
59
+ function formatToolOutput(
60
+ toolName: string,
61
+ args: Record<string, unknown>,
62
+ result: string
63
+ ): FormattedToolOutput {
64
+ const lineCount = result.split("\n").length;
65
+
66
+ switch (toolName) {
67
+ case "read_file": {
68
+ const filePath = args.path as string || args.file_path as string || "unknown";
69
+ const shortPath = filePath.split("/").slice(-2).join("/");
70
+ return {
71
+ header: `Read(${shortPath})`,
72
+ summary: `Read ${lineCount} lines`,
73
+ showContent: false,
74
+ };
75
+ }
76
+
77
+ case "write_file": {
78
+ const filePath = args.path as string || args.file_path as string || "unknown";
79
+ const shortPath = filePath.split("/").slice(-2).join("/");
80
+ const content = args.content as string || "";
81
+ const writtenLines = content.split("\n").length;
82
+ return {
83
+ header: `Write(${shortPath})`,
84
+ summary: `Wrote ${writtenLines} lines`,
85
+ showContent: false,
86
+ };
87
+ }
88
+
89
+ case "list_directory": {
90
+ const dirPath = args.path as string || args.directory as string || ".";
91
+ const shortPath = dirPath === "." ? "." : dirPath.split("/").slice(-2).join("/");
92
+ const entries = result.trim().split("\n").filter(Boolean).length;
93
+ return {
94
+ header: `ListDir(${shortPath})`,
95
+ summary: `${entries} entries`,
96
+ showContent: true,
97
+ content: result.length > 500 ? result.slice(0, 500) + "\n..." : result,
98
+ };
99
+ }
100
+
101
+ case "bash": {
102
+ const command = args.command as string || "";
103
+ const shortCommand = command.length > 60 ? command.slice(0, 60) + "..." : command;
104
+ const hasOutput = Boolean(result && result !== "(no output)");
105
+ return {
106
+ header: `Bash: ${shortCommand}`,
107
+ summary: hasOutput ? `${lineCount} lines` : "(no output)",
108
+ showContent: hasOutput && lineCount <= 30,
109
+ content: hasOutput ? (result.length > 1000 ? result.slice(0, 1000) + "\n..." : result) : undefined,
110
+ };
111
+ }
112
+
113
+ case "search_files":
114
+ case "grep": {
115
+ const pattern = args.pattern as string || "";
116
+ const matches = result.trim().split("\n").filter(Boolean).length;
117
+ return {
118
+ header: `Search: "${pattern}"`,
119
+ summary: `${matches} matches`,
120
+ showContent: matches <= 20,
121
+ content: matches <= 20 ? result : result.split("\n").slice(0, 20).join("\n") + `\n... (${matches - 20} more)`,
122
+ };
123
+ }
124
+
125
+ default: {
126
+ // For unknown tools, show truncated content
127
+ return {
128
+ header: toolName,
129
+ summary: `${lineCount} lines`,
130
+ showContent: lineCount <= 20,
131
+ content: result.length > 1000 ? result.slice(0, 1000) + "\n..." : result,
132
+ };
133
+ }
134
+ }
135
+ }
136
+
38
137
  // ─────────────────────────────────────────────────────────────────────────────
39
138
  // Message Display Components
40
139
  // ─────────────────────────────────────────────────────────────────────────────
41
140
 
42
- const MessageBubble: React.FC<{ message: ChatMessage }> = ({ message }) => {
141
+ const MessageBubble: React.FC<{ message: ChatMessage }> = React.memo(({ message }) => {
43
142
  const roleColors: Record<string, string> = {
44
143
  user: "cyan",
45
144
  assistant: "green",
@@ -47,33 +146,76 @@ const MessageBubble: React.FC<{ message: ChatMessage }> = ({ message }) => {
47
146
  tool: "magenta",
48
147
  };
49
148
 
149
+ // Handle user-initiated bash commands (!command) - show raw output
150
+ if (message.role === "tool" && message.isUserBash) {
151
+ return (
152
+ <Box flexDirection="column" marginBottom={1}>
153
+ <Text wrap="wrap">{message.content}</Text>
154
+ </Box>
155
+ );
156
+ }
157
+
158
+ // Handle agent tool messages with pretty formatting
159
+ if (message.role === "tool" && message.toolName && message.toolArgs) {
160
+ const formatted = formatToolOutput(message.toolName, message.toolArgs, message.content);
161
+ return (
162
+ <Box flexDirection="column" marginBottom={1}>
163
+ <Text color="magenta">
164
+ {formatted.header}
165
+ </Text>
166
+ <Box marginLeft={2}>
167
+ <Text color="gray">⎿ {formatted.summary}</Text>
168
+ </Box>
169
+ {formatted.showContent && formatted.content && (
170
+ <Box marginLeft={4} flexDirection="column">
171
+ <Text wrap="wrap" dimColor>{formatted.content}</Text>
172
+ </Box>
173
+ )}
174
+ </Box>
175
+ );
176
+ }
177
+
178
+ // Handle tool messages without args (fallback)
179
+ if (message.role === "tool") {
180
+ const label = message.toolName || "Tool";
181
+ const content = message.content.length > 1000
182
+ ? message.content.slice(0, 1000) + "\n... (truncated)"
183
+ : message.content;
184
+ return (
185
+ <Box flexDirection="column" marginBottom={1}>
186
+ <Text color="magenta">{label}:</Text>
187
+ <Box marginLeft={2}>
188
+ <Text wrap="wrap" dimColor>{content}</Text>
189
+ </Box>
190
+ </Box>
191
+ );
192
+ }
193
+
50
194
  const roleLabels: Record<string, string> = {
51
195
  user: "You",
52
196
  assistant: "Agent",
53
197
  system: "System",
54
- tool: `Tool${message.toolName ? `: ${message.toolName}` : ""}`,
55
198
  };
56
199
 
57
200
  const color = roleColors[message.role] || "white";
58
201
  const label = roleLabels[message.role] || message.role;
59
202
 
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
203
  return (
67
204
  <Box flexDirection="column" marginBottom={1}>
68
205
  <Text color={color} bold>
69
206
  {label}:
70
207
  </Text>
71
208
  <Box marginLeft={2}>
72
- <Text wrap="wrap">{content}</Text>
209
+ {message.role === "assistant" ? (
210
+ <Markdown>{message.content}</Markdown>
211
+ ) : (
212
+ <Text wrap="wrap">{message.content}</Text>
213
+ )}
73
214
  </Box>
74
215
  </Box>
75
216
  );
76
- };
217
+ });
218
+ MessageBubble.displayName = 'MessageBubble';
77
219
 
78
220
  const StreamingMessage: React.FC<{ content: string }> = ({ content }) => (
79
221
  <Box flexDirection="column" marginBottom={1}>
@@ -81,20 +223,36 @@ const StreamingMessage: React.FC<{ content: string }> = ({ content }) => (
81
223
  Agent: <Text color="gray">(streaming...)</Text>
82
224
  </Text>
83
225
  <Box marginLeft={2}>
84
- <Text wrap="wrap">{content || "..."}</Text>
226
+ {content ? <Markdown>{content}</Markdown> : <Text>...</Text>}
85
227
  </Box>
86
228
  </Box>
87
229
  );
88
230
 
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
- );
231
+ const ToolCallIndicator: React.FC<{ toolCall: ToolCallInfo }> = React.memo(({ toolCall }) => {
232
+ // Show a nice summary of what tool is being called
233
+ let displayText = toolCall.name;
234
+ if (toolCall.name === "bash" && toolCall.args.command) {
235
+ const cmd = toolCall.args.command as string;
236
+ displayText = `bash: ${cmd.length > 50 ? cmd.slice(0, 50) + "..." : cmd}`;
237
+ } else if ((toolCall.name === "read_file" || toolCall.name === "write_file") && (toolCall.args.path || toolCall.args.file_path)) {
238
+ const path = (toolCall.args.path || toolCall.args.file_path) as string;
239
+ displayText = `${toolCall.name}: ${path.split("/").slice(-2).join("/")}`;
240
+ } else if (toolCall.name === "list_directory" && (toolCall.args.path || toolCall.args.directory)) {
241
+ const path = (toolCall.args.path || toolCall.args.directory) as string;
242
+ displayText = `list_directory: ${path}`;
243
+ }
96
244
 
97
- const FileReferencesIndicator: React.FC<{ refs: FileReference[] }> = ({ refs }) => {
245
+ return (
246
+ <Box marginBottom={1}>
247
+ <Text color="magenta">
248
+ <Spinner type="dots" /> {displayText}
249
+ </Text>
250
+ </Box>
251
+ );
252
+ });
253
+ ToolCallIndicator.displayName = 'ToolCallIndicator';
254
+
255
+ const FileReferencesIndicator: React.FC<{ refs: FileReference[] }> = React.memo(({ refs }) => {
98
256
  if (refs.length === 0) return null;
99
257
 
100
258
  return (
@@ -111,7 +269,8 @@ const FileReferencesIndicator: React.FC<{ refs: FileReference[] }> = ({ refs })
111
269
  ))}
112
270
  </Box>
113
271
  );
114
- };
272
+ });
273
+ FileReferencesIndicator.displayName = 'FileReferencesIndicator';
115
274
 
116
275
  // File autocomplete dropdown
117
276
  const FileSuggestions: React.FC<{
@@ -145,14 +304,49 @@ const FileSuggestions: React.FC<{
145
304
 
146
305
  // Extract @path from end of input
147
306
  function extractAtPath(input: string): { path: string; start: number } | null {
148
- // Match @ followed by path characters at end of string
149
307
  const match = input.match(/@([\w\-.\/]*)$/);
150
308
  if (!match) return null;
151
309
  return { path: match[1], start: input.length - match[0].length };
152
310
  }
153
311
 
154
- const StatusBar: React.FC<{ budgetSummary: string; isProcessing: boolean }> = ({
155
- budgetSummary,
312
+ // Model selection component
313
+ const ModelSelector: React.FC<{
314
+ selectedIndex: number;
315
+ currentModel: string | undefined;
316
+ }> = ({ selectedIndex, currentModel }) => {
317
+ const effectiveModel = currentModel || DEFAULT_MLE_MODEL;
318
+
319
+ return (
320
+ <Box flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={1} marginBottom={1}>
321
+ <Text color="cyan" bold>
322
+ Select a model: <Text color="gray">(↑/↓ navigate, Enter select, Esc cancel)</Text>
323
+ </Text>
324
+ <Box marginTop={1} flexDirection="column">
325
+ {AVAILABLE_MLE_MODELS.map((model, i) => {
326
+ const isSelected = i === selectedIndex;
327
+ const isCurrent = model.id === effectiveModel;
328
+ return (
329
+ <Box key={model.id}>
330
+ <Text
331
+ color={isSelected ? "black" : isCurrent ? "cyan" : "white"}
332
+ backgroundColor={isSelected ? "cyan" : undefined}
333
+ bold={isSelected}
334
+ >
335
+ {" "}{isSelected ? "❯" : " "} {model.name}
336
+ <Text dimColor={!isSelected}> - {model.description}</Text>
337
+ {isCurrent && <Text color={isSelected ? "black" : "green"}> (current)</Text>}
338
+ {" "}
339
+ </Text>
340
+ </Box>
341
+ );
342
+ })}
343
+ </Box>
344
+ </Box>
345
+ );
346
+ };
347
+
348
+ const StatusBar: React.FC<{ serverStatus: string; isProcessing: boolean }> = ({
349
+ serverStatus,
156
350
  isProcessing,
157
351
  }) => (
158
352
  <Box
@@ -163,7 +357,7 @@ const StatusBar: React.FC<{ budgetSummary: string; isProcessing: boolean }> = ({
163
357
  >
164
358
  <Text dimColor>
165
359
  {isProcessing ? "Processing... | " : ""}
166
- {budgetSummary}
360
+ {serverStatus}
167
361
  </Text>
168
362
  </Box>
169
363
  );
@@ -173,11 +367,10 @@ const StatusBar: React.FC<{ budgetSummary: string; isProcessing: boolean }> = ({
173
367
  // ─────────────────────────────────────────────────────────────────────────────
174
368
 
175
369
  export interface ChatAppProps {
176
- config: AgentConfig;
177
370
  initialMessage?: string;
178
371
  }
179
372
 
180
- export const ChatApp: React.FC<ChatAppProps> = ({ config, initialMessage }) => {
373
+ export const ChatApp: React.FC<ChatAppProps> = ({ initialMessage }) => {
181
374
  const { exit } = useApp();
182
375
  const { isRawModeSupported } = useStdin();
183
376
 
@@ -191,8 +384,11 @@ export const ChatApp: React.FC<ChatAppProps> = ({ config, initialMessage }) => {
191
384
  });
192
385
 
193
386
  const [input, setInput] = useState("");
194
- const [agent, setAgent] = useState<Agent | null>(null);
195
- const [budgetSummary, setBudgetSummary] = useState("Initializing...");
387
+ const [client, setClient] = useState<WebSocketClient | null>(null);
388
+ const [toolExecutor, setToolExecutor] = useState<ToolExecutor | null>(null);
389
+ const [fileResolver, setFileResolver] = useState<FileResolver | null>(null);
390
+ const [serverStatus, setServerStatus] = useState("Connecting...");
391
+ const [history, setHistory] = useState<HistoryMessage[]>([]);
196
392
 
197
393
  // Autocomplete state
198
394
  const [suggestions, setSuggestions] = useState<string[]>([]);
@@ -200,94 +396,67 @@ export const ChatApp: React.FC<ChatAppProps> = ({ config, initialMessage }) => {
200
396
  const [showSuggestions, setShowSuggestions] = useState(false);
201
397
  const [currentAtPath, setCurrentAtPath] = useState<{ path: string; start: number } | null>(null);
202
398
 
399
+ // Model selection state
400
+ const [showModelSelect, setShowModelSelect] = useState(false);
401
+ const [modelSelectedIndex, setModelSelectedIndex] = useState(0);
402
+ const [currentMleModel, setCurrentMleModel] = useState<string | undefined>(getMleModel());
403
+
203
404
  // Streaming content refs for throttled updates
204
405
  const streamBufferRef = useRef("");
205
406
  const streamUpdateTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
206
407
 
207
- // Initialize agent
408
+ // Session tracking refs
409
+ const sessionStartTimeRef = useRef<number>(Date.now());
410
+ const toolCallCountRef = useRef<number>(0);
411
+ const messageCountRef = useRef<number>(0);
412
+
413
+ // Initialize client, tool executor, and file resolver
208
414
  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
- });
415
+ // Track session start
416
+ sessionStartTimeRef.current = Date.now();
417
+ toolCallCountRef.current = 0;
418
+ const model = getMleModel() || DEFAULT_MLE_MODEL;
419
+ trackChatSessionStart("mle-agent", model);
420
+
421
+ const newClient = new WebSocketClient();
422
+ setClient(newClient);
273
423
 
274
- // Register tools
275
- newAgent.registerTool(createBashTool({ cwd: process.cwd() }));
276
- newAgent.registerTools(createFilesystemTools());
277
- newAgent.registerTool(createSandboxTool());
424
+ const executor = new ToolExecutor({ workingDirectory: process.cwd() });
425
+ setToolExecutor(executor);
278
426
 
279
- setAgent(newAgent);
427
+ const resolver = new FileResolver(process.cwd());
428
+ setFileResolver(resolver);
280
429
 
281
- // Update budget summary - only when agent is initialized
282
- setBudgetSummary(newAgent.getBudgetStatus().summary);
430
+ // Check server health and connect
431
+ newClient.health().then(async (healthy) => {
432
+ if (healthy) {
433
+ try {
434
+ await newClient.connect();
435
+ setServerStatus("Connected to Pioneer server (WebSocket)");
436
+ } catch (err) {
437
+ setServerStatus("⚠️ WebSocket connection failed");
438
+ }
439
+ } else {
440
+ setServerStatus("⚠️ Server not reachable - is Pioneer running on port 5001?");
441
+ }
442
+ });
443
+
444
+ return () => {
445
+ // Track session end
446
+ const durationMs = Date.now() - sessionStartTimeRef.current;
447
+ trackChatSessionEnd(durationMs, messageCountRef.current, toolCallCountRef.current);
448
+ newClient.disconnect();
449
+ };
450
+ }, []);
283
451
 
284
- return () => { };
285
- }, [config]);
452
+ // Keep message count ref in sync for telemetry
453
+ useEffect(() => {
454
+ messageCountRef.current = state.messages.filter(m => m.role === "user").length;
455
+ }, [state.messages]);
286
456
 
287
457
  // Update file suggestions when input changes
288
458
  useEffect(() => {
289
- if (!agent || state.isProcessing) {
290
- // Only update if currently showing suggestions
459
+ if (!fileResolver || state.isProcessing) {
291
460
  if (showSuggestions) {
292
461
  setSuggestions([]);
293
462
  setShowSuggestions(false);
@@ -297,33 +466,31 @@ export const ChatApp: React.FC<ChatAppProps> = ({ config, initialMessage }) => {
297
466
 
298
467
  const atPath = extractAtPath(input);
299
468
 
300
- // Only update currentAtPath if it changed
301
469
  if (JSON.stringify(atPath) !== JSON.stringify(currentAtPath)) {
302
470
  setCurrentAtPath(atPath);
303
471
  }
304
472
 
305
473
  if (atPath) {
306
- const newSuggestions = agent.getFileSuggestions(atPath.path);
474
+ const newSuggestions = fileResolver.getSuggestions(atPath.path);
307
475
  setSuggestions(newSuggestions);
308
476
  setShowSuggestions(newSuggestions.length > 0);
309
477
  setSelectedIndex(0);
310
478
  } else if (showSuggestions) {
311
- // Only clear if currently showing
312
479
  setSuggestions([]);
313
480
  setShowSuggestions(false);
314
481
  }
315
- }, [input, agent, state.isProcessing]);
482
+ }, [input, fileResolver, state.isProcessing]);
316
483
 
317
484
  // Handle initial message if provided
318
485
  useEffect(() => {
319
- if (agent && initialMessage && state.messages.length === 0) {
486
+ if (client && initialMessage && state.messages.length === 0) {
320
487
  handleSubmit(initialMessage);
321
488
  }
322
- }, [agent, initialMessage]);
489
+ }, [client, initialMessage]);
323
490
 
324
491
  const handleSubmit = useCallback(
325
492
  async (value: string) => {
326
- if (!agent || !value.trim() || state.isProcessing) return;
493
+ if (!client || !value.trim() || state.isProcessing) return;
327
494
 
328
495
  const userMessage = value.trim();
329
496
  setInput("");
@@ -335,8 +502,8 @@ export const ChatApp: React.FC<ChatAppProps> = ({ config, initialMessage }) => {
335
502
  }
336
503
 
337
504
  if (userMessage === "/clear") {
338
- agent.clearHistory();
339
505
  streamBufferRef.current = "";
506
+ setHistory([]);
340
507
  setState({
341
508
  messages: [],
342
509
  isProcessing: false,
@@ -360,15 +527,8 @@ export const ChatApp: React.FC<ChatAppProps> = ({ config, initialMessage }) => {
360
527
  /help - Show this help
361
528
  /exit - Exit the chat
362
529
  /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
530
+ /status - Check server connection
531
+ /model - Select AI model (Haiku/Sonnet/Opus)
372
532
 
373
533
  Direct bash mode:
374
534
  !<command> - Execute command directly (no agent)
@@ -376,7 +536,7 @@ Direct bash mode:
376
536
  !pwd - Example: print working directory
377
537
 
378
538
  File references:
379
- @file.ts - Include file contents
539
+ @file.ts - Include file contents in message
380
540
  @src/ - Include directory listing
381
541
  @package.json - Include any file by path`,
382
542
  timestamp: new Date(),
@@ -387,119 +547,62 @@ File references:
387
547
  }
388
548
 
389
549
  if (userMessage === "/tools") {
390
- const tools = agent.getTools();
391
- const toolList = tools
392
- .map((t) => ` • ${t.name}: ${t.description}`)
393
- .join("\n");
394
- setState((s) => ({
395
- ...s,
396
- messages: [
397
- ...s.messages,
398
- {
399
- role: "system",
400
- content: `Available tools:\n${toolList}`,
401
- timestamp: new Date(),
402
- },
403
- ],
404
- }));
405
- return;
406
- }
407
-
408
- if (userMessage === "/model") {
409
- const info = agent.getModelInfo();
410
- const modelHelp = `🤖 Current Model
411
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
412
- Provider: ${info.provider}
413
- Model: ${info.model}
414
-
415
- Switch with /model <name>. Examples:
416
- Anthropic (Claude 4.5):
417
- claude-opus-4-5-20251101 (strongest)
418
- claude-sonnet-4-5-20250929 (balanced)
419
- claude-haiku-4-5-20251001 (fast/cheap)
420
- OpenAI:
421
- gpt-4o, gpt-4o-mini, o1, o3-mini`;
422
- setState((s) => ({
423
- ...s,
424
- messages: [...s.messages, { role: "system", content: modelHelp, timestamp: new Date() }],
425
- }));
426
- return;
427
- }
428
-
429
- if (userMessage.startsWith("/model ")) {
430
- const newModel = userMessage.slice(7).trim();
431
- if (newModel) {
432
- agent.setModel(newModel);
550
+ try {
551
+ const tools = await client.listTools();
552
+ const toolList = tools
553
+ .map((t) => ` • ${t.name}: ${t.description}`)
554
+ .join("\n");
555
+ setState((s) => ({
556
+ ...s,
557
+ messages: [
558
+ ...s.messages,
559
+ {
560
+ role: "system",
561
+ content: `Available tools:\n${toolList}`,
562
+ timestamp: new Date(),
563
+ },
564
+ ],
565
+ }));
566
+ } catch (error) {
433
567
  setState((s) => ({
434
568
  ...s,
435
- messages: [...s.messages, { role: "system", content: `✓ Switched to model: ${newModel}`, timestamp: new Date() }],
569
+ messages: [
570
+ ...s.messages,
571
+ {
572
+ role: "system",
573
+ content: `Error fetching tools: ${error}`,
574
+ timestamp: new Date(),
575
+ },
576
+ ],
436
577
  }));
437
578
  }
438
579
  return;
439
580
  }
440
581
 
441
- if (userMessage === "/budget") {
442
- const status = agent.getBudgetStatus();
443
- const maxTools = agent.getMaxToolCalls();
444
- const budgetInfo = `📊 Budget Status
445
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
446
- ${status.summary}
447
- Max tool calls: ${maxTools}
448
-
449
- ⚙️ Adjust Limits
450
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
451
- /budget tokens <n> Set max tokens (0 = unlimited)
452
- /budget cost <n> Set max cost in USD (0 = unlimited)
453
- /budget time <n> Set max time in seconds (0 = unlimited)
454
- /budget tools <n> Set max tool calls per turn (0 = unlimited)
455
- /budget unlimited Remove all limits`;
456
-
582
+ if (userMessage === "/status") {
583
+ const healthy = await client.health();
457
584
  setState((s) => ({
458
585
  ...s,
459
586
  messages: [
460
587
  ...s.messages,
461
- { role: "system", content: budgetInfo, timestamp: new Date() },
588
+ {
589
+ role: "system",
590
+ content: healthy
591
+ ? "✅ Connected to Pioneer server"
592
+ : "❌ Cannot reach Pioneer server",
593
+ timestamp: new Date(),
594
+ },
462
595
  ],
463
596
  }));
464
597
  return;
465
598
  }
466
599
 
467
- // Budget adjustment commands
468
- if (userMessage.startsWith("/budget ")) {
469
- const parts = userMessage.split(" ");
470
- const subCmd = parts[1];
471
- const value = parts[2];
472
-
473
- let message = "";
474
- if (subCmd === "unlimited") {
475
- agent.updateBudget({ maxTokens: undefined, maxCost: undefined, maxTime: undefined, maxIterations: undefined });
476
- agent.setMaxToolCalls(1000);
477
- message = "✓ All limits removed";
478
- } else if (subCmd === "tokens" && value) {
479
- const val = parseInt(value, 10);
480
- agent.updateBudget({ maxTokens: val === 0 ? undefined : val });
481
- message = val === 0 ? "✓ Token limit removed" : `✓ Token limit set to ${val.toLocaleString()}`;
482
- } else if (subCmd === "cost" && value) {
483
- const val = parseFloat(value);
484
- agent.updateBudget({ maxCost: val === 0 ? undefined : val });
485
- message = val === 0 ? "✓ Cost limit removed" : `✓ Cost limit set to $${val}`;
486
- } else if (subCmd === "time" && value) {
487
- const val = parseInt(value, 10);
488
- agent.updateBudget({ maxTime: val === 0 ? undefined : val });
489
- message = val === 0 ? "✓ Time limit removed" : `✓ Time limit set to ${val}s`;
490
- } else if (subCmd === "tools" && value) {
491
- const val = parseInt(value, 10);
492
- agent.setMaxToolCalls(val === 0 ? 1000 : val);
493
- message = val === 0 ? "✓ Tool call limit removed" : `✓ Max tool calls set to ${val}`;
494
- } else {
495
- message = "Usage: /budget [tokens|cost|time|tools] <value> or /budget unlimited";
496
- }
497
-
498
- setState((s) => ({
499
- ...s,
500
- messages: [...s.messages, { role: "system", content: message, timestamp: new Date() }],
501
- }));
502
- setBudgetSummary(agent.getBudgetStatus().summary);
600
+ if (userMessage === "/model") {
601
+ // Find the index of the current model
602
+ const currentModel = currentMleModel || DEFAULT_MLE_MODEL;
603
+ const currentIndex = AVAILABLE_MLE_MODELS.findIndex(m => m.id === currentModel);
604
+ setModelSelectedIndex(currentIndex >= 0 ? currentIndex : 0);
605
+ setShowModelSelect(true);
503
606
  return;
504
607
  }
505
608
 
@@ -508,14 +611,12 @@ Max tool calls: ${maxTools}
508
611
  const bashCommand = userMessage.slice(1).trim();
509
612
  if (!bashCommand) return;
510
613
 
511
- // Show command being executed
512
614
  setState((s) => ({
513
615
  ...s,
514
616
  messages: [...s.messages, { role: "user", content: userMessage, timestamp: new Date() }],
515
617
  isProcessing: true,
516
618
  }));
517
619
 
518
- // Execute directly and collect output
519
620
  let output = "";
520
621
  try {
521
622
  for await (const chunk of executeBashStream(bashCommand, { cwd: process.cwd() })) {
@@ -529,12 +630,36 @@ Max tool calls: ${maxTools}
529
630
 
530
631
  setState((s) => ({
531
632
  ...s,
532
- messages: [...s.messages, { role: "tool", content: output || "(no output)", timestamp: new Date(), toolName: "bash" }],
633
+ messages: [...s.messages, {
634
+ role: "tool",
635
+ content: output || "(no output)",
636
+ timestamp: new Date(),
637
+ toolName: "bash",
638
+ isUserBash: true,
639
+ }],
533
640
  isProcessing: false,
534
641
  }));
535
642
  return;
536
643
  }
537
644
 
645
+ // Process file references (@file syntax) using FileResolver
646
+ let processedMessage = userMessage;
647
+ let fileRefs: FileReference[] = [];
648
+ if (fileResolver) {
649
+ const resolved = fileResolver.resolve(userMessage);
650
+ fileRefs = resolved.references;
651
+ // Prepend the context block to the message if there are resolved files
652
+ if (resolved.contextBlock) {
653
+ processedMessage = resolved.contextBlock + "\n" + userMessage;
654
+ }
655
+ }
656
+
657
+ // Update file references display
658
+ setState((s) => ({
659
+ ...s,
660
+ fileReferences: fileRefs,
661
+ }));
662
+
538
663
  // Add user message
539
664
  streamBufferRef.current = "";
540
665
  if (streamUpdateTimerRef.current) {
@@ -549,29 +674,146 @@ Max tool calls: ${maxTools}
549
674
  ],
550
675
  isProcessing: true,
551
676
  streamingContent: "",
552
- fileReferences: [],
553
677
  error: null,
554
678
  }));
555
679
 
556
- try {
557
- const response = await agent.chat(userMessage, true);
558
-
559
- // Clear any pending stream update
560
- if (streamUpdateTimerRef.current) {
561
- clearTimeout(streamUpdateTimerRef.current);
562
- streamUpdateTimerRef.current = null;
563
- }
564
- streamBufferRef.current = "";
680
+ // Track final response for history
681
+ let finalResponse = "";
565
682
 
566
- setState((s) => ({
567
- ...s,
568
- messages: [
569
- ...s.messages,
570
- { role: "assistant", content: response, timestamp: new Date() },
571
- ],
572
- isProcessing: false,
573
- streamingContent: "",
574
- }));
683
+ try {
684
+ await client.chat(
685
+ processedMessage,
686
+ {
687
+ onStream: (chunk) => {
688
+ streamBufferRef.current += chunk;
689
+ if (!streamUpdateTimerRef.current) {
690
+ streamUpdateTimerRef.current = setTimeout(() => {
691
+ setState((s) => ({
692
+ ...s,
693
+ streamingContent: streamBufferRef.current,
694
+ }));
695
+ streamUpdateTimerRef.current = null;
696
+ }, 50);
697
+ }
698
+ },
699
+ onToolStart: (name, callId, args) => {
700
+ toolCallCountRef.current++;
701
+ setState((s) => ({
702
+ ...s,
703
+ currentToolCall: { name, args: args as Record<string, unknown> },
704
+ }));
705
+ },
706
+ onToolCall: async (call) => {
707
+ // Execute tool locally using ToolExecutor
708
+ // NOTE: Don't add message here - server will send tool_result event
709
+ // which triggers onToolResult and adds the message there
710
+ if (!toolExecutor) {
711
+ throw new Error("Tool executor not initialized");
712
+ }
713
+ setState((s) => ({
714
+ ...s,
715
+ currentToolCall: { name: call.tool, args: call.args },
716
+ }));
717
+ const result = await toolExecutor.execute(call.tool, call.args);
718
+ // Keep currentToolCall for onToolResult to use
719
+ return result;
720
+ },
721
+ onToolResult: (name, callId, result) => {
722
+ // Tool result from server (both Felix tools AND client-side tool results echoed back)
723
+ setState((s) => {
724
+ // Get args from currentToolCall if available
725
+ const toolArgs = s.currentToolCall?.name === name ? s.currentToolCall.args : {};
726
+ return {
727
+ ...s,
728
+ currentToolCall: null,
729
+ messages: [
730
+ ...s.messages,
731
+ {
732
+ role: "tool",
733
+ content: typeof result === "string" ? result : JSON.stringify(result, null, 2),
734
+ timestamp: new Date(),
735
+ toolName: name,
736
+ toolArgs,
737
+ },
738
+ ],
739
+ };
740
+ });
741
+ },
742
+ onToolError: (name, callId, error) => {
743
+ setState((s) => ({
744
+ ...s,
745
+ currentToolCall: null,
746
+ messages: [
747
+ ...s.messages,
748
+ {
749
+ role: "tool",
750
+ content: `Error: ${error}`,
751
+ timestamp: new Date(),
752
+ toolName: name,
753
+ },
754
+ ],
755
+ }));
756
+ },
757
+ onAssistantMessage: (content) => {
758
+ finalResponse = content;
759
+ },
760
+ onError: (error) => {
761
+ setState((s) => ({
762
+ ...s,
763
+ error: error.message,
764
+ isProcessing: false,
765
+ }));
766
+ },
767
+ onDone: (backendMessages) => {
768
+ // Clear any pending stream update
769
+ if (streamUpdateTimerRef.current) {
770
+ clearTimeout(streamUpdateTimerRef.current);
771
+ streamUpdateTimerRef.current = null;
772
+ }
773
+ streamBufferRef.current = "";
774
+
775
+ console.log("[DEBUG] ChatApp onDone received messages:", backendMessages?.length, "roles:", backendMessages?.map(m => m.role));
776
+
777
+ // Update history with full message history from backend (includes tool calls)
778
+ if (backendMessages && backendMessages.length > 0) {
779
+ // Backend sends the complete history including this turn's messages
780
+ console.log("[DEBUG] Setting history to backendMessages");
781
+ setHistory(backendMessages);
782
+ } else if (finalResponse) {
783
+ // Fallback if backend doesn't send messages (legacy)
784
+ setHistory((h) => [
785
+ ...h,
786
+ { role: "user", content: processedMessage },
787
+ { role: "assistant", content: finalResponse },
788
+ ]);
789
+ }
790
+
791
+ // Update UI state
792
+ if (finalResponse) {
793
+ setState((s) => ({
794
+ ...s,
795
+ messages: [
796
+ ...s.messages,
797
+ { role: "assistant", content: finalResponse, timestamp: new Date() },
798
+ ],
799
+ isProcessing: false,
800
+ streamingContent: "",
801
+ }));
802
+ } else {
803
+ setState((s) => ({
804
+ ...s,
805
+ isProcessing: false,
806
+ streamingContent: "",
807
+ }));
808
+ }
809
+ },
810
+ },
811
+ {
812
+ history: history,
813
+ fileReferences: fileRefs.filter(r => r.exists).map(r => r.path),
814
+ config: currentMleModel ? { model: currentMleModel } : undefined,
815
+ }
816
+ );
575
817
  } catch (error) {
576
818
  streamBufferRef.current = "";
577
819
  setState((s) => ({
@@ -580,10 +822,8 @@ Max tool calls: ${maxTools}
580
822
  isProcessing: false,
581
823
  }));
582
824
  }
583
-
584
- setBudgetSummary(agent.getBudgetStatus().summary);
585
825
  },
586
- [agent, state.isProcessing, exit]
826
+ [client, toolExecutor, fileResolver, state.isProcessing, history, exit, currentMleModel]
587
827
  );
588
828
 
589
829
  // Insert selected suggestion into input
@@ -596,17 +836,21 @@ Max tool calls: ${maxTools}
596
836
  setSuggestions([]);
597
837
  }, [input, currentAtPath]);
598
838
 
599
- // Stop the agent
839
+ // Stop the request (disconnect and reconnect)
600
840
  const handleStop = useCallback(() => {
601
- if (agent && state.isProcessing) {
602
- agent.stop();
841
+ if (client && state.isProcessing) {
842
+ client.disconnect();
843
+ // Reconnect for future messages
844
+ client.connect().catch(() => {
845
+ setServerStatus("⚠️ Reconnection failed");
846
+ });
603
847
  setState((s) => ({
604
848
  ...s,
605
849
  isProcessing: false,
606
850
  currentToolCall: null,
607
851
  }));
608
852
  }
609
- }, [agent, state.isProcessing]);
853
+ }, [client, state.isProcessing]);
610
854
 
611
855
  // Handle keyboard shortcuts
612
856
  useInput((char, key) => {
@@ -615,12 +859,46 @@ Max tool calls: ${maxTools}
615
859
  return;
616
860
  }
617
861
 
618
- // Escape to stop agent when processing
619
862
  if (key.escape && state.isProcessing) {
620
863
  handleStop();
621
864
  return;
622
865
  }
623
866
 
867
+ // Handle model selection navigation
868
+ if (showModelSelect) {
869
+ if (key.downArrow) {
870
+ setModelSelectedIndex((i) => Math.min(i + 1, AVAILABLE_MLE_MODELS.length - 1));
871
+ return;
872
+ }
873
+ if (key.upArrow) {
874
+ setModelSelectedIndex((i) => Math.max(i - 1, 0));
875
+ return;
876
+ }
877
+ if (key.return) {
878
+ const selectedModel = AVAILABLE_MLE_MODELS[modelSelectedIndex];
879
+ setMleModel(selectedModel.id);
880
+ setCurrentMleModel(selectedModel.id);
881
+ setShowModelSelect(false);
882
+ setState((s) => ({
883
+ ...s,
884
+ messages: [
885
+ ...s.messages,
886
+ {
887
+ role: "system",
888
+ content: `✓ Model set to: ${selectedModel.name} (${selectedModel.id})`,
889
+ timestamp: new Date(),
890
+ },
891
+ ],
892
+ }));
893
+ return;
894
+ }
895
+ if (key.escape) {
896
+ setShowModelSelect(false);
897
+ return;
898
+ }
899
+ return; // Consume all other input when model select is shown
900
+ }
901
+
624
902
  // Handle suggestion navigation when suggestions are shown
625
903
  if (showSuggestions && suggestions.length > 0) {
626
904
  if (key.downArrow) {
@@ -642,13 +920,13 @@ Max tool calls: ${maxTools}
642
920
  }
643
921
  });
644
922
 
645
- if (!agent) {
923
+ if (!client) {
646
924
  return (
647
925
  <Box>
648
926
  <Text color="yellow">
649
927
  <Spinner type="dots" />
650
928
  </Text>
651
- <Text> Initializing agent...</Text>
929
+ <Text> Initializing...</Text>
652
930
  </Box>
653
931
  );
654
932
  }
@@ -660,7 +938,7 @@ Max tool calls: ${maxTools}
660
938
  <Text color="cyan" bold>
661
939
  Pioneer Agent
662
940
  </Text>
663
- <Text dimColor> — /help for commands, @ for files, Ctrl+C to exit</Text>
941
+ <Text dimColor> — /help for commands, /model to change AI, @ for files, Ctrl+C to exit</Text>
664
942
  </Box>
665
943
 
666
944
  {/* Messages */}
@@ -681,7 +959,7 @@ Max tool calls: ${maxTools}
681
959
 
682
960
  {/* Tool call indicator */}
683
961
  {state.currentToolCall && (
684
- <ToolCallIndicator toolName={state.currentToolCall} />
962
+ <ToolCallIndicator toolCall={state.currentToolCall} />
685
963
  )}
686
964
 
687
965
  {/* Error display */}
@@ -701,6 +979,14 @@ Max tool calls: ${maxTools}
701
979
  />
702
980
  )}
703
981
 
982
+ {/* Model selection overlay */}
983
+ {showModelSelect && (
984
+ <ModelSelector
985
+ selectedIndex={modelSelectedIndex}
986
+ currentModel={currentMleModel}
987
+ />
988
+ )}
989
+
704
990
  {/* Input area */}
705
991
  <Box marginTop={1}>
706
992
  {state.isProcessing ? (
@@ -721,7 +1007,7 @@ Max tool calls: ${maxTools}
721
1007
  value={input}
722
1008
  onChange={setInput}
723
1009
  onSubmit={handleSubmit}
724
- placeholder="Type a message... (@ for files)"
1010
+ placeholder="Type a message... (@ for files, ! for bash)"
725
1011
  />
726
1012
  ) : (
727
1013
  <Text dimColor>Raw mode not supported in this terminal</Text>
@@ -731,8 +1017,7 @@ Max tool calls: ${maxTools}
731
1017
  </Box>
732
1018
 
733
1019
  {/* Status bar */}
734
- <StatusBar budgetSummary={budgetSummary} isProcessing={state.isProcessing} />
1020
+ <StatusBar serverStatus={serverStatus} isProcessing={state.isProcessing} />
735
1021
  </Box>
736
1022
  );
737
1023
  };
738
-