apteva 0.4.20 → 0.4.26

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 (52) hide show
  1. package/dist/ActivityPage.cycn14ck.js +3 -0
  2. package/dist/{ApiDocsPage.kf6bbwkk.js → ApiDocsPage.3q5x9hhg.js} +2 -2
  3. package/dist/App.0wwyytz2.js +4 -0
  4. package/dist/{App.c90t3dxg.js → App.2prdcxgq.js} +3 -3
  5. package/dist/{App.2yy66bnp.js → App.40azyqz6.js} +3 -3
  6. package/dist/App.6ftxk387.js +4 -0
  7. package/dist/{App.jfx3der4.js → App.9bzz8dqh.js} +3 -3
  8. package/dist/App.a7h91mxr.js +4 -0
  9. package/dist/{App.7v1w3ys9.js → App.e54ynjf2.js} +3 -3
  10. package/dist/{App.edwahsvz.js → App.fq11mvc7.js} +2 -2
  11. package/dist/{App.2jmkqm8c.js → App.h6k4j1w9.js} +3 -3
  12. package/dist/App.jq5tmjws.js +267 -0
  13. package/dist/{App.q3bpx15d.js → App.k377qek6.js} +2 -2
  14. package/dist/{App.039re6cf.js → App.r2c5nw36.js} +3 -3
  15. package/dist/{App.n4jb3c22.js → App.sb2fg71h.js} +3 -3
  16. package/dist/App.wnap3h7r.js +4 -0
  17. package/dist/ConnectionsPage.6fyhqfhz.js +3 -0
  18. package/dist/McpPage.hk2qt1qt.js +3 -0
  19. package/dist/SettingsPage.gwpx9v7v.js +3 -0
  20. package/dist/SkillsPage.j5zech2z.js +3 -0
  21. package/dist/TasksPage.65dcf4vw.js +3 -0
  22. package/dist/TelemetryPage.07xrbd7k.js +3 -0
  23. package/dist/TestsPage.q6zfephf.js +3 -0
  24. package/dist/index.html +1 -1
  25. package/dist/styles.css +1 -1
  26. package/package.json +2 -2
  27. package/src/integrations/agentdojo.ts +1 -1
  28. package/src/providers.ts +2 -0
  29. package/src/routes/api/triggers.ts +45 -5
  30. package/src/web/App.tsx +1 -0
  31. package/src/web/components/activity/ActivityPage.tsx +347 -212
  32. package/src/web/components/agents/AgentCard.tsx +32 -3
  33. package/src/web/components/agents/AgentPanel.tsx +188 -4
  34. package/src/web/components/connections/IntegrationsTab.tsx +57 -31
  35. package/src/web/components/connections/TriggersTab.tsx +336 -159
  36. package/src/web/components/dashboard/Dashboard.tsx +39 -7
  37. package/src/web/components/layout/Header.tsx +0 -34
  38. package/src/web/components/layout/Sidebar.tsx +43 -3
  39. package/src/web/components/tasks/TasksPage.tsx +32 -6
  40. package/dist/ActivityPage.h769ek3a.js +0 -3
  41. package/dist/App.3515wsb4.js +0 -4
  42. package/dist/App.r0a2nmqs.js +0 -267
  43. package/dist/App.s2yrcz15.js +0 -4
  44. package/dist/App.s5j82a5j.js +0 -4
  45. package/dist/App.tg1b94tx.js +0 -4
  46. package/dist/ConnectionsPage.a67fjgbf.js +0 -3
  47. package/dist/McpPage.d4p3xvtk.js +0 -3
  48. package/dist/SettingsPage.46sqpe39.js +0 -3
  49. package/dist/SkillsPage.j9hkqm99.js +0 -3
  50. package/dist/TasksPage.6pvkb7s7.js +0 -3
  51. package/dist/TelemetryPage.5zq9msb5.js +0 -3
  52. package/dist/TestsPage.24432yqt.js +0 -3
@@ -1,17 +1,25 @@
1
- import React, { useState, useMemo, useEffect, useRef } from "react";
1
+ import React, { useState, useMemo, useEffect, useCallback, useRef } from "react";
2
2
  import { useAgentActivity, useAuth, useProjects, useTelemetryContext } from "../../context";
3
+ import { useTelemetry } from "../../context/TelemetryContext";
3
4
  import type { TelemetryEvent } from "../../context";
4
- import type { Agent } from "../../types";
5
- import { CloseIcon } from "../common/Icons";
5
+ import type { Agent, Task, Route } from "../../types";
6
+ import { RecurringIcon, ScheduledIcon, TaskOnceIcon } from "../common/Icons";
6
7
 
7
8
  interface ActivityPageProps {
8
9
  agents: Agent[];
9
10
  loading: boolean;
11
+ onNavigate?: (route: Route) => void;
10
12
  }
11
13
 
12
- export function ActivityPage({ agents, loading }: ActivityPageProps) {
14
+ export function ActivityPage({ agents, loading, onNavigate }: ActivityPageProps) {
15
+ const { authFetch } = useAuth();
13
16
  const { currentProjectId } = useProjects();
17
+ const { events: realtimeEvents, statusChangeCounter } = useTelemetryContext();
14
18
  const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
19
+ const [tasks, setTasks] = useState<Task[]>([]);
20
+ const [historicalActivities, setHistoricalActivities] = useState<TelemetryEvent[]>([]);
21
+ const lastProcessedTaskEventRef = useRef<string | null>(null);
22
+ const { events: taskTelemetryEvents } = useTelemetry({ category: "TASK" });
15
23
 
16
24
  const filteredAgents = useMemo(() => {
17
25
  if (currentProjectId === null) return agents;
@@ -19,127 +27,227 @@ export function ActivityPage({ agents, loading }: ActivityPageProps) {
19
27
  return agents.filter(a => a.projectId === currentProjectId);
20
28
  }, [agents, currentProjectId]);
21
29
 
22
- const selectedAgent = filteredAgents.find(a => a.id === selectedAgentId) || null;
30
+ const sortedAgents = useMemo(() => {
31
+ return [...filteredAgents].sort((a, b) => {
32
+ if (a.status === "running" && b.status !== "running") return -1;
33
+ if (b.status === "running" && a.status !== "running") return 1;
34
+ return a.name.localeCompare(b.name);
35
+ });
36
+ }, [filteredAgents]);
23
37
 
24
- return (
25
- <div className="flex-1 flex flex-col overflow-hidden">
26
- {/* Top: Agent Visualization */}
27
- <div className="flex-[3] min-h-0 p-6 overflow-auto">
28
- <ActivityVisualization
29
- agents={filteredAgents}
30
- loading={loading}
31
- selectedAgentId={selectedAgentId}
32
- onSelectAgent={setSelectedAgentId}
33
- />
34
- </div>
38
+ const runningCount = useMemo(() => filteredAgents.filter(a => a.status === "running").length, [filteredAgents]);
35
39
 
36
- {/* Bottom: Command + Stream */}
37
- <div className="flex-[2] min-h-0 border-t border-[#1a1a1a] flex">
38
- <QuickCommandPanel
39
- agent={selectedAgent}
40
- onClose={() => setSelectedAgentId(null)}
41
- />
42
- <LiveActivityStream agents={filteredAgents} />
43
- </div>
44
- </div>
45
- );
46
- }
40
+ const agentIds = useMemo(() => new Set(filteredAgents.map(a => a.id)), [filteredAgents]);
41
+ const agentNameMap = useMemo(() => {
42
+ const map = new Map<string, string>();
43
+ filteredAgents.forEach(a => map.set(a.id, a.name));
44
+ return map;
45
+ }, [filteredAgents]);
46
+
47
+ // Fetch tasks + historical activity
48
+ const fetchData = useCallback(async () => {
49
+ const projectParam = currentProjectId ? `&project_id=${encodeURIComponent(currentProjectId)}` : "";
50
+ const [tasksRes, activityRes] = await Promise.all([
51
+ authFetch(`/api/tasks?status=all${projectParam}`).catch(() => null),
52
+ authFetch(`/api/telemetry/events?type=thread_activity&limit=50${projectParam}`).catch(() => null),
53
+ ]);
54
+ if (tasksRes?.ok) {
55
+ const data = await tasksRes.json();
56
+ const list: Task[] = data.tasks || [];
57
+ list.sort((a, b) => {
58
+ const aPri = a.status === "running" ? 0 : a.status === "pending" ? 1 : a.status === "completed" ? 2 : 3;
59
+ const bPri = b.status === "running" ? 0 : b.status === "pending" ? 1 : b.status === "completed" ? 2 : 3;
60
+ if (aPri !== bPri) return aPri - bPri;
61
+ if (aPri <= 1) {
62
+ const aTs = (a.next_run || a.execute_at) ? new Date(a.next_run || a.execute_at!).getTime() : Infinity;
63
+ const bTs = (b.next_run || b.execute_at) ? new Date(b.next_run || b.execute_at!).getTime() : Infinity;
64
+ return aTs - bTs;
65
+ }
66
+ const aDate = a.completed_at || a.executed_at || a.created_at;
67
+ const bDate = b.completed_at || b.executed_at || b.created_at;
68
+ return new Date(bDate).getTime() - new Date(aDate).getTime();
69
+ });
70
+ setTasks(list);
71
+ }
72
+ if (activityRes?.ok) {
73
+ const data = await activityRes.json();
74
+ setHistoricalActivities(data.events || []);
75
+ }
76
+ }, [authFetch, currentProjectId]);
47
77
 
48
- // --- Visualization Grid ---
78
+ useEffect(() => {
79
+ fetchData();
80
+ }, [fetchData, statusChangeCounter]);
81
+
82
+ // Real-time task updates from telemetry (same pattern as TasksPage)
83
+ useEffect(() => {
84
+ if (!taskTelemetryEvents.length) return;
85
+ const latestEvent = taskTelemetryEvents[0];
86
+ if (!latestEvent || latestEvent.id === lastProcessedTaskEventRef.current) return;
87
+ const eventType = latestEvent.type;
88
+ if (eventType === "task_created" || eventType === "task_updated" || eventType === "task_deleted") {
89
+ lastProcessedTaskEventRef.current = latestEvent.id;
90
+ fetchData();
91
+ }
92
+ }, [taskTelemetryEvents, fetchData]);
93
+
94
+ // Merge realtime + historical thread_activity
95
+ const activities = useMemo(() => {
96
+ const realtimeThreadEvents = realtimeEvents.filter(e => e.type === "thread_activity");
97
+ const seen = new Set(realtimeThreadEvents.map(e => e.id));
98
+ const merged = [...realtimeThreadEvents];
99
+ for (const evt of historicalActivities) {
100
+ if (!seen.has(evt.id)) {
101
+ merged.push(evt);
102
+ seen.add(evt.id);
103
+ }
104
+ }
105
+ let filtered = merged.filter(e => agentIds.has(e.agent_id));
106
+ filtered.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
107
+ return filtered.slice(0, 50);
108
+ }, [realtimeEvents, historicalActivities, agentIds]);
49
109
 
50
- function ActivityVisualization({ agents, loading, selectedAgentId, onSelectAgent }: {
51
- agents: Agent[];
52
- loading: boolean;
53
- selectedAgentId: string | null;
54
- onSelectAgent: (id: string | null) => void;
55
- }) {
56
110
  if (loading) {
57
- return <div className="flex items-center justify-center h-full text-[#666]">Loading agents...</div>;
111
+ return <div className="flex-1 flex items-center justify-center text-[#666]">Loading...</div>;
58
112
  }
59
113
 
60
- if (agents.length === 0) {
61
- return (
62
- <div className="flex items-center justify-center h-full text-[#666]">
63
- <div className="text-center">
64
- <p className="text-lg">No agents found</p>
65
- <p className="text-sm text-[#444] mt-1">Create and start agents to see them here</p>
114
+ return (
115
+ <div className="flex-1 flex flex-col overflow-hidden">
116
+ {/* Header */}
117
+ <div className="px-6 pt-6 pb-4 shrink-0">
118
+ <div className="flex items-center justify-between">
119
+ <h1 className="text-xl font-semibold">Activity</h1>
120
+ <span className="text-sm text-[#666]">
121
+ {runningCount} of {filteredAgents.length} agents running
122
+ </span>
66
123
  </div>
67
124
  </div>
68
- );
69
- }
70
125
 
71
- const runningCount = agents.filter(a => a.status === "running").length;
126
+ {/* Three-column layout: 1/4 | 3/8 | 3/8 */}
127
+ <div className="flex-1 flex min-h-0 overflow-hidden">
128
+ {/* Left: Agents (1/4) */}
129
+ <div className="flex-[2] flex flex-col overflow-hidden border-r border-[#1a1a1a]">
130
+ <div className="px-4 py-2.5 border-b border-[#1a1a1a] shrink-0">
131
+ <h3 className="text-xs font-semibold text-[#666] uppercase tracking-wider">Agents</h3>
132
+ </div>
133
+ <div className="flex-1 overflow-auto px-3 py-2">
134
+ {sortedAgents.length === 0 ? (
135
+ <p className="text-sm text-[#555] px-2 py-4 text-center">No agents found</p>
136
+ ) : (
137
+ <div className="space-y-1">
138
+ {sortedAgents.map(agent => (
139
+ <AgentRow
140
+ key={agent.id}
141
+ agent={agent}
142
+ selected={selectedAgentId === agent.id}
143
+ onSelect={() => setSelectedAgentId(selectedAgentId === agent.id ? null : agent.id)}
144
+ />
145
+ ))}
146
+ </div>
147
+ )}
72
148
 
73
- return (
74
- <div className="h-full flex flex-col">
75
- <div className="mb-4 flex items-center justify-between">
76
- <h2 className="text-lg font-semibold">Activity</h2>
77
- <span className="text-sm text-[#666]">
78
- {runningCount} of {agents.length} running
79
- </span>
80
- </div>
81
- <div className="flex-1 flex items-center justify-center">
82
- <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-8">
83
- {agents.map(agent => (
84
- <AgentNode
85
- key={agent.id}
86
- agent={agent}
87
- selected={selectedAgentId === agent.id}
88
- onClick={() => onSelectAgent(selectedAgentId === agent.id ? null : agent.id)}
89
- />
90
- ))}
149
+ {selectedAgentId && (
150
+ <InlineCommand
151
+ agent={filteredAgents.find(a => a.id === selectedAgentId) || null}
152
+ />
153
+ )}
154
+ </div>
155
+ </div>
156
+
157
+ {/* Center: Activity Feed (3/8) */}
158
+ <div className="flex-[3] flex flex-col min-h-0 overflow-hidden border-r border-[#1a1a1a]">
159
+ <div className="px-4 py-2.5 border-b border-[#1a1a1a] flex items-center justify-between shrink-0">
160
+ <h3 className="text-xs font-semibold text-[#666] uppercase tracking-wider">Activity Feed</h3>
161
+ <span className="text-xs text-[#555]">{activities.length}</span>
162
+ </div>
163
+ <div className="flex-1 overflow-auto">
164
+ {activities.length === 0 ? (
165
+ <div className="p-6 text-center text-[#555] text-sm">
166
+ No activity yet. Agent activity will appear here in real-time.
167
+ </div>
168
+ ) : (
169
+ <div className="divide-y divide-[#1a1a1a]">
170
+ {activities.map(evt => (
171
+ <div key={evt.id} className="px-4 py-2.5 hover:bg-[#111]/50 transition">
172
+ <p className="text-sm truncate">{(evt.data?.activity as string) || "Working..."}</p>
173
+ <div className="flex items-center gap-2 text-[10px] text-[#555] mt-0.5">
174
+ <span className="text-[#666]">{agentNameMap.get(evt.agent_id) || evt.agent_id}</span>
175
+ <span className="text-[#444]">&middot;</span>
176
+ <span>{timeAgo(evt.timestamp)}</span>
177
+ </div>
178
+ </div>
179
+ ))}
180
+ </div>
181
+ )}
182
+ </div>
183
+ </div>
184
+
185
+ {/* Right: Tasks (3/8) */}
186
+ <div className="flex-[3] flex flex-col overflow-hidden">
187
+ <div className="px-4 py-2.5 border-b border-[#1a1a1a] flex items-center justify-between shrink-0">
188
+ <h3 className="text-xs font-semibold text-[#666] uppercase tracking-wider">Tasks</h3>
189
+ {onNavigate && (
190
+ <button onClick={() => onNavigate("tasks")} className="text-xs text-[#3b82f6] hover:text-[#60a5fa]">
191
+ View All
192
+ </button>
193
+ )}
194
+ </div>
195
+ <div className="flex-1 overflow-auto px-3 py-3">
196
+ {tasks.length === 0 ? (
197
+ <p className="text-sm text-[#555] px-2 py-4 text-center">No tasks yet</p>
198
+ ) : (
199
+ <div className="space-y-2.5">
200
+ {tasks.map(task => (
201
+ <TaskCard key={`${task.agentId}-${task.id}`} task={task} />
202
+ ))}
203
+ </div>
204
+ )}
205
+ </div>
91
206
  </div>
92
207
  </div>
93
208
  </div>
94
209
  );
95
210
  }
96
211
 
97
- // --- Agent Node ---
212
+ // --- Agent Row ---
98
213
 
99
- function AgentNode({ agent, selected, onClick }: {
214
+ function AgentRow({ agent, selected, onSelect }: {
100
215
  agent: Agent;
101
216
  selected: boolean;
102
- onClick: () => void;
217
+ onSelect: () => void;
103
218
  }) {
104
219
  const { isActive, type } = useAgentActivity(agent.id);
105
220
  const isRunning = agent.status === "running";
106
221
 
107
- const ringStyle = selected
108
- ? "ring-2 ring-[#f97316] shadow-[0_0_12px_rgba(249,115,22,0.3)]"
109
- : isRunning && isActive
110
- ? "ring-2 ring-green-400"
111
- : isRunning
112
- ? "ring-1 ring-[#3b82f6]/60"
113
- : "ring-1 ring-[#333]";
114
-
115
- const bgClass = isRunning
116
- ? isActive ? "bg-green-500/10" : "bg-[#1a1a1a]"
117
- : "bg-[#111]";
118
-
119
- const textClass = isRunning ? "text-[#e0e0e0]" : "text-[#555]";
120
-
121
222
  return (
122
223
  <button
123
- onClick={onClick}
124
- className="flex flex-col items-center gap-2 group"
224
+ onClick={onSelect}
225
+ className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition ${
226
+ selected
227
+ ? "bg-[#f97316]/10 border border-[#f97316]/30"
228
+ : "hover:bg-[#1a1a1a] border border-transparent"
229
+ }`}
125
230
  >
126
- <div className={`w-16 h-16 rounded-full ${bgClass} ${ringStyle} flex items-center justify-center transition-all duration-300 group-hover:scale-110 relative`}>
127
- <span className={`text-xl font-semibold ${textClass}`}>
128
- {agent.name.charAt(0).toUpperCase()}
129
- </span>
130
- {isActive && isRunning && (
131
- <div className="absolute inset-0 rounded-full bg-green-400/20 animate-ping" style={{ animationDuration: "1.5s" }} />
132
- )}
133
- {isRunning && isActive && (
134
- <div className="absolute inset-0 rounded-full animate-pulse" style={{ boxShadow: "0 0 12px 3px rgba(74, 222, 128, 0.4)" }} />
135
- )}
136
- </div>
137
- <div className="text-center max-w-[100px]">
138
- <p className={`text-xs font-medium truncate ${textClass}`}>{agent.name}</p>
231
+ <span
232
+ className={`w-2.5 h-2.5 rounded-full shrink-0 ${
233
+ isRunning && isActive
234
+ ? "bg-green-400 animate-pulse"
235
+ : isRunning
236
+ ? "bg-[#3b82f6]"
237
+ : "bg-[#444]"
238
+ }`}
239
+ />
240
+ <div className="flex-1 min-w-0">
241
+ <div className="flex items-center gap-2">
242
+ <span className={`text-sm font-medium truncate ${isRunning ? "" : "text-[#666]"}`}>
243
+ {agent.name}
244
+ </span>
245
+ <span className="text-[10px] text-[#555] shrink-0">{agent.provider}</span>
246
+ </div>
139
247
  {isActive && type ? (
140
- <p className="text-[10px] text-green-400 truncate">{type}</p>
248
+ <p className="text-[11px] text-green-400 truncate">{type}</p>
141
249
  ) : (
142
- <p className={`text-[10px] ${isRunning ? "text-[#3b82f6]" : "text-[#444]"}`}>
250
+ <p className={`text-[11px] ${isRunning ? "text-[#555]" : "text-[#444]"}`}>
143
251
  {isRunning ? "idle" : "stopped"}
144
252
  </p>
145
253
  )}
@@ -148,74 +256,9 @@ function AgentNode({ agent, selected, onClick }: {
148
256
  );
149
257
  }
150
258
 
151
- // --- Live Activity Stream ---
152
-
153
- const categoryColors: Record<string, string> = {
154
- LLM: "bg-purple-500/20 text-purple-400",
155
- TOOL: "bg-blue-500/20 text-blue-400",
156
- CHAT: "bg-green-500/20 text-green-400",
157
- ERROR: "bg-red-500/20 text-red-400",
158
- SYSTEM: "bg-gray-500/20 text-gray-400",
159
- TASK: "bg-yellow-500/20 text-yellow-400",
160
- MEMORY: "bg-cyan-500/20 text-cyan-400",
161
- MCP: "bg-orange-500/20 text-orange-400",
162
- };
163
-
164
- function LiveActivityStream({ agents }: { agents: Agent[] }) {
165
- const { events } = useTelemetryContext();
166
- const scrollRef = useRef<HTMLDivElement>(null);
167
-
168
- const agentIds = useMemo(() => new Set(agents.map(a => a.id)), [agents]);
169
- const agentNameMap = useMemo(() => {
170
- const map = new Map<string, string>();
171
- agents.forEach(a => map.set(a.id, a.name));
172
- return map;
173
- }, [agents]);
174
-
175
- const filteredEvents = useMemo(() => {
176
- return events
177
- .filter(e => agentIds.has(e.agent_id))
178
- .slice(0, 50);
179
- }, [events, agentIds]);
180
-
181
- return (
182
- <div className="flex-1 flex flex-col overflow-hidden border-l border-[#1a1a1a]">
183
- <div className="px-4 py-3 border-b border-[#1a1a1a] flex items-center justify-between shrink-0">
184
- <h3 className="font-semibold text-sm">Live Activity</h3>
185
- <span className="text-xs text-[#666]">{filteredEvents.length} events</span>
186
- </div>
187
- <div ref={scrollRef} className="flex-1 overflow-auto">
188
- {filteredEvents.length === 0 ? (
189
- <div className="p-4 text-center text-[#666] text-sm">
190
- No activity yet. Events appear in real-time.
191
- </div>
192
- ) : (
193
- <div className="divide-y divide-[#1a1a1a]">
194
- {filteredEvents.map(event => (
195
- <div key={event.id} className="px-4 py-2 hover:bg-[#111] transition" style={{ animation: "slideIn 0.3s ease-out" }}>
196
- <div className="flex items-center gap-2">
197
- <span className={`px-1.5 py-0.5 rounded text-[10px] font-medium ${categoryColors[event.category] || "bg-[#222] text-[#888]"}`}>
198
- {event.category}
199
- </span>
200
- <span className="text-xs font-medium truncate flex-1">{event.type}</span>
201
- <span className="text-[10px] text-[#555] shrink-0">{timeAgo(event.timestamp)}</span>
202
- </div>
203
- <div className="text-[10px] text-[#555] mt-0.5">
204
- {agentNameMap.get(event.agent_id) || event.agent_id}
205
- {event.duration_ms ? ` \u00b7 ${event.duration_ms}ms` : ""}
206
- </div>
207
- </div>
208
- ))}
209
- </div>
210
- )}
211
- </div>
212
- </div>
213
- );
214
- }
215
-
216
- // --- Quick Command Panel ---
259
+ // --- Inline Command ---
217
260
 
218
- function QuickCommandPanel({ agent, onClose }: { agent: Agent | null; onClose: () => void }) {
261
+ function InlineCommand({ agent }: { agent: Agent | null }) {
219
262
  const { authFetch } = useAuth();
220
263
  const [command, setCommand] = useState("");
221
264
  const [sending, setSending] = useState(false);
@@ -226,17 +269,13 @@ function QuickCommandPanel({ agent, onClose }: { agent: Agent | null; onClose: (
226
269
  setToast(null);
227
270
  }, [agent?.id]);
228
271
 
229
- if (!agent) {
230
- return (
231
- <div className="w-80 shrink-0 flex items-center justify-center text-[#555] text-sm p-4 text-center">
232
- Click an agent to send a quick command
233
- </div>
234
- );
235
- }
272
+ if (!agent) return null;
273
+
274
+ const isRunning = agent.status === "running";
236
275
 
237
276
  const handleSend = async () => {
238
277
  if (!command.trim() || sending) return;
239
- if (agent.status !== "running") {
278
+ if (!isRunning) {
240
279
  setToast("Agent is not running");
241
280
  setTimeout(() => setToast(null), 3000);
242
281
  return;
@@ -249,63 +288,96 @@ function QuickCommandPanel({ agent, onClose }: { agent: Agent | null; onClose: (
249
288
  body: JSON.stringify({ message: command, agent_id: agent.id }),
250
289
  });
251
290
  if (res.ok) {
252
- setToast("Command sent");
291
+ setToast("Sent");
253
292
  setCommand("");
254
293
  } else {
255
294
  const data = await res.json().catch(() => ({}));
256
- setToast(data.error || "Failed to send");
295
+ setToast(data.error || "Failed");
257
296
  }
258
297
  } catch {
259
- setToast("Failed to send command");
298
+ setToast("Failed to send");
260
299
  } finally {
261
300
  setSending(false);
262
301
  setTimeout(() => setToast(null), 3000);
263
302
  }
264
303
  };
265
304
 
266
- const isRunning = agent.status === "running";
305
+ return (
306
+ <div className="mt-2 bg-[#0a0a0a] border border-[#1a1a1a] rounded-lg p-2.5">
307
+ <div className="flex items-center justify-between mb-1.5">
308
+ <span className="text-[10px] text-[#666]">
309
+ Send to <span className="text-[#888]">{agent.name}</span>
310
+ </span>
311
+ {toast && (
312
+ <span className={`text-[10px] px-1.5 py-0.5 rounded ${
313
+ toast === "Sent" ? "bg-green-500/10 text-green-400" : "bg-red-500/10 text-red-400"
314
+ }`}>{toast}</span>
315
+ )}
316
+ </div>
317
+ <div className="flex gap-1.5">
318
+ <input
319
+ type="text"
320
+ value={command}
321
+ onChange={e => setCommand(e.target.value)}
322
+ onKeyDown={e => e.key === "Enter" && handleSend()}
323
+ placeholder={isRunning ? "Command..." : "Not running"}
324
+ disabled={sending || !isRunning}
325
+ autoFocus
326
+ className="flex-1 bg-[#111] border border-[#1a1a1a] rounded px-2 py-1.5 text-xs focus:outline-none focus:border-[#f97316] placeholder-[#444] disabled:opacity-50"
327
+ />
328
+ <button
329
+ onClick={handleSend}
330
+ disabled={sending || !command.trim() || !isRunning}
331
+ className="px-2.5 py-1.5 bg-[#f97316]/20 text-[#f97316] rounded text-xs font-medium hover:bg-[#f97316]/30 transition disabled:opacity-30"
332
+ >
333
+ {sending ? "..." : "Send"}
334
+ </button>
335
+ </div>
336
+ </div>
337
+ );
338
+ }
339
+
340
+ // --- Task Card (matches TasksPage style) ---
341
+
342
+ const taskStatusColors: Record<string, string> = {
343
+ pending: "bg-yellow-500/20 text-yellow-400",
344
+ running: "bg-blue-500/20 text-blue-400",
345
+ completed: "bg-green-500/20 text-green-400",
346
+ failed: "bg-red-500/20 text-red-400",
347
+ cancelled: "bg-gray-500/20 text-gray-400",
348
+ };
349
+
350
+ const TASK_DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
267
351
 
352
+ function TaskCard({ task }: { task: Task }) {
268
353
  return (
269
- <div className="w-80 shrink-0 flex flex-col">
270
- <div className="px-4 py-3 border-b border-[#1a1a1a] flex items-center justify-between shrink-0">
271
- <div className="min-w-0">
272
- <h3 className="font-semibold text-sm truncate">{agent.name}</h3>
273
- <p className={`text-[10px] ${isRunning ? "text-green-400" : "text-[#666]"}`}>
274
- {isRunning ? "Running" : "Stopped"}
275
- </p>
354
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-3 hover:border-[#333] transition">
355
+ <div className="flex items-start justify-between mb-1.5">
356
+ <div className="flex-1 min-w-0">
357
+ <h4 className="text-sm font-medium truncate">{task.title}</h4>
358
+ <p className="text-xs text-[#666]">{task.agentName}</p>
276
359
  </div>
277
- <button onClick={onClose} className="text-[#666] hover:text-[#e0e0e0] transition shrink-0 ml-2">
278
- <CloseIcon className="w-4 h-4" />
279
- </button>
360
+ <span className={`px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0 ml-2 ${taskStatusColors[task.status] || taskStatusColors.pending}`}>
361
+ {task.status}
362
+ </span>
280
363
  </div>
281
- <div className="flex-1 p-4 flex flex-col justify-end">
282
- {toast && (
283
- <div className={`mb-3 px-3 py-2 rounded text-xs ${
284
- toast === "Command sent"
285
- ? "bg-green-500/10 border border-green-500/20 text-green-400"
286
- : "bg-red-500/10 border border-red-500/20 text-red-400"
287
- }`}>
288
- {toast}
289
- </div>
364
+
365
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-[#555]">
366
+ <span className="flex items-center gap-1">
367
+ {task.type === "recurring"
368
+ ? <RecurringIcon className="w-3 h-3" />
369
+ : task.execute_at
370
+ ? <ScheduledIcon className="w-3 h-3" />
371
+ : <TaskOnceIcon className="w-3 h-3" />
372
+ }
373
+ {task.type === "recurring" && task.recurrence ? formatCronShort(task.recurrence) : task.type}
374
+ </span>
375
+ {task.next_run && (
376
+ <span className="text-[#f97316]">{formatTaskRelative(task.next_run)}</span>
377
+ )}
378
+ {!task.next_run && task.execute_at && (
379
+ <span className="text-[#f97316]">{formatTaskRelative(task.execute_at)}</span>
290
380
  )}
291
- <div className="flex gap-2">
292
- <input
293
- type="text"
294
- value={command}
295
- onChange={e => setCommand(e.target.value)}
296
- onKeyDown={e => e.key === "Enter" && handleSend()}
297
- placeholder={isRunning ? "Quick command..." : "Agent not running"}
298
- disabled={sending || !isRunning}
299
- className="flex-1 bg-[#111] border border-[#1a1a1a] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#f97316] placeholder-[#444] disabled:opacity-50"
300
- />
301
- <button
302
- onClick={handleSend}
303
- disabled={sending || !command.trim() || !isRunning}
304
- className="px-3 py-2 bg-[#f97316]/20 text-[#f97316] rounded text-sm font-medium hover:bg-[#f97316]/30 transition disabled:opacity-30"
305
- >
306
- {sending ? "..." : "Send"}
307
- </button>
308
- </div>
309
381
  </div>
310
382
  </div>
311
383
  );
@@ -313,6 +385,69 @@ function QuickCommandPanel({ agent, onClose }: { agent: Agent | null; onClose: (
313
385
 
314
386
  // --- Helpers ---
315
387
 
388
+ function formatCronShort(cron: string): string {
389
+ try {
390
+ const parts = cron.trim().split(/\s+/);
391
+ if (parts.length !== 5) return cron;
392
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
393
+
394
+ if (minute.startsWith("*/") && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
395
+ const n = parseInt(minute.slice(2));
396
+ return n === 1 ? "Every min" : `Every ${n}min`;
397
+ }
398
+ if (minute !== "*" && !minute.includes("/") && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
399
+ return "Hourly";
400
+ }
401
+ if (hour.startsWith("*/") && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
402
+ const n = parseInt(hour.slice(2));
403
+ return n === 1 ? "Hourly" : `Every ${n}h`;
404
+ }
405
+
406
+ const formatTime = (h: string, m: string): string => {
407
+ const hr = parseInt(h);
408
+ const mn = parseInt(m);
409
+ if (isNaN(hr)) return "";
410
+ const ampm = hr >= 12 ? "PM" : "AM";
411
+ const h12 = hr === 0 ? 12 : hr > 12 ? hr - 12 : hr;
412
+ return `${h12}:${mn.toString().padStart(2, "0")} ${ampm}`;
413
+ };
414
+
415
+ if (hour !== "*" && !hour.includes("/") && dayOfMonth === "*" && month === "*") {
416
+ const timeStr = formatTime(hour, minute);
417
+ if (dayOfWeek === "*") return `Daily ${timeStr}`;
418
+ const days = dayOfWeek.split(",").map(d => TASK_DAY_NAMES[parseInt(d.trim())] || d);
419
+ if (days.length === 1) return `${days[0]} ${timeStr}`;
420
+ return `${days.join(", ")} ${timeStr}`;
421
+ }
422
+ return cron;
423
+ } catch {
424
+ return cron;
425
+ }
426
+ }
427
+
428
+ function formatTaskRelative(dateStr: string): string {
429
+ const date = new Date(dateStr);
430
+ const now = new Date();
431
+ const diffMs = date.getTime() - now.getTime();
432
+ const isFuture = diffMs > 0;
433
+ const absDiffMs = Math.abs(diffMs);
434
+ const minutes = Math.floor(absDiffMs / 60000);
435
+ const hours = Math.floor(absDiffMs / 3600000);
436
+ const timeStr = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
437
+ const isToday = date.toDateString() === now.toDateString();
438
+ const tomorrow = new Date(now);
439
+ tomorrow.setDate(tomorrow.getDate() + 1);
440
+ const isTomorrow = date.toDateString() === tomorrow.toDateString();
441
+
442
+ if (isToday) {
443
+ if (minutes < 1) return "now";
444
+ if (minutes < 60) return isFuture ? `in ${minutes}m` : `${minutes}m ago`;
445
+ return isFuture ? `in ${hours}h (${timeStr})` : `${hours}h ago`;
446
+ }
447
+ if (isTomorrow) return `Tomorrow ${timeStr}`;
448
+ return `${TASK_DAY_NAMES[date.getDay()]} ${timeStr}`;
449
+ }
450
+
316
451
  function timeAgo(timestamp: string): string {
317
452
  const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000);
318
453
  if (seconds < 5) return "just now";