friday-code 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/bin/friday.js +2 -0
- package/dist/core/engine/agent.d.ts +99 -0
- package/dist/core/engine/agent.js +317 -0
- package/dist/core/engine/agent.js.map +1 -0
- package/dist/core/providers/registry.d.ts +31 -0
- package/dist/core/providers/registry.js +213 -0
- package/dist/core/providers/registry.js.map +1 -0
- package/dist/core/tools/tools.d.ts +416 -0
- package/dist/core/tools/tools.js +338 -0
- package/dist/core/tools/tools.js.map +1 -0
- package/dist/db/index.d.ts +10 -0
- package/dist/db/index.js +85 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +699 -0
- package/dist/db/schema.js +49 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/ui/components/components.d.ts +81 -0
- package/dist/ui/components/components.js +416 -0
- package/dist/ui/components/components.js.map +1 -0
- package/dist/ui/theme/theme.d.ts +77 -0
- package/dist/ui/theme/theme.js +64 -0
- package/dist/ui/theme/theme.js.map +1 -0
- package/dist/ui/views/App.d.ts +6 -0
- package/dist/ui/views/App.js +629 -0
- package/dist/ui/views/App.js.map +1 -0
- package/dist/ui/views/InputBox.d.ts +16 -0
- package/dist/ui/views/InputBox.js +202 -0
- package/dist/ui/views/InputBox.js.map +1 -0
- package/dist/ui/views/ModelSelector.d.ts +7 -0
- package/dist/ui/views/ModelSelector.js +119 -0
- package/dist/ui/views/ModelSelector.js.map +1 -0
- package/package.json +91 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { Box, Text, useApp, useStdout } from 'ink';
|
|
4
|
+
import { colors, icons } from '../theme/theme.js';
|
|
5
|
+
import { CollapsedRunCard, HeaderBar, HelpView, KeyboardBar, MessageBubble, RunTimelineCard, StatusBar, Toast, WelcomeScreen, } from '../components/components.js';
|
|
6
|
+
import { InputBox } from './InputBox.js';
|
|
7
|
+
import { ModelSelector } from './ModelSelector.js';
|
|
8
|
+
import { AgentEngine } from '../../core/engine/agent.js';
|
|
9
|
+
import { getSetting, setSetting } from '../../core/providers/registry.js';
|
|
10
|
+
import { existsSync, statSync } from 'fs';
|
|
11
|
+
import { resolve } from 'path';
|
|
12
|
+
const App = ({ initialDirectory }) => {
|
|
13
|
+
const { exit } = useApp();
|
|
14
|
+
const { stdout } = useStdout();
|
|
15
|
+
const [messages, setMessages] = useState([]);
|
|
16
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
17
|
+
const [showModelSelector, setShowModelSelector] = useState(false);
|
|
18
|
+
const [activeModel, setActiveModel] = useState(getSetting('active_model') || '');
|
|
19
|
+
const [activeProvider, setActiveProvider] = useState(getSetting('active_provider') || '');
|
|
20
|
+
const [workingDirectory, setWorkingDirectory] = useState(initialDirectory || process.cwd());
|
|
21
|
+
const [tokenUsage, setTokenUsage] = useState();
|
|
22
|
+
const [statusText, setStatusText] = useState('');
|
|
23
|
+
const [engineStatus, setEngineStatus] = useState('idle');
|
|
24
|
+
const [showWelcome, setShowWelcome] = useState(true);
|
|
25
|
+
const [termSize, setTermSize] = useState({ cols: stdout?.columns || 80, rows: stdout?.rows || 24 });
|
|
26
|
+
const engineRef = useRef(null);
|
|
27
|
+
const activeRunIdRef = useRef(null);
|
|
28
|
+
const lastUserMessageRef = useRef('');
|
|
29
|
+
const [pendingApproval, setPendingApproval] = useState(null);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const onResize = () => {
|
|
32
|
+
if (stdout) {
|
|
33
|
+
setTermSize({ cols: stdout.columns, rows: stdout.rows });
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
stdout?.on('resize', onResize);
|
|
37
|
+
return () => {
|
|
38
|
+
stdout?.off('resize', onResize);
|
|
39
|
+
};
|
|
40
|
+
}, [stdout]);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
engineRef.current = new AgentEngine({
|
|
43
|
+
workingDirectory,
|
|
44
|
+
});
|
|
45
|
+
engineRef.current.ensureConversation();
|
|
46
|
+
}, []);
|
|
47
|
+
const compact = termSize.cols < 100 || termSize.rows < 24;
|
|
48
|
+
// Each past turn (user + collapsed run) ≈ 3 visual lines.
|
|
49
|
+
// Reserve ~16 lines for current run + header + footer.
|
|
50
|
+
const maxPastTurns = Math.max(1, Math.floor((termSize.rows - 16) / 3));
|
|
51
|
+
const transcriptLimit = Math.max(3, maxPastTurns * 2 + 2); // pairs + current user+run
|
|
52
|
+
const displayScope = fitPath(workingDirectory, Math.max(20, termSize.cols - 24));
|
|
53
|
+
const visibleMessages = messages.slice(-transcriptLimit);
|
|
54
|
+
const hasUserMessages = useMemo(() => messages.some(message => message.type === 'user'), [messages]);
|
|
55
|
+
const showLaunchpad = showWelcome && !hasUserMessages;
|
|
56
|
+
const showKeyboardBar = termSize.rows >= 16;
|
|
57
|
+
const showStatusBar = termSize.rows >= 14;
|
|
58
|
+
const handleCommand = useCallback((input) => {
|
|
59
|
+
const parts = input.trim().split(/\s+/);
|
|
60
|
+
const command = parts[0]?.toLowerCase();
|
|
61
|
+
switch (command) {
|
|
62
|
+
case '/help':
|
|
63
|
+
setMessages(previous => [...previous, { type: 'help' }]);
|
|
64
|
+
return true;
|
|
65
|
+
case '/model':
|
|
66
|
+
case '/provider':
|
|
67
|
+
setShowModelSelector(true);
|
|
68
|
+
return true;
|
|
69
|
+
case '/clear':
|
|
70
|
+
setMessages([
|
|
71
|
+
{
|
|
72
|
+
type: 'toast',
|
|
73
|
+
message: 'Conversation cleared',
|
|
74
|
+
toastType: 'success',
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
77
|
+
setTokenUsage(undefined);
|
|
78
|
+
setStatusText('');
|
|
79
|
+
setEngineStatus('idle');
|
|
80
|
+
setShowWelcome(true);
|
|
81
|
+
engineRef.current?.clearHistory();
|
|
82
|
+
engineRef.current?.ensureConversation();
|
|
83
|
+
return true;
|
|
84
|
+
case '/new':
|
|
85
|
+
setMessages([
|
|
86
|
+
{
|
|
87
|
+
type: 'toast',
|
|
88
|
+
message: 'Fresh run ready',
|
|
89
|
+
toastType: 'success',
|
|
90
|
+
},
|
|
91
|
+
]);
|
|
92
|
+
setTokenUsage(undefined);
|
|
93
|
+
setStatusText('');
|
|
94
|
+
setEngineStatus('idle');
|
|
95
|
+
setShowWelcome(true);
|
|
96
|
+
engineRef.current?.clearHistory();
|
|
97
|
+
engineRef.current?.ensureConversation();
|
|
98
|
+
return true;
|
|
99
|
+
case '/scope': {
|
|
100
|
+
const nextPath = parts[1];
|
|
101
|
+
if (!nextPath) {
|
|
102
|
+
setMessages(previous => [
|
|
103
|
+
...previous,
|
|
104
|
+
{
|
|
105
|
+
type: 'toast',
|
|
106
|
+
message: `Current scope: ${workingDirectory}`,
|
|
107
|
+
toastType: 'info',
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
const resolved = resolve(workingDirectory, nextPath);
|
|
113
|
+
if (existsSync(resolved) && statSync(resolved).isDirectory()) {
|
|
114
|
+
setWorkingDirectory(resolved);
|
|
115
|
+
engineRef.current?.setWorkingDirectory(resolved);
|
|
116
|
+
setMessages(previous => [
|
|
117
|
+
...previous,
|
|
118
|
+
{
|
|
119
|
+
type: 'toast',
|
|
120
|
+
message: `Scope → ${resolved}`,
|
|
121
|
+
toastType: 'success',
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
setMessages(previous => [
|
|
127
|
+
...previous,
|
|
128
|
+
{
|
|
129
|
+
type: 'toast',
|
|
130
|
+
message: `Directory not found: ${resolved}`,
|
|
131
|
+
toastType: 'error',
|
|
132
|
+
},
|
|
133
|
+
]);
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
case '/history':
|
|
138
|
+
setMessages(previous => [
|
|
139
|
+
...previous,
|
|
140
|
+
{
|
|
141
|
+
type: 'toast',
|
|
142
|
+
message: `${engineRef.current?.getMessageCount() || 0} messages in history`,
|
|
143
|
+
toastType: 'info',
|
|
144
|
+
},
|
|
145
|
+
]);
|
|
146
|
+
return true;
|
|
147
|
+
case '/config': {
|
|
148
|
+
const key = parts[1];
|
|
149
|
+
const value = parts.slice(2).join(' ');
|
|
150
|
+
if (!key) {
|
|
151
|
+
// Show all config
|
|
152
|
+
const maxSteps = getSetting('max_steps') || '25';
|
|
153
|
+
const approval = engineRef.current?.getApprovalMode() ? 'on' : 'off';
|
|
154
|
+
const model = getSetting('active_model') || 'none';
|
|
155
|
+
const provider = getSetting('active_provider') || 'none';
|
|
156
|
+
setMessages(previous => [
|
|
157
|
+
...previous,
|
|
158
|
+
{ type: 'toast', message: `maxSteps: ${maxSteps} · approval: ${approval} · model: ${provider}/${model}`, toastType: 'info' },
|
|
159
|
+
]);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
const configKeys = {
|
|
163
|
+
maxSteps: () => {
|
|
164
|
+
const num = parseInt(value);
|
|
165
|
+
if (isNaN(num) || num < 1 || num > 100) {
|
|
166
|
+
setMessages(p => [...p, { type: 'toast', message: 'maxSteps must be 1-100', toastType: 'error' }]);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
setSetting('max_steps', String(num));
|
|
170
|
+
setMessages(p => [...p, { type: 'toast', message: `maxSteps → ${num}`, toastType: 'success' }]);
|
|
171
|
+
},
|
|
172
|
+
approval: () => {
|
|
173
|
+
const enabled = value === 'on' || value === 'true' || value === '1';
|
|
174
|
+
engineRef.current?.setApprovalMode(enabled);
|
|
175
|
+
setMessages(p => [...p, { type: 'toast', message: `Tool approval → ${enabled ? 'on' : 'off'}`, toastType: 'success' }]);
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
const handler = configKeys[key];
|
|
179
|
+
if (handler) {
|
|
180
|
+
handler();
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
setMessages(p => [...p, { type: 'toast', message: `Unknown config: ${key}. Available: maxSteps, approval`, toastType: 'error' }]);
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
case '/exit':
|
|
188
|
+
exit();
|
|
189
|
+
return true;
|
|
190
|
+
default:
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}, [exit, workingDirectory]);
|
|
194
|
+
const handleSubmit = useCallback(async (input) => {
|
|
195
|
+
if (showWelcome) {
|
|
196
|
+
setShowWelcome(false);
|
|
197
|
+
}
|
|
198
|
+
if (input.startsWith('/') && handleCommand(input)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (!engineRef.current) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
lastUserMessageRef.current = input;
|
|
205
|
+
setIsGenerating(true);
|
|
206
|
+
setEngineStatus('running');
|
|
207
|
+
setStatusText('Working...');
|
|
208
|
+
setTokenUsage(undefined);
|
|
209
|
+
setPendingApproval(null);
|
|
210
|
+
const runId = createMessageId('run');
|
|
211
|
+
activeRunIdRef.current = runId;
|
|
212
|
+
setMessages(previous => [
|
|
213
|
+
...previous,
|
|
214
|
+
{ type: 'user', content: input },
|
|
215
|
+
createRunMessage(runId),
|
|
216
|
+
]);
|
|
217
|
+
try {
|
|
218
|
+
for await (const event of engineRef.current.run(input)) {
|
|
219
|
+
if (event.type === 'tool-approval-request') {
|
|
220
|
+
// Show approval prompt in timeline
|
|
221
|
+
setMessages(previous => updateRunMessage(previous, runId, run => appendRunNode(run, {
|
|
222
|
+
id: `${run.id}-approval-${event.toolCallId}`,
|
|
223
|
+
kind: 'approval',
|
|
224
|
+
label: event.toolName,
|
|
225
|
+
detail: formatToolArgs(event.toolName, event.args ? JSON.stringify(event.args) : ''),
|
|
226
|
+
stepNumber: event.stepNumber,
|
|
227
|
+
toolCallId: event.toolCallId,
|
|
228
|
+
status: 'running',
|
|
229
|
+
})));
|
|
230
|
+
setPendingApproval({
|
|
231
|
+
toolCallId: event.toolCallId,
|
|
232
|
+
toolName: event.toolName,
|
|
233
|
+
args: event.args,
|
|
234
|
+
});
|
|
235
|
+
setStatusText(`Approve ${event.toolName}?`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
handleStreamEvent(event, {
|
|
239
|
+
runId,
|
|
240
|
+
setMessages,
|
|
241
|
+
setStatusText,
|
|
242
|
+
setTokenUsage,
|
|
243
|
+
setEngineStatus,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
setEngineStatus('error');
|
|
249
|
+
setMessages(previous => markRunErrored(previous, runId, error.message || 'Unknown error'));
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
setIsGenerating(false);
|
|
253
|
+
setStatusText('');
|
|
254
|
+
activeRunIdRef.current = null;
|
|
255
|
+
setPendingApproval(null);
|
|
256
|
+
}
|
|
257
|
+
}, [handleCommand, showWelcome]);
|
|
258
|
+
const handleCancel = useCallback(() => {
|
|
259
|
+
engineRef.current?.abort();
|
|
260
|
+
setIsGenerating(false);
|
|
261
|
+
setStatusText('');
|
|
262
|
+
setEngineStatus('idle');
|
|
263
|
+
setMessages(previous => markRunCancelled(previous, activeRunIdRef.current));
|
|
264
|
+
setMessages(previous => [
|
|
265
|
+
...previous,
|
|
266
|
+
{
|
|
267
|
+
type: 'toast',
|
|
268
|
+
message: 'Generation cancelled',
|
|
269
|
+
toastType: 'warning',
|
|
270
|
+
},
|
|
271
|
+
]);
|
|
272
|
+
}, []);
|
|
273
|
+
const handleApproval = useCallback((approved) => {
|
|
274
|
+
if (!pendingApproval || !engineRef.current)
|
|
275
|
+
return;
|
|
276
|
+
const runId = activeRunIdRef.current;
|
|
277
|
+
engineRef.current.respondToApproval(approved);
|
|
278
|
+
// Update approval node in timeline
|
|
279
|
+
if (runId) {
|
|
280
|
+
setMessages(previous => updateRunMessage(previous, runId, run => {
|
|
281
|
+
const approvalNodeId = `${run.id}-approval-${pendingApproval.toolCallId}`;
|
|
282
|
+
return {
|
|
283
|
+
...run,
|
|
284
|
+
nodes: run.nodes.map(n => n.id === approvalNodeId
|
|
285
|
+
? { ...n, status: approved ? 'done' : 'error' }
|
|
286
|
+
: n),
|
|
287
|
+
};
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
setPendingApproval(null);
|
|
291
|
+
setStatusText(approved ? 'Continuing...' : 'Tool denied');
|
|
292
|
+
}, [pendingApproval]);
|
|
293
|
+
const handleRetry = useCallback(() => {
|
|
294
|
+
if (engineStatus !== 'error' || !lastUserMessageRef.current)
|
|
295
|
+
return;
|
|
296
|
+
handleSubmit(lastUserMessageRef.current);
|
|
297
|
+
}, [engineStatus, handleSubmit]);
|
|
298
|
+
const handleModelSelect = useCallback((providerId, modelId) => {
|
|
299
|
+
setActiveProvider(providerId);
|
|
300
|
+
setActiveModel(modelId);
|
|
301
|
+
setShowModelSelector(false);
|
|
302
|
+
setMessages(previous => [
|
|
303
|
+
...previous,
|
|
304
|
+
{
|
|
305
|
+
type: 'toast',
|
|
306
|
+
message: `Model → ${providerId}/${modelId}`,
|
|
307
|
+
toastType: 'success',
|
|
308
|
+
},
|
|
309
|
+
]);
|
|
310
|
+
}, []);
|
|
311
|
+
return (_jsxs(Box, { flexDirection: "column", width: termSize.cols, height: termSize.rows, children: [_jsx(HeaderBar, { provider: activeProvider, model: activeModel || 'none', scope: displayScope, status: engineStatus, compact: compact }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [showLaunchpad ? (_jsx(WelcomeScreen, { model: activeModel, provider: activeProvider, scope: displayScope, compact: compact })) : null, _jsx(Box, { flexDirection: "column", marginTop: showLaunchpad ? 1 : 0, children: visibleMessages.map((message, index) => {
|
|
312
|
+
switch (message.type) {
|
|
313
|
+
case 'user':
|
|
314
|
+
return _jsx(MessageBubble, { role: "user", content: message.content }, index);
|
|
315
|
+
case 'run': {
|
|
316
|
+
// Find if this is the last (most recent) run in the transcript
|
|
317
|
+
const isLastRun = !visibleMessages.slice(index + 1).some(m => m.type === 'run');
|
|
318
|
+
if (!isLastRun && message.status !== 'running') {
|
|
319
|
+
// Collapse past completed runs to save viewport space
|
|
320
|
+
return _jsx(CollapsedRunCard, { nodes: message.nodes, status: message.status }, message.id);
|
|
321
|
+
}
|
|
322
|
+
return (_jsx(RunTimelineCard, { status: message.status, nodes: message.nodes, isStreaming: message.isStreaming, compact: compact, viewportWidth: termSize.cols, viewportHeight: termSize.rows }, message.id));
|
|
323
|
+
}
|
|
324
|
+
case 'help':
|
|
325
|
+
return _jsx(HelpView, {}, index);
|
|
326
|
+
case 'toast':
|
|
327
|
+
return _jsx(Toast, { message: message.message, type: message.toastType }, index);
|
|
328
|
+
case 'error':
|
|
329
|
+
return _jsx(Text, { color: colors.red, children: ` ${icons.fail} ${message.message}` }, index);
|
|
330
|
+
default:
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}) })] }), showModelSelector ? (_jsx(Box, { marginTop: 1, children: _jsx(ModelSelector, { onSelect: handleModelSelect, onCancel: () => setShowModelSelector(false) }) })) : null, !showModelSelector ? (_jsx(InputBox, { onSubmit: handleSubmit, isGenerating: isGenerating, onCancel: handleCancel, onApprove: () => handleApproval(true), onDeny: () => handleApproval(false), onRetry: handleRetry, pendingApproval: !!pendingApproval, canRetry: engineStatus === 'error', workingDirectory: workingDirectory, phase: engineStatus, viewportWidth: termSize.cols })) : null, showKeyboardBar ? (_jsx(KeyboardBar, { isGenerating: isGenerating, compact: compact, pendingApproval: !!pendingApproval, canRetry: engineStatus === 'error' })) : null, showStatusBar ? (_jsx(StatusBar, { tokens: tokenUsage, status: statusText, engineStatus: engineStatus, compact: compact })) : null] }));
|
|
334
|
+
};
|
|
335
|
+
function handleStreamEvent(event, handlers) {
|
|
336
|
+
const { runId, setMessages, setStatusText, setTokenUsage, setEngineStatus } = handlers;
|
|
337
|
+
switch (event.type) {
|
|
338
|
+
case 'status':
|
|
339
|
+
setStatusText(event.message);
|
|
340
|
+
break;
|
|
341
|
+
case 'step-start':
|
|
342
|
+
// No separate tracking needed — timeline nodes handle everything
|
|
343
|
+
break;
|
|
344
|
+
case 'reasoning':
|
|
345
|
+
setMessages(previous => updateRunMessage(previous, runId, run => appendRunNode(run, {
|
|
346
|
+
id: `${run.id}-thinking-${event.stepNumber}`,
|
|
347
|
+
kind: 'thinking',
|
|
348
|
+
label: `thinking`,
|
|
349
|
+
detail: event.text,
|
|
350
|
+
stepNumber: event.stepNumber,
|
|
351
|
+
status: 'running',
|
|
352
|
+
})));
|
|
353
|
+
break;
|
|
354
|
+
case 'tool-call-streaming-start':
|
|
355
|
+
setMessages(previous => updateRunMessage(previous, runId, run => appendRunNode(run, {
|
|
356
|
+
id: `${run.id}-tool-call-${event.toolCallId}`,
|
|
357
|
+
kind: 'tool-call',
|
|
358
|
+
label: event.toolName,
|
|
359
|
+
detail: '',
|
|
360
|
+
stepNumber: event.stepNumber,
|
|
361
|
+
toolCallId: event.toolCallId,
|
|
362
|
+
status: 'running',
|
|
363
|
+
})));
|
|
364
|
+
setStatusText(`Running ${event.toolName}`);
|
|
365
|
+
break;
|
|
366
|
+
case 'tool-call-delta':
|
|
367
|
+
// Don't update UI on every delta — wait for full tool-call
|
|
368
|
+
break;
|
|
369
|
+
case 'tool-call':
|
|
370
|
+
setMessages(previous => updateRunMessage(previous, runId, run => appendRunNode(run, {
|
|
371
|
+
id: `${run.id}-tool-call-${event.toolCallId}`,
|
|
372
|
+
kind: 'tool-call',
|
|
373
|
+
label: event.toolName,
|
|
374
|
+
detail: formatToolArgs(event.toolName, event.args ? JSON.stringify(event.args) : ''),
|
|
375
|
+
stepNumber: event.stepNumber,
|
|
376
|
+
toolCallId: event.toolCallId,
|
|
377
|
+
status: 'running',
|
|
378
|
+
})));
|
|
379
|
+
setStatusText(`Running ${event.toolName}`);
|
|
380
|
+
break;
|
|
381
|
+
case 'tool-result':
|
|
382
|
+
// Update tool-call node status, then add result node
|
|
383
|
+
setMessages(previous => updateRunMessage(previous, runId, run => {
|
|
384
|
+
const nodes = run.nodes.map(node => (node.kind === 'tool-call' && node.toolCallId === event.toolCallId
|
|
385
|
+
? { ...node, status: hasError(event.result) ? 'error' : 'done' }
|
|
386
|
+
: node));
|
|
387
|
+
return appendRunNode({ ...run, nodes }, {
|
|
388
|
+
id: `${run.id}-tool-result-${event.toolCallId}`,
|
|
389
|
+
kind: 'tool-result',
|
|
390
|
+
label: event.toolName,
|
|
391
|
+
detail: formatToolResult(event.result),
|
|
392
|
+
stepNumber: event.stepNumber,
|
|
393
|
+
toolCallId: event.toolCallId,
|
|
394
|
+
status: hasError(event.result) ? 'error' : 'done',
|
|
395
|
+
});
|
|
396
|
+
}));
|
|
397
|
+
break;
|
|
398
|
+
case 'step-finish':
|
|
399
|
+
// No plan system to update — step transitions handled naturally
|
|
400
|
+
break;
|
|
401
|
+
case 'text-delta':
|
|
402
|
+
setMessages(previous => updateRunMessage(previous, runId, run => appendRunNode(run, {
|
|
403
|
+
id: `${run.id}-text-${event.stepNumber}`,
|
|
404
|
+
kind: 'text',
|
|
405
|
+
label: 'response',
|
|
406
|
+
detail: event.text,
|
|
407
|
+
stepNumber: event.stepNumber,
|
|
408
|
+
status: 'running',
|
|
409
|
+
})));
|
|
410
|
+
break;
|
|
411
|
+
case 'finish': {
|
|
412
|
+
const stepLabel = `${event.totalSteps} step${event.totalSteps === 1 ? '' : 's'}`;
|
|
413
|
+
const tokLabel = event.usage
|
|
414
|
+
? `${event.usage.promptTokens}+${event.usage.completionTokens} tok`
|
|
415
|
+
: '';
|
|
416
|
+
const doneDetail = [stepLabel, tokLabel].filter(Boolean).join(' · ');
|
|
417
|
+
setMessages(previous => updateRunMessage(previous, runId, run => appendRunNode({
|
|
418
|
+
...run,
|
|
419
|
+
status: 'complete',
|
|
420
|
+
isStreaming: false,
|
|
421
|
+
}, {
|
|
422
|
+
id: `${run.id}-done`,
|
|
423
|
+
kind: 'done',
|
|
424
|
+
label: 'done',
|
|
425
|
+
detail: doneDetail || undefined,
|
|
426
|
+
status: 'done',
|
|
427
|
+
})));
|
|
428
|
+
setTokenUsage(event.usage
|
|
429
|
+
? { prompt: event.usage.promptTokens, completion: event.usage.completionTokens }
|
|
430
|
+
: undefined);
|
|
431
|
+
setEngineStatus('complete');
|
|
432
|
+
setStatusText(stepLabel);
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
case 'error':
|
|
436
|
+
setEngineStatus('error');
|
|
437
|
+
setMessages(previous => markRunErrored(previous, runId, event.error));
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// ═══════════════════════════════════════════════════
|
|
442
|
+
// RUN MESSAGE HELPERS
|
|
443
|
+
// ═══════════════════════════════════════════════════
|
|
444
|
+
function createRunMessage(id) {
|
|
445
|
+
return {
|
|
446
|
+
type: 'run',
|
|
447
|
+
id,
|
|
448
|
+
status: 'running',
|
|
449
|
+
isStreaming: true,
|
|
450
|
+
nodes: [],
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function markRunErrored(messages, runId, error) {
|
|
454
|
+
return updateRunMessage(messages, runId, run => appendRunNode({
|
|
455
|
+
...run,
|
|
456
|
+
status: 'error',
|
|
457
|
+
isStreaming: false,
|
|
458
|
+
}, {
|
|
459
|
+
id: `${run.id}-error-${run.nodes.length}`,
|
|
460
|
+
kind: 'phase',
|
|
461
|
+
label: error,
|
|
462
|
+
status: 'error',
|
|
463
|
+
}));
|
|
464
|
+
}
|
|
465
|
+
function markRunCancelled(messages, runId) {
|
|
466
|
+
if (!runId) {
|
|
467
|
+
return messages;
|
|
468
|
+
}
|
|
469
|
+
return updateRunMessage(messages, runId, run => ({
|
|
470
|
+
...run,
|
|
471
|
+
status: 'cancelled',
|
|
472
|
+
isStreaming: false,
|
|
473
|
+
}));
|
|
474
|
+
}
|
|
475
|
+
function updateRunMessage(messages, runId, updater) {
|
|
476
|
+
return messages.map(message => {
|
|
477
|
+
if (message.type !== 'run' || message.id !== runId) {
|
|
478
|
+
return message;
|
|
479
|
+
}
|
|
480
|
+
return updater(message);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
function appendRunNode(run, node) {
|
|
484
|
+
// Merge text/thinking deltas into existing node of same kind + step.
|
|
485
|
+
// Search backward (not just last node) because smoothStream can delay
|
|
486
|
+
// text-deltas past step-finish/done nodes.
|
|
487
|
+
if (node.kind === 'thinking' || node.kind === 'text') {
|
|
488
|
+
for (let i = run.nodes.length - 1; i >= Math.max(0, run.nodes.length - 6); i--) {
|
|
489
|
+
const existing = run.nodes[i];
|
|
490
|
+
if (existing.kind === node.kind && existing.stepNumber === node.stepNumber) {
|
|
491
|
+
return {
|
|
492
|
+
...run,
|
|
493
|
+
nodes: [
|
|
494
|
+
...run.nodes.slice(0, i),
|
|
495
|
+
{
|
|
496
|
+
...existing,
|
|
497
|
+
detail: (existing.detail || '') + (node.detail || ''),
|
|
498
|
+
status: node.status ?? existing.status,
|
|
499
|
+
},
|
|
500
|
+
...run.nodes.slice(i + 1),
|
|
501
|
+
],
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Upsert tool-call nodes (same toolCallId)
|
|
507
|
+
if (node.kind === 'tool-call') {
|
|
508
|
+
const existingIndex = run.nodes.findIndex(existing => existing.kind === 'tool-call' && existing.toolCallId === node.toolCallId);
|
|
509
|
+
if (existingIndex !== -1) {
|
|
510
|
+
const nodes = [...run.nodes];
|
|
511
|
+
const existing = nodes[existingIndex];
|
|
512
|
+
nodes[existingIndex] = {
|
|
513
|
+
...existing,
|
|
514
|
+
label: node.label,
|
|
515
|
+
detail: node.detail || existing.detail,
|
|
516
|
+
status: node.status ?? existing.status,
|
|
517
|
+
};
|
|
518
|
+
return { ...run, nodes };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// Deduplicate identical phase nodes
|
|
522
|
+
const lastNode = run.nodes[run.nodes.length - 1];
|
|
523
|
+
if (node.kind === 'phase' && lastNode?.kind === 'phase' && lastNode.label === node.label) {
|
|
524
|
+
return run;
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
...run,
|
|
528
|
+
nodes: [...run.nodes, node],
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
// ═══════════════════════════════════════════════════
|
|
532
|
+
// FORMATTING HELPERS
|
|
533
|
+
// ═══════════════════════════════════════════════════
|
|
534
|
+
function createMessageId(prefix) {
|
|
535
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
536
|
+
}
|
|
537
|
+
function fitPath(pathname, limit) {
|
|
538
|
+
const home = process.env.HOME || '';
|
|
539
|
+
const shortened = home && pathname.startsWith(home) ? `~${pathname.slice(home.length)}` : pathname;
|
|
540
|
+
if (shortened.length <= limit) {
|
|
541
|
+
return shortened;
|
|
542
|
+
}
|
|
543
|
+
const parts = shortened.split('/').filter(Boolean);
|
|
544
|
+
if (parts.length <= 2) {
|
|
545
|
+
return `${shortened.slice(0, Math.max(0, limit - 1))}…`;
|
|
546
|
+
}
|
|
547
|
+
const prefix = shortened.startsWith('~') ? '~' : '';
|
|
548
|
+
const tail = parts.slice(-2).join('/');
|
|
549
|
+
const compact = `${prefix}/…/${tail}`.replace('//', '/');
|
|
550
|
+
if (compact.length <= limit) {
|
|
551
|
+
return compact;
|
|
552
|
+
}
|
|
553
|
+
return `…${shortened.slice(-(limit - 1))}`;
|
|
554
|
+
}
|
|
555
|
+
function formatToolArgs(toolName, argsText) {
|
|
556
|
+
try {
|
|
557
|
+
const args = JSON.parse(argsText);
|
|
558
|
+
if (typeof args === 'object' && args !== null) {
|
|
559
|
+
const entries = Object.entries(args);
|
|
560
|
+
if (entries.length === 0)
|
|
561
|
+
return `${toolName}()`;
|
|
562
|
+
const parts = entries.map(([k, v]) => {
|
|
563
|
+
const val = typeof v === 'string' ? (v.length > 40 ? `${v.slice(0, 37)}…` : v) : JSON.stringify(v);
|
|
564
|
+
return `${k}: ${val}`;
|
|
565
|
+
});
|
|
566
|
+
return parts.join(', ');
|
|
567
|
+
}
|
|
568
|
+
return argsText;
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
return argsText;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function formatToolResult(result) {
|
|
575
|
+
if (typeof result === 'string') {
|
|
576
|
+
try {
|
|
577
|
+
const parsed = JSON.parse(result);
|
|
578
|
+
return formatToolResultObject(parsed);
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
return result.length > 120 ? `${result.slice(0, 117)}…` : result;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (typeof result === 'object' && result !== null) {
|
|
585
|
+
return formatToolResultObject(result);
|
|
586
|
+
}
|
|
587
|
+
return String(result).slice(0, 120);
|
|
588
|
+
}
|
|
589
|
+
function formatToolResultObject(obj) {
|
|
590
|
+
if (!obj || typeof obj !== 'object')
|
|
591
|
+
return String(obj).slice(0, 120);
|
|
592
|
+
const record = obj;
|
|
593
|
+
// Directory listing: { entries: [...] }
|
|
594
|
+
if (record.entries && Array.isArray(record.entries)) {
|
|
595
|
+
const names = record.entries.slice(0, 5).map((e) => {
|
|
596
|
+
const entry = e;
|
|
597
|
+
return entry.name || entry.path || '?';
|
|
598
|
+
});
|
|
599
|
+
const suffix = record.entries.length > 5 ? ` +${record.entries.length - 5} more` : '';
|
|
600
|
+
return `${record.entries.length} items: ${names.join(', ')}${suffix}`;
|
|
601
|
+
}
|
|
602
|
+
// Success result: { success: true, path: "..." }
|
|
603
|
+
if (record.success === true) {
|
|
604
|
+
if (record.path)
|
|
605
|
+
return String(record.path);
|
|
606
|
+
if (record.output && typeof record.output === 'string') {
|
|
607
|
+
return record.output.length > 120 ? `${record.output.slice(0, 117)}…` : record.output;
|
|
608
|
+
}
|
|
609
|
+
return 'ok';
|
|
610
|
+
}
|
|
611
|
+
// Error result
|
|
612
|
+
if (record.error) {
|
|
613
|
+
return `error: ${String(record.error).slice(0, 100)}`;
|
|
614
|
+
}
|
|
615
|
+
// File content
|
|
616
|
+
if (record.content && typeof record.content === 'string') {
|
|
617
|
+
return record.content.length > 120 ? `${record.content.slice(0, 117)}…` : record.content;
|
|
618
|
+
}
|
|
619
|
+
if (record.output && typeof record.output === 'string') {
|
|
620
|
+
return record.output.length > 120 ? `${record.output.slice(0, 117)}…` : record.output;
|
|
621
|
+
}
|
|
622
|
+
const json = JSON.stringify(obj);
|
|
623
|
+
return json.length > 120 ? `${json.slice(0, 117)}…` : json;
|
|
624
|
+
}
|
|
625
|
+
function hasError(result) {
|
|
626
|
+
return typeof result === 'object' && result !== null && 'error' in result && Boolean(result.error);
|
|
627
|
+
}
|
|
628
|
+
export default App;
|
|
629
|
+
//# sourceMappingURL=App.js.map
|