@alpaca-editor/core 1.0.4132 β 1.0.4134
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/dist/editor/ai/AgentTerminal.js +300 -90
- package/dist/editor/ai/AgentTerminal.js.map +1 -1
- package/dist/editor/ai/ToolCallDisplay.d.ts +1 -1
- package/dist/editor/ai/ToolCallDisplay.js +24 -49
- package/dist/editor/ai/ToolCallDisplay.js.map +1 -1
- package/dist/editor/field-types/richtext/components/ReactSlate.js +3 -3
- package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -1
- package/dist/editor/field-types/richtext/components/ToolbarButton.js +1 -1
- package/dist/editor/field-types/richtext/components/ToolbarButton.js.map +1 -1
- package/dist/editor/services/aiService.d.ts +2 -0
- package/dist/editor/services/aiService.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/package.json +1 -1
- package/src/editor/ai/AgentTerminal.tsx +407 -181
- package/src/editor/ai/ToolCallDisplay.tsx +185 -200
- package/src/editor/field-types/richtext/components/ReactSlate.tsx +22 -23
- package/src/editor/field-types/richtext/components/ToolbarButton.tsx +1 -1
- package/src/editor/services/aiService.ts +3 -0
- package/src/revision.ts +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { useEffect, useState, useRef, useCallback, useLayoutEffect, useMemo, } from "react";
|
|
3
3
|
import { Send, AlertCircle, Loader2, User, Wand2, Square, Mic, MicOff, ChevronDown, ChevronUp, ListTodo, } from "lucide-react";
|
|
4
4
|
import { DancingDots } from "./DancingDots";
|
|
@@ -18,6 +18,106 @@ import { Tooltip, TooltipTrigger, TooltipContent, } from "../../components/ui/to
|
|
|
18
18
|
const UserMessage = ({ message }) => {
|
|
19
19
|
return (_jsxs("div", { className: "flex gap-3 p-4", children: [_jsx("div", { className: "flex-shrink-0", children: _jsx(User, { className: "h-6 w-6 text-blue-600", strokeWidth: 1 }) }), _jsxs("div", { className: "min-w-0 flex-1 select-text", children: [_jsxs("div", { className: "mb-1 flex items-center gap-2", children: [_jsx("span", { className: "text-xs font-medium text-gray-900", children: "You" }), message.createdDate && (_jsx("span", { className: "text-xs text-gray-400", children: formatTime(new Date(message.createdDate)) }))] }), _jsx("div", { className: "prose prose-sm max-w-none text-xs text-gray-700 select-text", children: message.content })] })] }));
|
|
20
20
|
};
|
|
21
|
+
// Helper to extract todos from potentially incomplete JSON during streaming
|
|
22
|
+
const extractPartialTodos = (jsonText) => {
|
|
23
|
+
// First try to parse complete JSON
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(jsonText);
|
|
26
|
+
return Array.isArray(parsed) ? parsed : parsed?.items || [];
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
// If JSON is incomplete, try to extract whatever todo items we can find
|
|
30
|
+
const items = [];
|
|
31
|
+
// Look for individual todo objects in the partial JSON
|
|
32
|
+
// Match patterns like: { "text": "...", "done": false, "note": "..." }
|
|
33
|
+
// Handle various field orderings (text can be anywhere in the object)
|
|
34
|
+
const textPattern = /"text"\s*:\s*"([^"]+)"/g;
|
|
35
|
+
const textMatches = [];
|
|
36
|
+
let textMatch;
|
|
37
|
+
while ((textMatch = textPattern.exec(jsonText)) !== null) {
|
|
38
|
+
if (textMatch[1]) {
|
|
39
|
+
textMatches.push({
|
|
40
|
+
text: textMatch[1],
|
|
41
|
+
startIdx: textMatch.index,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// For each text field found, try to find the enclosing object
|
|
46
|
+
for (const { text, startIdx } of textMatches) {
|
|
47
|
+
// Find the opening brace before this text field
|
|
48
|
+
let openBrace = -1;
|
|
49
|
+
for (let i = startIdx - 1; i >= 0; i--) {
|
|
50
|
+
if (jsonText[i] === "{") {
|
|
51
|
+
openBrace = i;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
if (jsonText[i] === "}")
|
|
55
|
+
break; // Hit another object's end
|
|
56
|
+
}
|
|
57
|
+
if (openBrace === -1)
|
|
58
|
+
continue;
|
|
59
|
+
// Find the closing brace after this text field
|
|
60
|
+
let closeBrace = -1;
|
|
61
|
+
let depth = 0;
|
|
62
|
+
for (let i = openBrace; i < jsonText.length; i++) {
|
|
63
|
+
if (jsonText[i] === "{")
|
|
64
|
+
depth++;
|
|
65
|
+
if (jsonText[i] === "}") {
|
|
66
|
+
depth--;
|
|
67
|
+
if (depth === 0) {
|
|
68
|
+
closeBrace = i;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Extract the object and try to parse it
|
|
74
|
+
const objStr = closeBrace !== -1
|
|
75
|
+
? jsonText.substring(openBrace, closeBrace + 1)
|
|
76
|
+
: jsonText.substring(openBrace) + "}"; // Try to close incomplete object
|
|
77
|
+
try {
|
|
78
|
+
const obj = JSON.parse(objStr);
|
|
79
|
+
if (obj.text) {
|
|
80
|
+
items.push({
|
|
81
|
+
text: obj.text,
|
|
82
|
+
done: obj.done === true,
|
|
83
|
+
note: obj.note || undefined,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
// Skip malformed objects
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Also try to extract from partial objects at the end
|
|
92
|
+
// Look for the last opening brace and try to parse up to where we have valid content
|
|
93
|
+
const lines = jsonText.split("\n");
|
|
94
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
95
|
+
const partialJson = lines.slice(0, i + 1).join("\n");
|
|
96
|
+
// Try to close any open braces/brackets
|
|
97
|
+
let testJson = partialJson;
|
|
98
|
+
const openBraces = (testJson.match(/\{/g) || []).length;
|
|
99
|
+
const closeBraces = (testJson.match(/\}/g) || []).length;
|
|
100
|
+
const openBrackets = (testJson.match(/\[/g) || []).length;
|
|
101
|
+
const closeBrackets = (testJson.match(/\]/g) || []).length;
|
|
102
|
+
// Add missing closing characters
|
|
103
|
+
testJson += "]".repeat(openBrackets - closeBrackets);
|
|
104
|
+
testJson += "}".repeat(openBraces - closeBraces);
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(testJson);
|
|
107
|
+
const partialItems = Array.isArray(parsed)
|
|
108
|
+
? parsed
|
|
109
|
+
: parsed?.items || [];
|
|
110
|
+
if (partialItems.length > items.length) {
|
|
111
|
+
return partialItems;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return items;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
21
121
|
const extractTodosFromMessages = (messages) => {
|
|
22
122
|
const todos = [];
|
|
23
123
|
const fencedTodoToken = "```todo_list";
|
|
@@ -48,13 +148,20 @@ const extractTodosFromMessages = (messages) => {
|
|
|
48
148
|
break;
|
|
49
149
|
try {
|
|
50
150
|
let jsonText = "";
|
|
151
|
+
let isComplete = true;
|
|
51
152
|
if (isFenced) {
|
|
52
153
|
const afterToken = todoStart + fencedTodoToken.length;
|
|
53
154
|
const closePos = content.indexOf("```", afterToken);
|
|
54
|
-
if (closePos === -1)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
155
|
+
if (closePos === -1) {
|
|
156
|
+
// Incomplete fenced block - extract what we have so far
|
|
157
|
+
jsonText = content.slice(afterToken).trim();
|
|
158
|
+
isComplete = false;
|
|
159
|
+
cursor = content.length; // Process till end
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
jsonText = content.slice(afterToken, closePos).trim();
|
|
163
|
+
cursor = closePos + 3;
|
|
164
|
+
}
|
|
58
165
|
}
|
|
59
166
|
else {
|
|
60
167
|
const afterToken = todoStart + plainTodoToken.length;
|
|
@@ -74,24 +181,54 @@ const extractTodosFromMessages = (messages) => {
|
|
|
74
181
|
}
|
|
75
182
|
}
|
|
76
183
|
}
|
|
77
|
-
if (braceEnd === -1)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
184
|
+
if (braceEnd === -1) {
|
|
185
|
+
// Incomplete JSON - extract what we have
|
|
186
|
+
jsonText = content.slice(braceStart).trim();
|
|
187
|
+
isComplete = false;
|
|
188
|
+
cursor = content.length;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
jsonText = content.slice(braceStart, braceEnd + 1).trim();
|
|
192
|
+
cursor = braceEnd + 1;
|
|
193
|
+
}
|
|
81
194
|
}
|
|
82
|
-
|
|
83
|
-
const todoItems =
|
|
84
|
-
|
|
195
|
+
// Use the partial extraction helper for incomplete JSON
|
|
196
|
+
const todoItems = isComplete
|
|
197
|
+
? (() => {
|
|
198
|
+
try {
|
|
199
|
+
const parsed = JSON.parse(jsonText);
|
|
200
|
+
return Array.isArray(parsed) ? parsed : parsed?.items || [];
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
})()
|
|
206
|
+
: extractPartialTodos(jsonText);
|
|
207
|
+
const title = (() => {
|
|
208
|
+
try {
|
|
209
|
+
const parsed = JSON.parse(jsonText);
|
|
210
|
+
return Array.isArray(parsed) ? undefined : parsed?.title;
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
})();
|
|
85
216
|
todoItems.forEach((item) => {
|
|
86
217
|
if (!item)
|
|
87
218
|
return;
|
|
88
|
-
const text = item.text ||
|
|
219
|
+
const text = item.text ||
|
|
220
|
+
item.content ||
|
|
221
|
+
item.label ||
|
|
222
|
+
String(item.task || item.title || "");
|
|
89
223
|
if (!text)
|
|
90
224
|
return;
|
|
91
225
|
todos.push({
|
|
92
226
|
id: item.id,
|
|
93
227
|
text,
|
|
94
|
-
done: !!(item.done ??
|
|
228
|
+
done: !!(item.done ??
|
|
229
|
+
item.completed ??
|
|
230
|
+
item.checked ??
|
|
231
|
+
item.status === "completed"),
|
|
95
232
|
note: item.note || item.description,
|
|
96
233
|
messageId: message.id,
|
|
97
234
|
sourceTitle: title,
|
|
@@ -107,25 +244,97 @@ const extractTodosFromMessages = (messages) => {
|
|
|
107
244
|
return todos;
|
|
108
245
|
};
|
|
109
246
|
// TodoListPanel component
|
|
110
|
-
const TodoListPanel = ({ messages }) => {
|
|
247
|
+
const TodoListPanel = ({ messages, agentMetadata, }) => {
|
|
111
248
|
const [isExpanded, setIsExpanded] = useState(true);
|
|
112
|
-
const todos = useMemo(() =>
|
|
113
|
-
|
|
249
|
+
const todos = useMemo(() => {
|
|
250
|
+
// First try to get todos from agent metadata (real-time updates)
|
|
251
|
+
const metadataTodos = (() => {
|
|
252
|
+
try {
|
|
253
|
+
const context = agentMetadata?.additionalData?.context;
|
|
254
|
+
const todoList = context?.todoList;
|
|
255
|
+
if (todoList?.items && Array.isArray(todoList.items)) {
|
|
256
|
+
return todoList.items
|
|
257
|
+
.map((item, idx) => ({
|
|
258
|
+
id: item.id || `metadata-${idx}`,
|
|
259
|
+
text: item.text ||
|
|
260
|
+
item.label ||
|
|
261
|
+
String(item.task || item.title || ""),
|
|
262
|
+
done: !!(item.done ?? item.completed ?? item.checked),
|
|
263
|
+
note: item.note || item.description,
|
|
264
|
+
messageId: undefined,
|
|
265
|
+
sourceTitle: todoList.title,
|
|
266
|
+
}))
|
|
267
|
+
.filter((item) => item.text);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
// Fallback to extracting from messages
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
})();
|
|
275
|
+
// If we have metadata todos, use them; otherwise extract from messages
|
|
276
|
+
if (metadataTodos && metadataTodos.length > 0) {
|
|
277
|
+
return metadataTodos;
|
|
278
|
+
}
|
|
279
|
+
return extractTodosFromMessages(messages);
|
|
280
|
+
}, [messages, agentMetadata]);
|
|
281
|
+
// Check if there's an active streaming message with incomplete todo content
|
|
114
282
|
const isUpdating = useMemo(() => {
|
|
115
283
|
return messages.some((msg) => {
|
|
116
284
|
if (msg.role !== "assistant" || msg.isCompleted)
|
|
117
285
|
return false;
|
|
118
286
|
const content = msg.content || "";
|
|
119
|
-
|
|
287
|
+
// Check for incomplete fenced todo blocks
|
|
288
|
+
const fencedStart = content.indexOf("```todo_list");
|
|
289
|
+
if (fencedStart !== -1) {
|
|
290
|
+
const afterStart = fencedStart + "```todo_list".length;
|
|
291
|
+
const closePos = content.indexOf("```", afterStart);
|
|
292
|
+
if (closePos === -1) {
|
|
293
|
+
// Incomplete fenced block
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Check for incomplete plain todo blocks
|
|
298
|
+
const plainStart = content.indexOf("todo_list");
|
|
299
|
+
if (plainStart !== -1 && plainStart !== fencedStart) {
|
|
300
|
+
const before = plainStart > 0 ? content[plainStart - 1] : "\n";
|
|
301
|
+
if (before === "\n" || before === "\r" || plainStart === 0) {
|
|
302
|
+
const braceStart = content.indexOf("{", plainStart);
|
|
303
|
+
if (braceStart !== -1) {
|
|
304
|
+
let depth = 0;
|
|
305
|
+
let braceEnd = -1;
|
|
306
|
+
for (let i = braceStart; i < content.length; i++) {
|
|
307
|
+
if (content[i] === "{")
|
|
308
|
+
depth++;
|
|
309
|
+
if (content[i] === "}") {
|
|
310
|
+
depth--;
|
|
311
|
+
if (depth === 0) {
|
|
312
|
+
braceEnd = i;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (braceEnd === -1) {
|
|
318
|
+
// Incomplete plain block
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return false;
|
|
120
325
|
});
|
|
121
326
|
}, [messages]);
|
|
122
327
|
if (todos.length === 0 && !isUpdating)
|
|
123
328
|
return null;
|
|
124
329
|
const completedCount = todos.filter((t) => t.done).length;
|
|
125
330
|
const totalCount = todos.length;
|
|
126
|
-
return (_jsxs("div", { className: "border-t border-gray-200 bg-gray-50", children: [_jsxs("button", { onClick: () => setIsExpanded(!isExpanded), className: "flex w-full cursor-pointer items-center justify-between px-4 py-2 text-left transition-colors hover:bg-gray-100", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ListTodo, { className: "h-4 w-4 text-gray-500", strokeWidth: 1 }), _jsx("span", { className: "text-xs font-medium text-gray-700", children: "Todo List" }), isUpdating ? (_jsxs("span", { className: "flex items-center gap-1 text-xs text-blue-600", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin", strokeWidth: 1 }), "Updating..."] })) : (_jsxs("span", { className: "text-xs text-gray-500", children: [completedCount, "/", totalCount, " completed"] }))] }), isExpanded ? (_jsx(ChevronUp, { className: "h-4 w-4 text-gray-500", strokeWidth: 1 })) : (_jsx(ChevronDown, { className: "h-4 w-4 text-gray-500", strokeWidth: 1 }))] }), isExpanded && (
|
|
127
|
-
|
|
128
|
-
|
|
331
|
+
return (_jsxs("div", { className: "border-t border-gray-200 bg-gray-50", children: [_jsxs("button", { onClick: () => setIsExpanded(!isExpanded), className: "flex w-full cursor-pointer items-center justify-between px-4 py-2 text-left transition-colors hover:bg-gray-100", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(ListTodo, { className: "h-4 w-4 text-gray-500", strokeWidth: 1 }), _jsx("span", { className: "text-xs font-medium text-gray-700", children: "Todo List" }), isUpdating ? (_jsxs("span", { className: "flex items-center gap-1 text-xs text-blue-600", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin", strokeWidth: 1 }), "Updating..."] })) : (_jsxs("span", { className: "text-xs text-gray-500", children: [completedCount, "/", totalCount, " completed"] }))] }), isExpanded ? (_jsx(ChevronUp, { className: "h-4 w-4 text-gray-500", strokeWidth: 1 })) : (_jsx(ChevronDown, { className: "h-4 w-4 text-gray-500", strokeWidth: 1 }))] }), isExpanded && (_jsxs("div", { className: "max-h-64 overflow-y-auto px-4 pb-3", children: [todos.length > 0 && (_jsx("div", { className: "space-y-1.5", children: todos.map((todo, idx) => (_jsxs("div", { className: "flex items-start gap-2 rounded bg-white p-2 text-xs", children: [_jsx("div", { className: "flex-shrink-0 pt-0.5", children: todo.done ? (_jsx("div", { className: "flex h-4 w-4 items-center justify-center rounded border-2 border-green-500 bg-green-500", children: _jsx("svg", { className: "h-3 w-3 text-white", fill: "none", strokeWidth: 2, stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) }) })) : (_jsx("div", { className: "h-4 w-4 rounded border-2 border-gray-300" })) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: `${todo.done
|
|
332
|
+
? "text-gray-500 line-through"
|
|
333
|
+
: "text-gray-900"}`, children: todo.text }), todo.note && (_jsx("div", { className: "mt-0.5 text-xs text-gray-500", children: todo.note }))] })] }, todo.id || `${todo.messageId}-${idx}`))) })), isUpdating && (_jsxs("div", { className: `flex items-center gap-2 rounded px-3 py-2 text-xs ${todos.length > 0
|
|
334
|
+
? "mt-2 bg-blue-50 text-blue-700"
|
|
335
|
+
: "justify-center bg-white text-gray-500"}`, children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin", strokeWidth: 1 }), _jsx("span", { children: todos.length > 0
|
|
336
|
+
? "Updating todo list..."
|
|
337
|
+
: "Loading todo list..." })] }))] }))] }));
|
|
129
338
|
};
|
|
130
339
|
const groupConsecutiveMessages = (agentMessages) => {
|
|
131
340
|
// Work directly with the messages array - streaming messages are identified by their properties
|
|
@@ -222,7 +431,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
222
431
|
const [agent, setAgent] = useState(undefined);
|
|
223
432
|
const [messages, setMessages] = useState([]);
|
|
224
433
|
const [prompt, setPrompt] = useState("");
|
|
225
|
-
const [inputPlaceholder, setInputPlaceholder] = useState("Type your message... (Enter to send, Ctrl+Enter for new line)");
|
|
434
|
+
const [inputPlaceholder, setInputPlaceholder] = useState("Type your message... (Enter to send, Shift+Enter or Ctrl+Enter for new line)");
|
|
226
435
|
const [isLoading, setIsLoading] = useState(false);
|
|
227
436
|
const [isConnecting, setIsConnecting] = useState(false);
|
|
228
437
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
@@ -320,6 +529,8 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
320
529
|
}, [messages]);
|
|
321
530
|
const [error, setError] = useState(null);
|
|
322
531
|
const [costLimitExceeded, setCostLimitExceeded] = useState(null);
|
|
532
|
+
// Live running totals from backend status updates (tokenUsage)
|
|
533
|
+
const [liveTotals, setLiveTotals] = useState(null);
|
|
323
534
|
// Flag to track when we should create a new message
|
|
324
535
|
const shouldCreateNewMessage = useRef(false);
|
|
325
536
|
// Keep a ref to the current messages for immediate access
|
|
@@ -487,7 +698,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
487
698
|
});
|
|
488
699
|
throw new Error("No agent available");
|
|
489
700
|
}
|
|
490
|
-
|
|
701
|
+
// Reduced: avoid verbose logging during streaming
|
|
491
702
|
return {
|
|
492
703
|
id: messageId,
|
|
493
704
|
agentId: currentAgent.id,
|
|
@@ -594,7 +805,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
594
805
|
// Final check before adding to prevent duplicates
|
|
595
806
|
const finalCheck = prev.find((msg) => msg.id === toolCallMessageId);
|
|
596
807
|
if (finalCheck) {
|
|
597
|
-
console.log("#!# β οΈ Tool call message already exists in state, skipping:", toolCallMessageId);
|
|
598
808
|
return prev;
|
|
599
809
|
}
|
|
600
810
|
const updated = [...prev, newStreamMessage];
|
|
@@ -603,9 +813,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
603
813
|
});
|
|
604
814
|
}
|
|
605
815
|
}
|
|
606
|
-
else {
|
|
607
|
-
console.log("π Adding tool call to existing message:", toolCallMessageId);
|
|
608
|
-
}
|
|
609
816
|
}
|
|
610
817
|
// Add tool call to the message in the array
|
|
611
818
|
if (toolCallMessageId && message.data && toolCallId) {
|
|
@@ -667,10 +874,8 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
667
874
|
}
|
|
668
875
|
// Update tool result directly in the messages array
|
|
669
876
|
if (!resultMessageId) {
|
|
670
|
-
console.warn("β οΈ No messageId available for tool result");
|
|
671
877
|
return;
|
|
672
878
|
}
|
|
673
|
-
console.log("π Updating tool result in message:", resultMessageId);
|
|
674
879
|
// Update the message with tool result
|
|
675
880
|
setMessages((prev) => {
|
|
676
881
|
const updated = prev.map((msg) => {
|
|
@@ -729,7 +934,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
729
934
|
};
|
|
730
935
|
updatedMessage.toolCalls = [...updatedMessage.toolCalls, toolCall];
|
|
731
936
|
}
|
|
732
|
-
|
|
937
|
+
// Updated tool calls count
|
|
733
938
|
return updatedMessage;
|
|
734
939
|
});
|
|
735
940
|
messagesRef.current = updated;
|
|
@@ -763,16 +968,12 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
763
968
|
abortControllerRef.current = abortController;
|
|
764
969
|
try {
|
|
765
970
|
setIsConnecting(true);
|
|
766
|
-
|
|
971
|
+
// Reduced: minimal logging
|
|
767
972
|
// Expose agent id globally for approval actions
|
|
768
973
|
window.currentAgentId = currentAgent.id;
|
|
769
|
-
|
|
770
|
-
|
|
974
|
+
// Expose id for approval actions
|
|
975
|
+
// Connecting to agent stream
|
|
771
976
|
await connectToAgentStream(currentAgent.id, (message) => {
|
|
772
|
-
console.log("π¨ Received stream message:", {
|
|
773
|
-
type: message.type,
|
|
774
|
-
data: message.data,
|
|
775
|
-
});
|
|
776
977
|
switch (message.type) {
|
|
777
978
|
case "contentChunk":
|
|
778
979
|
handleContentChunk(message, currentAgent);
|
|
@@ -786,15 +987,30 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
786
987
|
case "statusUpdate":
|
|
787
988
|
try {
|
|
788
989
|
const kind = message?.data?.kind;
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
990
|
+
// Live token usage totals update from backend
|
|
991
|
+
if (kind === "tokenUsage") {
|
|
992
|
+
const totals = message?.data?.totals;
|
|
993
|
+
if (totals) {
|
|
994
|
+
setLiveTotals({
|
|
995
|
+
input: Number(totals.totalInputTokens) || 0,
|
|
996
|
+
output: Number(totals.totalOutputTokens) || 0,
|
|
997
|
+
cached: Number(totals.totalCachedInputTokens) || 0,
|
|
998
|
+
inputCost: Number(totals.totalInputTokenCost) || 0,
|
|
999
|
+
outputCost: Number(totals.totalOutputTokenCost) || 0,
|
|
1000
|
+
cachedCost: Number(totals.totalCachedInputTokenCost) || 0,
|
|
1001
|
+
totalCost: Number(totals.totalCost) || 0,
|
|
1002
|
+
currency: totals.currency,
|
|
1003
|
+
});
|
|
1004
|
+
// Force a re-render to update cost display immediately
|
|
1005
|
+
setMessages((prev) => [...prev]);
|
|
1006
|
+
}
|
|
1007
|
+
break;
|
|
1008
|
+
}
|
|
793
1009
|
if (kind === "toolApprovalsRequired") {
|
|
794
1010
|
const data = message.data || {};
|
|
795
1011
|
const msgId = data.messageId;
|
|
796
1012
|
const ids = data.toolCallIds || [];
|
|
797
|
-
|
|
1013
|
+
// Pause stream until approval
|
|
798
1014
|
// Annotate tool calls with a temporary pending marker so UI can reflect paused state on reload
|
|
799
1015
|
if (msgId && Array.isArray(ids) && ids.length > 0) {
|
|
800
1016
|
setMessages((prev) => {
|
|
@@ -899,21 +1115,9 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
899
1115
|
const data = message.data || {};
|
|
900
1116
|
const toolCallId = data.toolCallId;
|
|
901
1117
|
const msgId = data.messageId;
|
|
902
|
-
|
|
903
|
-
kind,
|
|
904
|
-
toolCallId,
|
|
905
|
-
msgId,
|
|
906
|
-
data,
|
|
907
|
-
});
|
|
1118
|
+
// Processing tool approval
|
|
908
1119
|
if (toolCallId && msgId) {
|
|
909
1120
|
setMessages((prev) => {
|
|
910
|
-
console.log("π Looking for message:", {
|
|
911
|
-
targetMsgId: msgId,
|
|
912
|
-
availableMessages: prev.map((m) => ({
|
|
913
|
-
id: m.id,
|
|
914
|
-
toolCallsCount: m.toolCalls?.length || 0,
|
|
915
|
-
})),
|
|
916
|
-
});
|
|
917
1121
|
const updated = prev.map((m) => {
|
|
918
1122
|
if (m.id !== msgId)
|
|
919
1123
|
return m;
|
|
@@ -925,11 +1129,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
925
1129
|
? " (approved)"
|
|
926
1130
|
: " (rejected)";
|
|
927
1131
|
const newFunctionName = (tc.functionName || "") + suffix;
|
|
928
|
-
|
|
929
|
-
toolCallId,
|
|
930
|
-
oldName: tc.functionName,
|
|
931
|
-
newName: newFunctionName,
|
|
932
|
-
});
|
|
1132
|
+
// Update function name with approval suffix
|
|
933
1133
|
return {
|
|
934
1134
|
...tc,
|
|
935
1135
|
functionName: newFunctionName,
|
|
@@ -948,7 +1148,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
948
1148
|
break;
|
|
949
1149
|
case "completed":
|
|
950
1150
|
const completedMessageId = message.data?.messageId;
|
|
951
|
-
console.log("πΎ Stream completed for message:", completedMessageId, "messages count:", messages.length);
|
|
952
1151
|
// If the completed event carries full messages, merge them into state
|
|
953
1152
|
try {
|
|
954
1153
|
const completionMessages = message?.data
|
|
@@ -1012,6 +1211,22 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
1012
1211
|
updatedMessage.tokensUsed =
|
|
1013
1212
|
(updatedMessage.inputTokens || 0) +
|
|
1014
1213
|
(updatedMessage.outputTokens || 0);
|
|
1214
|
+
// Update cost data if provided in the completed event
|
|
1215
|
+
if (data.inputTokenCost !== undefined) {
|
|
1216
|
+
updatedMessage.inputTokenCost = data.inputTokenCost;
|
|
1217
|
+
}
|
|
1218
|
+
if (data.outputTokenCost !== undefined) {
|
|
1219
|
+
updatedMessage.outputTokenCost =
|
|
1220
|
+
data.outputTokenCost;
|
|
1221
|
+
}
|
|
1222
|
+
if (data.cachedInputTokenCost !== undefined ||
|
|
1223
|
+
data.cachedTokenCost !== undefined) {
|
|
1224
|
+
updatedMessage.cachedInputTokenCost =
|
|
1225
|
+
data.cachedInputTokenCost ?? data.cachedTokenCost;
|
|
1226
|
+
}
|
|
1227
|
+
if (data.totalCost !== undefined) {
|
|
1228
|
+
updatedMessage.totalCost = data.totalCost;
|
|
1229
|
+
}
|
|
1015
1230
|
// Handle content that might only be sent in the completed event
|
|
1016
1231
|
if (data.deltaContent && data.deltaContent.trim()) {
|
|
1017
1232
|
if (!data.isIncremental) {
|
|
@@ -1101,7 +1316,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
1101
1316
|
}
|
|
1102
1317
|
}
|
|
1103
1318
|
finally {
|
|
1104
|
-
console.log("π Stream connection finished, cleaning up");
|
|
1105
1319
|
setIsConnecting(false);
|
|
1106
1320
|
// Guard: clear waiting state if connection finished without content
|
|
1107
1321
|
setIsWaitingForResponse(false);
|
|
@@ -1115,18 +1329,13 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
1115
1329
|
return;
|
|
1116
1330
|
// Check if we're already connected
|
|
1117
1331
|
if (abortControllerRef.current) {
|
|
1118
|
-
console.log("π Already connected to stream, skipping reconnect");
|
|
1119
1332
|
return;
|
|
1120
1333
|
}
|
|
1121
1334
|
const msgs = messagesRef.current || [];
|
|
1122
1335
|
const hasPending = msgs.some((m) => (m.toolCalls || []).some((tc) => (tc.functionName || "").includes("(pending approval)")));
|
|
1123
1336
|
if (!hasPending) {
|
|
1124
|
-
console.log("π No pending approvals; reconnecting stream");
|
|
1125
1337
|
await connectToStream(currentAgent);
|
|
1126
1338
|
}
|
|
1127
|
-
else {
|
|
1128
|
-
console.log("βΈοΈ Still have pending approvals, not reconnecting yet");
|
|
1129
|
-
}
|
|
1130
1339
|
}
|
|
1131
1340
|
catch (err) {
|
|
1132
1341
|
console.error("β Error attempting reconnect:", err);
|
|
@@ -1142,11 +1351,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
1142
1351
|
const approved = !!detail.approved;
|
|
1143
1352
|
if (!messageId || !toolCallId)
|
|
1144
1353
|
return;
|
|
1145
|
-
|
|
1146
|
-
messageId,
|
|
1147
|
-
toolCallId,
|
|
1148
|
-
approved,
|
|
1149
|
-
});
|
|
1354
|
+
// Approval resolved; update local state
|
|
1150
1355
|
setMessages((prev) => {
|
|
1151
1356
|
const updated = prev.map((m) => {
|
|
1152
1357
|
if (m.id !== messageId)
|
|
@@ -1184,10 +1389,9 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
1184
1389
|
const loadAgent = useCallback(async () => {
|
|
1185
1390
|
try {
|
|
1186
1391
|
if (agentStub.status === "new") {
|
|
1187
|
-
console.log("β
Setting up new agent", agentStub.id);
|
|
1188
1392
|
// Set agent ID immediately for new agents
|
|
1189
1393
|
window.currentAgentId = agentStub.id;
|
|
1190
|
-
|
|
1394
|
+
// Set currentAgentId for new agent
|
|
1191
1395
|
// Derive initial profile from provided metadata if present
|
|
1192
1396
|
const initialProfileIdFromMeta = (() => {
|
|
1193
1397
|
try {
|
|
@@ -1369,7 +1573,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
1369
1573
|
setMessages(agentData.messages || []);
|
|
1370
1574
|
// Set agent ID for existing agents too
|
|
1371
1575
|
window.currentAgentId = agentData.id;
|
|
1372
|
-
console.log("π Setting currentAgentId for existing agent:", agentData.id);
|
|
1373
1576
|
// Parse metadata from DB if present (do not seed for existing agents)
|
|
1374
1577
|
const parsedMeta = (() => {
|
|
1375
1578
|
try {
|
|
@@ -1392,16 +1595,10 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
1392
1595
|
// Connect to stream if agent is running (handle both string and numeric status)
|
|
1393
1596
|
const isRunning = agentData.status === "running" || agentData.status === 1;
|
|
1394
1597
|
if (isRunning) {
|
|
1395
|
-
console.log("π Agent is running, connecting to stream...", {
|
|
1396
|
-
agentId: agentData.id,
|
|
1397
|
-
status: agentData.status,
|
|
1398
|
-
messageCount: agentData.messages?.length || 0,
|
|
1399
|
-
});
|
|
1400
1598
|
// Use setTimeout to ensure state updates are processed first
|
|
1401
1599
|
setTimeout(async () => {
|
|
1402
1600
|
// Check if we're already connecting to avoid duplicate connections
|
|
1403
1601
|
if (abortControllerRef.current) {
|
|
1404
|
-
console.log("β οΈ loadAgent: Already connected to stream, skipping duplicate connection");
|
|
1405
1602
|
return;
|
|
1406
1603
|
}
|
|
1407
1604
|
// Reset streaming state for reconnection
|
|
@@ -1411,7 +1608,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
1411
1608
|
const hasPending = (agentData.messages || []).some((m) => (m.toolCalls || []).some((tc) => typeof tc?.functionName === "string" &&
|
|
1412
1609
|
tc.functionName.includes("(pending approval)")));
|
|
1413
1610
|
if (hasPending) {
|
|
1414
|
-
console.log("βΈοΈ loadAgent: Pending approvals detected, delaying stream reconnect");
|
|
1415
1611
|
return;
|
|
1416
1612
|
}
|
|
1417
1613
|
}
|
|
@@ -1426,7 +1622,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
1426
1622
|
// For new agents that don't exist yet, this is expected
|
|
1427
1623
|
if (err?.message?.includes("404") ||
|
|
1428
1624
|
err?.message?.includes("not found")) {
|
|
1429
|
-
|
|
1625
|
+
// Agent does not exist, treat as new
|
|
1430
1626
|
setAgent(undefined);
|
|
1431
1627
|
setMessages([]);
|
|
1432
1628
|
setError(null);
|
|
@@ -1495,7 +1691,6 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
1495
1691
|
useEffect(() => {
|
|
1496
1692
|
return () => {
|
|
1497
1693
|
if (abortControllerRef.current) {
|
|
1498
|
-
console.log("Cleaning up stream connection");
|
|
1499
1694
|
abortControllerRef.current.abort();
|
|
1500
1695
|
}
|
|
1501
1696
|
};
|
|
@@ -1655,7 +1850,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
1655
1850
|
deterministic: deterministicFlags.deterministic,
|
|
1656
1851
|
seed: deterministicFlags.seed,
|
|
1657
1852
|
};
|
|
1658
|
-
|
|
1853
|
+
// Starting agent
|
|
1659
1854
|
// Set waiting state to show dancing dots immediately
|
|
1660
1855
|
setIsWaitingForResponse(true);
|
|
1661
1856
|
// Start idle timer; dots appear only if no chunks for >1s
|
|
@@ -1690,7 +1885,12 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
1690
1885
|
}
|
|
1691
1886
|
};
|
|
1692
1887
|
const handleKeyPress = (e) => {
|
|
1693
|
-
|
|
1888
|
+
// Submit only on plain Enter (no Ctrl/Meta/Shift/Alt)
|
|
1889
|
+
if (e.key === "Enter" &&
|
|
1890
|
+
!e.ctrlKey &&
|
|
1891
|
+
!e.metaKey &&
|
|
1892
|
+
!e.shiftKey &&
|
|
1893
|
+
!e.altKey) {
|
|
1694
1894
|
e.preventDefault();
|
|
1695
1895
|
handleSubmit();
|
|
1696
1896
|
}
|
|
@@ -2284,7 +2484,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
2284
2484
|
resetDotsTimer,
|
|
2285
2485
|
]);
|
|
2286
2486
|
if (isLoading) {
|
|
2287
|
-
return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsxs("div", { className: "flex items-center gap-2 text-
|
|
2487
|
+
return (_jsx("div", { className: "flex h-full items-center justify-center", children: _jsxs("div", { className: "flex items-center gap-2 text-xs text-gray-500", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin", strokeWidth: 1 }), "Loading agent..."] }) }));
|
|
2288
2488
|
}
|
|
2289
2489
|
// Calculate total token usage for cost display
|
|
2290
2490
|
const totalTokens = calculateTotalTokens(messages);
|
|
@@ -2366,7 +2566,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
2366
2566
|
sendQuickMessage(text);
|
|
2367
2567
|
} }, groupIndex));
|
|
2368
2568
|
}
|
|
2369
|
-
}) }), _jsx("div", { className: showDots ? "visible" : "invisible", children: _jsx(DancingDots, {}) }), _jsx("div", { ref: messagesEndRef })] }), renderContextInfoBar(), _jsx(TodoListPanel, { messages: messages }), _jsxs("div", { className: "border-t border-gray-200 p-4", children: [_jsx("div", { className: "flex items-stretch gap-2", children: _jsx(Textarea, { ref: textareaRef, value: prompt, onChange: (e) => {
|
|
2569
|
+
}) }), _jsx("div", { className: showDots ? "visible" : "invisible", children: _jsx(DancingDots, {}) }), _jsx("div", { ref: messagesEndRef })] }), renderContextInfoBar(), _jsx(TodoListPanel, { messages: messages, agentMetadata: agentMetadata }), _jsxs("div", { className: "border-t border-gray-200 p-4", children: [_jsx("div", { className: "flex items-stretch gap-2", children: _jsx(Textarea, { ref: textareaRef, value: prompt, onChange: (e) => {
|
|
2370
2570
|
setPrompt(e.target.value);
|
|
2371
2571
|
// Reset history index when user starts typing
|
|
2372
2572
|
if (currentHistoryIndex !== -1) {
|
|
@@ -2435,7 +2635,17 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
2435
2635
|
setShowPredefined(false);
|
|
2436
2636
|
if (textareaRef.current)
|
|
2437
2637
|
textareaRef.current.focus();
|
|
2438
|
-
}, children: p.title }, index))) }) })] })) : null] }), _jsxs("div", { className: "flex items-center gap-1 self-end", children: [isVoiceSupported ? (_jsx(Button, { onClick: toggleVoice, size: "sm", className: "h-5.5 w-5.5 cursor-pointer rounded-full", title: isListening ? "Stop voice input" : "Start voice input", "aria-label": isListening ? "Stop voice input" : "Start voice input", "aria-pressed": isListening, children: isListening ? (_jsx(MicOff, { className: "size-3", strokeWidth: 1 })) : (_jsx(Mic, { className: "size-3", strokeWidth: 1 })) })) : null, _jsx(Button, { onClick: isExecuting ? handleStop : handleSubmit, disabled: !isExecuting && !prompt.trim(), size: "sm", className: "h-5.5 w-5.5 cursor-pointer rounded-full", title: isExecuting ? "Stop" : "Send", "aria-label": isExecuting ? "Stop" : "Send", "data-testid": "agent-send-stop-button", "data-executing": isExecuting ? "true" : "false", children: isExecuting ? (_jsx(Square, { className: "size-3", strokeWidth: 1 })) : (_jsx(Send, { className: "size-3", strokeWidth: 1 })) })] })] }), _jsxs("div", { className: "mt-1 flex items-center gap-2 text-[10px] text-gray-500", children: [_jsx(AgentCostDisplay, { totalTokens:
|
|
2638
|
+
}, children: p.title }, index))) }) })] })) : null] }), _jsxs("div", { className: "flex items-center gap-1 self-end", children: [isVoiceSupported ? (_jsx(Button, { onClick: toggleVoice, size: "sm", className: "h-5.5 w-5.5 cursor-pointer rounded-full", title: isListening ? "Stop voice input" : "Start voice input", "aria-label": isListening ? "Stop voice input" : "Start voice input", "aria-pressed": isListening, children: isListening ? (_jsx(MicOff, { className: "size-3", strokeWidth: 1 })) : (_jsx(Mic, { className: "size-3", strokeWidth: 1 })) })) : null, _jsx(Button, { onClick: isExecuting ? handleStop : handleSubmit, disabled: !isExecuting && !prompt.trim(), size: "sm", className: "h-5.5 w-5.5 cursor-pointer rounded-full", title: isExecuting ? "Stop" : "Send", "aria-label": isExecuting ? "Stop" : "Send", "data-testid": "agent-send-stop-button", "data-executing": isExecuting ? "true" : "false", children: isExecuting ? (_jsx(Square, { className: "size-3", strokeWidth: 1 })) : (_jsx(Send, { className: "size-3", strokeWidth: 1 })) })] })] }), _jsxs("div", { className: "mt-1 flex items-center gap-2 text-[10px] text-gray-500", children: [_jsx(AgentCostDisplay, { totalTokens: liveTotals
|
|
2639
|
+
? {
|
|
2640
|
+
input: liveTotals.input,
|
|
2641
|
+
output: liveTotals.output,
|
|
2642
|
+
cached: liveTotals.cached,
|
|
2643
|
+
inputCost: liveTotals.inputCost,
|
|
2644
|
+
outputCost: liveTotals.outputCost,
|
|
2645
|
+
cachedCost: liveTotals.cachedCost,
|
|
2646
|
+
totalCost: liveTotals.totalCost,
|
|
2647
|
+
}
|
|
2648
|
+
: totalTokens }), (() => {
|
|
2439
2649
|
try {
|
|
2440
2650
|
const s = window.__agentContextWindowStatus;
|
|
2441
2651
|
if (!s || !s.contextWindowTokens)
|
|
@@ -2452,7 +2662,7 @@ export function AgentTerminal({ agentStub, initialMetadata, profiles, }) {
|
|
|
2452
2662
|
};
|
|
2453
2663
|
if (!pct)
|
|
2454
2664
|
return null;
|
|
2455
|
-
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: "rounded border border-gray-200 bg-gray-50 px-2 py-0.5
|
|
2665
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: "cursor-help rounded border border-gray-200 bg-gray-50 px-2 py-0.5", children: ["Context: ", pct] }) }), _jsx(TooltipContent, { side: "top", sideOffset: 6, children: _jsxs("div", { className: "max-w-[320px] space-y-1 text-xs", children: [_jsxs("div", { children: [_jsx("span", { className: "font-semibold", children: "Model:" }), " ", s.model, s.normalizedModel && ` (${s.normalizedModel})`] }), _jsxs("div", { children: [_jsx("span", { className: "font-semibold", children: "Context window:" }), " ", formatTokens(s.estimatedInputTokens || 0), " /", " ", formatTokens(s.contextWindowTokens), " tokens"] }), typeof s.maxCompletionTokens === "number" && (_jsxs("div", { children: [_jsx("span", { className: "font-semibold", children: "Max completion:" }), " ", formatTokens(s.maxCompletionTokens), " tokens"] })), _jsxs("div", { children: [_jsx("span", { className: "font-semibold", children: "Used:" }), " ", pct] })] }) })] }));
|
|
2456
2666
|
}
|
|
2457
2667
|
catch {
|
|
2458
2668
|
return null;
|