@fastino-ai/pioneer-cli 0.2.2 → 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.
- package/.claude/settings.local.json +7 -1
- package/.cursor/rules/api-documentation.mdc +14 -0
- package/.cursor/rules/backend-location-rule.mdc +5 -0
- package/Medical_NER_Dataset_1.jsonl +50 -0
- package/README.md +4 -1
- package/bun.lock +52 -0
- package/package.json +5 -2
- package/src/api.ts +551 -22
- package/src/chat/ChatApp.tsx +548 -263
- package/src/client/ToolExecutor.ts +175 -0
- package/src/client/WebSocketClient.ts +333 -0
- package/src/client/index.ts +2 -0
- package/src/config.ts +49 -139
- package/src/index.tsx +796 -106
- package/src/telemetry.ts +173 -0
- package/src/tests/config.test.ts +19 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/filesystem.ts +1 -1
- package/src/tools/index.ts +2 -9
- package/src/tools/sandbox.ts +1 -1
- package/src/tools/types.ts +25 -0
- package/src/utils/index.ts +6 -0
- package/fastino-ai-pioneer-cli-0.2.0.tgz +0 -0
- package/ner_dataset.json +0 -111
- package/src/agent/Agent.ts +0 -342
- package/src/agent/BudgetManager.ts +0 -167
- package/src/agent/LLMClient.ts +0 -435
- package/src/agent/ToolRegistry.ts +0 -97
- package/src/agent/index.ts +0 -15
- package/src/agent/types.ts +0 -84
- package/src/evolution/EvalRunner.ts +0 -301
- package/src/evolution/EvolutionEngine.ts +0 -319
- package/src/evolution/FeedbackCollector.ts +0 -197
- package/src/evolution/ModelTrainer.ts +0 -371
- package/src/evolution/index.ts +0 -18
- package/src/evolution/types.ts +0 -110
- package/src/tools/modal.ts +0 -269
- package/src/tools/training.ts +0 -443
- package/src/tools/wandb.ts +0 -348
- /package/src/{agent → utils}/FileResolver.ts +0 -0
package/src/chat/ChatApp.tsx
CHANGED
|
@@ -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 {
|
|
11
|
-
import
|
|
12
|
-
import type { FileReference } from "../
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
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:
|
|
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
|
-
|
|
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
|
-
<
|
|
226
|
+
{content ? <Markdown>{content}</Markdown> : <Text>...</Text>}
|
|
85
227
|
</Box>
|
|
86
228
|
</Box>
|
|
87
229
|
);
|
|
88
230
|
|
|
89
|
-
const ToolCallIndicator: React.FC<{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
{
|
|
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> = ({
|
|
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 [
|
|
195
|
-
const [
|
|
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
|
-
//
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
newAgent.registerTools(createFilesystemTools());
|
|
277
|
-
newAgent.registerTool(createSandboxTool());
|
|
424
|
+
const executor = new ToolExecutor({ workingDirectory: process.cwd() });
|
|
425
|
+
setToolExecutor(executor);
|
|
278
426
|
|
|
279
|
-
|
|
427
|
+
const resolver = new FileResolver(process.cwd());
|
|
428
|
+
setFileResolver(resolver);
|
|
280
429
|
|
|
281
|
-
//
|
|
282
|
-
|
|
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
|
-
|
|
285
|
-
|
|
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 (!
|
|
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 =
|
|
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,
|
|
482
|
+
}, [input, fileResolver, state.isProcessing]);
|
|
316
483
|
|
|
317
484
|
// Handle initial message if provided
|
|
318
485
|
useEffect(() => {
|
|
319
|
-
if (
|
|
486
|
+
if (client && initialMessage && state.messages.length === 0) {
|
|
320
487
|
handleSubmit(initialMessage);
|
|
321
488
|
}
|
|
322
|
-
}, [
|
|
489
|
+
}, [client, initialMessage]);
|
|
323
490
|
|
|
324
491
|
const handleSubmit = useCallback(
|
|
325
492
|
async (value: string) => {
|
|
326
|
-
if (!
|
|
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
|
-
/
|
|
364
|
-
/
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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: [
|
|
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 === "/
|
|
442
|
-
const
|
|
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
|
-
{
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
const
|
|
471
|
-
|
|
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, {
|
|
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
|
-
|
|
557
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
[
|
|
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
|
|
839
|
+
// Stop the request (disconnect and reconnect)
|
|
600
840
|
const handleStop = useCallback(() => {
|
|
601
|
-
if (
|
|
602
|
-
|
|
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
|
-
}, [
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
|
1020
|
+
<StatusBar serverStatus={serverStatus} isProcessing={state.isProcessing} />
|
|
735
1021
|
</Box>
|
|
736
1022
|
);
|
|
737
1023
|
};
|
|
738
|
-
|