apteva 0.4.31 → 0.4.41

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 (89) hide show
  1. package/dist/ActivityPage.7907h64p.js +3 -0
  2. package/dist/ApiDocsPage.k3jjenpq.js +4 -0
  3. package/dist/App.01nq20st.js +4 -0
  4. package/dist/App.1maqvamf.js +4 -0
  5. package/dist/App.2yjrh32f.js +4 -0
  6. package/dist/App.3qw8nben.js +20 -0
  7. package/dist/App.7fb3e7mp.js +4 -0
  8. package/dist/App.7sy3wq8c.js +4 -0
  9. package/dist/App.apjrmctz.js +57 -0
  10. package/dist/App.av6t2yhe.js +4 -0
  11. package/dist/App.jqj5a094.js +46 -0
  12. package/dist/App.mc7xf85h.js +4 -0
  13. package/dist/App.myxqcj9x.js +4 -0
  14. package/dist/App.nm91r1mp.js +13 -0
  15. package/dist/App.qcknavjz.js +221 -0
  16. package/dist/App.vc7vfhg4.js +4 -0
  17. package/dist/App.z4s9zkw5.js +4 -0
  18. package/dist/ConnectionsPage.z1pw5xe2.js +3 -0
  19. package/dist/McpPage.8vc97z0b.js +3 -0
  20. package/dist/SettingsPage.p61bz8kd.js +3 -0
  21. package/dist/SkillsPage.r9x43g3g.js +3 -0
  22. package/dist/TasksPage.1e0zkye4.js +3 -0
  23. package/dist/TelemetryPage.p9vbe4gf.js +3 -0
  24. package/dist/TestsPage.d4xy504e.js +3 -0
  25. package/dist/ThreadsPage.m016am3x.js +3 -0
  26. package/dist/index.html +1 -1
  27. package/dist/styles.css +1 -1
  28. package/package.json +8 -7
  29. package/src/crypto.ts +4 -3
  30. package/src/db.ts +153 -28
  31. package/src/integrations/agentdojo.ts +94 -12
  32. package/src/integrations/index.ts +7 -0
  33. package/src/mcp-platform.ts +494 -121
  34. package/src/providers.ts +12 -12
  35. package/src/routes/api/agent-utils.ts +59 -46
  36. package/src/routes/api/agents.ts +52 -1
  37. package/src/routes/api/integrations.ts +11 -5
  38. package/src/routes/api/mcp.ts +5 -4
  39. package/src/routes/api/meta-agent.ts +35 -1
  40. package/src/routes/api/projects.ts +3 -3
  41. package/src/routes/api/providers.ts +121 -30
  42. package/src/routes/api/skills.ts +2 -3
  43. package/src/routes/api/system.ts +8 -13
  44. package/src/server.ts +31 -32
  45. package/src/triggers/agentdojo.ts +2 -2
  46. package/src/web/App.tsx +18 -10
  47. package/src/web/components/activity/ActivityPage.tsx +241 -388
  48. package/src/web/components/agents/AgentCard.tsx +5 -13
  49. package/src/web/components/common/Icons.tsx +8 -0
  50. package/src/web/components/common/Select.tsx +4 -3
  51. package/src/web/components/dashboard/Dashboard.tsx +155 -30
  52. package/src/web/components/index.ts +1 -1
  53. package/src/web/components/layout/Sidebar.tsx +7 -1
  54. package/src/web/components/mcp/IntegrationsPanel.tsx +126 -35
  55. package/src/web/components/mcp/McpPage.tsx +10 -1
  56. package/src/web/components/meta-agent/MetaAgent.tsx +4 -2
  57. package/src/web/components/settings/SettingsPage.tsx +133 -48
  58. package/src/web/components/tasks/TasksPage.tsx +48 -16
  59. package/src/web/components/telemetry/TelemetryPage.tsx +184 -0
  60. package/src/web/components/threads/ThreadsPage.tsx +313 -0
  61. package/src/web/context/AuthContext.tsx +3 -3
  62. package/src/web/context/ProjectContext.tsx +3 -3
  63. package/src/web/context/TelemetryContext.tsx +24 -6
  64. package/src/web/context/index.ts +1 -1
  65. package/src/web/styles.css +20 -4
  66. package/src/web/types.ts +4 -3
  67. package/dist/ActivityPage.41nbye4r.js +0 -3
  68. package/dist/ApiDocsPage.4smnt8m3.js +0 -4
  69. package/dist/App.0sbax9et.js +0 -4
  70. package/dist/App.0ws427h8.js +0 -4
  71. package/dist/App.6q6bar8b.js +0 -4
  72. package/dist/App.80301vdb.js +0 -4
  73. package/dist/App.af2wg84v.js +0 -267
  74. package/dist/App.ca1rz1ph.js +0 -4
  75. package/dist/App.ensa6z0r.js +0 -4
  76. package/dist/App.f8g7tych.js +0 -13
  77. package/dist/App.mvtqv6qc.js +0 -20
  78. package/dist/App.ncgc9cxy.js +0 -4
  79. package/dist/App.p0fb1pds.js +0 -4
  80. package/dist/App.pmaq48sj.js +0 -4
  81. package/dist/App.yv87t9m5.js +0 -4
  82. package/dist/App.zjmfm8p6.js +0 -4
  83. package/dist/ConnectionsPage.anb3rv9a.js +0 -3
  84. package/dist/McpPage.y396h6fy.js +0 -3
  85. package/dist/SettingsPage.p1hc60gk.js +0 -3
  86. package/dist/SkillsPage.yj3xdsay.js +0 -3
  87. package/dist/TasksPage.sjv0khtv.js +0 -3
  88. package/dist/TelemetryPage.2qm4w16r.js +0 -3
  89. package/dist/TestsPage.zzs4qfj8.js +0 -3
@@ -1,9 +1,8 @@
1
1
  import React, { useState, useMemo, useEffect, useCallback, useRef } from "react";
2
- import { useAgentActivity, useAuth, useProjects, useTelemetryContext } from "../../context";
3
- import { useTelemetry } from "../../context/TelemetryContext";
2
+ import { useAuth, useProjects, useTelemetryContext } from "../../context";
4
3
  import type { TelemetryEvent } from "../../context";
5
- import type { Agent, Task, Route } from "../../types";
6
- import { RecurringIcon, ScheduledIcon, TaskOnceIcon } from "../common/Icons";
4
+ import type { Agent, Route } from "../../types";
5
+ import { Select } from "../common/Select";
7
6
 
8
7
  interface ActivityPageProps {
9
8
  agents: Agent[];
@@ -11,15 +10,97 @@ interface ActivityPageProps {
11
10
  onNavigate?: (route: Route) => void;
12
11
  }
13
12
 
13
+ // Event types we show in the timeline (skip noisy internal ones)
14
+ const VISIBLE_TYPES = new Set([
15
+ "thread_activity",
16
+ "agent_started",
17
+ "agent_stopped",
18
+ "agent_error",
19
+ "task_created",
20
+ "task_updated",
21
+ "task_deleted",
22
+ "task_executed",
23
+ "llm_request",
24
+ "tool_invocation",
25
+ "mcp_request",
26
+ "mcp_tool_execution",
27
+ ]);
28
+
29
+ // Category colors for the timeline dot
30
+ const CATEGORY_COLORS: Record<string, string> = {
31
+ CHAT: "bg-green-400",
32
+ LLM: "bg-purple-400",
33
+ TOOL: "bg-blue-400",
34
+ TASK: "bg-yellow-400",
35
+ MEMORY: "bg-cyan-400",
36
+ MCP: "bg-orange-400",
37
+ SYSTEM: "bg-gray-400",
38
+ ERROR: "bg-red-400",
39
+ };
40
+
41
+ function describeEvent(evt: TelemetryEvent, agentName: string): string {
42
+ const data = evt.data || {};
43
+ switch (evt.type) {
44
+ case "thread_activity":
45
+ return (data.activity as string) || "Working...";
46
+ case "agent_started":
47
+ return "Agent started";
48
+ case "agent_stopped":
49
+ return data.reason ? `Agent stopped (${data.reason})` : "Agent stopped";
50
+ case "agent_error":
51
+ return evt.error || "Agent error";
52
+ case "llm_request":
53
+ return "Thinking...";
54
+ case "tool_invocation": {
55
+ const toolRaw = (data.tool_name || "") as string;
56
+ if (!toolRaw) return "Using tools";
57
+ const toolFormatted = toolRaw.replace(/[-_]/g, " ").replace(/\b\w/g, c => c.toUpperCase());
58
+ return `Tool: ${toolFormatted}`;
59
+ }
60
+ case "task_created":
61
+ return data.title ? `Task created: ${data.title}` : "Task created";
62
+ case "task_updated": {
63
+ const status = data.status as string | undefined;
64
+ const title = data.title as string | undefined;
65
+ const statusLabel = status ? status.charAt(0).toUpperCase() + status.slice(1) : null;
66
+ if (title && statusLabel) return `Task ${statusLabel}: ${title}`;
67
+ if (statusLabel) return `Task ${statusLabel}`;
68
+ if (title) return `Task updated: ${title}`;
69
+ return "Task updated";
70
+ }
71
+ case "task_deleted":
72
+ return "Task deleted";
73
+ case "task_executed": {
74
+ const title = data.title as string | undefined;
75
+ return title ? `Task started: ${title}` : "Task started";
76
+ }
77
+ case "memory_stored":
78
+ return "Memory stored";
79
+ case "memory_retrieved":
80
+ return "Memory retrieved";
81
+ case "mcp_request":
82
+ return data.server ? `MCP request to ${data.server}` : "MCP request";
83
+ case "mcp_tool_execution": {
84
+ const rawName = (data.tool_name || data.tool || "") as string;
85
+ if (!rawName) return "MCP tool call";
86
+ // "Server__tool-name-here" -> take part after __, format dashes/underscores to spaces, title case
87
+ const toolPart = rawName.includes("__") ? rawName.split("__").slice(1).join("__") : rawName;
88
+ const formatted = toolPart.replace(/[-_]/g, " ").replace(/\b\w/g, c => c.toUpperCase());
89
+ return `MCP: ${formatted}`;
90
+ }
91
+ default:
92
+ return evt.type.replace(/_/g, " ");
93
+ }
94
+ }
95
+
14
96
  export function ActivityPage({ agents, loading, onNavigate }: ActivityPageProps) {
15
97
  const { authFetch } = useAuth();
16
98
  const { currentProjectId } = useProjects();
17
99
  const { events: realtimeEvents, statusChangeCounter } = useTelemetryContext();
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" });
100
+ const [historicalEvents, setHistoricalEvents] = useState<TelemetryEvent[]>([]);
101
+ const [filterAgentId, setFilterAgentId] = useState<string>("");
102
+ const [seenIds] = useState(() => new Set<string>());
103
+ const hasLoadedHistory = useRef(false);
23
104
 
24
105
  const filteredAgents = useMemo(() => {
25
106
  if (currentProjectId === null) return agents;
@@ -27,16 +108,6 @@ export function ActivityPage({ agents, loading, onNavigate }: ActivityPageProps)
27
108
  return agents.filter(a => a.projectId === currentProjectId);
28
109
  }, [agents, currentProjectId]);
29
110
 
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]);
37
-
38
- const runningCount = useMemo(() => filteredAgents.filter(a => a.status === "running").length, [filteredAgents]);
39
-
40
111
  const agentIds = useMemo(() => new Set(filteredAgents.map(a => a.id)), [filteredAgents]);
41
112
  const agentNameMap = useMemo(() => {
42
113
  const map = new Map<string, string>();
@@ -44,410 +115,192 @@ export function ActivityPage({ agents, loading, onNavigate }: ActivityPageProps)
44
115
  return map;
45
116
  }, [filteredAgents]);
46
117
 
47
- // Fetch tasks + historical activity
48
- const fetchData = useCallback(async () => {
118
+ // Fetch historical events
119
+ const fetchHistory = useCallback(async () => {
49
120
  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
- }
121
+ try {
122
+ const res = await authFetch(`/api/telemetry/events?limit=200${projectParam}`);
123
+ if (res.ok) {
124
+ const data = await res.json();
125
+ setHistoricalEvents(data.events || []);
126
+ }
127
+ } catch {}
76
128
  }, [authFetch, currentProjectId]);
77
129
 
78
130
  useEffect(() => {
79
- fetchData();
80
- }, [fetchData, statusChangeCounter]);
131
+ fetchHistory();
132
+ }, [fetchHistory, statusChangeCounter]);
81
133
 
82
- // Real-time task updates from telemetry (same pattern as TasksPage)
134
+ // Mark historical events as seen so they don't animate
83
135
  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();
136
+ if (historicalEvents.length > 0 && !hasLoadedHistory.current) {
137
+ for (const evt of historicalEvents) {
138
+ seenIds.add(evt.id);
139
+ }
140
+ // Also mark any realtime events already present at mount
141
+ for (const evt of realtimeEvents) {
142
+ seenIds.add(evt.id);
143
+ }
144
+ hasLoadedHistory.current = true;
91
145
  }
92
- }, [taskTelemetryEvents, fetchData]);
146
+ }, [historicalEvents, realtimeEvents, seenIds]);
147
+
148
+ // Merge realtime + historical, filter, sort
149
+ const timeline = useMemo(() => {
150
+ const seen = new Set<string>();
151
+ const merged: TelemetryEvent[] = [];
152
+
153
+ const shouldShow = (evt: TelemetryEvent) => {
154
+ if (!VISIBLE_TYPES.has(evt.type) || !agentIds.has(evt.agent_id) || evt.data?.parent_id) return false;
155
+ // MCP tool: only show completed calls (with duration), skip the start event
156
+ if (evt.type === "mcp_tool_execution" && !evt.duration_ms) return false;
157
+ return true;
158
+ };
93
159
 
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)) {
160
+ for (const evt of realtimeEvents) {
161
+ if (!seen.has(evt.id) && shouldShow(evt)) {
101
162
  merged.push(evt);
102
163
  seen.add(evt.id);
103
164
  }
104
165
  }
105
- let filtered = merged.filter(e => agentIds.has(e.agent_id));
166
+ for (const evt of historicalEvents) {
167
+ if (!seen.has(evt.id) && shouldShow(evt)) {
168
+ merged.push(evt);
169
+ seen.add(evt.id);
170
+ }
171
+ }
172
+
173
+ let filtered = merged;
174
+ if (filterAgentId) {
175
+ filtered = filtered.filter(e => e.agent_id === filterAgentId);
176
+ }
177
+
106
178
  filtered.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
107
- return filtered.slice(0, 50);
108
- }, [realtimeEvents, historicalActivities, agentIds]);
179
+ return filtered.slice(0, 200);
180
+ }, [realtimeEvents, historicalEvents, agentIds, filterAgentId]);
181
+
182
+ // Group by date for section headers
183
+ const groupedTimeline = useMemo(() => {
184
+ const groups: { label: string; events: TelemetryEvent[] }[] = [];
185
+ let currentLabel = "";
186
+ const today = new Date().toDateString();
187
+ const yesterday = new Date(Date.now() - 86400000).toDateString();
188
+
189
+ for (const evt of timeline) {
190
+ const dateStr = new Date(evt.timestamp).toDateString();
191
+ const label = dateStr === today ? "Today" : dateStr === yesterday ? "Yesterday" : dateStr;
192
+ if (label !== currentLabel) {
193
+ currentLabel = label;
194
+ groups.push({ label, events: [] });
195
+ }
196
+ groups[groups.length - 1].events.push(evt);
197
+ }
198
+ return groups;
199
+ }, [timeline]);
109
200
 
110
201
  if (loading) {
111
202
  return <div className="flex-1 flex items-center justify-center text-[#666]">Loading...</div>;
112
203
  }
113
204
 
205
+ const runningCount = filteredAgents.filter(a => a.status === "running").length;
206
+
114
207
  return (
115
208
  <div className="flex-1 flex flex-col overflow-hidden">
116
209
  {/* Header */}
117
210
  <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>
123
- </div>
124
- </div>
125
-
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>
211
+ <div className="flex items-center justify-between gap-4">
212
+ <div>
213
+ <h1 className="text-xl font-semibold">Activity</h1>
214
+ <p className="text-sm text-[#666] mt-0.5">
215
+ {runningCount} of {filteredAgents.length} agents running
216
+ </p>
132
217
  </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
- )}
148
-
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
- )}
218
+ <div className="w-48">
219
+ <Select
220
+ value={filterAgentId}
221
+ onChange={setFilterAgentId}
222
+ placeholder="All agents"
223
+ options={[
224
+ { value: "", label: "All agents" },
225
+ ...filteredAgents.map(a => ({ value: a.id, label: a.name })),
226
+ ]}
227
+ />
182
228
  </div>
183
229
  </div>
230
+ </div>
184
231
 
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
- )}
232
+ {/* Timeline */}
233
+ <div className="flex-1 overflow-auto px-6 pb-6">
234
+ {timeline.length === 0 ? (
235
+ <div className="flex flex-col items-center justify-center py-20 text-[#555]">
236
+ <p className="text-lg mb-2">No activity yet</p>
237
+ <p className="text-sm">Agent activity will appear here in real-time.</p>
194
238
  </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
- ))}
239
+ ) : (
240
+ <div className="max-w-2xl">
241
+ {groupedTimeline.map(group => (
242
+ <div key={group.label}>
243
+ {/* Date header */}
244
+ <div className="sticky top-0 z-10 bg-[#0a0a0a]/95 backdrop-blur-sm py-2 mb-1">
245
+ <span className="text-xs font-semibold text-[#666] uppercase tracking-wider">
246
+ {group.label}
247
+ </span>
248
+ </div>
249
+
250
+ {/* Events */}
251
+ <div className="relative ml-3 border-l border-[#1a1a1a]">
252
+ {group.events.map(evt => {
253
+ const isNew = !seenIds.has(evt.id);
254
+ if (isNew) seenIds.add(evt.id); // mark seen after first render
255
+ const agentName = agentNameMap.get(evt.agent_id) || evt.agent_id.slice(0, 8);
256
+ const dotColor = evt.level === "error"
257
+ ? "bg-red-400"
258
+ : CATEGORY_COLORS[evt.category] || "bg-[#555]";
259
+
260
+ return (
261
+ <div
262
+ key={evt.id}
263
+ className={`relative pl-6 pr-3 py-2.5 hover:bg-[#111]/50 transition-colors ${
264
+ isNew ? "animate-slideIn" : ""
265
+ }`}
266
+ >
267
+ {/* Timeline dot */}
268
+ <span className={`absolute left-[-4.5px] top-[14px] w-[9px] h-[9px] rounded-full ${dotColor} ring-2 ring-[#0a0a0a]`} />
269
+
270
+ <div className="flex items-start justify-between gap-3">
271
+ <div className="flex-1 min-w-0">
272
+ <p className={`text-sm ${evt.level === "error" ? "text-red-400" : ""}`}>
273
+ {describeEvent(evt, agentName)}
274
+ </p>
275
+ <div className="flex items-center gap-2 text-[11px] text-[#555] mt-0.5">
276
+ <span className="text-[#888] font-medium">{agentName}</span>
277
+ <span className="text-[#333]">&middot;</span>
278
+ <span className="text-[#555]">{evt.category}</span>
279
+ {evt.duration_ms != null && evt.duration_ms > 0 && (
280
+ <>
281
+ <span className="text-[#333]">&middot;</span>
282
+ <span>{evt.duration_ms < 1000 ? `${evt.duration_ms}ms` : `${(evt.duration_ms / 1000).toFixed(1)}s`}</span>
283
+ </>
284
+ )}
285
+ </div>
286
+ </div>
287
+ <span className="text-[11px] text-[#555] shrink-0 pt-0.5">
288
+ {timeAgo(evt.timestamp)}
289
+ </span>
290
+ </div>
291
+ </div>
292
+ );
293
+ })}
294
+ </div>
203
295
  </div>
204
- )}
296
+ ))}
205
297
  </div>
206
- </div>
207
- </div>
208
- </div>
209
- );
210
- }
211
-
212
- // --- Agent Row ---
213
-
214
- function AgentRow({ agent, selected, onSelect }: {
215
- agent: Agent;
216
- selected: boolean;
217
- onSelect: () => void;
218
- }) {
219
- const { isActive, label } = useAgentActivity(agent.id);
220
- const isRunning = agent.status === "running";
221
-
222
- return (
223
- <button
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
- }`}
230
- >
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>
247
- {isActive && label ? (
248
- <p className="text-[11px] text-green-400 truncate">{label}</p>
249
- ) : (
250
- <p className={`text-[11px] ${isRunning ? "text-[#555]" : "text-[#444]"}`}>
251
- {isRunning ? "idle" : "stopped"}
252
- </p>
253
- )}
254
- </div>
255
- </button>
256
- );
257
- }
258
-
259
- // --- Inline Command ---
260
-
261
- function InlineCommand({ agent }: { agent: Agent | null }) {
262
- const { authFetch } = useAuth();
263
- const [command, setCommand] = useState("");
264
- const [sending, setSending] = useState(false);
265
- const [toast, setToast] = useState<string | null>(null);
266
-
267
- useEffect(() => {
268
- setCommand("");
269
- setToast(null);
270
- }, [agent?.id]);
271
-
272
- if (!agent) return null;
273
-
274
- const isRunning = agent.status === "running";
275
-
276
- const handleSend = async () => {
277
- if (!command.trim() || sending) return;
278
- if (!isRunning) {
279
- setToast("Agent is not running");
280
- setTimeout(() => setToast(null), 3000);
281
- return;
282
- }
283
- setSending(true);
284
- try {
285
- const res = await authFetch(`/api/agents/${agent.id}/chat`, {
286
- method: "POST",
287
- headers: { "Content-Type": "application/json" },
288
- body: JSON.stringify({ message: command, agent_id: agent.id }),
289
- });
290
- if (res.ok) {
291
- setToast("Sent");
292
- setCommand("");
293
- } else {
294
- const data = await res.json().catch(() => ({}));
295
- setToast(data.error || "Failed");
296
- }
297
- } catch {
298
- setToast("Failed to send");
299
- } finally {
300
- setSending(false);
301
- setTimeout(() => setToast(null), 3000);
302
- }
303
- };
304
-
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"];
351
-
352
- function TaskCard({ task }: { task: Task }) {
353
- return (
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>
359
- </div>
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>
363
- </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>
380
298
  )}
381
299
  </div>
382
300
  </div>
383
301
  );
384
302
  }
385
303
 
386
- // --- Helpers ---
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
-
451
304
  function timeAgo(timestamp: string): string {
452
305
  const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000);
453
306
  if (seconds < 5) return "just now";