@alpaca-editor/core 1.0.4123 β 1.0.4128
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/AgentCostDisplay.js +3 -1
- package/dist/editor/ai/AgentCostDisplay.js.map +1 -1
- package/dist/editor/ai/AgentTerminal.js +349 -21
- package/dist/editor/ai/AgentTerminal.js.map +1 -1
- package/dist/editor/ai/ContextInfoBar.js +31 -10
- package/dist/editor/ai/ContextInfoBar.js.map +1 -1
- package/dist/editor/ai/ToolCallDisplay.d.ts +6 -0
- package/dist/editor/ai/ToolCallDisplay.js +118 -14
- package/dist/editor/ai/ToolCallDisplay.js.map +1 -1
- package/dist/editor/ai/types.d.ts +5 -0
- package/dist/editor/services/agentService.d.ts +16 -0
- package/dist/editor/services/agentService.js +39 -11
- package/dist/editor/services/agentService.js.map +1 -1
- package/dist/editor/ui/Splitter.js +72 -2
- package/dist/editor/ui/Splitter.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/styles.css +25 -0
- package/package.json +1 -1
- package/src/editor/ai/AgentCostDisplay.tsx +3 -1
- package/src/editor/ai/AgentTerminal.tsx +494 -19
- package/src/editor/ai/ContextInfoBar.tsx +83 -58
- package/src/editor/ai/ToolCallDisplay.tsx +193 -11
- package/src/editor/ai/types.ts +6 -0
- package/src/editor/services/agentService.ts +69 -11
- package/src/editor/ui/Splitter.tsx +88 -2
- package/src/revision.ts +2 -2
|
@@ -4,6 +4,7 @@ import React, {
|
|
|
4
4
|
useRef,
|
|
5
5
|
useCallback,
|
|
6
6
|
useLayoutEffect,
|
|
7
|
+
useMemo,
|
|
7
8
|
} from "react";
|
|
8
9
|
import {
|
|
9
10
|
Send,
|
|
@@ -14,6 +15,9 @@ import {
|
|
|
14
15
|
Square,
|
|
15
16
|
Mic,
|
|
16
17
|
MicOff,
|
|
18
|
+
ChevronDown,
|
|
19
|
+
ChevronUp,
|
|
20
|
+
ListTodo,
|
|
17
21
|
} from "lucide-react";
|
|
18
22
|
import { DancingDots } from "./DancingDots";
|
|
19
23
|
import {
|
|
@@ -78,6 +82,220 @@ const UserMessage = ({ message }: { message: AgentChatMessage }) => {
|
|
|
78
82
|
);
|
|
79
83
|
};
|
|
80
84
|
|
|
85
|
+
// Extract all todos from messages
|
|
86
|
+
interface TodoItem {
|
|
87
|
+
id?: string;
|
|
88
|
+
text: string;
|
|
89
|
+
done?: boolean;
|
|
90
|
+
note?: string;
|
|
91
|
+
messageId: string;
|
|
92
|
+
sourceTitle?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const extractTodosFromMessages = (messages: AgentChatMessage[]): TodoItem[] => {
|
|
96
|
+
const todos: TodoItem[] = [];
|
|
97
|
+
const fencedTodoToken = "```todo_list";
|
|
98
|
+
const plainTodoToken = "todo_list";
|
|
99
|
+
|
|
100
|
+
for (const message of messages) {
|
|
101
|
+
if (message.role !== "assistant" || !message.content) continue;
|
|
102
|
+
|
|
103
|
+
const content = message.content;
|
|
104
|
+
let cursor = 0;
|
|
105
|
+
|
|
106
|
+
while (cursor < content.length) {
|
|
107
|
+
const nextFenced = content.indexOf(fencedTodoToken, cursor);
|
|
108
|
+
const nextPlain = content.indexOf(plainTodoToken, cursor);
|
|
109
|
+
|
|
110
|
+
let todoStart = -1;
|
|
111
|
+
let isFenced = false;
|
|
112
|
+
|
|
113
|
+
if (nextFenced !== -1 && (nextPlain === -1 || nextFenced < nextPlain)) {
|
|
114
|
+
todoStart = nextFenced;
|
|
115
|
+
isFenced = true;
|
|
116
|
+
} else if (nextPlain !== -1) {
|
|
117
|
+
// Check if it's at line start
|
|
118
|
+
const before = nextPlain > 0 ? content[nextPlain - 1] : "\n";
|
|
119
|
+
if (before === "\n" || before === "\r" || nextPlain === 0) {
|
|
120
|
+
todoStart = nextPlain;
|
|
121
|
+
isFenced = false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (todoStart === -1) break;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
let jsonText = "";
|
|
129
|
+
if (isFenced) {
|
|
130
|
+
const afterToken = todoStart + fencedTodoToken.length;
|
|
131
|
+
const closePos = content.indexOf("```", afterToken);
|
|
132
|
+
if (closePos === -1) break;
|
|
133
|
+
jsonText = content.slice(afterToken, closePos).trim();
|
|
134
|
+
cursor = closePos + 3;
|
|
135
|
+
} else {
|
|
136
|
+
const afterToken = todoStart + plainTodoToken.length;
|
|
137
|
+
const braceStart = content.indexOf("{", afterToken);
|
|
138
|
+
if (braceStart === -1) break;
|
|
139
|
+
|
|
140
|
+
let depth = 0;
|
|
141
|
+
let braceEnd = -1;
|
|
142
|
+
for (let i = braceStart; i < content.length; i++) {
|
|
143
|
+
if (content[i] === "{") depth++;
|
|
144
|
+
if (content[i] === "}") {
|
|
145
|
+
depth--;
|
|
146
|
+
if (depth === 0) {
|
|
147
|
+
braceEnd = i;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (braceEnd === -1) break;
|
|
153
|
+
jsonText = content.slice(braceStart, braceEnd + 1).trim();
|
|
154
|
+
cursor = braceEnd + 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const parsed = JSON.parse(jsonText);
|
|
158
|
+
const todoItems = Array.isArray(parsed) ? parsed : parsed?.items || [];
|
|
159
|
+
const title = Array.isArray(parsed) ? undefined : parsed?.title;
|
|
160
|
+
|
|
161
|
+
todoItems.forEach((item: any) => {
|
|
162
|
+
if (!item) return;
|
|
163
|
+
const text =
|
|
164
|
+
item.text || item.label || String(item.task || item.title || "");
|
|
165
|
+
if (!text) return;
|
|
166
|
+
todos.push({
|
|
167
|
+
id: item.id,
|
|
168
|
+
text,
|
|
169
|
+
done: !!(item.done ?? item.completed ?? item.checked),
|
|
170
|
+
note: item.note || item.description,
|
|
171
|
+
messageId: message.id,
|
|
172
|
+
sourceTitle: title,
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
} catch (e) {
|
|
176
|
+
cursor++;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return todos;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// TodoListPanel component
|
|
186
|
+
const TodoListPanel = ({ messages }: { messages: AgentChatMessage[] }) => {
|
|
187
|
+
const [isExpanded, setIsExpanded] = useState(true);
|
|
188
|
+
const todos = useMemo(() => extractTodosFromMessages(messages), [messages]);
|
|
189
|
+
|
|
190
|
+
// Check if there's an active streaming message with todo content
|
|
191
|
+
const isUpdating = useMemo(() => {
|
|
192
|
+
return messages.some((msg) => {
|
|
193
|
+
if (msg.role !== "assistant" || msg.isCompleted) return false;
|
|
194
|
+
const content = msg.content || "";
|
|
195
|
+
return content.includes("```todo_list") || content.includes("todo_list");
|
|
196
|
+
});
|
|
197
|
+
}, [messages]);
|
|
198
|
+
|
|
199
|
+
if (todos.length === 0 && !isUpdating) return null;
|
|
200
|
+
|
|
201
|
+
const completedCount = todos.filter((t) => t.done).length;
|
|
202
|
+
const totalCount = todos.length;
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div className="border-t border-gray-200 bg-gray-50">
|
|
206
|
+
<button
|
|
207
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
208
|
+
className="flex w-full cursor-pointer items-center justify-between px-4 py-2 text-left transition-colors hover:bg-gray-100"
|
|
209
|
+
>
|
|
210
|
+
<div className="flex items-center gap-2">
|
|
211
|
+
<ListTodo className="h-4 w-4 text-gray-500" strokeWidth={1} />
|
|
212
|
+
<span className="text-xs font-medium text-gray-700">Todo List</span>
|
|
213
|
+
{isUpdating ? (
|
|
214
|
+
<span className="flex items-center gap-1 text-xs text-blue-600">
|
|
215
|
+
<Loader2 className="h-3 w-3 animate-spin" strokeWidth={1} />
|
|
216
|
+
Updating...
|
|
217
|
+
</span>
|
|
218
|
+
) : (
|
|
219
|
+
<span className="text-xs text-gray-500">
|
|
220
|
+
{completedCount}/{totalCount} completed
|
|
221
|
+
</span>
|
|
222
|
+
)}
|
|
223
|
+
</div>
|
|
224
|
+
{isExpanded ? (
|
|
225
|
+
<ChevronUp className="h-4 w-4 text-gray-500" strokeWidth={1} />
|
|
226
|
+
) : (
|
|
227
|
+
<ChevronDown className="h-4 w-4 text-gray-500" strokeWidth={1} />
|
|
228
|
+
)}
|
|
229
|
+
</button>
|
|
230
|
+
{isExpanded && (
|
|
231
|
+
<div className="max-h-64 overflow-y-auto px-4 pb-3">
|
|
232
|
+
{isUpdating && todos.length === 0 ? (
|
|
233
|
+
<div className="flex items-center justify-center gap-2 py-4 text-xs text-gray-500">
|
|
234
|
+
<Loader2 className="h-4 w-4 animate-spin" strokeWidth={1} />
|
|
235
|
+
<span>Loading todo list...</span>
|
|
236
|
+
</div>
|
|
237
|
+
) : (
|
|
238
|
+
<>
|
|
239
|
+
<div className="space-y-1.5">
|
|
240
|
+
{todos.map((todo, idx) => (
|
|
241
|
+
<div
|
|
242
|
+
key={todo.id || `${todo.messageId}-${idx}`}
|
|
243
|
+
className="flex items-start gap-2 rounded bg-white p-2 text-xs"
|
|
244
|
+
>
|
|
245
|
+
<div className="flex-shrink-0 pt-0.5">
|
|
246
|
+
{todo.done ? (
|
|
247
|
+
<div className="flex h-4 w-4 items-center justify-center rounded border-2 border-green-500 bg-green-500">
|
|
248
|
+
<svg
|
|
249
|
+
className="h-3 w-3 text-white"
|
|
250
|
+
fill="none"
|
|
251
|
+
strokeWidth={2}
|
|
252
|
+
stroke="currentColor"
|
|
253
|
+
viewBox="0 0 24 24"
|
|
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}
|
|
275
|
+
</div>
|
|
276
|
+
{todo.note && (
|
|
277
|
+
<div className="mt-0.5 text-xs text-gray-500">
|
|
278
|
+
{todo.note}
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
</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
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
</>
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
};
|
|
298
|
+
|
|
81
299
|
// Group consecutive assistant messages together for bundling
|
|
82
300
|
interface MessageGroup {
|
|
83
301
|
type: "user" | "assistant-group";
|
|
@@ -172,6 +390,8 @@ const convertAgentMessagesToAiFormat = (
|
|
|
172
390
|
result: toolCall.functionResult,
|
|
173
391
|
error: toolCall.functionError,
|
|
174
392
|
},
|
|
393
|
+
// Pass through approval info if present on the tool call
|
|
394
|
+
requiresApproval: (toolCall as any).requiresApproval,
|
|
175
395
|
}))
|
|
176
396
|
: [],
|
|
177
397
|
};
|
|
@@ -697,6 +917,7 @@ export function AgentTerminal({
|
|
|
697
917
|
isCompleted: false,
|
|
698
918
|
responseTimeMs: message.data.responseTimeMs,
|
|
699
919
|
createdDate: new Date().toISOString(),
|
|
920
|
+
requiresApproval: message.data?.requiresApproval,
|
|
700
921
|
};
|
|
701
922
|
|
|
702
923
|
// Check for duplicates using the current messages ref
|
|
@@ -838,6 +1059,18 @@ export function AgentTerminal({
|
|
|
838
1059
|
messagesRef.current = updated;
|
|
839
1060
|
return updated;
|
|
840
1061
|
});
|
|
1062
|
+
|
|
1063
|
+
// Dispatch a local event so the UI can attempt reconnect when approvals get resolved while paused
|
|
1064
|
+
try {
|
|
1065
|
+
const ev = new CustomEvent("agent:toolApprovalResolved", {
|
|
1066
|
+
detail: {
|
|
1067
|
+
messageId: resultMessageId,
|
|
1068
|
+
toolCallId: resultToolCallId,
|
|
1069
|
+
approved: !(message.data?.functionError || message.data?.error),
|
|
1070
|
+
},
|
|
1071
|
+
} as any);
|
|
1072
|
+
window.dispatchEvent(ev);
|
|
1073
|
+
} catch {}
|
|
841
1074
|
// Tool result activity; reset idle timer
|
|
842
1075
|
resetDotsTimer();
|
|
843
1076
|
},
|
|
@@ -863,9 +1096,21 @@ export function AgentTerminal({
|
|
|
863
1096
|
|
|
864
1097
|
console.log("π connectToStream: Starting stream connection");
|
|
865
1098
|
|
|
1099
|
+
// Expose agent id globally for approval actions
|
|
1100
|
+
(window as any).currentAgentId = currentAgent.id;
|
|
1101
|
+
console.log("π Setting currentAgentId:", currentAgent.id);
|
|
1102
|
+
|
|
1103
|
+
console.log(
|
|
1104
|
+
"π Attempting to connect to agent stream for:",
|
|
1105
|
+
currentAgent.id,
|
|
1106
|
+
);
|
|
866
1107
|
await connectToAgentStream(
|
|
867
1108
|
currentAgent.id,
|
|
868
1109
|
(message: AgentStreamMessage) => {
|
|
1110
|
+
console.log("π¨ Received stream message:", {
|
|
1111
|
+
type: message.type,
|
|
1112
|
+
data: message.data,
|
|
1113
|
+
});
|
|
869
1114
|
switch (message.type) {
|
|
870
1115
|
case "contentChunk":
|
|
871
1116
|
handleContentChunk(message, currentAgent);
|
|
@@ -882,6 +1127,53 @@ export function AgentTerminal({
|
|
|
882
1127
|
case "statusUpdate":
|
|
883
1128
|
try {
|
|
884
1129
|
const kind = (message as any)?.data?.kind;
|
|
1130
|
+
console.log("π‘ Received status update:", {
|
|
1131
|
+
kind,
|
|
1132
|
+
data: (message as any).data,
|
|
1133
|
+
});
|
|
1134
|
+
if (kind === "toolApprovalsRequired") {
|
|
1135
|
+
const data = (message as any).data || {};
|
|
1136
|
+
const msgId: string | undefined = data.messageId;
|
|
1137
|
+
const ids: string[] = data.toolCallIds || [];
|
|
1138
|
+
console.log(
|
|
1139
|
+
"βΈοΈ Approvals required; pausing stream until approval:",
|
|
1140
|
+
{ msgId, ids },
|
|
1141
|
+
);
|
|
1142
|
+
|
|
1143
|
+
// Annotate tool calls with a temporary pending marker so UI can reflect paused state on reload
|
|
1144
|
+
if (msgId && Array.isArray(ids) && ids.length > 0) {
|
|
1145
|
+
setMessages((prev) => {
|
|
1146
|
+
const updated = prev.map((m) => {
|
|
1147
|
+
if (m.id !== msgId) return m;
|
|
1148
|
+
const existingToolCalls = m.toolCalls || [];
|
|
1149
|
+
const updatedToolCalls = existingToolCalls.map(
|
|
1150
|
+
(tc) => {
|
|
1151
|
+
if (!ids.includes(tc.toolCallId)) return tc;
|
|
1152
|
+
const fn = tc.functionName || "";
|
|
1153
|
+
return {
|
|
1154
|
+
...tc,
|
|
1155
|
+
functionName: fn.includes("(pending approval)")
|
|
1156
|
+
? fn
|
|
1157
|
+
: fn + " (pending approval)",
|
|
1158
|
+
};
|
|
1159
|
+
},
|
|
1160
|
+
);
|
|
1161
|
+
return { ...m, toolCalls: updatedToolCalls };
|
|
1162
|
+
});
|
|
1163
|
+
messagesRef.current = updated;
|
|
1164
|
+
return updated;
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Proactively stop the current stream so that the page can be reloaded safely or stay idle until approval
|
|
1169
|
+
try {
|
|
1170
|
+
abortControllerRef.current?.abort();
|
|
1171
|
+
abortControllerRef.current = null;
|
|
1172
|
+
setIsConnecting(false);
|
|
1173
|
+
setIsWaitingForResponse(false);
|
|
1174
|
+
} catch {}
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
885
1177
|
if (kind === "contextWindow") {
|
|
886
1178
|
const data = (message as any).data || {};
|
|
887
1179
|
// Store last context window status in a ref so we can render it below
|
|
@@ -902,8 +1194,10 @@ export function AgentTerminal({
|
|
|
902
1194
|
// Merge incoming context into local metadata
|
|
903
1195
|
setAgentMetadata((prev) => {
|
|
904
1196
|
const current = (prev || {}) as AgentMetadata;
|
|
1197
|
+
// Exclude top-level context to avoid duplicate keys when spreading
|
|
1198
|
+
const { context: _, ...currentWithoutContext } = current;
|
|
905
1199
|
const next: AgentMetadata = {
|
|
906
|
-
...
|
|
1200
|
+
...currentWithoutContext,
|
|
907
1201
|
additionalData: {
|
|
908
1202
|
...(current.additionalData || {}),
|
|
909
1203
|
context: nextContext,
|
|
@@ -942,6 +1236,58 @@ export function AgentTerminal({
|
|
|
942
1236
|
return prevAgent;
|
|
943
1237
|
}
|
|
944
1238
|
});
|
|
1239
|
+
} else if (
|
|
1240
|
+
kind === "toolApprovalGranted" ||
|
|
1241
|
+
kind === "toolApprovalRejected"
|
|
1242
|
+
) {
|
|
1243
|
+
const data = (message as any).data || {};
|
|
1244
|
+
const toolCallId: string | undefined = data.toolCallId;
|
|
1245
|
+
const msgId: string | undefined = data.messageId;
|
|
1246
|
+
console.log("π§ Processing tool approval:", {
|
|
1247
|
+
kind,
|
|
1248
|
+
toolCallId,
|
|
1249
|
+
msgId,
|
|
1250
|
+
data,
|
|
1251
|
+
});
|
|
1252
|
+
if (toolCallId && msgId) {
|
|
1253
|
+
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
|
+
const updated = prev.map((m) => {
|
|
1262
|
+
if (m.id !== msgId) return m;
|
|
1263
|
+
const existingToolCalls = m.toolCalls || [];
|
|
1264
|
+
const updatedToolCalls = existingToolCalls.map(
|
|
1265
|
+
(tc) => {
|
|
1266
|
+
if (tc.toolCallId !== toolCallId) return tc;
|
|
1267
|
+
const suffix =
|
|
1268
|
+
kind === "toolApprovalGranted"
|
|
1269
|
+
? " (approved)"
|
|
1270
|
+
: " (rejected)";
|
|
1271
|
+
const newFunctionName =
|
|
1272
|
+
(tc.functionName || "") + suffix;
|
|
1273
|
+
console.log("π·οΈ Updating function name:", {
|
|
1274
|
+
toolCallId,
|
|
1275
|
+
oldName: tc.functionName,
|
|
1276
|
+
newName: newFunctionName,
|
|
1277
|
+
});
|
|
1278
|
+
return {
|
|
1279
|
+
...tc,
|
|
1280
|
+
functionName: newFunctionName,
|
|
1281
|
+
};
|
|
1282
|
+
},
|
|
1283
|
+
);
|
|
1284
|
+
return { ...m, toolCalls: updatedToolCalls };
|
|
1285
|
+
});
|
|
1286
|
+
messagesRef.current = updated;
|
|
1287
|
+
return updated;
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
break;
|
|
945
1291
|
}
|
|
946
1292
|
} catch {}
|
|
947
1293
|
break;
|
|
@@ -1116,6 +1462,7 @@ export function AgentTerminal({
|
|
|
1116
1462
|
setError("Failed to connect to agent stream");
|
|
1117
1463
|
}
|
|
1118
1464
|
} finally {
|
|
1465
|
+
console.log("π Stream connection finished, cleaning up");
|
|
1119
1466
|
setIsConnecting(false);
|
|
1120
1467
|
// Guard: clear waiting state if connection finished without content
|
|
1121
1468
|
setIsWaitingForResponse(false);
|
|
@@ -1124,11 +1471,100 @@ export function AgentTerminal({
|
|
|
1124
1471
|
[agent?.id, handleContentChunk, handleToolCall, handleToolResult],
|
|
1125
1472
|
);
|
|
1126
1473
|
|
|
1474
|
+
// Attempt to reconnect stream when all pending approvals are resolved
|
|
1475
|
+
const attemptReconnectIfNoPending = useCallback(async () => {
|
|
1476
|
+
try {
|
|
1477
|
+
const currentAgent = agent;
|
|
1478
|
+
if (!currentAgent) return;
|
|
1479
|
+
|
|
1480
|
+
// Check if we're already connected
|
|
1481
|
+
if (abortControllerRef.current) {
|
|
1482
|
+
console.log("π Already connected to stream, skipping reconnect");
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const msgs = messagesRef.current || [];
|
|
1487
|
+
const hasPending = msgs.some((m) =>
|
|
1488
|
+
(m.toolCalls || []).some((tc) =>
|
|
1489
|
+
(tc.functionName || "").includes("(pending approval)"),
|
|
1490
|
+
),
|
|
1491
|
+
);
|
|
1492
|
+
|
|
1493
|
+
if (!hasPending) {
|
|
1494
|
+
console.log("π No pending approvals; reconnecting stream");
|
|
1495
|
+
await connectToStream(currentAgent);
|
|
1496
|
+
} else {
|
|
1497
|
+
console.log("βΈοΈ Still have pending approvals, not reconnecting yet");
|
|
1498
|
+
}
|
|
1499
|
+
} catch (err) {
|
|
1500
|
+
console.error("β Error attempting reconnect:", err);
|
|
1501
|
+
}
|
|
1502
|
+
}, [agent, connectToStream]);
|
|
1503
|
+
|
|
1504
|
+
// Listen for local approval resolution (when stream is paused) to update UI and reconnect
|
|
1505
|
+
useEffect(() => {
|
|
1506
|
+
const onApprovalResolved = (ev: any) => {
|
|
1507
|
+
try {
|
|
1508
|
+
const detail = ev?.detail || {};
|
|
1509
|
+
const messageId: string | undefined = detail.messageId;
|
|
1510
|
+
const toolCallId: string | undefined = detail.toolCallId;
|
|
1511
|
+
const approved: boolean = !!detail.approved;
|
|
1512
|
+
if (!messageId || !toolCallId) return;
|
|
1513
|
+
|
|
1514
|
+
console.log("π Approval resolved:", {
|
|
1515
|
+
messageId,
|
|
1516
|
+
toolCallId,
|
|
1517
|
+
approved,
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
setMessages((prev) => {
|
|
1521
|
+
const updated = prev.map((m) => {
|
|
1522
|
+
if (m.id !== messageId) return m;
|
|
1523
|
+
const updatedToolCalls = (m.toolCalls || []).map((tc) => {
|
|
1524
|
+
if (tc.toolCallId !== toolCallId) return tc;
|
|
1525
|
+
const base = (tc.functionName || "")
|
|
1526
|
+
.replace(" (pending approval)", "")
|
|
1527
|
+
.replace(" (approved)", "")
|
|
1528
|
+
.replace(" (rejected)", "");
|
|
1529
|
+
return {
|
|
1530
|
+
...tc,
|
|
1531
|
+
functionName: base + (approved ? " (approved)" : " (rejected)"),
|
|
1532
|
+
};
|
|
1533
|
+
});
|
|
1534
|
+
return { ...m, toolCalls: updatedToolCalls };
|
|
1535
|
+
});
|
|
1536
|
+
messagesRef.current = updated;
|
|
1537
|
+
return updated;
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
// Try to reconnect if no more pending approvals remain
|
|
1541
|
+
setTimeout(() => {
|
|
1542
|
+
attemptReconnectIfNoPending();
|
|
1543
|
+
}, 100);
|
|
1544
|
+
} catch (err) {
|
|
1545
|
+
console.error("β Error handling approval resolution:", err);
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1549
|
+
window.addEventListener(
|
|
1550
|
+
"agent:toolApprovalResolved",
|
|
1551
|
+
onApprovalResolved as EventListener,
|
|
1552
|
+
);
|
|
1553
|
+
return () =>
|
|
1554
|
+
window.removeEventListener(
|
|
1555
|
+
"agent:toolApprovalResolved",
|
|
1556
|
+
onApprovalResolved as EventListener,
|
|
1557
|
+
);
|
|
1558
|
+
}, [attemptReconnectIfNoPending]);
|
|
1559
|
+
|
|
1127
1560
|
// Load agent data and messages
|
|
1128
1561
|
const loadAgent = useCallback(async () => {
|
|
1129
1562
|
try {
|
|
1130
1563
|
if (agentStub.status === "new") {
|
|
1131
|
-
console.log("β
Setting up new agent");
|
|
1564
|
+
console.log("β
Setting up new agent", agentStub.id);
|
|
1565
|
+
// Set agent ID immediately for new agents
|
|
1566
|
+
(window as any).currentAgentId = agentStub.id;
|
|
1567
|
+
console.log("π Setting currentAgentId for new agent:", agentStub.id);
|
|
1132
1568
|
// Derive initial profile from provided metadata if present
|
|
1133
1569
|
const initialProfileIdFromMeta = (() => {
|
|
1134
1570
|
try {
|
|
@@ -1334,12 +1770,24 @@ export function AgentTerminal({
|
|
|
1334
1770
|
setAgent(agentData);
|
|
1335
1771
|
setMessages(agentData.messages || []);
|
|
1336
1772
|
|
|
1773
|
+
// Set agent ID for existing agents too
|
|
1774
|
+
(window as any).currentAgentId = agentData.id;
|
|
1775
|
+
console.log(
|
|
1776
|
+
"π Setting currentAgentId for existing agent:",
|
|
1777
|
+
agentData.id,
|
|
1778
|
+
);
|
|
1779
|
+
|
|
1337
1780
|
// Parse metadata from DB if present (do not seed for existing agents)
|
|
1338
1781
|
const parsedMeta: AgentMetadata | null = (() => {
|
|
1339
1782
|
try {
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1783
|
+
if (!agentData.metadata) return null;
|
|
1784
|
+
const meta = JSON.parse(agentData.metadata) as AgentMetadata;
|
|
1785
|
+
// Clean up: remove top-level context if present (should only be in additionalData)
|
|
1786
|
+
if (meta && meta.context) {
|
|
1787
|
+
const { context: _, ...cleanMeta } = meta;
|
|
1788
|
+
return cleanMeta as AgentMetadata;
|
|
1789
|
+
}
|
|
1790
|
+
return meta;
|
|
1343
1791
|
} catch {
|
|
1344
1792
|
return null;
|
|
1345
1793
|
}
|
|
@@ -1370,6 +1818,23 @@ export function AgentTerminal({
|
|
|
1370
1818
|
// Reset streaming state for reconnection
|
|
1371
1819
|
shouldCreateNewMessage.current = false;
|
|
1372
1820
|
|
|
1821
|
+
// If there are pending approvals in current messages, skip reconnect for now
|
|
1822
|
+
try {
|
|
1823
|
+
const hasPending = (agentData.messages || []).some((m: any) =>
|
|
1824
|
+
(m.toolCalls || []).some(
|
|
1825
|
+
(tc: any) =>
|
|
1826
|
+
typeof tc?.functionName === "string" &&
|
|
1827
|
+
tc.functionName.includes("(pending approval)"),
|
|
1828
|
+
),
|
|
1829
|
+
);
|
|
1830
|
+
if (hasPending) {
|
|
1831
|
+
console.log(
|
|
1832
|
+
"βΈοΈ loadAgent: Pending approvals detected, delaying stream reconnect",
|
|
1833
|
+
);
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
} catch {}
|
|
1837
|
+
|
|
1373
1838
|
// Use the existing connectToStream function with the loaded agent data
|
|
1374
1839
|
await connectToStream(agentData);
|
|
1375
1840
|
}, 100);
|
|
@@ -1530,13 +1995,6 @@ export function AgentTerminal({
|
|
|
1530
1995
|
}
|
|
1531
1996
|
}, [messages, scrollToBottom, shouldAutoScroll]);
|
|
1532
1997
|
|
|
1533
|
-
// Re-apply bottom alignment when loading dots appear/disappear, as it changes the content height
|
|
1534
|
-
useEffect(() => {
|
|
1535
|
-
if (shouldAutoScroll) {
|
|
1536
|
-
scrollToBottom();
|
|
1537
|
-
}
|
|
1538
|
-
}, [showDots, shouldAutoScroll, scrollToBottom]);
|
|
1539
|
-
|
|
1540
1998
|
// Persist any pending settings (mode/model) once an agent exists server-side
|
|
1541
1999
|
const persistPendingSettingsIfNeeded = useCallback(async () => {
|
|
1542
2000
|
try {
|
|
@@ -1857,8 +2315,10 @@ export function AgentTerminal({
|
|
|
1857
2315
|
) => {
|
|
1858
2316
|
if (!agent?.id) return;
|
|
1859
2317
|
const current = agentMetadata || {};
|
|
2318
|
+
// Exclude top-level context to avoid duplicate keys when spreading
|
|
2319
|
+
const { context: _, ...currentWithoutContext } = current;
|
|
1860
2320
|
const next: AgentMetadata = {
|
|
1861
|
-
...
|
|
2321
|
+
...currentWithoutContext,
|
|
1862
2322
|
additionalData: {
|
|
1863
2323
|
...(current.additionalData || {}),
|
|
1864
2324
|
context: {
|
|
@@ -1940,8 +2400,10 @@ export function AgentTerminal({
|
|
|
1940
2400
|
return; // Page already exists
|
|
1941
2401
|
}
|
|
1942
2402
|
|
|
2403
|
+
// Exclude top-level context to avoid duplicate keys when spreading
|
|
2404
|
+
const { context: _, ...currentWithoutContext } = current;
|
|
1943
2405
|
const next: AgentMetadata = {
|
|
1944
|
-
...
|
|
2406
|
+
...currentWithoutContext,
|
|
1945
2407
|
additionalData: {
|
|
1946
2408
|
...(current.additionalData || {}),
|
|
1947
2409
|
context: {
|
|
@@ -1984,8 +2446,10 @@ export function AgentTerminal({
|
|
|
1984
2446
|
|
|
1985
2447
|
if (newComponentIds.length === 0) return; // No new components to add
|
|
1986
2448
|
|
|
2449
|
+
// Exclude top-level context to avoid duplicate keys when spreading
|
|
2450
|
+
const { context: _, ...currentWithoutContext } = current;
|
|
1987
2451
|
const next: AgentMetadata = {
|
|
1988
|
-
...
|
|
2452
|
+
...currentWithoutContext,
|
|
1989
2453
|
additionalData: {
|
|
1990
2454
|
...(current.additionalData || {}),
|
|
1991
2455
|
context: {
|
|
@@ -2025,8 +2489,10 @@ export function AgentTerminal({
|
|
|
2025
2489
|
const newComponentIds = ids.filter((id) => !!id && !existingIds.has(id));
|
|
2026
2490
|
if (newComponentIds.length === 0) return;
|
|
2027
2491
|
|
|
2492
|
+
// Exclude top-level context to avoid duplicate keys when spreading
|
|
2493
|
+
const { context: _, ...currentWithoutContext } = current;
|
|
2028
2494
|
const next: AgentMetadata = {
|
|
2029
|
-
...
|
|
2495
|
+
...currentWithoutContext,
|
|
2030
2496
|
additionalData: {
|
|
2031
2497
|
...(current.additionalData || {}),
|
|
2032
2498
|
context: {
|
|
@@ -2077,8 +2543,10 @@ export function AgentTerminal({
|
|
|
2077
2543
|
|
|
2078
2544
|
if (pagesToAdd.length === 0) return;
|
|
2079
2545
|
|
|
2546
|
+
// Exclude top-level context to avoid duplicate keys when spreading
|
|
2547
|
+
const { context: _, ...currentWithoutContext } = current;
|
|
2080
2548
|
const next: AgentMetadata = {
|
|
2081
|
-
...
|
|
2549
|
+
...currentWithoutContext,
|
|
2082
2550
|
additionalData: {
|
|
2083
2551
|
...(current.additionalData || {}),
|
|
2084
2552
|
context: {
|
|
@@ -2188,8 +2656,10 @@ export function AgentTerminal({
|
|
|
2188
2656
|
})();
|
|
2189
2657
|
|
|
2190
2658
|
const current = agentMetadata || {};
|
|
2659
|
+
// Exclude top-level context to avoid duplicate keys when spreading
|
|
2660
|
+
const { context: _, ...currentWithoutContext } = current;
|
|
2191
2661
|
const next: AgentMetadata = {
|
|
2192
|
-
...
|
|
2662
|
+
...currentWithoutContext,
|
|
2193
2663
|
additionalData: {
|
|
2194
2664
|
...(current.additionalData || {}),
|
|
2195
2665
|
context: {
|
|
@@ -2544,13 +3014,18 @@ export function AgentTerminal({
|
|
|
2544
3014
|
})}
|
|
2545
3015
|
</div>
|
|
2546
3016
|
|
|
2547
|
-
{showDots
|
|
3017
|
+
<div className={showDots ? "visible" : "invisible"}>
|
|
3018
|
+
<DancingDots />
|
|
3019
|
+
</div>
|
|
2548
3020
|
<div ref={messagesEndRef} />
|
|
2549
3021
|
</div>
|
|
2550
3022
|
|
|
2551
3023
|
{/* Context Info Bar */}
|
|
2552
3024
|
{renderContextInfoBar()}
|
|
2553
3025
|
|
|
3026
|
+
{/* Todo List Panel */}
|
|
3027
|
+
<TodoListPanel messages={messages} />
|
|
3028
|
+
|
|
2554
3029
|
{/* Input */}
|
|
2555
3030
|
<div className="border-t border-gray-200 p-4">
|
|
2556
3031
|
<div className="flex items-stretch gap-2">
|