@fastino-ai/pioneer-cli 0.2.10 → 0.2.11
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 +28 -0
- package/dist/index.js +248 -0
- package/dist/yoga.wasm +0 -0
- package/package.json +14 -6
- package/src/api.ts +0 -3187
- package/src/chat/ChatApp.tsx +0 -1028
- package/src/chat/index.ts +0 -7
- package/src/client/ToolExecutor.ts +0 -175
- package/src/client/WebSocketClient.ts +0 -379
- package/src/client/index.ts +0 -2
- package/src/config.ts +0 -225
- package/src/index.tsx +0 -6677
- package/src/telemetry.ts +0 -173
- package/src/tests/api.test.ts +0 -104
- package/src/tests/config-functions.test.ts +0 -163
- package/src/tests/config.test.ts +0 -33
- package/src/tests/file-resolver-edge-cases.test.ts +0 -92
- package/src/tests/telemetry.test.ts +0 -111
- package/src/tests/tool-types.test.ts +0 -104
- package/src/tests/utils.test.ts +0 -90
- package/src/tools/bash.ts +0 -184
- package/src/tools/filesystem.ts +0 -444
- package/src/tools/index.ts +0 -22
- package/src/tools/sandbox.ts +0 -310
- package/src/tools/types.ts +0 -25
- package/src/utils/FileResolver.ts +0 -321
- package/src/utils/index.ts +0 -6
package/src/chat/ChatApp.tsx
DELETED
|
@@ -1,1028 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ChatApp - Main interactive chat interface using Ink
|
|
3
|
-
* Uses Pioneer backend via WebSocket for agent, executes CLI tools locally
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React, { useState, useEffect, useCallback, useRef } from "react";
|
|
7
|
-
import { Box, Text, useApp, useInput, useStdin } from "ink";
|
|
8
|
-
import TextInput from "ink-text-input";
|
|
9
|
-
import Spinner from "ink-spinner";
|
|
10
|
-
import Markdown from "@inkkit/ink-markdown";
|
|
11
|
-
|
|
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";
|
|
19
|
-
|
|
20
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
-
// Types
|
|
22
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
interface ChatMessage {
|
|
25
|
-
role: "user" | "assistant" | "system" | "tool";
|
|
26
|
-
content: string;
|
|
27
|
-
timestamp: Date;
|
|
28
|
-
toolName?: string;
|
|
29
|
-
toolArgs?: Record<string, unknown>;
|
|
30
|
-
isStreaming?: boolean;
|
|
31
|
-
isUserBash?: boolean; // For !command - show raw output
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface ToolCallInfo {
|
|
35
|
-
name: string;
|
|
36
|
-
args: Record<string, unknown>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface ChatState {
|
|
40
|
-
messages: ChatMessage[];
|
|
41
|
-
isProcessing: boolean;
|
|
42
|
-
streamingContent: string;
|
|
43
|
-
currentToolCall: ToolCallInfo | null;
|
|
44
|
-
fileReferences: FileReference[];
|
|
45
|
-
error: string | null;
|
|
46
|
-
}
|
|
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
|
-
|
|
137
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
138
|
-
// Message Display Components
|
|
139
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
140
|
-
|
|
141
|
-
const MessageBubble: React.FC<{ message: ChatMessage }> = React.memo(({ message }) => {
|
|
142
|
-
const roleColors: Record<string, string> = {
|
|
143
|
-
user: "cyan",
|
|
144
|
-
assistant: "green",
|
|
145
|
-
system: "yellow",
|
|
146
|
-
tool: "magenta",
|
|
147
|
-
};
|
|
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
|
-
|
|
194
|
-
const roleLabels: Record<string, string> = {
|
|
195
|
-
user: "You",
|
|
196
|
-
assistant: "Agent",
|
|
197
|
-
system: "System",
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const color = roleColors[message.role] || "white";
|
|
201
|
-
const label = roleLabels[message.role] || message.role;
|
|
202
|
-
|
|
203
|
-
return (
|
|
204
|
-
<Box flexDirection="column" marginBottom={1}>
|
|
205
|
-
<Text color={color} bold>
|
|
206
|
-
{label}:
|
|
207
|
-
</Text>
|
|
208
|
-
<Box marginLeft={2}>
|
|
209
|
-
{message.role === "assistant" ? (
|
|
210
|
-
<Markdown>{message.content}</Markdown>
|
|
211
|
-
) : (
|
|
212
|
-
<Text wrap="wrap">{message.content}</Text>
|
|
213
|
-
)}
|
|
214
|
-
</Box>
|
|
215
|
-
</Box>
|
|
216
|
-
);
|
|
217
|
-
});
|
|
218
|
-
MessageBubble.displayName = 'MessageBubble';
|
|
219
|
-
|
|
220
|
-
const StreamingMessage: React.FC<{ content: string }> = ({ content }) => (
|
|
221
|
-
<Box flexDirection="column" marginBottom={1}>
|
|
222
|
-
<Text color="green" bold>
|
|
223
|
-
Agent: <Text color="gray">(streaming...)</Text>
|
|
224
|
-
</Text>
|
|
225
|
-
<Box marginLeft={2}>
|
|
226
|
-
{content ? <Markdown>{content}</Markdown> : <Text>...</Text>}
|
|
227
|
-
</Box>
|
|
228
|
-
</Box>
|
|
229
|
-
);
|
|
230
|
-
|
|
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
|
-
}
|
|
244
|
-
|
|
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 }) => {
|
|
256
|
-
if (refs.length === 0) return null;
|
|
257
|
-
|
|
258
|
-
return (
|
|
259
|
-
<Box flexDirection="column" marginBottom={1}>
|
|
260
|
-
<Text color="blue" bold>📎 Referenced files:</Text>
|
|
261
|
-
{refs.map((ref, i) => (
|
|
262
|
-
<Box key={i} marginLeft={2}>
|
|
263
|
-
<Text color={ref.exists ? "blue" : "red"}>
|
|
264
|
-
{ref.exists ? "✓" : "✗"} {ref.relativePath}
|
|
265
|
-
{ref.isDirectory ? "/" : ""}
|
|
266
|
-
{ref.error ? ` (${ref.error})` : ""}
|
|
267
|
-
</Text>
|
|
268
|
-
</Box>
|
|
269
|
-
))}
|
|
270
|
-
</Box>
|
|
271
|
-
);
|
|
272
|
-
});
|
|
273
|
-
FileReferencesIndicator.displayName = 'FileReferencesIndicator';
|
|
274
|
-
|
|
275
|
-
// File autocomplete dropdown
|
|
276
|
-
const FileSuggestions: React.FC<{
|
|
277
|
-
suggestions: string[];
|
|
278
|
-
selectedIndex: number;
|
|
279
|
-
searchPath: string;
|
|
280
|
-
}> = ({ suggestions, selectedIndex, searchPath }) => {
|
|
281
|
-
if (suggestions.length === 0) return null;
|
|
282
|
-
|
|
283
|
-
return (
|
|
284
|
-
<Box flexDirection="column" borderStyle="round" borderColor="blue" paddingX={1} marginBottom={1}>
|
|
285
|
-
<Text color="blue" dimColor>
|
|
286
|
-
📁 {searchPath || "./"} <Text color="gray">(↑↓ navigate, Tab select, Esc close)</Text>
|
|
287
|
-
</Text>
|
|
288
|
-
{suggestions.slice(0, 10).map((suggestion, i) => (
|
|
289
|
-
<Box key={i}>
|
|
290
|
-
<Text
|
|
291
|
-
color={i === selectedIndex ? "black" : "white"}
|
|
292
|
-
backgroundColor={i === selectedIndex ? "blue" : undefined}
|
|
293
|
-
>
|
|
294
|
-
{" "}{suggestion.endsWith("/") ? "📁" : "📄"} {suggestion}{" "}
|
|
295
|
-
</Text>
|
|
296
|
-
</Box>
|
|
297
|
-
))}
|
|
298
|
-
{suggestions.length > 10 && (
|
|
299
|
-
<Text dimColor> ... and {suggestions.length - 10} more</Text>
|
|
300
|
-
)}
|
|
301
|
-
</Box>
|
|
302
|
-
);
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
// Extract @path from end of input
|
|
306
|
-
function extractAtPath(input: string): { path: string; start: number } | null {
|
|
307
|
-
const match = input.match(/@([\w\-.\/]*)$/);
|
|
308
|
-
if (!match) return null;
|
|
309
|
-
return { path: match[1], start: input.length - match[0].length };
|
|
310
|
-
}
|
|
311
|
-
|
|
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,
|
|
350
|
-
isProcessing,
|
|
351
|
-
}) => (
|
|
352
|
-
<Box
|
|
353
|
-
borderStyle="single"
|
|
354
|
-
borderColor="gray"
|
|
355
|
-
paddingX={1}
|
|
356
|
-
marginTop={1}
|
|
357
|
-
>
|
|
358
|
-
<Text dimColor>
|
|
359
|
-
{isProcessing ? "Processing... | " : ""}
|
|
360
|
-
{serverStatus}
|
|
361
|
-
</Text>
|
|
362
|
-
</Box>
|
|
363
|
-
);
|
|
364
|
-
|
|
365
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
366
|
-
// Main Chat Component
|
|
367
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
368
|
-
|
|
369
|
-
export interface ChatAppProps {
|
|
370
|
-
initialMessage?: string;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
export const ChatApp: React.FC<ChatAppProps> = ({ initialMessage }) => {
|
|
374
|
-
const { exit } = useApp();
|
|
375
|
-
const { isRawModeSupported } = useStdin();
|
|
376
|
-
|
|
377
|
-
const [state, setState] = useState<ChatState>({
|
|
378
|
-
messages: [],
|
|
379
|
-
isProcessing: false,
|
|
380
|
-
streamingContent: "",
|
|
381
|
-
currentToolCall: null,
|
|
382
|
-
fileReferences: [],
|
|
383
|
-
error: null,
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
const [input, setInput] = useState("");
|
|
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[]>([]);
|
|
392
|
-
|
|
393
|
-
// Autocomplete state
|
|
394
|
-
const [suggestions, setSuggestions] = useState<string[]>([]);
|
|
395
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
396
|
-
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
397
|
-
const [currentAtPath, setCurrentAtPath] = useState<{ path: string; start: number } | null>(null);
|
|
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
|
-
|
|
404
|
-
// Streaming content refs for throttled updates
|
|
405
|
-
const streamBufferRef = useRef("");
|
|
406
|
-
const streamUpdateTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
407
|
-
|
|
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
|
|
414
|
-
useEffect(() => {
|
|
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);
|
|
423
|
-
|
|
424
|
-
const executor = new ToolExecutor({ workingDirectory: process.cwd() });
|
|
425
|
-
setToolExecutor(executor);
|
|
426
|
-
|
|
427
|
-
const resolver = new FileResolver(process.cwd());
|
|
428
|
-
setFileResolver(resolver);
|
|
429
|
-
|
|
430
|
-
// Check server health and connect
|
|
431
|
-
const serverUrl = newClient.getWebSocketUrl();
|
|
432
|
-
newClient.health().then(async (healthy) => {
|
|
433
|
-
if (healthy) {
|
|
434
|
-
try {
|
|
435
|
-
await newClient.connect();
|
|
436
|
-
setServerStatus("Connected to Pioneer server (WebSocket)");
|
|
437
|
-
} catch (err) {
|
|
438
|
-
const errorMsg = err instanceof Error ? err.message : "Unknown error";
|
|
439
|
-
setServerStatus(`⚠️ WebSocket failed (${serverUrl}): ${errorMsg}`);
|
|
440
|
-
}
|
|
441
|
-
} else {
|
|
442
|
-
setServerStatus(`⚠️ Server not reachable at ${serverUrl}`);
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
return () => {
|
|
447
|
-
// Track session end
|
|
448
|
-
const durationMs = Date.now() - sessionStartTimeRef.current;
|
|
449
|
-
trackChatSessionEnd(durationMs, messageCountRef.current, toolCallCountRef.current);
|
|
450
|
-
newClient.disconnect();
|
|
451
|
-
};
|
|
452
|
-
}, []);
|
|
453
|
-
|
|
454
|
-
// Keep message count ref in sync for telemetry
|
|
455
|
-
useEffect(() => {
|
|
456
|
-
messageCountRef.current = state.messages.filter(m => m.role === "user").length;
|
|
457
|
-
}, [state.messages]);
|
|
458
|
-
|
|
459
|
-
// Update file suggestions when input changes
|
|
460
|
-
useEffect(() => {
|
|
461
|
-
if (!fileResolver || state.isProcessing) {
|
|
462
|
-
if (showSuggestions) {
|
|
463
|
-
setSuggestions([]);
|
|
464
|
-
setShowSuggestions(false);
|
|
465
|
-
}
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const atPath = extractAtPath(input);
|
|
470
|
-
|
|
471
|
-
if (JSON.stringify(atPath) !== JSON.stringify(currentAtPath)) {
|
|
472
|
-
setCurrentAtPath(atPath);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (atPath) {
|
|
476
|
-
const newSuggestions = fileResolver.getSuggestions(atPath.path);
|
|
477
|
-
setSuggestions(newSuggestions);
|
|
478
|
-
setShowSuggestions(newSuggestions.length > 0);
|
|
479
|
-
setSelectedIndex(0);
|
|
480
|
-
} else if (showSuggestions) {
|
|
481
|
-
setSuggestions([]);
|
|
482
|
-
setShowSuggestions(false);
|
|
483
|
-
}
|
|
484
|
-
}, [input, fileResolver, state.isProcessing]);
|
|
485
|
-
|
|
486
|
-
// Handle initial message if provided
|
|
487
|
-
useEffect(() => {
|
|
488
|
-
if (client && initialMessage && state.messages.length === 0) {
|
|
489
|
-
handleSubmit(initialMessage);
|
|
490
|
-
}
|
|
491
|
-
}, [client, initialMessage]);
|
|
492
|
-
|
|
493
|
-
const handleSubmit = useCallback(
|
|
494
|
-
async (value: string) => {
|
|
495
|
-
if (!client || !value.trim() || state.isProcessing) return;
|
|
496
|
-
|
|
497
|
-
const userMessage = value.trim();
|
|
498
|
-
setInput("");
|
|
499
|
-
|
|
500
|
-
// Handle special commands
|
|
501
|
-
if (userMessage === "/exit" || userMessage === "/quit") {
|
|
502
|
-
exit();
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
if (userMessage === "/clear") {
|
|
507
|
-
streamBufferRef.current = "";
|
|
508
|
-
setHistory([]);
|
|
509
|
-
setState({
|
|
510
|
-
messages: [],
|
|
511
|
-
isProcessing: false,
|
|
512
|
-
streamingContent: "",
|
|
513
|
-
currentToolCall: null,
|
|
514
|
-
fileReferences: [],
|
|
515
|
-
error: null,
|
|
516
|
-
});
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
if (userMessage === "/help") {
|
|
521
|
-
setState((s) => ({
|
|
522
|
-
...s,
|
|
523
|
-
messages: [
|
|
524
|
-
...s.messages,
|
|
525
|
-
{
|
|
526
|
-
role: "system",
|
|
527
|
-
content: `Available commands:
|
|
528
|
-
/clear - Clear conversation history
|
|
529
|
-
/help - Show this help
|
|
530
|
-
/exit - Exit the chat
|
|
531
|
-
/tools - List available tools
|
|
532
|
-
/status - Check server connection
|
|
533
|
-
/model - Select AI model (Haiku/Sonnet/Opus)
|
|
534
|
-
|
|
535
|
-
Direct bash mode:
|
|
536
|
-
!<command> - Execute command directly (no agent)
|
|
537
|
-
!ls -la - Example: list files
|
|
538
|
-
!pwd - Example: print working directory
|
|
539
|
-
|
|
540
|
-
File references:
|
|
541
|
-
@file.ts - Include file contents in message
|
|
542
|
-
@src/ - Include directory listing
|
|
543
|
-
@package.json - Include any file by path`,
|
|
544
|
-
timestamp: new Date(),
|
|
545
|
-
},
|
|
546
|
-
],
|
|
547
|
-
}));
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
if (userMessage === "/tools") {
|
|
552
|
-
try {
|
|
553
|
-
const tools = await client.listTools();
|
|
554
|
-
const toolList = tools
|
|
555
|
-
.map((t) => ` • ${t.name}: ${t.description}`)
|
|
556
|
-
.join("\n");
|
|
557
|
-
setState((s) => ({
|
|
558
|
-
...s,
|
|
559
|
-
messages: [
|
|
560
|
-
...s.messages,
|
|
561
|
-
{
|
|
562
|
-
role: "system",
|
|
563
|
-
content: `Available tools:\n${toolList}`,
|
|
564
|
-
timestamp: new Date(),
|
|
565
|
-
},
|
|
566
|
-
],
|
|
567
|
-
}));
|
|
568
|
-
} catch (error) {
|
|
569
|
-
setState((s) => ({
|
|
570
|
-
...s,
|
|
571
|
-
messages: [
|
|
572
|
-
...s.messages,
|
|
573
|
-
{
|
|
574
|
-
role: "system",
|
|
575
|
-
content: `Error fetching tools: ${error}`,
|
|
576
|
-
timestamp: new Date(),
|
|
577
|
-
},
|
|
578
|
-
],
|
|
579
|
-
}));
|
|
580
|
-
}
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
if (userMessage === "/status") {
|
|
585
|
-
const healthy = await client.health();
|
|
586
|
-
setState((s) => ({
|
|
587
|
-
...s,
|
|
588
|
-
messages: [
|
|
589
|
-
...s.messages,
|
|
590
|
-
{
|
|
591
|
-
role: "system",
|
|
592
|
-
content: healthy
|
|
593
|
-
? "✅ Connected to Pioneer server"
|
|
594
|
-
: "❌ Cannot reach Pioneer server",
|
|
595
|
-
timestamp: new Date(),
|
|
596
|
-
},
|
|
597
|
-
],
|
|
598
|
-
}));
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
if (userMessage === "/model") {
|
|
603
|
-
// Find the index of the current model
|
|
604
|
-
const currentModel = currentMleModel || DEFAULT_MLE_MODEL;
|
|
605
|
-
const currentIndex = AVAILABLE_MLE_MODELS.findIndex(m => m.id === currentModel);
|
|
606
|
-
setModelSelectedIndex(currentIndex >= 0 ? currentIndex : 0);
|
|
607
|
-
setShowModelSelect(true);
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// Direct bash mode: commands starting with ! execute directly without agent
|
|
612
|
-
if (userMessage.startsWith("!")) {
|
|
613
|
-
const bashCommand = userMessage.slice(1).trim();
|
|
614
|
-
if (!bashCommand) return;
|
|
615
|
-
|
|
616
|
-
setState((s) => ({
|
|
617
|
-
...s,
|
|
618
|
-
messages: [...s.messages, { role: "user", content: userMessage, timestamp: new Date() }],
|
|
619
|
-
isProcessing: true,
|
|
620
|
-
}));
|
|
621
|
-
|
|
622
|
-
let output = "";
|
|
623
|
-
try {
|
|
624
|
-
for await (const chunk of executeBashStream(bashCommand, { cwd: process.cwd() })) {
|
|
625
|
-
if (chunk.type === "stdout" || chunk.type === "stderr") {
|
|
626
|
-
output += chunk.data;
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
} catch (err) {
|
|
630
|
-
output = `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
setState((s) => ({
|
|
634
|
-
...s,
|
|
635
|
-
messages: [...s.messages, {
|
|
636
|
-
role: "tool",
|
|
637
|
-
content: output || "(no output)",
|
|
638
|
-
timestamp: new Date(),
|
|
639
|
-
toolName: "bash",
|
|
640
|
-
isUserBash: true,
|
|
641
|
-
}],
|
|
642
|
-
isProcessing: false,
|
|
643
|
-
}));
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// Process file references (@file syntax) using FileResolver
|
|
648
|
-
let processedMessage = userMessage;
|
|
649
|
-
let fileRefs: FileReference[] = [];
|
|
650
|
-
if (fileResolver) {
|
|
651
|
-
const resolved = fileResolver.resolve(userMessage);
|
|
652
|
-
fileRefs = resolved.references;
|
|
653
|
-
// Prepend the context block to the message if there are resolved files
|
|
654
|
-
if (resolved.contextBlock) {
|
|
655
|
-
processedMessage = resolved.contextBlock + "\n" + userMessage;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// Update file references display
|
|
660
|
-
setState((s) => ({
|
|
661
|
-
...s,
|
|
662
|
-
fileReferences: fileRefs,
|
|
663
|
-
}));
|
|
664
|
-
|
|
665
|
-
// Add user message
|
|
666
|
-
streamBufferRef.current = "";
|
|
667
|
-
if (streamUpdateTimerRef.current) {
|
|
668
|
-
clearTimeout(streamUpdateTimerRef.current);
|
|
669
|
-
streamUpdateTimerRef.current = null;
|
|
670
|
-
}
|
|
671
|
-
setState((s) => ({
|
|
672
|
-
...s,
|
|
673
|
-
messages: [
|
|
674
|
-
...s.messages,
|
|
675
|
-
{ role: "user", content: userMessage, timestamp: new Date() },
|
|
676
|
-
],
|
|
677
|
-
isProcessing: true,
|
|
678
|
-
streamingContent: "",
|
|
679
|
-
error: null,
|
|
680
|
-
}));
|
|
681
|
-
|
|
682
|
-
// Track final response for history
|
|
683
|
-
let finalResponse = "";
|
|
684
|
-
|
|
685
|
-
try {
|
|
686
|
-
await client.chat(
|
|
687
|
-
processedMessage,
|
|
688
|
-
{
|
|
689
|
-
onStream: (chunk) => {
|
|
690
|
-
streamBufferRef.current += chunk;
|
|
691
|
-
if (!streamUpdateTimerRef.current) {
|
|
692
|
-
streamUpdateTimerRef.current = setTimeout(() => {
|
|
693
|
-
setState((s) => ({
|
|
694
|
-
...s,
|
|
695
|
-
streamingContent: streamBufferRef.current,
|
|
696
|
-
}));
|
|
697
|
-
streamUpdateTimerRef.current = null;
|
|
698
|
-
}, 50);
|
|
699
|
-
}
|
|
700
|
-
},
|
|
701
|
-
onToolStart: (name, callId, args) => {
|
|
702
|
-
toolCallCountRef.current++;
|
|
703
|
-
setState((s) => ({
|
|
704
|
-
...s,
|
|
705
|
-
currentToolCall: { name, args: args as Record<string, unknown> },
|
|
706
|
-
}));
|
|
707
|
-
},
|
|
708
|
-
onToolCall: async (call) => {
|
|
709
|
-
// Execute tool locally using ToolExecutor
|
|
710
|
-
// NOTE: Don't add message here - server will send tool_result event
|
|
711
|
-
// which triggers onToolResult and adds the message there
|
|
712
|
-
if (!toolExecutor) {
|
|
713
|
-
throw new Error("Tool executor not initialized");
|
|
714
|
-
}
|
|
715
|
-
setState((s) => ({
|
|
716
|
-
...s,
|
|
717
|
-
currentToolCall: { name: call.tool, args: call.args },
|
|
718
|
-
}));
|
|
719
|
-
const result = await toolExecutor.execute(call.tool, call.args);
|
|
720
|
-
// Keep currentToolCall for onToolResult to use
|
|
721
|
-
return result;
|
|
722
|
-
},
|
|
723
|
-
onToolResult: (name, callId, result) => {
|
|
724
|
-
// Tool result from server (both Felix tools AND client-side tool results echoed back)
|
|
725
|
-
setState((s) => {
|
|
726
|
-
// Get args from currentToolCall if available
|
|
727
|
-
const toolArgs = s.currentToolCall?.name === name ? s.currentToolCall.args : {};
|
|
728
|
-
return {
|
|
729
|
-
...s,
|
|
730
|
-
currentToolCall: null,
|
|
731
|
-
messages: [
|
|
732
|
-
...s.messages,
|
|
733
|
-
{
|
|
734
|
-
role: "tool",
|
|
735
|
-
content: typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
736
|
-
timestamp: new Date(),
|
|
737
|
-
toolName: name,
|
|
738
|
-
toolArgs,
|
|
739
|
-
},
|
|
740
|
-
],
|
|
741
|
-
};
|
|
742
|
-
});
|
|
743
|
-
},
|
|
744
|
-
onToolError: (name, callId, error) => {
|
|
745
|
-
setState((s) => ({
|
|
746
|
-
...s,
|
|
747
|
-
currentToolCall: null,
|
|
748
|
-
messages: [
|
|
749
|
-
...s.messages,
|
|
750
|
-
{
|
|
751
|
-
role: "tool",
|
|
752
|
-
content: `Error: ${error}`,
|
|
753
|
-
timestamp: new Date(),
|
|
754
|
-
toolName: name,
|
|
755
|
-
},
|
|
756
|
-
],
|
|
757
|
-
}));
|
|
758
|
-
},
|
|
759
|
-
onAssistantMessage: (content) => {
|
|
760
|
-
finalResponse = content;
|
|
761
|
-
},
|
|
762
|
-
onError: (error) => {
|
|
763
|
-
setState((s) => ({
|
|
764
|
-
...s,
|
|
765
|
-
error: error.message,
|
|
766
|
-
isProcessing: false,
|
|
767
|
-
}));
|
|
768
|
-
},
|
|
769
|
-
onDone: (backendMessages) => {
|
|
770
|
-
// Clear any pending stream update
|
|
771
|
-
if (streamUpdateTimerRef.current) {
|
|
772
|
-
clearTimeout(streamUpdateTimerRef.current);
|
|
773
|
-
streamUpdateTimerRef.current = null;
|
|
774
|
-
}
|
|
775
|
-
streamBufferRef.current = "";
|
|
776
|
-
|
|
777
|
-
console.log("[DEBUG] ChatApp onDone received messages:", backendMessages?.length, "roles:", backendMessages?.map(m => m.role));
|
|
778
|
-
|
|
779
|
-
// Update history with full message history from backend (includes tool calls)
|
|
780
|
-
if (backendMessages && backendMessages.length > 0) {
|
|
781
|
-
// Backend sends the complete history including this turn's messages
|
|
782
|
-
console.log("[DEBUG] Setting history to backendMessages");
|
|
783
|
-
setHistory(backendMessages);
|
|
784
|
-
} else if (finalResponse) {
|
|
785
|
-
// Fallback if backend doesn't send messages (legacy)
|
|
786
|
-
setHistory((h) => [
|
|
787
|
-
...h,
|
|
788
|
-
{ role: "user", content: processedMessage },
|
|
789
|
-
{ role: "assistant", content: finalResponse },
|
|
790
|
-
]);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// Update UI state
|
|
794
|
-
if (finalResponse) {
|
|
795
|
-
setState((s) => ({
|
|
796
|
-
...s,
|
|
797
|
-
messages: [
|
|
798
|
-
...s.messages,
|
|
799
|
-
{ role: "assistant", content: finalResponse, timestamp: new Date() },
|
|
800
|
-
],
|
|
801
|
-
isProcessing: false,
|
|
802
|
-
streamingContent: "",
|
|
803
|
-
}));
|
|
804
|
-
} else {
|
|
805
|
-
setState((s) => ({
|
|
806
|
-
...s,
|
|
807
|
-
isProcessing: false,
|
|
808
|
-
streamingContent: "",
|
|
809
|
-
}));
|
|
810
|
-
}
|
|
811
|
-
},
|
|
812
|
-
},
|
|
813
|
-
{
|
|
814
|
-
history: history,
|
|
815
|
-
fileReferences: fileRefs.filter(r => r.exists).map(r => r.path),
|
|
816
|
-
config: currentMleModel ? { model: currentMleModel } : undefined,
|
|
817
|
-
}
|
|
818
|
-
);
|
|
819
|
-
} catch (error) {
|
|
820
|
-
streamBufferRef.current = "";
|
|
821
|
-
setState((s) => ({
|
|
822
|
-
...s,
|
|
823
|
-
error: error instanceof Error ? error.message : String(error),
|
|
824
|
-
isProcessing: false,
|
|
825
|
-
}));
|
|
826
|
-
}
|
|
827
|
-
},
|
|
828
|
-
[client, toolExecutor, fileResolver, state.isProcessing, history, exit, currentMleModel]
|
|
829
|
-
);
|
|
830
|
-
|
|
831
|
-
// Insert selected suggestion into input
|
|
832
|
-
const insertSuggestion = useCallback((suggestion: string) => {
|
|
833
|
-
if (!currentAtPath) return;
|
|
834
|
-
const before = input.slice(0, currentAtPath.start);
|
|
835
|
-
const newInput = before + "@" + suggestion;
|
|
836
|
-
setInput(newInput);
|
|
837
|
-
setShowSuggestions(false);
|
|
838
|
-
setSuggestions([]);
|
|
839
|
-
}, [input, currentAtPath]);
|
|
840
|
-
|
|
841
|
-
// Stop the request (disconnect and reconnect)
|
|
842
|
-
const handleStop = useCallback(() => {
|
|
843
|
-
if (client && state.isProcessing) {
|
|
844
|
-
client.disconnect();
|
|
845
|
-
// Reconnect for future messages
|
|
846
|
-
client.connect().catch(() => {
|
|
847
|
-
setServerStatus("⚠️ Reconnection failed");
|
|
848
|
-
});
|
|
849
|
-
setState((s) => ({
|
|
850
|
-
...s,
|
|
851
|
-
isProcessing: false,
|
|
852
|
-
currentToolCall: null,
|
|
853
|
-
}));
|
|
854
|
-
}
|
|
855
|
-
}, [client, state.isProcessing]);
|
|
856
|
-
|
|
857
|
-
// Handle keyboard shortcuts
|
|
858
|
-
useInput(
|
|
859
|
-
(char, key) => {
|
|
860
|
-
if (key.ctrl && char === "c") {
|
|
861
|
-
exit();
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
if (key.escape && state.isProcessing) {
|
|
866
|
-
handleStop();
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// Handle model selection navigation
|
|
871
|
-
if (showModelSelect) {
|
|
872
|
-
if (key.downArrow) {
|
|
873
|
-
setModelSelectedIndex((i) => Math.min(i + 1, AVAILABLE_MLE_MODELS.length - 1));
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
if (key.upArrow) {
|
|
877
|
-
setModelSelectedIndex((i) => Math.max(i - 1, 0));
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
if (key.return) {
|
|
881
|
-
const selectedModel = AVAILABLE_MLE_MODELS[modelSelectedIndex];
|
|
882
|
-
setMleModel(selectedModel.id);
|
|
883
|
-
setCurrentMleModel(selectedModel.id);
|
|
884
|
-
setShowModelSelect(false);
|
|
885
|
-
setState((s) => ({
|
|
886
|
-
...s,
|
|
887
|
-
messages: [
|
|
888
|
-
...s.messages,
|
|
889
|
-
{
|
|
890
|
-
role: "system",
|
|
891
|
-
content: `✓ Model set to: ${selectedModel.name} (${selectedModel.id})`,
|
|
892
|
-
timestamp: new Date(),
|
|
893
|
-
},
|
|
894
|
-
],
|
|
895
|
-
}));
|
|
896
|
-
return;
|
|
897
|
-
}
|
|
898
|
-
if (key.escape) {
|
|
899
|
-
setShowModelSelect(false);
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
return; // Consume all other input when model select is shown
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
// Handle suggestion navigation when suggestions are shown
|
|
906
|
-
if (showSuggestions && suggestions.length > 0) {
|
|
907
|
-
if (key.downArrow) {
|
|
908
|
-
setSelectedIndex((i) => Math.min(i + 1, suggestions.length - 1));
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
if (key.upArrow) {
|
|
912
|
-
setSelectedIndex((i) => Math.max(i - 1, 0));
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
if (key.tab) {
|
|
916
|
-
insertSuggestion(suggestions[selectedIndex]);
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
if (key.escape) {
|
|
920
|
-
setShowSuggestions(false);
|
|
921
|
-
return;
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
},
|
|
925
|
-
{ isActive: isRawModeSupported }
|
|
926
|
-
);
|
|
927
|
-
|
|
928
|
-
if (!client) {
|
|
929
|
-
return (
|
|
930
|
-
<Box>
|
|
931
|
-
<Text color="yellow">
|
|
932
|
-
<Spinner type="dots" />
|
|
933
|
-
</Text>
|
|
934
|
-
<Text> Initializing...</Text>
|
|
935
|
-
</Box>
|
|
936
|
-
);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
return (
|
|
940
|
-
<Box flexDirection="column" paddingX={1}>
|
|
941
|
-
{/* Header */}
|
|
942
|
-
<Box marginBottom={1}>
|
|
943
|
-
<Text color="cyan" bold>
|
|
944
|
-
Pioneer Agent
|
|
945
|
-
</Text>
|
|
946
|
-
<Text dimColor> — /help for commands, /model to change AI, @ for files, Ctrl+C to exit</Text>
|
|
947
|
-
</Box>
|
|
948
|
-
|
|
949
|
-
{/* Messages */}
|
|
950
|
-
<Box flexDirection="column" flexGrow={1}>
|
|
951
|
-
{state.messages.map((msg, i) => (
|
|
952
|
-
<MessageBubble key={i} message={msg} />
|
|
953
|
-
))}
|
|
954
|
-
|
|
955
|
-
{/* Streaming content */}
|
|
956
|
-
{state.isProcessing && state.streamingContent && (
|
|
957
|
-
<StreamingMessage content={state.streamingContent} />
|
|
958
|
-
)}
|
|
959
|
-
|
|
960
|
-
{/* File references indicator */}
|
|
961
|
-
{state.fileReferences.length > 0 && (
|
|
962
|
-
<FileReferencesIndicator refs={state.fileReferences} />
|
|
963
|
-
)}
|
|
964
|
-
|
|
965
|
-
{/* Tool call indicator */}
|
|
966
|
-
{state.currentToolCall && (
|
|
967
|
-
<ToolCallIndicator toolCall={state.currentToolCall} />
|
|
968
|
-
)}
|
|
969
|
-
|
|
970
|
-
{/* Error display */}
|
|
971
|
-
{state.error && (
|
|
972
|
-
<Box marginBottom={1}>
|
|
973
|
-
<Text color="red">Error: {state.error}</Text>
|
|
974
|
-
</Box>
|
|
975
|
-
)}
|
|
976
|
-
</Box>
|
|
977
|
-
|
|
978
|
-
{/* File suggestions dropdown */}
|
|
979
|
-
{showSuggestions && !state.isProcessing && (
|
|
980
|
-
<FileSuggestions
|
|
981
|
-
suggestions={suggestions}
|
|
982
|
-
selectedIndex={selectedIndex}
|
|
983
|
-
searchPath={currentAtPath?.path || ""}
|
|
984
|
-
/>
|
|
985
|
-
)}
|
|
986
|
-
|
|
987
|
-
{/* Model selection overlay */}
|
|
988
|
-
{showModelSelect && (
|
|
989
|
-
<ModelSelector
|
|
990
|
-
selectedIndex={modelSelectedIndex}
|
|
991
|
-
currentModel={currentMleModel}
|
|
992
|
-
/>
|
|
993
|
-
)}
|
|
994
|
-
|
|
995
|
-
{/* Input area */}
|
|
996
|
-
<Box marginTop={1}>
|
|
997
|
-
{state.isProcessing ? (
|
|
998
|
-
<Box>
|
|
999
|
-
<Text color="yellow">
|
|
1000
|
-
<Spinner type="dots" />
|
|
1001
|
-
</Text>
|
|
1002
|
-
<Text> Processing... </Text>
|
|
1003
|
-
<Text dimColor>(Esc to stop)</Text>
|
|
1004
|
-
</Box>
|
|
1005
|
-
) : (
|
|
1006
|
-
<Box>
|
|
1007
|
-
<Text color="cyan" bold>
|
|
1008
|
-
{">"}{" "}
|
|
1009
|
-
</Text>
|
|
1010
|
-
{isRawModeSupported ? (
|
|
1011
|
-
<TextInput
|
|
1012
|
-
value={input}
|
|
1013
|
-
onChange={setInput}
|
|
1014
|
-
onSubmit={handleSubmit}
|
|
1015
|
-
placeholder="Type a message... (@ for files, ! for bash)"
|
|
1016
|
-
/>
|
|
1017
|
-
) : (
|
|
1018
|
-
<Text dimColor>Raw mode not supported in this terminal</Text>
|
|
1019
|
-
)}
|
|
1020
|
-
</Box>
|
|
1021
|
-
)}
|
|
1022
|
-
</Box>
|
|
1023
|
-
|
|
1024
|
-
{/* Status bar */}
|
|
1025
|
-
<StatusBar serverStatus={serverStatus} isProcessing={state.isProcessing} />
|
|
1026
|
-
</Box>
|
|
1027
|
-
);
|
|
1028
|
-
};
|