@alpaca-editor/core 1.0.4049 → 1.0.4053

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.
Files changed (69) hide show
  1. package/dist/components/ui/textarea.js +1 -1
  2. package/dist/components/ui/textarea.js.map +1 -1
  3. package/dist/editor/Terminal.js +3 -3
  4. package/dist/editor/Terminal.js.map +1 -1
  5. package/dist/editor/ai/AgentCostDisplay.js +2 -2
  6. package/dist/editor/ai/AgentCostDisplay.js.map +1 -1
  7. package/dist/editor/ai/AgentHistory.d.ts +4 -4
  8. package/dist/editor/ai/AgentHistory.js +1 -1
  9. package/dist/editor/ai/AgentHistory.js.map +1 -1
  10. package/dist/editor/ai/AgentTerminal.d.ts +4 -0
  11. package/dist/editor/ai/AgentTerminal.js +753 -0
  12. package/dist/editor/ai/AgentTerminal.js.map +1 -0
  13. package/dist/editor/ai/Agents.d.ts +1 -3
  14. package/dist/editor/ai/Agents.js +213 -353
  15. package/dist/editor/ai/Agents.js.map +1 -1
  16. package/dist/editor/ai/AiPromptPopover.js +2 -2
  17. package/dist/editor/ai/AiPromptPopover.js.map +1 -1
  18. package/dist/editor/ai/AiResponseMessage.d.ts +0 -1
  19. package/dist/editor/ai/AiResponseMessage.js +23 -143
  20. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  21. package/dist/editor/ai/AiTerminal.d.ts +5 -23
  22. package/dist/editor/ai/AiTerminal.js +81 -824
  23. package/dist/editor/ai/AiTerminal.js.map +1 -1
  24. package/dist/editor/ai/DancingDots.d.ts +1 -0
  25. package/dist/editor/ai/DancingDots.js +6 -0
  26. package/dist/editor/ai/DancingDots.js.map +1 -0
  27. package/dist/editor/ai/ToolCallDisplay.d.ts +37 -0
  28. package/dist/editor/ai/ToolCallDisplay.js +154 -0
  29. package/dist/editor/ai/ToolCallDisplay.js.map +1 -0
  30. package/dist/editor/client/EditorClient.js +5 -1
  31. package/dist/editor/client/EditorClient.js.map +1 -1
  32. package/dist/editor/services/agentService.d.ts +23 -30
  33. package/dist/editor/services/agentService.js +62 -124
  34. package/dist/editor/services/agentService.js.map +1 -1
  35. package/dist/editor/sidebar/GraphQL.js +1 -0
  36. package/dist/editor/sidebar/GraphQL.js.map +1 -1
  37. package/dist/editor/sidebar/ViewSelector.js +8 -6
  38. package/dist/editor/sidebar/ViewSelector.js.map +1 -1
  39. package/dist/editor/ui/Section.js +4 -3
  40. package/dist/editor/ui/Section.js.map +1 -1
  41. package/dist/editor/utils.d.ts +4 -0
  42. package/dist/editor/utils.js +23 -0
  43. package/dist/editor/utils.js.map +1 -1
  44. package/dist/revision.d.ts +2 -2
  45. package/dist/revision.js +2 -2
  46. package/dist/styles.css +18 -33
  47. package/package.json +1 -1
  48. package/src/components/ui/textarea.tsx +1 -1
  49. package/src/editor/Terminal.tsx +4 -4
  50. package/src/editor/ai/AgentCostDisplay.tsx +7 -11
  51. package/src/editor/ai/AgentHistory.tsx +7 -9
  52. package/src/editor/ai/AgentTerminal.tsx +1094 -0
  53. package/src/editor/ai/Agents.tsx +340 -477
  54. package/src/editor/ai/AiPromptPopover.tsx +2 -2
  55. package/src/editor/ai/AiResponseMessage.tsx +85 -366
  56. package/src/editor/ai/AiTerminal.tsx +142 -1213
  57. package/src/editor/ai/DancingDots.tsx +14 -0
  58. package/src/editor/ai/ToolCallDisplay.tsx +363 -0
  59. package/src/editor/client/EditorClient.tsx +6 -1
  60. package/src/editor/services/agentService.ts +89 -162
  61. package/src/editor/sidebar/GraphQL.tsx +1 -0
  62. package/src/editor/sidebar/ViewSelector.tsx +82 -57
  63. package/src/editor/ui/Section.tsx +4 -3
  64. package/src/editor/utils.ts +29 -0
  65. package/src/revision.ts +2 -2
  66. package/dist/editor/ai/EditorAiTerminal.d.ts +0 -6
  67. package/dist/editor/ai/EditorAiTerminal.js +0 -7
  68. package/dist/editor/ai/EditorAiTerminal.js.map +0 -1
  69. package/src/editor/ai/EditorAiTerminal.tsx +0 -23
@@ -1,481 +1,320 @@
1
1
  import React, { useState, useRef, useEffect } from "react";
2
- import { AiTerminalOptions } from "./AiTerminal";
3
- import { EditorAiTerminal } from "./EditorAiTerminal";
4
- import { AgentHistory } from "./AgentHistory";
2
+
5
3
  import { SimpleIconButton } from "../ui/SimpleIconButton";
6
- import { Plus, X, MoreHorizontal } from "lucide-react";
4
+ import { Plus, X, History, MoreVertical, Trash } from "lucide-react";
7
5
  import { cn } from "../../lib/utils";
6
+
8
7
  import {
9
- getAgents,
10
- getAgent,
11
- getChatHistory,
12
- closeAgent,
8
+ Popover,
9
+ PopoverContent,
10
+ PopoverTrigger,
11
+ } from "../../components/ui/popover";
12
+ import {
13
+ Agent,
14
+ getActiveAgents,
15
+ getClosedAgents,
16
+ closeAgent as closeAgentService,
13
17
  deleteAgent,
14
- AgentChat,
15
- AgentChatMessage,
16
18
  } from "../services/agentService";
17
- import { Message } from "./AiTerminal";
18
- import {
19
- DropdownMenu,
20
- DropdownMenuContent,
21
- DropdownMenuItem,
22
- DropdownMenuTrigger,
23
- } from "../../components/ui/dropdown-menu";
24
-
19
+ import { AgentTerminal } from "./AgentTerminal";
25
20
  import { useEditContext } from "../client/editContext";
26
- import { AiContext } from "./AiTerminal";
27
-
28
- interface TerminalInstance {
29
- id: string;
30
- title: string;
31
- agentId?: string;
32
- options?: AiTerminalOptions;
33
- messages?: Message[];
34
- }
35
21
 
36
- function convertAgentMessagesToTerminalMessages(
37
- agentMessages: AgentChatMessage[],
38
- ): Message[] {
39
- return (
40
- agentMessages
41
- // Keep all messages including tool results for complete conversation context
42
- .map((msg) => ({
43
- id: msg.id,
44
- content: msg.content,
45
- name: msg.name,
46
- role: msg.role,
47
- tool_calls:
48
- msg.toolCalls?.map((toolCall: any) => ({
49
- id: toolCall.toolCallId,
50
- displayName: toolCall.functionName,
51
- function: {
52
- name: toolCall.functionName,
53
- arguments: toolCall.functionArguments || "",
54
- result: toolCall.functionResult,
55
- error: toolCall.functionError,
56
- },
57
- })) || [],
58
- tool_call_id: msg.toolCallId,
59
- }))
60
- );
61
- }
22
+ // function convertAgentMessagesToTerminalMessages(
23
+ // agentMessages: AgentChatMessage[],
24
+ // ): Message[] {
25
+ // return agentMessages.map((msg) => ({
26
+ // id: msg.id,
27
+ // content: msg.content,
28
+ // name: msg.name,
29
+ // role: msg.role,
30
+ // tool_calls: msg.functionName
31
+ // ? [
32
+ // {
33
+ // id: msg.toolCallId || msg.id,
34
+ // displayName: msg.functionName,
35
+ // function: {
36
+ // name: msg.functionName,
37
+ // arguments: msg.functionArguments || "",
38
+ // },
39
+ // },
40
+ // ]
41
+ // : [],
42
+ // tool_call_id: msg.toolCallId,
43
+ // }));
44
+ // }
45
+
46
+ const ACTIVE_AGENT_STORAGE_KEY = "editor.activeAgentId";
47
+
48
+ export function Agents({ closeButton }: { closeButton?: React.ReactNode }) {
49
+ const [agents, setAgents] = useState<Agent[]>([]);
50
+ const [activeAgentId, setActiveAgentId] = useState<string | null>(null);
51
+ const [historyPopoverOpen, setHistoryPopoverOpen] = useState(false);
52
+ const [menuPopoverOpen, setMenuPopoverOpen] = useState(false);
53
+ const [loadingAgents, setLoadingAgents] = useState(false);
54
+ const [inactiveAgents, setInactiveAgents] = useState<Agent[]>([]);
62
55
 
63
- function formatDateToLocalTime(dateString: string): string {
64
- // Ensure the date string is treated as UTC if it doesn't have timezone info
65
- const normalizedDate =
66
- dateString.includes("Z") ||
67
- dateString.includes("+") ||
68
- dateString.includes("-", 19)
69
- ? dateString
70
- : dateString + "Z";
56
+ const editContext = useEditContext();
71
57
 
72
- return new Date(normalizedDate).toLocaleString();
73
- }
58
+ // Helper function to get the most recently updated agent
59
+ const getMostRecentAgent = (agentList: Agent[]): Agent | null => {
60
+ if (agentList.length === 0) return null;
61
+ return agentList.reduce((mostRecent, current) => {
62
+ return new Date(current.updatedDate) > new Date(mostRecent.updatedDate)
63
+ ? current
64
+ : mostRecent;
65
+ });
66
+ };
74
67
 
75
- export function Agents({
76
- closeButton,
77
- initialOptions,
78
- }: {
79
- closeButton?: React.ReactNode;
80
- initialOptions?: AiTerminalOptions;
81
- }) {
82
- const [terminals, setTerminals] = useState<TerminalInstance[]>([]);
83
- const [activeTerminalId, setActiveTerminalId] = useState<string | null>(null);
84
- const [closedAgents, setClosedAgents] = useState<AgentChat[]>([]);
85
- const [historyPopoverOpen, setHistoryPopoverOpen] = useState(false);
86
- const [loadingAgents, setLoadingAgents] = useState(true);
68
+ // Helper function to set active agent and persist to localStorage
69
+ const setActiveAgentIdWithStorage = (agentId: string | null) => {
70
+ console.log("setActiveAgentIdWithStorage", agentId);
71
+ setActiveAgentId(agentId);
87
72
 
88
- const nextTerminalNumber = useRef(1);
89
- const editContext = useEditContext();
73
+ if (agentId) {
74
+ localStorage.setItem(ACTIVE_AGENT_STORAGE_KEY, agentId);
75
+ } else {
76
+ localStorage.removeItem(ACTIVE_AGENT_STORAGE_KEY);
77
+ }
78
+ };
90
79
 
91
- // Create AI context for service calls
92
- const createAgentContext = (): AiContext => ({
93
- promptData: {
94
- itemid: editContext?.currentItemDescriptor?.id,
95
- language: editContext?.currentItemDescriptor?.language || "en",
96
- version: editContext?.currentItemDescriptor?.version || 1,
97
- },
98
- });
80
+ // Initialize with a default agent if none exist
81
+ useEffect(() => {
82
+ if (agents.length === 0) {
83
+ const defaultAgent: Agent = {
84
+ status: "new",
85
+ id: crypto.randomUUID(),
86
+ name: `New Agent`,
87
+ updatedDate: new Date().toISOString(),
88
+ userId: "",
89
+ };
90
+ setAgents([defaultAgent]);
91
+ setActiveAgentId(defaultAgent.id);
92
+ }
93
+ }, [agents.length]);
99
94
 
100
95
  // Load agents from backend on mount
101
96
  useEffect(() => {
102
97
  loadAgentsFromBackend();
103
98
  }, []);
104
99
 
105
- // Listen for WebSocket messages about agent changes
100
+ // Subscribe to websocket messages for agent-started events
106
101
  useEffect(() => {
107
- if (!editContext) return;
108
-
109
- const removeSocketListener = editContext.addSocketMessageListener(
110
- (message) => {
111
- if (
112
- message.type === "agent-started" ||
113
- message.type === "agent-updated"
114
- ) {
115
- // Refresh agents list when new agents are started or updated
116
- loadAgentsFromBackend();
102
+ if (!editContext?.addSocketMessageListener) return;
103
+
104
+ const unsubscribe = editContext.addSocketMessageListener(
105
+ (message: { type: string; payload: any }) => {
106
+ if (message.type === "agent-started") {
107
+ const { agentId, agentName } = message.payload;
108
+
109
+ if (!agentId || !agentName) {
110
+ console.warn(
111
+ "Invalid agent-started message payload:",
112
+ message.payload,
113
+ );
114
+ return;
115
+ }
116
+
117
+ setAgents((prevAgents) => {
118
+ // Check if agent already exists
119
+ const existingAgentIndex = prevAgents.findIndex(
120
+ (agent) => agent.id === agentId,
121
+ );
122
+
123
+ if (existingAgentIndex !== -1) {
124
+ // Update existing agent name
125
+ const updatedAgents = [...prevAgents];
126
+ const existingAgent = updatedAgents[existingAgentIndex]!;
127
+ updatedAgents[existingAgentIndex] = {
128
+ ...existingAgent,
129
+ name: agentName,
130
+ status: "running" as const,
131
+ updatedDate: new Date().toISOString(),
132
+ };
133
+ return updatedAgents;
134
+ } else {
135
+ // Add new agent to the array
136
+ const newAgent: Agent = {
137
+ id: agentId,
138
+ name: agentName,
139
+ status: "running" as const,
140
+ userId: "", // Will be populated from backend if needed
141
+ updatedDate: new Date().toISOString(),
142
+ };
143
+ return [...prevAgents, newAgent];
144
+ }
145
+ });
117
146
  }
118
147
  },
119
148
  );
120
149
 
121
- return removeSocketListener;
122
- }, [editContext]);
150
+ return unsubscribe;
151
+ }, [editContext?.addSocketMessageListener]);
123
152
 
124
153
  const loadAgentsFromBackend = async () => {
125
154
  try {
126
155
  setLoadingAgents(true);
127
- const context = createAgentContext();
128
156
 
129
- // Load all non-closed agents
130
- const activeAgentsResult = await getAgents(context);
157
+ // Load active agents
158
+ const activeAgentsResult = await getActiveAgents();
131
159
 
132
- // Load closed agents for history
133
- const closedAgentsResult = await getChatHistory(context, "closed", 20);
134
-
135
- if (activeAgentsResult && Array.isArray(activeAgentsResult)) {
136
- const activeAgents = activeAgentsResult;
137
-
138
- // Create terminals for active agents
139
- const activeTerminals: TerminalInstance[] = await Promise.all(
140
- activeAgents.map(async (agent: any, index: number) => {
141
- // Load messages for each active agent
142
- const agentWithMessages = await loadAgentMessages(agent.id);
143
-
144
- return {
145
- id: `agent-${agent.id}`,
146
- title: agent.name || "New Agent",
147
- agentId: agent.id,
148
- options: {
149
- ...initialOptions,
150
- // Pass cost information from loaded agent
151
- totalCost: agentWithMessages?.totalCost,
152
- totalInputTokenCost: agentWithMessages?.totalInputTokenCost,
153
- totalOutputTokenCost: agentWithMessages?.totalOutputTokenCost,
154
- totalCachedTokenCost:
155
- agentWithMessages?.totalCachedInputTokenCost,
156
- totalInputTokens: agentWithMessages?.totalInputTokens,
157
- totalOutputTokens: agentWithMessages?.totalOutputTokens,
158
- totalCachedTokens: agentWithMessages?.totalCachedInputTokens,
159
- },
160
- messages: agentWithMessages?.messages
161
- ? convertAgentMessagesToTerminalMessages(
162
- agentWithMessages.messages,
163
- )
164
- : [],
165
- };
166
- }),
160
+ setAgents(activeAgentsResult);
161
+
162
+ // Determine which agent to select as active
163
+ let selectedAgentId: string | null = null;
164
+
165
+ if (activeAgentsResult.length > 0) {
166
+ // Try to restore the previously active agent from localStorage
167
+ const storedAgentId = localStorage.getItem(ACTIVE_AGENT_STORAGE_KEY);
168
+
169
+ const storedAgent = activeAgentsResult.find(
170
+ (agent) => agent.id === storedAgentId,
167
171
  );
168
172
 
169
- // Set the first active terminal as active, or create a new one if none exist
170
- if (activeTerminals.length > 0) {
171
- setTerminals(activeTerminals);
172
- setActiveTerminalId(activeTerminals[0]!.id);
173
+ if (storedAgent) {
174
+ selectedAgentId = storedAgent.id;
173
175
  } else {
174
- // Create a default terminal if no active agents
175
- const defaultTerminal: TerminalInstance = {
176
- id: `terminal-${nextTerminalNumber.current}`,
177
- title: "New Agent",
178
- options: initialOptions,
179
- };
180
- setTerminals([defaultTerminal]);
181
- setActiveTerminalId(defaultTerminal.id);
182
- nextTerminalNumber.current++;
176
+ // Fall back to the most recently updated agent
177
+ console.log("get most recent agent", activeAgentsResult);
178
+ const mostRecentAgent = getMostRecentAgent(activeAgentsResult);
179
+ selectedAgentId = mostRecentAgent?.id || null;
183
180
  }
184
181
  }
185
182
 
186
- if (closedAgentsResult && Array.isArray(closedAgentsResult)) {
187
- setClosedAgents(closedAgentsResult);
188
- }
183
+ console.log("set selectedAgentId", selectedAgentId);
184
+
185
+ setActiveAgentIdWithStorage(selectedAgentId);
186
+
187
+ // Load closed agents for history
188
+ const closedAgentsResult = await getClosedAgents();
189
+
190
+ setInactiveAgents(closedAgentsResult);
189
191
  } catch (error) {
190
192
  console.error("Failed to load agents:", error);
191
- // Create a default terminal on error
192
- const defaultTerminal: TerminalInstance = {
193
- id: `terminal-${nextTerminalNumber.current}`,
194
- title: `Agent ${nextTerminalNumber.current}`,
195
- options: initialOptions,
196
- };
197
- setTerminals([defaultTerminal]);
198
- setActiveTerminalId(defaultTerminal.id);
199
- nextTerminalNumber.current++;
200
193
  } finally {
201
194
  setLoadingAgents(false);
202
195
  }
203
196
  };
204
197
 
205
- const loadAgentMessages = async (
206
- agentId: string,
207
- ): Promise<AgentChat | null> => {
208
- try {
209
- const context = createAgentContext();
210
- const result = await getAgent(agentId, context);
211
- if (result && result.id) {
212
- return result;
213
- }
214
- } catch (error) {
215
- console.error(`Failed to load messages for agent ${agentId}:`, error);
216
- }
217
- return null;
218
- };
219
-
220
- const addTerminal = () => {
221
- const newTerminal: TerminalInstance = {
222
- id: `terminal-${nextTerminalNumber.current}`,
223
- title: `Agent ${nextTerminalNumber.current}`,
224
- options: undefined,
198
+ // const loadAgentMessages = async (
199
+ // agentId: string,
200
+ // ): Promise<AgentChat | null> => {
201
+ // try {
202
+ // const result = await getAgent(agentId);
203
+ // if (result.type === "success" && result.data) {
204
+ // return result.data;
205
+ // }
206
+ // } catch (error) {
207
+ // console.error(`Failed to load messages for agent ${agentId}:`, error);
208
+ // }
209
+ // return null;
210
+ // };
211
+
212
+ const addAgent = () => {
213
+ const newAgent: Agent = {
214
+ status: "new",
215
+ id: crypto.randomUUID(),
216
+ name: `New Agent`,
217
+ updatedDate: new Date().toISOString(),
218
+ userId: "",
225
219
  };
226
- setTerminals((prev) => [...prev, newTerminal]);
227
- setActiveTerminalId(newTerminal.id);
228
- nextTerminalNumber.current++;
220
+ setAgents((prev) => [...prev, newAgent]);
221
+ setActiveAgentIdWithStorage(newAgent.id);
229
222
  };
230
223
 
231
- const createNewAgent = (options: AiTerminalOptions) => {
232
- const newTerminal: TerminalInstance = {
233
- id: `terminal-${nextTerminalNumber.current}`,
234
- title: `Agent ${nextTerminalNumber.current}`,
235
- options: options,
236
- };
237
- setTerminals((prev) => [...prev, newTerminal]);
238
- setActiveTerminalId(newTerminal.id);
239
- nextTerminalNumber.current++;
240
- };
241
-
242
- const closeTerminal = async (terminalId: string) => {
243
- const terminal = terminals.find((t) => t.id === terminalId);
244
- if (!terminal) return;
245
-
246
- // If this terminal has an associated agent, show confirmation
247
- if (terminal.agentId) {
248
- editContext?.confirm({
249
- header: "Close Agent",
250
- message:
251
- "Are you sure you want to close this agent? This will abort any running execution and mark the agent as closed.",
252
- acceptLabel: "Close Agent",
253
- rejectLabel: "Cancel",
254
- accept: () => performCloseTerminal(terminalId),
255
- reject: () => {}, // Do nothing on reject
256
- });
257
- return;
224
+ const closeAgent = async (agentId: string) => {
225
+ try {
226
+ // Permanently close the agent in the backend
227
+ await closeAgentService(agentId);
228
+ } catch (error) {
229
+ console.error("Failed to close agent:", error);
230
+ // Continue with UI cleanup even if backend call fails
258
231
  }
259
232
 
260
- // For terminals without agents, close immediately
261
- performCloseTerminal(terminalId);
262
- };
263
-
264
- const performCloseTerminal = async (terminalId: string) => {
265
- const terminal = terminals.find((t) => t.id === terminalId);
266
-
267
- try {
268
- // If this terminal has an associated agent, close it in the backend
269
- if (terminal?.agentId) {
270
- const context = createAgentContext();
271
- await closeAgent(terminal.agentId, context);
272
- }
233
+ setAgents((prev) => {
234
+ const filtered = prev.filter((a) => a.id !== agentId);
273
235
 
274
- // Remove terminal from local state
275
- setTerminals((prev) => {
276
- const filtered = prev.filter((t) => t.id !== terminalId);
277
-
278
- // If we're closing the active terminal, switch to the first remaining one
279
- // or create a new one if this was the last terminal
280
- if (activeTerminalId === terminalId) {
281
- if (filtered.length > 0) {
282
- setActiveTerminalId(filtered[0]!.id);
283
- } else {
284
- // Create a new terminal if this was the last one
285
- const newTerminal: TerminalInstance = {
286
- id: `terminal-${nextTerminalNumber.current}`,
287
- title: "New Agent",
288
- options: initialOptions,
289
- };
290
- nextTerminalNumber.current++;
291
- setActiveTerminalId(newTerminal.id);
292
- return [newTerminal];
293
- }
236
+ // If we're closing the active terminal, switch to the most recent remaining one or clear storage
237
+ if (activeAgentId === agentId) {
238
+ if (filtered.length > 0) {
239
+ const mostRecentAgent = getMostRecentAgent(filtered);
240
+ setActiveAgentIdWithStorage(mostRecentAgent?.id || null);
241
+ } else {
242
+ setActiveAgentIdWithStorage(null);
294
243
  }
244
+ }
295
245
 
296
- return filtered;
297
- });
298
- } catch (error) {
299
- console.error("Failed to close agent:", error);
300
- editContext?.showToast("Failed to close agent. Removing from UI.");
301
- // Still remove the terminal from UI even if backend call fails
302
- setTerminals((prev) => prev.filter((t) => t.id !== terminalId));
303
- }
246
+ return filtered;
247
+ });
304
248
  };
305
249
 
306
- const closeOtherTerminals = async () => {
307
- if (!activeTerminalId || terminals.length <= 1) {
308
- return;
309
- }
250
+ const closeOtherAgents = async () => {
251
+ if (!activeAgentId) return;
310
252
 
311
- // Get all terminals except the active one
312
- const otherTerminals = terminals.filter((t) => t.id !== activeTerminalId);
313
-
314
- // Show confirmation if any of the other terminals have agents
315
- const hasAgents = otherTerminals.some((t) => t.agentId);
316
-
317
- if (hasAgents) {
318
- editContext?.confirm({
319
- header: "Close Other Agents",
320
- message: `Are you sure you want to close ${otherTerminals.length} other agent${otherTerminals.length > 1 ? "s" : ""}? This will abort any running executions and mark them as closed.`,
321
- acceptLabel: "Close Others",
322
- rejectLabel: "Cancel",
323
- accept: async () => {
324
- // Close all other terminals
325
- for (const terminal of otherTerminals) {
326
- try {
327
- if (terminal.agentId) {
328
- const context = createAgentContext();
329
- await closeAgent(terminal.agentId, context);
330
- }
331
- } catch (error) {
332
- console.error("Failed to close agent:", error);
333
- }
334
- }
253
+ // Get agents to close (all except active)
254
+ const agentsToClose = agents.filter((a) => a.id !== activeAgentId);
335
255
 
336
- // Keep only the active terminal
337
- const activeTerminal = terminals.find(
338
- (t) => t.id === activeTerminalId,
339
- );
340
- if (activeTerminal) {
341
- setTerminals([activeTerminal]);
342
- }
343
- },
344
- reject: () => {}, // Do nothing on reject
345
- });
346
- } else {
347
- // No agents, just close immediately
348
- const activeTerminal = terminals.find((t) => t.id === activeTerminalId);
349
- if (activeTerminal) {
350
- setTerminals([activeTerminal]);
256
+ // Permanently close each agent in the backend
257
+ const closePromises = agentsToClose.map(async (agent) => {
258
+ try {
259
+ await closeAgentService(agent.id);
260
+ } catch (error) {
261
+ console.error(`Failed to close agent ${agent.id}:`, error);
351
262
  }
352
- }
353
- };
263
+ });
354
264
 
355
- const closeAllTerminals = async () => {
356
- // Show confirmation if any terminals have agents
357
- const hasAgents = terminals.some((t) => t.agentId);
358
-
359
- if (hasAgents) {
360
- editContext?.confirm({
361
- header: "Close All Agents",
362
- message: `Are you sure you want to close all ${terminals.length} agent${terminals.length > 1 ? "s" : ""}? This will abort any running executions and mark them as closed.`,
363
- acceptLabel: "Close All",
364
- rejectLabel: "Cancel",
365
- accept: async () => {
366
- // Close all terminals with agents
367
- for (const terminal of terminals) {
368
- try {
369
- if (terminal.agentId) {
370
- const context = createAgentContext();
371
- await closeAgent(terminal.agentId, context);
372
- }
373
- } catch (error) {
374
- console.error("Failed to close agent:", error);
375
- }
376
- }
265
+ // Wait for all close operations to complete
266
+ await Promise.all(closePromises);
377
267
 
378
- // Create a new default terminal
379
- const defaultTerminal: TerminalInstance = {
380
- id: `terminal-${nextTerminalNumber.current}`,
381
- title: "New Agent",
382
- options: initialOptions,
383
- };
384
- setTerminals([defaultTerminal]);
385
- setActiveTerminalId(defaultTerminal.id);
386
- nextTerminalNumber.current++;
387
- },
388
- reject: () => {}, // Do nothing on reject
389
- });
390
- } else {
391
- // No agents, just create a new default terminal
392
- const defaultTerminal: TerminalInstance = {
393
- id: `terminal-${nextTerminalNumber.current}`,
394
- title: "New Agent",
395
- options: initialOptions,
396
- };
397
- setTerminals([defaultTerminal]);
398
- setActiveTerminalId(defaultTerminal.id);
399
- nextTerminalNumber.current++;
400
- }
268
+ setAgents((prev) => {
269
+ return prev.filter((a) => a.id === activeAgentId);
270
+ });
271
+ setMenuPopoverOpen(false);
401
272
  };
402
273
 
403
- const openAgentFromHistory = async (agent: AgentChat) => {
274
+ const openAgentFromHistory = async (agent: Agent) => {
404
275
  // Check if this agent is already open as a terminal
405
- const existingTerminal = terminals.find((t) => t.agentId === agent.id);
276
+ const existingAgent = agents.find((a) => a.id === agent.id);
406
277
 
407
- if (existingTerminal) {
408
- // Switch to existing terminal
409
- setActiveTerminalId(existingTerminal.id);
278
+ if (existingAgent) {
279
+ // Switch to existing agent
280
+ setActiveAgentIdWithStorage(existingAgent.id);
410
281
  setHistoryPopoverOpen(false);
411
282
  return;
412
283
  }
413
284
 
414
- // Load the agent with messages
415
- const agentWithMessages = await loadAgentMessages(agent.id);
416
-
417
- // Create new terminal for this agent
418
- const newTerminal: TerminalInstance = {
419
- id: `agent-${agent.id}`,
420
- title: agent.name,
421
- agentId: agent.id,
422
- options: {
423
- ...initialOptions,
424
- // Pass cost information from loaded agent
425
- totalCost: agentWithMessages?.totalCost,
426
- totalInputTokenCost: agentWithMessages?.totalInputTokenCost,
427
- totalOutputTokenCost: agentWithMessages?.totalOutputTokenCost,
428
- totalCachedTokenCost: agentWithMessages?.totalCachedInputTokenCost,
429
- totalInputTokens: agentWithMessages?.totalInputTokens,
430
- totalOutputTokens: agentWithMessages?.totalOutputTokens,
431
- totalCachedTokens: agentWithMessages?.totalCachedInputTokens,
432
- },
433
- messages: agentWithMessages?.messages
434
- ? convertAgentMessagesToTerminalMessages(agentWithMessages.messages)
435
- : [],
285
+ // Add the closed agent to the active agents list
286
+ const reopenedAgent: Agent = {
287
+ ...agent,
288
+ // Keep the original status to allow AgentTerminal to load the full agent data
436
289
  };
437
290
 
438
- setTerminals((prev) => [...prev, newTerminal]);
439
- setActiveTerminalId(newTerminal.id);
291
+ setAgents((prev) => [...prev, reopenedAgent]);
292
+ setActiveAgentIdWithStorage(reopenedAgent.id);
440
293
  setHistoryPopoverOpen(false);
441
294
  };
442
295
 
443
- const deleteAgentFromHistory = async (agent: AgentChat) => {
444
- editContext?.confirm({
445
- header: "Delete Agent",
446
- message: `Are you sure you want to permanently delete "${agent.name}"? This action cannot be undone and will remove the agent and all its messages from the database.`,
447
- acceptLabel: "Delete Permanently",
448
- rejectLabel: "Cancel",
449
- accept: async () => {
450
- try {
451
- const context = createAgentContext();
452
- await deleteAgent(agent.id, context);
453
-
454
- // Remove from closed agents list
455
- setClosedAgents((prev) => prev.filter((a) => a.id !== agent.id));
456
-
457
- // If this agent is currently open as a terminal, close it
458
- const existingTerminal = terminals.find(
459
- (t) => t.agentId === agent.id,
460
- );
461
- if (existingTerminal) {
462
- performCloseTerminal(existingTerminal.id);
463
- }
296
+ const deleteAgentFromHistory = async (
297
+ agentId: string,
298
+ event: React.MouseEvent,
299
+ ) => {
300
+ event.stopPropagation(); // Prevent opening the agent when clicking delete
464
301
 
465
- editContext?.showToast("Agent deleted successfully");
466
- } catch (error) {
467
- console.error("Failed to delete agent:", error);
468
- editContext?.showToast("Failed to delete agent. Please try again.");
469
- }
470
- },
471
- reject: () => {}, // Do nothing on reject
472
- });
302
+ try {
303
+ // Permanently delete the agent from the backend
304
+ await deleteAgent(agentId);
305
+
306
+ // Remove from inactive agents list
307
+ setInactiveAgents((prev) => prev.filter((agent) => agent.id !== agentId));
308
+ } catch (error) {
309
+ console.error("Failed to delete agent:", error);
310
+ // You might want to show a user-facing error message here
311
+ }
473
312
  };
474
313
 
475
314
  if (loadingAgents) {
476
315
  return (
477
316
  <div className="flex h-full items-center justify-center">
478
- <div className="text-xs text-gray-500">Loading agents...</div>
317
+ <div className="text-sm text-gray-500">Loading agents...</div>
479
318
  </div>
480
319
  );
481
320
  }
@@ -484,81 +323,121 @@ export function Agents({
484
323
  <div className="flex h-full flex-col">
485
324
  {/* Tab Header */}
486
325
  <div className="flex items-center border-b border-gray-200 bg-gray-50">
487
- <div className="flex flex-1 overflow-x-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
488
- {terminals.map((terminal) => (
326
+ <div className="flex flex-1 overflow-x-auto">
327
+ {agents.map((agent) => (
489
328
  <div
490
- key={terminal.id}
329
+ key={agent.id}
491
330
  className={cn(
492
331
  "flex min-w-0 cursor-pointer items-center gap-1 border-r border-gray-200 px-3 py-2 text-xs",
493
- activeTerminalId === terminal.id
332
+ activeAgentId === agent.id
494
333
  ? "border-b-white bg-white"
495
334
  : "hover:bg-gray-100",
496
335
  )}
497
- onClick={() => setActiveTerminalId(terminal.id)}
336
+ onClick={() => setActiveAgentIdWithStorage(agent.id)}
498
337
  >
499
- <span className="truncate">{terminal.title}</span>
500
- <SimpleIconButton
501
- onClick={(e) => {
502
- e.stopPropagation();
503
- closeTerminal(terminal.id);
504
- }}
505
- icon={<X className="size-3" />}
506
- label="Close"
507
- className="ml-1 opacity-60 hover:opacity-100"
508
- />
338
+ <span className="truncate">{agent.name}</span>
339
+ {agents.length > 1 && (
340
+ <SimpleIconButton
341
+ onClick={(e) => {
342
+ e.stopPropagation();
343
+ closeAgent(agent.id);
344
+ }}
345
+ icon={<X className="size-2" strokeWidth={1} />}
346
+ label="Close"
347
+ className="ml-1 opacity-60 hover:opacity-100"
348
+ />
349
+ )}
509
350
  </div>
510
351
  ))}
511
352
  </div>
512
353
 
513
- {/* Add Terminal Button */}
514
- <div className="flex items-center px-1">
515
- <SimpleIconButton
516
- onClick={addTerminal}
517
- icon={<Plus className="size-4" />}
518
- label="Add Agent"
519
- className="text-gray-600 hover:text-gray-800"
520
- />
521
- </div>
522
-
523
- {/* Agent History */}
354
+ {/* History Popover */}
524
355
  <div className="flex items-center px-1">
525
- <AgentHistory
526
- closedAgents={closedAgents}
527
- isOpen={historyPopoverOpen}
356
+ <Popover
357
+ open={historyPopoverOpen}
528
358
  onOpenChange={setHistoryPopoverOpen}
529
- onOpenAgent={openAgentFromHistory}
530
- onDeleteAgent={deleteAgentFromHistory}
531
- formatDateToLocalTime={formatDateToLocalTime}
532
- />
359
+ >
360
+ <PopoverTrigger asChild>
361
+ <SimpleIconButton
362
+ onClick={() => {}}
363
+ icon={<History className="size-4" strokeWidth={1} />}
364
+ label="Agent History"
365
+ className="text-gray-600 hover:text-gray-800"
366
+ />
367
+ </PopoverTrigger>
368
+ <PopoverContent className="w-64 p-0" align="end">
369
+ <div className="border-b border-gray-100 px-3 py-2 text-xs font-medium text-gray-500">
370
+ Closed Agents
371
+ </div>
372
+ <div className="max-h-80 overflow-y-auto">
373
+ {inactiveAgents.length === 0 ? (
374
+ <div className="px-3 py-2 text-xs text-gray-500">
375
+ No closed agents found
376
+ </div>
377
+ ) : (
378
+ inactiveAgents.map((agent) => (
379
+ <div
380
+ key={agent.id}
381
+ className="cursor-pointer border-b border-gray-50 px-3 py-2 text-xs hover:bg-gray-50"
382
+ onClick={() => openAgentFromHistory(agent)}
383
+ >
384
+ <div className="flex items-center justify-between">
385
+ <div className="min-w-0 flex-1">
386
+ <div className="truncate font-medium text-gray-900">
387
+ {agent.name}
388
+ </div>
389
+ <div className="text-xs text-gray-400">
390
+ {new Date(agent.updatedDate).toLocaleString()}
391
+ </div>
392
+ </div>
393
+ <SimpleIconButton
394
+ onClick={(e) => deleteAgentFromHistory(agent.id, e)}
395
+ icon={<Trash className="size-3" strokeWidth={1} />}
396
+ label="Delete Agent"
397
+ className="ml-2 text-red-600 opacity-60 hover:text-red-700 hover:opacity-100"
398
+ />
399
+ </div>
400
+ </div>
401
+ ))
402
+ )}
403
+ </div>
404
+ </PopoverContent>
405
+ </Popover>
533
406
  </div>
534
407
 
535
- {/* More Options Menu */}
408
+ {/* Menu Popover */}
536
409
  <div className="flex items-center px-1">
537
- <DropdownMenu>
538
- <DropdownMenuTrigger asChild>
410
+ <Popover open={menuPopoverOpen} onOpenChange={setMenuPopoverOpen}>
411
+ <PopoverTrigger asChild>
539
412
  <SimpleIconButton
540
413
  onClick={() => {}}
541
- icon={<MoreHorizontal className="size-4" />}
542
- label="More Options"
414
+ icon={<MoreVertical className="size-4" strokeWidth={1} />}
415
+ label="Menu"
543
416
  className="text-gray-600 hover:text-gray-800"
544
417
  />
545
- </DropdownMenuTrigger>
546
- <DropdownMenuContent align="end">
547
- <DropdownMenuItem
548
- onClick={closeOtherTerminals}
549
- disabled={terminals.length <= 1}
550
- className="cursor-pointer"
551
- >
552
- Close Other
553
- </DropdownMenuItem>
554
- <DropdownMenuItem
555
- onClick={closeAllTerminals}
556
- className="cursor-pointer"
557
- >
558
- Close All
559
- </DropdownMenuItem>
560
- </DropdownMenuContent>
561
- </DropdownMenu>
418
+ </PopoverTrigger>
419
+ <PopoverContent className="w-48 p-0" align="end">
420
+ <div className="py-1">
421
+ <button
422
+ onClick={closeOtherAgents}
423
+ disabled={agents.length <= 1}
424
+ className="w-full px-3 py-2 text-left text-xs hover:bg-gray-50 disabled:cursor-not-allowed disabled:text-gray-400"
425
+ >
426
+ Close Other
427
+ </button>
428
+ </div>
429
+ </PopoverContent>
430
+ </Popover>
431
+ </div>
432
+
433
+ {/* Add Terminal Button */}
434
+ <div className="flex items-center px-1">
435
+ <SimpleIconButton
436
+ onClick={addAgent}
437
+ icon={<Plus className="size-4" strokeWidth={1} />}
438
+ label="Add Terminal"
439
+ className="text-gray-600 hover:text-gray-800"
440
+ />
562
441
  </div>
563
442
 
564
443
  {/* Main Close Button */}
@@ -567,33 +446,17 @@ export function Agents({
567
446
  )}
568
447
  </div>
569
448
 
570
- {/* Terminal Content */}
449
+ {/* Agent Content */}
571
450
  <div className="relative flex-1">
572
- {terminals.map((terminal) => (
451
+ {agents.map((agent) => (
573
452
  <div
574
- key={terminal.id}
453
+ key={agent.id}
575
454
  className={cn(
576
455
  "absolute inset-0",
577
- activeTerminalId === terminal.id ? "block" : "hidden",
456
+ activeAgentId === agent.id ? "block" : "hidden",
578
457
  )}
579
458
  >
580
- <EditorAiTerminal
581
- options={{
582
- ...terminal.options,
583
- // Pass the pre-loaded messages to the terminal
584
- initialMessages: terminal.messages,
585
- // Pass the agentId to maintain continuity
586
- agentId: terminal.agentId,
587
- }}
588
- onAgentNameUpdate={(name) => {
589
- // Update the terminal title when agent name is updated
590
- setTerminals((prev) =>
591
- prev.map((t) =>
592
- t.id === terminal.id ? { ...t, title: name } : t,
593
- ),
594
- );
595
- }}
596
- />
459
+ <AgentTerminal agentStub={agent} />
597
460
  </div>
598
461
  ))}
599
462
  </div>