@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
|
@@ -88,10 +88,116 @@ interface TodoItem {
|
|
|
88
88
|
text: string;
|
|
89
89
|
done?: boolean;
|
|
90
90
|
note?: string;
|
|
91
|
-
messageId
|
|
91
|
+
messageId?: string;
|
|
92
92
|
sourceTitle?: string;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// Helper to extract todos from potentially incomplete JSON during streaming
|
|
96
|
+
const extractPartialTodos = (jsonText: string): any[] => {
|
|
97
|
+
// First try to parse complete JSON
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(jsonText);
|
|
100
|
+
return Array.isArray(parsed) ? parsed : parsed?.items || [];
|
|
101
|
+
} catch (e) {
|
|
102
|
+
// If JSON is incomplete, try to extract whatever todo items we can find
|
|
103
|
+
const items: any[] = [];
|
|
104
|
+
|
|
105
|
+
// Look for individual todo objects in the partial JSON
|
|
106
|
+
// Match patterns like: { "text": "...", "done": false, "note": "..." }
|
|
107
|
+
// Handle various field orderings (text can be anywhere in the object)
|
|
108
|
+
const textPattern = /"text"\s*:\s*"([^"]+)"/g;
|
|
109
|
+
const textMatches: Array<{ text: string; startIdx: number }> = [];
|
|
110
|
+
let textMatch;
|
|
111
|
+
while ((textMatch = textPattern.exec(jsonText)) !== null) {
|
|
112
|
+
if (textMatch[1]) {
|
|
113
|
+
textMatches.push({
|
|
114
|
+
text: textMatch[1],
|
|
115
|
+
startIdx: textMatch.index,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// For each text field found, try to find the enclosing object
|
|
121
|
+
for (const { text, startIdx } of textMatches) {
|
|
122
|
+
// Find the opening brace before this text field
|
|
123
|
+
let openBrace = -1;
|
|
124
|
+
for (let i = startIdx - 1; i >= 0; i--) {
|
|
125
|
+
if (jsonText[i] === "{") {
|
|
126
|
+
openBrace = i;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
if (jsonText[i] === "}") break; // Hit another object's end
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (openBrace === -1) continue;
|
|
133
|
+
|
|
134
|
+
// Find the closing brace after this text field
|
|
135
|
+
let closeBrace = -1;
|
|
136
|
+
let depth = 0;
|
|
137
|
+
for (let i = openBrace; i < jsonText.length; i++) {
|
|
138
|
+
if (jsonText[i] === "{") depth++;
|
|
139
|
+
if (jsonText[i] === "}") {
|
|
140
|
+
depth--;
|
|
141
|
+
if (depth === 0) {
|
|
142
|
+
closeBrace = i;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Extract the object and try to parse it
|
|
149
|
+
const objStr =
|
|
150
|
+
closeBrace !== -1
|
|
151
|
+
? jsonText.substring(openBrace, closeBrace + 1)
|
|
152
|
+
: jsonText.substring(openBrace) + "}"; // Try to close incomplete object
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const obj = JSON.parse(objStr);
|
|
156
|
+
if (obj.text) {
|
|
157
|
+
items.push({
|
|
158
|
+
text: obj.text,
|
|
159
|
+
done: obj.done === true,
|
|
160
|
+
note: obj.note || undefined,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
} catch (e) {
|
|
164
|
+
// Skip malformed objects
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Also try to extract from partial objects at the end
|
|
169
|
+
// Look for the last opening brace and try to parse up to where we have valid content
|
|
170
|
+
const lines = jsonText.split("\n");
|
|
171
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
172
|
+
const partialJson = lines.slice(0, i + 1).join("\n");
|
|
173
|
+
// Try to close any open braces/brackets
|
|
174
|
+
let testJson = partialJson;
|
|
175
|
+
const openBraces = (testJson.match(/\{/g) || []).length;
|
|
176
|
+
const closeBraces = (testJson.match(/\}/g) || []).length;
|
|
177
|
+
const openBrackets = (testJson.match(/\[/g) || []).length;
|
|
178
|
+
const closeBrackets = (testJson.match(/\]/g) || []).length;
|
|
179
|
+
|
|
180
|
+
// Add missing closing characters
|
|
181
|
+
testJson += "]".repeat(openBrackets - closeBrackets);
|
|
182
|
+
testJson += "}".repeat(openBraces - closeBraces);
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const parsed = JSON.parse(testJson);
|
|
186
|
+
const partialItems = Array.isArray(parsed)
|
|
187
|
+
? parsed
|
|
188
|
+
: parsed?.items || [];
|
|
189
|
+
if (partialItems.length > items.length) {
|
|
190
|
+
return partialItems;
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return items;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
95
201
|
const extractTodosFromMessages = (messages: AgentChatMessage[]): TodoItem[] => {
|
|
96
202
|
const todos: TodoItem[] = [];
|
|
97
203
|
const fencedTodoToken = "```todo_list";
|
|
@@ -126,12 +232,20 @@ const extractTodosFromMessages = (messages: AgentChatMessage[]): TodoItem[] => {
|
|
|
126
232
|
|
|
127
233
|
try {
|
|
128
234
|
let jsonText = "";
|
|
235
|
+
let isComplete = true;
|
|
236
|
+
|
|
129
237
|
if (isFenced) {
|
|
130
238
|
const afterToken = todoStart + fencedTodoToken.length;
|
|
131
239
|
const closePos = content.indexOf("```", afterToken);
|
|
132
|
-
if (closePos === -1)
|
|
133
|
-
|
|
134
|
-
|
|
240
|
+
if (closePos === -1) {
|
|
241
|
+
// Incomplete fenced block - extract what we have so far
|
|
242
|
+
jsonText = content.slice(afterToken).trim();
|
|
243
|
+
isComplete = false;
|
|
244
|
+
cursor = content.length; // Process till end
|
|
245
|
+
} else {
|
|
246
|
+
jsonText = content.slice(afterToken, closePos).trim();
|
|
247
|
+
cursor = closePos + 3;
|
|
248
|
+
}
|
|
135
249
|
} else {
|
|
136
250
|
const afterToken = todoStart + plainTodoToken.length;
|
|
137
251
|
const braceStart = content.indexOf("{", afterToken);
|
|
@@ -149,24 +263,56 @@ const extractTodosFromMessages = (messages: AgentChatMessage[]): TodoItem[] => {
|
|
|
149
263
|
}
|
|
150
264
|
}
|
|
151
265
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
266
|
+
|
|
267
|
+
if (braceEnd === -1) {
|
|
268
|
+
// Incomplete JSON - extract what we have
|
|
269
|
+
jsonText = content.slice(braceStart).trim();
|
|
270
|
+
isComplete = false;
|
|
271
|
+
cursor = content.length;
|
|
272
|
+
} else {
|
|
273
|
+
jsonText = content.slice(braceStart, braceEnd + 1).trim();
|
|
274
|
+
cursor = braceEnd + 1;
|
|
275
|
+
}
|
|
155
276
|
}
|
|
156
277
|
|
|
157
|
-
|
|
158
|
-
const todoItems =
|
|
159
|
-
|
|
278
|
+
// Use the partial extraction helper for incomplete JSON
|
|
279
|
+
const todoItems = isComplete
|
|
280
|
+
? (() => {
|
|
281
|
+
try {
|
|
282
|
+
const parsed = JSON.parse(jsonText);
|
|
283
|
+
return Array.isArray(parsed) ? parsed : parsed?.items || [];
|
|
284
|
+
} catch (e) {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
})()
|
|
288
|
+
: extractPartialTodos(jsonText);
|
|
289
|
+
|
|
290
|
+
const title = (() => {
|
|
291
|
+
try {
|
|
292
|
+
const parsed = JSON.parse(jsonText);
|
|
293
|
+
return Array.isArray(parsed) ? undefined : parsed?.title;
|
|
294
|
+
} catch (e) {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
})();
|
|
160
298
|
|
|
161
299
|
todoItems.forEach((item: any) => {
|
|
162
300
|
if (!item) return;
|
|
163
301
|
const text =
|
|
164
|
-
item.text ||
|
|
302
|
+
item.text ||
|
|
303
|
+
item.content ||
|
|
304
|
+
item.label ||
|
|
305
|
+
String(item.task || item.title || "");
|
|
165
306
|
if (!text) return;
|
|
166
307
|
todos.push({
|
|
167
308
|
id: item.id,
|
|
168
309
|
text,
|
|
169
|
-
done: !!(
|
|
310
|
+
done: !!(
|
|
311
|
+
item.done ??
|
|
312
|
+
item.completed ??
|
|
313
|
+
item.checked ??
|
|
314
|
+
item.status === "completed"
|
|
315
|
+
),
|
|
170
316
|
note: item.note || item.description,
|
|
171
317
|
messageId: message.id,
|
|
172
318
|
sourceTitle: title,
|
|
@@ -183,22 +329,101 @@ const extractTodosFromMessages = (messages: AgentChatMessage[]): TodoItem[] => {
|
|
|
183
329
|
};
|
|
184
330
|
|
|
185
331
|
// TodoListPanel component
|
|
186
|
-
const TodoListPanel = ({
|
|
332
|
+
const TodoListPanel = ({
|
|
333
|
+
messages,
|
|
334
|
+
agentMetadata,
|
|
335
|
+
}: {
|
|
336
|
+
messages: AgentChatMessage[];
|
|
337
|
+
agentMetadata: AgentMetadata | null;
|
|
338
|
+
}) => {
|
|
187
339
|
const [isExpanded, setIsExpanded] = useState(true);
|
|
188
|
-
const todos = useMemo(() => extractTodosFromMessages(messages), [messages]);
|
|
189
340
|
|
|
190
|
-
|
|
341
|
+
const todos = useMemo(() => {
|
|
342
|
+
// First try to get todos from agent metadata (real-time updates)
|
|
343
|
+
const metadataTodos = (() => {
|
|
344
|
+
try {
|
|
345
|
+
const context = (agentMetadata as any)?.additionalData?.context;
|
|
346
|
+
const todoList = context?.todoList;
|
|
347
|
+
if (todoList?.items && Array.isArray(todoList.items)) {
|
|
348
|
+
return todoList.items
|
|
349
|
+
.map((item: any, idx: number) => ({
|
|
350
|
+
id: item.id || `metadata-${idx}`,
|
|
351
|
+
text:
|
|
352
|
+
item.text ||
|
|
353
|
+
item.label ||
|
|
354
|
+
String(item.task || item.title || ""),
|
|
355
|
+
done: !!(item.done ?? item.completed ?? item.checked),
|
|
356
|
+
note: item.note || item.description,
|
|
357
|
+
messageId: undefined,
|
|
358
|
+
sourceTitle: todoList.title,
|
|
359
|
+
}))
|
|
360
|
+
.filter((item: any) => item.text);
|
|
361
|
+
}
|
|
362
|
+
} catch (e) {
|
|
363
|
+
// Fallback to extracting from messages
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
})();
|
|
367
|
+
|
|
368
|
+
// If we have metadata todos, use them; otherwise extract from messages
|
|
369
|
+
if (metadataTodos && metadataTodos.length > 0) {
|
|
370
|
+
return metadataTodos;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return extractTodosFromMessages(messages);
|
|
374
|
+
}, [messages, agentMetadata]);
|
|
375
|
+
|
|
376
|
+
// Check if there's an active streaming message with incomplete todo content
|
|
191
377
|
const isUpdating = useMemo(() => {
|
|
192
378
|
return messages.some((msg) => {
|
|
193
379
|
if (msg.role !== "assistant" || msg.isCompleted) return false;
|
|
194
380
|
const content = msg.content || "";
|
|
195
|
-
|
|
381
|
+
|
|
382
|
+
// Check for incomplete fenced todo blocks
|
|
383
|
+
const fencedStart = content.indexOf("```todo_list");
|
|
384
|
+
if (fencedStart !== -1) {
|
|
385
|
+
const afterStart = fencedStart + "```todo_list".length;
|
|
386
|
+
const closePos = content.indexOf("```", afterStart);
|
|
387
|
+
if (closePos === -1) {
|
|
388
|
+
// Incomplete fenced block
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Check for incomplete plain todo blocks
|
|
394
|
+
const plainStart = content.indexOf("todo_list");
|
|
395
|
+
if (plainStart !== -1 && plainStart !== fencedStart) {
|
|
396
|
+
const before = plainStart > 0 ? content[plainStart - 1] : "\n";
|
|
397
|
+
if (before === "\n" || before === "\r" || plainStart === 0) {
|
|
398
|
+
const braceStart = content.indexOf("{", plainStart);
|
|
399
|
+
if (braceStart !== -1) {
|
|
400
|
+
let depth = 0;
|
|
401
|
+
let braceEnd = -1;
|
|
402
|
+
for (let i = braceStart; i < content.length; i++) {
|
|
403
|
+
if (content[i] === "{") depth++;
|
|
404
|
+
if (content[i] === "}") {
|
|
405
|
+
depth--;
|
|
406
|
+
if (depth === 0) {
|
|
407
|
+
braceEnd = i;
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (braceEnd === -1) {
|
|
413
|
+
// Incomplete plain block
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return false;
|
|
196
421
|
});
|
|
197
422
|
}, [messages]);
|
|
198
423
|
|
|
199
424
|
if (todos.length === 0 && !isUpdating) return null;
|
|
200
425
|
|
|
201
|
-
const completedCount = todos.filter((t) => t.done).length;
|
|
426
|
+
const completedCount = todos.filter((t: TodoItem) => t.done).length;
|
|
202
427
|
const totalCount = todos.length;
|
|
203
428
|
|
|
204
429
|
return (
|
|
@@ -229,66 +454,69 @@ const TodoListPanel = ({ messages }: { messages: AgentChatMessage[] }) => {
|
|
|
229
454
|
</button>
|
|
230
455
|
{isExpanded && (
|
|
231
456
|
<div className="max-h-64 overflow-y-auto px-4 pb-3">
|
|
232
|
-
{
|
|
233
|
-
<div className="
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
<path
|
|
256
|
-
strokeLinecap="round"
|
|
257
|
-
strokeLinejoin="round"
|
|
258
|
-
d="M5 13l4 4L19 7"
|
|
259
|
-
/>
|
|
260
|
-
</svg>
|
|
261
|
-
</div>
|
|
262
|
-
) : (
|
|
263
|
-
<div className="h-4 w-4 rounded border-2 border-gray-300" />
|
|
264
|
-
)}
|
|
265
|
-
</div>
|
|
266
|
-
<div className="min-w-0 flex-1">
|
|
267
|
-
<div
|
|
268
|
-
className={`${
|
|
269
|
-
todo.done
|
|
270
|
-
? "text-gray-500 line-through"
|
|
271
|
-
: "text-gray-900"
|
|
272
|
-
}`}
|
|
273
|
-
>
|
|
274
|
-
{todo.text}
|
|
457
|
+
{todos.length > 0 && (
|
|
458
|
+
<div className="space-y-1.5">
|
|
459
|
+
{todos.map((todo: TodoItem, idx: number) => (
|
|
460
|
+
<div
|
|
461
|
+
key={todo.id || `${todo.messageId}-${idx}`}
|
|
462
|
+
className="flex items-start gap-2 rounded bg-white p-2 text-xs"
|
|
463
|
+
>
|
|
464
|
+
<div className="flex-shrink-0 pt-0.5">
|
|
465
|
+
{todo.done ? (
|
|
466
|
+
<div className="flex h-4 w-4 items-center justify-center rounded border-2 border-green-500 bg-green-500">
|
|
467
|
+
<svg
|
|
468
|
+
className="h-3 w-3 text-white"
|
|
469
|
+
fill="none"
|
|
470
|
+
strokeWidth={2}
|
|
471
|
+
stroke="currentColor"
|
|
472
|
+
viewBox="0 0 24 24"
|
|
473
|
+
>
|
|
474
|
+
<path
|
|
475
|
+
strokeLinecap="round"
|
|
476
|
+
strokeLinejoin="round"
|
|
477
|
+
d="M5 13l4 4L19 7"
|
|
478
|
+
/>
|
|
479
|
+
</svg>
|
|
275
480
|
</div>
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
481
|
+
) : (
|
|
482
|
+
<div className="h-4 w-4 rounded border-2 border-gray-300" />
|
|
483
|
+
)}
|
|
484
|
+
</div>
|
|
485
|
+
<div className="min-w-0 flex-1">
|
|
486
|
+
<div
|
|
487
|
+
className={`${
|
|
488
|
+
todo.done
|
|
489
|
+
? "text-gray-500 line-through"
|
|
490
|
+
: "text-gray-900"
|
|
491
|
+
}`}
|
|
492
|
+
>
|
|
493
|
+
{todo.text}
|
|
281
494
|
</div>
|
|
495
|
+
{todo.note && (
|
|
496
|
+
<div className="mt-0.5 text-xs text-gray-500">
|
|
497
|
+
{todo.note}
|
|
498
|
+
</div>
|
|
499
|
+
)}
|
|
282
500
|
</div>
|
|
283
|
-
))}
|
|
284
|
-
</div>
|
|
285
|
-
{isUpdating && todos.length > 0 && (
|
|
286
|
-
<div className="mt-2 flex items-center gap-2 rounded bg-blue-50 px-3 py-2 text-xs text-blue-700">
|
|
287
|
-
<Loader2 className="h-3 w-3 animate-spin" strokeWidth={1} />
|
|
288
|
-
<span>Updating todo list...</span>
|
|
289
501
|
</div>
|
|
290
|
-
)}
|
|
291
|
-
|
|
502
|
+
))}
|
|
503
|
+
</div>
|
|
504
|
+
)}
|
|
505
|
+
{isUpdating && (
|
|
506
|
+
<div
|
|
507
|
+
className={`flex items-center gap-2 rounded px-3 py-2 text-xs ${
|
|
508
|
+
todos.length > 0
|
|
509
|
+
? "mt-2 bg-blue-50 text-blue-700"
|
|
510
|
+
: "justify-center bg-white text-gray-500"
|
|
511
|
+
}`}
|
|
512
|
+
>
|
|
513
|
+
<Loader2 className="h-3 w-3 animate-spin" strokeWidth={1} />
|
|
514
|
+
<span>
|
|
515
|
+
{todos.length > 0
|
|
516
|
+
? "Updating todo list..."
|
|
517
|
+
: "Loading todo list..."}
|
|
518
|
+
</span>
|
|
519
|
+
</div>
|
|
292
520
|
)}
|
|
293
521
|
</div>
|
|
294
522
|
)}
|
|
@@ -423,7 +651,7 @@ export function AgentTerminal({
|
|
|
423
651
|
const [messages, setMessages] = useState<AgentChatMessage[]>([]);
|
|
424
652
|
const [prompt, setPrompt] = useState("");
|
|
425
653
|
const [inputPlaceholder, setInputPlaceholder] = useState<string>(
|
|
426
|
-
"Type your message... (Enter to send, Ctrl+Enter for new line)",
|
|
654
|
+
"Type your message... (Enter to send, Shift+Enter or Ctrl+Enter for new line)",
|
|
427
655
|
);
|
|
428
656
|
const [isLoading, setIsLoading] = useState(false);
|
|
429
657
|
const [isConnecting, setIsConnecting] = useState(false);
|
|
@@ -552,6 +780,18 @@ export function AgentTerminal({
|
|
|
552
780
|
initialCostLimit: number;
|
|
553
781
|
} | null>(null);
|
|
554
782
|
|
|
783
|
+
// Live running totals from backend status updates (tokenUsage)
|
|
784
|
+
const [liveTotals, setLiveTotals] = useState<{
|
|
785
|
+
input: number;
|
|
786
|
+
output: number;
|
|
787
|
+
cached: number;
|
|
788
|
+
inputCost: number;
|
|
789
|
+
outputCost: number;
|
|
790
|
+
cachedCost: number;
|
|
791
|
+
totalCost: number;
|
|
792
|
+
currency?: string;
|
|
793
|
+
} | null>(null);
|
|
794
|
+
|
|
555
795
|
// Flag to track when we should create a new message
|
|
556
796
|
const shouldCreateNewMessage = useRef(false);
|
|
557
797
|
|
|
@@ -724,10 +964,7 @@ export function AgentTerminal({
|
|
|
724
964
|
});
|
|
725
965
|
throw new Error("No agent available");
|
|
726
966
|
}
|
|
727
|
-
|
|
728
|
-
"✅ Creating new stream message with agent:",
|
|
729
|
-
currentAgent.id,
|
|
730
|
-
);
|
|
967
|
+
// Reduced: avoid verbose logging during streaming
|
|
731
968
|
return {
|
|
732
969
|
id: messageId,
|
|
733
970
|
agentId: currentAgent.id,
|
|
@@ -873,10 +1110,6 @@ export function AgentTerminal({
|
|
|
873
1110
|
(msg) => msg.id === toolCallMessageId,
|
|
874
1111
|
);
|
|
875
1112
|
if (finalCheck) {
|
|
876
|
-
console.log(
|
|
877
|
-
"#!# ⚠️ Tool call message already exists in state, skipping:",
|
|
878
|
-
toolCallMessageId,
|
|
879
|
-
);
|
|
880
1113
|
return prev;
|
|
881
1114
|
}
|
|
882
1115
|
|
|
@@ -886,11 +1119,6 @@ export function AgentTerminal({
|
|
|
886
1119
|
return updated;
|
|
887
1120
|
});
|
|
888
1121
|
}
|
|
889
|
-
} else {
|
|
890
|
-
console.log(
|
|
891
|
-
"📋 Adding tool call to existing message:",
|
|
892
|
-
toolCallMessageId,
|
|
893
|
-
);
|
|
894
1122
|
}
|
|
895
1123
|
}
|
|
896
1124
|
|
|
@@ -971,12 +1199,9 @@ export function AgentTerminal({
|
|
|
971
1199
|
|
|
972
1200
|
// Update tool result directly in the messages array
|
|
973
1201
|
if (!resultMessageId) {
|
|
974
|
-
console.warn("⚠️ No messageId available for tool result");
|
|
975
1202
|
return;
|
|
976
1203
|
}
|
|
977
1204
|
|
|
978
|
-
console.log("📋 Updating tool result in message:", resultMessageId);
|
|
979
|
-
|
|
980
1205
|
// Update the message with tool result
|
|
981
1206
|
setMessages((prev) => {
|
|
982
1207
|
const updated = prev.map((msg) => {
|
|
@@ -1049,10 +1274,7 @@ export function AgentTerminal({
|
|
|
1049
1274
|
updatedMessage.toolCalls = [...updatedMessage.toolCalls, toolCall];
|
|
1050
1275
|
}
|
|
1051
1276
|
|
|
1052
|
-
|
|
1053
|
-
"🔧 Updated tool calls count:",
|
|
1054
|
-
updatedMessage.toolCalls.length,
|
|
1055
|
-
);
|
|
1277
|
+
// Updated tool calls count
|
|
1056
1278
|
|
|
1057
1279
|
return updatedMessage;
|
|
1058
1280
|
});
|
|
@@ -1094,23 +1316,16 @@ export function AgentTerminal({
|
|
|
1094
1316
|
try {
|
|
1095
1317
|
setIsConnecting(true);
|
|
1096
1318
|
|
|
1097
|
-
|
|
1319
|
+
// Reduced: minimal logging
|
|
1098
1320
|
|
|
1099
1321
|
// Expose agent id globally for approval actions
|
|
1100
1322
|
(window as any).currentAgentId = currentAgent.id;
|
|
1101
|
-
|
|
1323
|
+
// Expose id for approval actions
|
|
1102
1324
|
|
|
1103
|
-
|
|
1104
|
-
"🌐 Attempting to connect to agent stream for:",
|
|
1105
|
-
currentAgent.id,
|
|
1106
|
-
);
|
|
1325
|
+
// Connecting to agent stream
|
|
1107
1326
|
await connectToAgentStream(
|
|
1108
1327
|
currentAgent.id,
|
|
1109
1328
|
(message: AgentStreamMessage) => {
|
|
1110
|
-
console.log("📨 Received stream message:", {
|
|
1111
|
-
type: message.type,
|
|
1112
|
-
data: message.data,
|
|
1113
|
-
});
|
|
1114
1329
|
switch (message.type) {
|
|
1115
1330
|
case "contentChunk":
|
|
1116
1331
|
handleContentChunk(message, currentAgent);
|
|
@@ -1127,18 +1342,31 @@ export function AgentTerminal({
|
|
|
1127
1342
|
case "statusUpdate":
|
|
1128
1343
|
try {
|
|
1129
1344
|
const kind = (message as any)?.data?.kind;
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1345
|
+
// Live token usage totals update from backend
|
|
1346
|
+
if (kind === "tokenUsage") {
|
|
1347
|
+
const totals = (message as any)?.data?.totals;
|
|
1348
|
+
if (totals) {
|
|
1349
|
+
setLiveTotals({
|
|
1350
|
+
input: Number(totals.totalInputTokens) || 0,
|
|
1351
|
+
output: Number(totals.totalOutputTokens) || 0,
|
|
1352
|
+
cached: Number(totals.totalCachedInputTokens) || 0,
|
|
1353
|
+
inputCost: Number(totals.totalInputTokenCost) || 0,
|
|
1354
|
+
outputCost: Number(totals.totalOutputTokenCost) || 0,
|
|
1355
|
+
cachedCost:
|
|
1356
|
+
Number(totals.totalCachedInputTokenCost) || 0,
|
|
1357
|
+
totalCost: Number(totals.totalCost) || 0,
|
|
1358
|
+
currency: totals.currency,
|
|
1359
|
+
});
|
|
1360
|
+
// Force a re-render to update cost display immediately
|
|
1361
|
+
setMessages((prev) => [...prev]);
|
|
1362
|
+
}
|
|
1363
|
+
break;
|
|
1364
|
+
}
|
|
1134
1365
|
if (kind === "toolApprovalsRequired") {
|
|
1135
1366
|
const data = (message as any).data || {};
|
|
1136
1367
|
const msgId: string | undefined = data.messageId;
|
|
1137
1368
|
const ids: string[] = data.toolCallIds || [];
|
|
1138
|
-
|
|
1139
|
-
"⏸️ Approvals required; pausing stream until approval:",
|
|
1140
|
-
{ msgId, ids },
|
|
1141
|
-
);
|
|
1369
|
+
// Pause stream until approval
|
|
1142
1370
|
|
|
1143
1371
|
// Annotate tool calls with a temporary pending marker so UI can reflect paused state on reload
|
|
1144
1372
|
if (msgId && Array.isArray(ids) && ids.length > 0) {
|
|
@@ -1243,21 +1471,9 @@ export function AgentTerminal({
|
|
|
1243
1471
|
const data = (message as any).data || {};
|
|
1244
1472
|
const toolCallId: string | undefined = data.toolCallId;
|
|
1245
1473
|
const msgId: string | undefined = data.messageId;
|
|
1246
|
-
|
|
1247
|
-
kind,
|
|
1248
|
-
toolCallId,
|
|
1249
|
-
msgId,
|
|
1250
|
-
data,
|
|
1251
|
-
});
|
|
1474
|
+
// Processing tool approval
|
|
1252
1475
|
if (toolCallId && msgId) {
|
|
1253
1476
|
setMessages((prev) => {
|
|
1254
|
-
console.log("🔍 Looking for message:", {
|
|
1255
|
-
targetMsgId: msgId,
|
|
1256
|
-
availableMessages: prev.map((m) => ({
|
|
1257
|
-
id: m.id,
|
|
1258
|
-
toolCallsCount: m.toolCalls?.length || 0,
|
|
1259
|
-
})),
|
|
1260
|
-
});
|
|
1261
1477
|
const updated = prev.map((m) => {
|
|
1262
1478
|
if (m.id !== msgId) return m;
|
|
1263
1479
|
const existingToolCalls = m.toolCalls || [];
|
|
@@ -1270,11 +1486,7 @@ export function AgentTerminal({
|
|
|
1270
1486
|
: " (rejected)";
|
|
1271
1487
|
const newFunctionName =
|
|
1272
1488
|
(tc.functionName || "") + suffix;
|
|
1273
|
-
|
|
1274
|
-
toolCallId,
|
|
1275
|
-
oldName: tc.functionName,
|
|
1276
|
-
newName: newFunctionName,
|
|
1277
|
-
});
|
|
1489
|
+
// Update function name with approval suffix
|
|
1278
1490
|
return {
|
|
1279
1491
|
...tc,
|
|
1280
1492
|
functionName: newFunctionName,
|
|
@@ -1294,12 +1506,6 @@ export function AgentTerminal({
|
|
|
1294
1506
|
|
|
1295
1507
|
case "completed":
|
|
1296
1508
|
const completedMessageId = message.data?.messageId;
|
|
1297
|
-
console.log(
|
|
1298
|
-
"💾 Stream completed for message:",
|
|
1299
|
-
completedMessageId,
|
|
1300
|
-
"messages count:",
|
|
1301
|
-
messages.length,
|
|
1302
|
-
);
|
|
1303
1509
|
|
|
1304
1510
|
// If the completed event carries full messages, merge them into state
|
|
1305
1511
|
try {
|
|
@@ -1367,6 +1573,25 @@ export function AgentTerminal({
|
|
|
1367
1573
|
(updatedMessage.inputTokens || 0) +
|
|
1368
1574
|
(updatedMessage.outputTokens || 0);
|
|
1369
1575
|
|
|
1576
|
+
// Update cost data if provided in the completed event
|
|
1577
|
+
if (data.inputTokenCost !== undefined) {
|
|
1578
|
+
updatedMessage.inputTokenCost = data.inputTokenCost;
|
|
1579
|
+
}
|
|
1580
|
+
if (data.outputTokenCost !== undefined) {
|
|
1581
|
+
updatedMessage.outputTokenCost =
|
|
1582
|
+
data.outputTokenCost;
|
|
1583
|
+
}
|
|
1584
|
+
if (
|
|
1585
|
+
data.cachedInputTokenCost !== undefined ||
|
|
1586
|
+
data.cachedTokenCost !== undefined
|
|
1587
|
+
) {
|
|
1588
|
+
updatedMessage.cachedInputTokenCost =
|
|
1589
|
+
data.cachedInputTokenCost ?? data.cachedTokenCost;
|
|
1590
|
+
}
|
|
1591
|
+
if (data.totalCost !== undefined) {
|
|
1592
|
+
updatedMessage.totalCost = data.totalCost;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1370
1595
|
// Handle content that might only be sent in the completed event
|
|
1371
1596
|
if (data.deltaContent && data.deltaContent.trim()) {
|
|
1372
1597
|
if (!data.isIncremental) {
|
|
@@ -1462,7 +1687,6 @@ export function AgentTerminal({
|
|
|
1462
1687
|
setError("Failed to connect to agent stream");
|
|
1463
1688
|
}
|
|
1464
1689
|
} finally {
|
|
1465
|
-
console.log("🔌 Stream connection finished, cleaning up");
|
|
1466
1690
|
setIsConnecting(false);
|
|
1467
1691
|
// Guard: clear waiting state if connection finished without content
|
|
1468
1692
|
setIsWaitingForResponse(false);
|
|
@@ -1479,7 +1703,6 @@ export function AgentTerminal({
|
|
|
1479
1703
|
|
|
1480
1704
|
// Check if we're already connected
|
|
1481
1705
|
if (abortControllerRef.current) {
|
|
1482
|
-
console.log("🔄 Already connected to stream, skipping reconnect");
|
|
1483
1706
|
return;
|
|
1484
1707
|
}
|
|
1485
1708
|
|
|
@@ -1491,10 +1714,7 @@ export function AgentTerminal({
|
|
|
1491
1714
|
);
|
|
1492
1715
|
|
|
1493
1716
|
if (!hasPending) {
|
|
1494
|
-
console.log("🔄 No pending approvals; reconnecting stream");
|
|
1495
1717
|
await connectToStream(currentAgent);
|
|
1496
|
-
} else {
|
|
1497
|
-
console.log("⏸️ Still have pending approvals, not reconnecting yet");
|
|
1498
1718
|
}
|
|
1499
1719
|
} catch (err) {
|
|
1500
1720
|
console.error("❌ Error attempting reconnect:", err);
|
|
@@ -1511,11 +1731,7 @@ export function AgentTerminal({
|
|
|
1511
1731
|
const approved: boolean = !!detail.approved;
|
|
1512
1732
|
if (!messageId || !toolCallId) return;
|
|
1513
1733
|
|
|
1514
|
-
|
|
1515
|
-
messageId,
|
|
1516
|
-
toolCallId,
|
|
1517
|
-
approved,
|
|
1518
|
-
});
|
|
1734
|
+
// Approval resolved; update local state
|
|
1519
1735
|
|
|
1520
1736
|
setMessages((prev) => {
|
|
1521
1737
|
const updated = prev.map((m) => {
|
|
@@ -1561,10 +1777,9 @@ export function AgentTerminal({
|
|
|
1561
1777
|
const loadAgent = useCallback(async () => {
|
|
1562
1778
|
try {
|
|
1563
1779
|
if (agentStub.status === "new") {
|
|
1564
|
-
console.log("✅ Setting up new agent", agentStub.id);
|
|
1565
1780
|
// Set agent ID immediately for new agents
|
|
1566
1781
|
(window as any).currentAgentId = agentStub.id;
|
|
1567
|
-
|
|
1782
|
+
// Set currentAgentId for new agent
|
|
1568
1783
|
// Derive initial profile from provided metadata if present
|
|
1569
1784
|
const initialProfileIdFromMeta = (() => {
|
|
1570
1785
|
try {
|
|
@@ -1772,10 +1987,6 @@ export function AgentTerminal({
|
|
|
1772
1987
|
|
|
1773
1988
|
// Set agent ID for existing agents too
|
|
1774
1989
|
(window as any).currentAgentId = agentData.id;
|
|
1775
|
-
console.log(
|
|
1776
|
-
"🔗 Setting currentAgentId for existing agent:",
|
|
1777
|
-
agentData.id,
|
|
1778
|
-
);
|
|
1779
1990
|
|
|
1780
1991
|
// Parse metadata from DB if present (do not seed for existing agents)
|
|
1781
1992
|
const parsedMeta: AgentMetadata | null = (() => {
|
|
@@ -1800,18 +2011,10 @@ export function AgentTerminal({
|
|
|
1800
2011
|
const isRunning =
|
|
1801
2012
|
agentData.status === "running" || (agentData.status as any) === 1;
|
|
1802
2013
|
if (isRunning) {
|
|
1803
|
-
console.log("🚀 Agent is running, connecting to stream...", {
|
|
1804
|
-
agentId: agentData.id,
|
|
1805
|
-
status: agentData.status,
|
|
1806
|
-
messageCount: agentData.messages?.length || 0,
|
|
1807
|
-
});
|
|
1808
2014
|
// Use setTimeout to ensure state updates are processed first
|
|
1809
2015
|
setTimeout(async () => {
|
|
1810
2016
|
// Check if we're already connecting to avoid duplicate connections
|
|
1811
2017
|
if (abortControllerRef.current) {
|
|
1812
|
-
console.log(
|
|
1813
|
-
"⚠️ loadAgent: Already connected to stream, skipping duplicate connection",
|
|
1814
|
-
);
|
|
1815
2018
|
return;
|
|
1816
2019
|
}
|
|
1817
2020
|
|
|
@@ -1828,9 +2031,6 @@ export function AgentTerminal({
|
|
|
1828
2031
|
),
|
|
1829
2032
|
);
|
|
1830
2033
|
if (hasPending) {
|
|
1831
|
-
console.log(
|
|
1832
|
-
"⏸️ loadAgent: Pending approvals detected, delaying stream reconnect",
|
|
1833
|
-
);
|
|
1834
2034
|
return;
|
|
1835
2035
|
}
|
|
1836
2036
|
} catch {}
|
|
@@ -1846,7 +2046,7 @@ export function AgentTerminal({
|
|
|
1846
2046
|
err?.message?.includes("404") ||
|
|
1847
2047
|
err?.message?.includes("not found")
|
|
1848
2048
|
) {
|
|
1849
|
-
|
|
2049
|
+
// Agent does not exist, treat as new
|
|
1850
2050
|
setAgent(undefined);
|
|
1851
2051
|
setMessages([]);
|
|
1852
2052
|
setError(null);
|
|
@@ -1924,7 +2124,6 @@ export function AgentTerminal({
|
|
|
1924
2124
|
useEffect(() => {
|
|
1925
2125
|
return () => {
|
|
1926
2126
|
if (abortControllerRef.current) {
|
|
1927
|
-
console.log("Cleaning up stream connection");
|
|
1928
2127
|
abortControllerRef.current.abort();
|
|
1929
2128
|
}
|
|
1930
2129
|
};
|
|
@@ -2107,7 +2306,7 @@ export function AgentTerminal({
|
|
|
2107
2306
|
seed: deterministicFlags.seed,
|
|
2108
2307
|
};
|
|
2109
2308
|
|
|
2110
|
-
|
|
2309
|
+
// Starting agent
|
|
2111
2310
|
|
|
2112
2311
|
// Set waiting state to show dancing dots immediately
|
|
2113
2312
|
setIsWaitingForResponse(true);
|
|
@@ -2149,7 +2348,14 @@ export function AgentTerminal({
|
|
|
2149
2348
|
};
|
|
2150
2349
|
|
|
2151
2350
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
2152
|
-
|
|
2351
|
+
// Submit only on plain Enter (no Ctrl/Meta/Shift/Alt)
|
|
2352
|
+
if (
|
|
2353
|
+
e.key === "Enter" &&
|
|
2354
|
+
!e.ctrlKey &&
|
|
2355
|
+
!e.metaKey &&
|
|
2356
|
+
!e.shiftKey &&
|
|
2357
|
+
!e.altKey
|
|
2358
|
+
) {
|
|
2153
2359
|
e.preventDefault();
|
|
2154
2360
|
handleSubmit();
|
|
2155
2361
|
}
|
|
@@ -2825,7 +3031,7 @@ export function AgentTerminal({
|
|
|
2825
3031
|
if (isLoading) {
|
|
2826
3032
|
return (
|
|
2827
3033
|
<div className="flex h-full items-center justify-center">
|
|
2828
|
-
<div className="flex items-center gap-2 text-
|
|
3034
|
+
<div className="flex items-center gap-2 text-xs text-gray-500">
|
|
2829
3035
|
<Loader2 className="h-4 w-4 animate-spin" strokeWidth={1} />
|
|
2830
3036
|
Loading agent...
|
|
2831
3037
|
</div>
|
|
@@ -3024,7 +3230,7 @@ export function AgentTerminal({
|
|
|
3024
3230
|
{renderContextInfoBar()}
|
|
3025
3231
|
|
|
3026
3232
|
{/* Todo List Panel */}
|
|
3027
|
-
<TodoListPanel messages={messages} />
|
|
3233
|
+
<TodoListPanel messages={messages} agentMetadata={agentMetadata} />
|
|
3028
3234
|
|
|
3029
3235
|
{/* Input */}
|
|
3030
3236
|
<div className="border-t border-gray-200 p-4">
|
|
@@ -3102,19 +3308,25 @@ export function AgentTerminal({
|
|
|
3102
3308
|
</TooltipTrigger>
|
|
3103
3309
|
<TooltipContent side="top" sideOffset={6}>
|
|
3104
3310
|
<div className="max-w-[320px] space-y-1">
|
|
3105
|
-
|
|
3106
|
-
<span className="font-semibold text-green-500">Ask</span>:
|
|
3107
|
-
access as configured by the profile (Ask Mode
|
|
3311
|
+
<div>
|
|
3312
|
+
<span className="font-semibold text-green-500">Ask</span>:
|
|
3313
|
+
Limited tool access as configured by the profile (Ask Mode
|
|
3314
|
+
Tools).
|
|
3108
3315
|
</div>
|
|
3109
3316
|
<div>
|
|
3110
|
-
<span className="font-semibold text-amber-500">
|
|
3111
|
-
|
|
3112
|
-
|
|
3317
|
+
<span className="font-semibold text-amber-500">
|
|
3318
|
+
Restricted
|
|
3319
|
+
</span>
|
|
3320
|
+
: Full tool access, but writes are limited to pages/items in
|
|
3321
|
+
the current context. Creating new items or updating existing
|
|
3322
|
+
items outside the current context requires explicit
|
|
3323
|
+
approval.
|
|
3113
3324
|
</div>
|
|
3114
3325
|
<div>
|
|
3115
|
-
<span className="font-semibold text-red-500">Agent</span>:
|
|
3116
|
-
access; can write across the site/project only
|
|
3117
|
-
|
|
3326
|
+
<span className="font-semibold text-red-500">Agent</span>:
|
|
3327
|
+
Full tool access; can write across the site/project only
|
|
3328
|
+
limited by user permissions.
|
|
3329
|
+
</div>
|
|
3118
3330
|
</div>
|
|
3119
3331
|
</TooltipContent>
|
|
3120
3332
|
</Tooltip>
|
|
@@ -3246,7 +3458,21 @@ export function AgentTerminal({
|
|
|
3246
3458
|
</div>
|
|
3247
3459
|
</div>
|
|
3248
3460
|
<div className="mt-1 flex items-center gap-2 text-[10px] text-gray-500">
|
|
3249
|
-
<AgentCostDisplay
|
|
3461
|
+
<AgentCostDisplay
|
|
3462
|
+
totalTokens={
|
|
3463
|
+
liveTotals
|
|
3464
|
+
? {
|
|
3465
|
+
input: liveTotals.input,
|
|
3466
|
+
output: liveTotals.output,
|
|
3467
|
+
cached: liveTotals.cached,
|
|
3468
|
+
inputCost: liveTotals.inputCost,
|
|
3469
|
+
outputCost: liveTotals.outputCost,
|
|
3470
|
+
cachedCost: liveTotals.cachedCost,
|
|
3471
|
+
totalCost: liveTotals.totalCost,
|
|
3472
|
+
}
|
|
3473
|
+
: totalTokens
|
|
3474
|
+
}
|
|
3475
|
+
/>
|
|
3250
3476
|
{(() => {
|
|
3251
3477
|
try {
|
|
3252
3478
|
const s = (window as any).__agentContextWindowStatus;
|
|
@@ -3255,7 +3481,7 @@ export function AgentTerminal({
|
|
|
3255
3481
|
typeof s.contextUsedPercent === "number"
|
|
3256
3482
|
? `${s.contextUsedPercent.toFixed(1)}%`
|
|
3257
3483
|
: undefined;
|
|
3258
|
-
|
|
3484
|
+
|
|
3259
3485
|
// Helper function to format tokens as "k"
|
|
3260
3486
|
const formatTokens = (tokens: number) => {
|
|
3261
3487
|
if (tokens >= 1000) {
|
|
@@ -3269,7 +3495,7 @@ export function AgentTerminal({
|
|
|
3269
3495
|
return (
|
|
3270
3496
|
<Tooltip>
|
|
3271
3497
|
<TooltipTrigger asChild>
|
|
3272
|
-
<div className="rounded border border-gray-200 bg-gray-50 px-2 py-0.5
|
|
3498
|
+
<div className="cursor-help rounded border border-gray-200 bg-gray-50 px-2 py-0.5">
|
|
3273
3499
|
Context: {pct}
|
|
3274
3500
|
</div>
|
|
3275
3501
|
</TooltipTrigger>
|