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,5 +1,6 @@
1
1
  import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
2
2
  import { TasksIcon, CloseIcon, RecurringIcon, ScheduledIcon, TaskOnceIcon } from "../common/Icons";
3
+ import { Select } from "../common/Select";
3
4
  import { useAuth, useProjects } from "../../context";
4
5
  import { useTelemetry } from "../../context/TelemetryContext";
5
6
  import type { Task, TaskTrajectoryStep, ToolUseBlock, ToolResultBlock } from "../../types";
@@ -14,6 +15,7 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
14
15
  const [tasks, setTasks] = useState<Task[]>([]);
15
16
  const [loading, setLoading] = useState(true);
16
17
  const [filter, setFilter] = useState<string>("all");
18
+ const [agentFilter, setAgentFilter] = useState<string>("all");
17
19
  const [selectedTask, setSelectedTask] = useState<Task | null>(null);
18
20
  const [loadingTask, setLoadingTask] = useState(false);
19
21
  const lastProcessedEventRef = useRef<string | null>(null);
@@ -87,9 +89,23 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
87
89
  }
88
90
  }, [authFetch]);
89
91
 
92
+ // Extract unique agents from tasks for the agent filter
93
+ const uniqueAgents = useMemo(() => {
94
+ const map = new Map<string, string>();
95
+ for (const t of tasks) {
96
+ const id = t.agentId || (t as any).agent_id;
97
+ const name = t.agentName || (t as any).agent_name;
98
+ if (id && name && !map.has(id)) {
99
+ map.set(id, name);
100
+ }
101
+ }
102
+ return Array.from(map.entries()).sort((a, b) => a[1].localeCompare(b[1]));
103
+ }, [tasks]);
104
+
90
105
  // Sort tasks: running first, then pending by next execution (soonest first), then completed/failed by date
91
106
  const sortedTasks = useMemo(() => {
92
- return [...tasks].sort((a, b) => {
107
+ const filtered = agentFilter === "all" ? tasks : tasks.filter(t => (t.agentId || (t as any).agent_id) === agentFilter);
108
+ return [...filtered].sort((a, b) => {
93
109
  // Running tasks first
94
110
  if (a.status === "running" && b.status !== "running") return -1;
95
111
  if (b.status === "running" && a.status !== "running") return 1;
@@ -111,7 +127,7 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
111
127
  const bDate = b.completed_at || b.executed_at || b.created_at;
112
128
  return new Date(bDate).getTime() - new Date(aDate).getTime();
113
129
  });
114
- }, [tasks]);
130
+ }, [tasks, agentFilter]);
115
131
 
116
132
  const statusColors: Record<string, string> = {
117
133
  pending: "bg-yellow-500/20 text-yellow-400",
@@ -141,20 +157,36 @@ export function TasksPage({ onSelectAgent }: TasksPageProps) {
141
157
  View tasks from all running agents
142
158
  </p>
143
159
  </div>
144
- <div className="flex gap-2 overflow-x-auto scrollbar-hide pb-1">
145
- {filterOptions.map(opt => (
146
- <button
147
- key={opt.value}
148
- onClick={() => setFilter(opt.value)}
149
- className={`px-3 py-1.5 rounded text-sm transition whitespace-nowrap ${
150
- filter === opt.value
151
- ? "bg-[#f97316] text-black"
152
- : "bg-[#1a1a1a] hover:bg-[#222]"
153
- }`}
154
- >
155
- {opt.label}
156
- </button>
157
- ))}
160
+ <div className="flex items-center gap-3 flex-wrap pb-1">
161
+ <div className="flex gap-2 overflow-x-auto scrollbar-hide">
162
+ {filterOptions.map(opt => (
163
+ <button
164
+ key={opt.value}
165
+ onClick={() => setFilter(opt.value)}
166
+ className={`px-3 py-1.5 rounded text-sm transition whitespace-nowrap ${
167
+ filter === opt.value
168
+ ? "bg-[#f97316] text-black"
169
+ : "bg-[#1a1a1a] hover:bg-[#222]"
170
+ }`}
171
+ >
172
+ {opt.label}
173
+ </button>
174
+ ))}
175
+ </div>
176
+ {uniqueAgents.length > 0 && (
177
+ <div className="w-48">
178
+ <Select
179
+ value={agentFilter}
180
+ onChange={setAgentFilter}
181
+ placeholder="All agents"
182
+ compact
183
+ options={[
184
+ { value: "all", label: "All agents" },
185
+ ...uniqueAgents.map(([id, name]) => ({ value: id, label: name })),
186
+ ]}
187
+ />
188
+ </div>
189
+ )}
158
190
  </div>
159
191
  </div>
160
192
 
@@ -1,6 +1,10 @@
1
1
  import React, { useState, useEffect, useMemo, useRef, useCallback } from "react";
2
2
  import { Select } from "../common/Select";
3
3
  import { useTelemetryContext, useProjects, useAuth, type TelemetryEvent } from "../../context";
4
+ import {
5
+ AreaChart, Area, BarChart, Bar,
6
+ XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend,
7
+ } from "recharts";
4
8
 
5
9
  interface TelemetryStats {
6
10
  total_events: number;
@@ -20,6 +24,15 @@ interface UsageByAgent {
20
24
  errors: number;
21
25
  }
22
26
 
27
+ interface DailyUsage {
28
+ date: string;
29
+ input_tokens: number;
30
+ output_tokens: number;
31
+ llm_calls: number;
32
+ tool_calls: number;
33
+ errors: number;
34
+ }
35
+
23
36
  // Helper to extract stats from a single event
24
37
  function extractEventStats(event: TelemetryEvent): {
25
38
  llm_calls: number;
@@ -50,6 +63,7 @@ export function TelemetryPage() {
50
63
  const [fetchedStats, setFetchedStats] = useState<TelemetryStats | null>(null);
51
64
  const [historicalEvents, setHistoricalEvents] = useState<TelemetryEvent[]>([]);
52
65
  const [fetchedUsage, setFetchedUsage] = useState<UsageByAgent[]>([]);
66
+ const [dailyUsage, setDailyUsage] = useState<DailyUsage[]>([]);
53
67
  const [loading, setLoading] = useState(true);
54
68
  const [filter, setFilter] = useState({
55
69
  level: "",
@@ -127,6 +141,18 @@ export function TelemetryPage() {
127
141
  const usageRes = await authFetch(`/api/telemetry/usage?${usageParams}`);
128
142
  const usageData = await usageRes.json();
129
143
  setFetchedUsage(usageData.usage || []);
144
+
145
+ // Fetch daily usage for charts
146
+ const dailyParams = new URLSearchParams();
147
+ dailyParams.set("group_by", "day");
148
+ if (projectParam) dailyParams.set("project_id", projectParam);
149
+ const dailyRes = await authFetch(`/api/telemetry/usage?${dailyParams}`);
150
+ const dailyData = await dailyRes.json();
151
+ // Sort by date ascending for charts
152
+ const sorted = (dailyData.usage || []).sort((a: DailyUsage, b: DailyUsage) =>
153
+ a.date.localeCompare(b.date)
154
+ );
155
+ setDailyUsage(sorted);
130
156
  } catch (e) {
131
157
  console.error("Failed to fetch telemetry:", e);
132
158
  }
@@ -371,6 +397,164 @@ export function TelemetryPage() {
371
397
  </div>
372
398
  )}
373
399
 
400
+ {/* Charts */}
401
+ {(() => {
402
+ // Use daily data if we have multiple days, otherwise aggregate events by hour
403
+ const useDaily = dailyUsage.length > 1;
404
+ const chartData = useDaily ? dailyUsage : (() => {
405
+ // Aggregate all visible events by hour
406
+ const buckets = new Map<string, { date: string; llm_calls: number; tool_calls: number; errors: number; input_tokens: number; output_tokens: number }>();
407
+ for (const event of allEvents) {
408
+ const d = new Date(event.timestamp);
409
+ const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:00`;
410
+ if (!buckets.has(key)) {
411
+ buckets.set(key, { date: key, llm_calls: 0, tool_calls: 0, errors: 0, input_tokens: 0, output_tokens: 0 });
412
+ }
413
+ const b = buckets.get(key)!;
414
+ const s = extractEventStats(event);
415
+ b.llm_calls += s.llm_calls;
416
+ b.tool_calls += s.tool_calls;
417
+ b.errors += s.errors;
418
+ b.input_tokens += s.input_tokens;
419
+ b.output_tokens += s.output_tokens;
420
+ }
421
+ return Array.from(buckets.values()).sort((a, b) => a.date.localeCompare(b.date));
422
+ })();
423
+ const chartLabel = useDaily ? "Daily" : "Hourly";
424
+
425
+ if (chartData.length === 0) return null;
426
+
427
+ return (
428
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
429
+ {/* Activity Chart */}
430
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4">
431
+ <h3 className="text-sm font-medium text-[#888] mb-4">{chartLabel} Activity</h3>
432
+ <ResponsiveContainer width="100%" height={200}>
433
+ <AreaChart data={chartData}>
434
+ <CartesianGrid strokeDasharray="3 3" stroke="#1a1a1a" />
435
+ <XAxis
436
+ dataKey="date"
437
+ stroke="#444"
438
+ tick={{ fill: "#666", fontSize: 11 }}
439
+ tickFormatter={(v) => {
440
+ if (!useDaily && v.includes(" ")) {
441
+ return v.split(" ")[1];
442
+ }
443
+ const d = new Date(v + "T00:00:00");
444
+ return `${d.getMonth() + 1}/${d.getDate()}`;
445
+ }}
446
+ />
447
+ <YAxis stroke="#444" tick={{ fill: "#666", fontSize: 11 }} allowDecimals={false} />
448
+ <Tooltip
449
+ contentStyle={{
450
+ backgroundColor: "#111",
451
+ border: "1px solid #333",
452
+ borderRadius: "8px",
453
+ fontSize: 12,
454
+ }}
455
+ labelStyle={{ color: "#888" }}
456
+ cursor={{ stroke: "rgba(255,255,255,0.1)" }}
457
+ labelFormatter={(v) => useDaily ? new Date(v + "T00:00:00").toLocaleDateString() : v}
458
+ />
459
+ <Legend
460
+ wrapperStyle={{ fontSize: 11 }}
461
+ iconType="circle"
462
+ iconSize={8}
463
+ />
464
+ <Area
465
+ type="monotone"
466
+ dataKey="llm_calls"
467
+ name="LLM Calls"
468
+ stroke="#f97316"
469
+ fill="#f97316"
470
+ fillOpacity={0.15}
471
+ strokeWidth={1.5}
472
+ />
473
+ <Area
474
+ type="monotone"
475
+ dataKey="tool_calls"
476
+ name="Tool Calls"
477
+ stroke="#fb923c"
478
+ fill="#fb923c"
479
+ fillOpacity={0.08}
480
+ strokeWidth={1.5}
481
+ />
482
+ <Area
483
+ type="monotone"
484
+ dataKey="errors"
485
+ name="Errors"
486
+ stroke="#ef4444"
487
+ fill="#ef4444"
488
+ fillOpacity={0.1}
489
+ strokeWidth={1.5}
490
+ />
491
+ </AreaChart>
492
+ </ResponsiveContainer>
493
+ </div>
494
+
495
+ {/* Token Usage Chart */}
496
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4">
497
+ <h3 className="text-sm font-medium text-[#888] mb-4">{chartLabel} Token Usage</h3>
498
+ <ResponsiveContainer width="100%" height={200}>
499
+ <BarChart data={chartData}>
500
+ <CartesianGrid strokeDasharray="3 3" stroke="#1a1a1a" />
501
+ <XAxis
502
+ dataKey="date"
503
+ stroke="#444"
504
+ tick={{ fill: "#666", fontSize: 11 }}
505
+ tickFormatter={(v) => {
506
+ if (!useDaily && v.includes(" ")) {
507
+ return v.split(" ")[1];
508
+ }
509
+ const d = new Date(v + "T00:00:00");
510
+ return `${d.getMonth() + 1}/${d.getDate()}`;
511
+ }}
512
+ />
513
+ <YAxis
514
+ stroke="#444"
515
+ tick={{ fill: "#666", fontSize: 11 }}
516
+ tickFormatter={(v) => {
517
+ if (v >= 1000000) return `${(v / 1000000).toFixed(1)}M`;
518
+ if (v >= 1000) return `${(v / 1000).toFixed(0)}K`;
519
+ return v;
520
+ }}
521
+ />
522
+ <Tooltip
523
+ contentStyle={{
524
+ backgroundColor: "#111",
525
+ border: "1px solid #333",
526
+ borderRadius: "8px",
527
+ fontSize: 12,
528
+ }}
529
+ labelStyle={{ color: "#888" }}
530
+ cursor={{ fill: "rgba(255,255,255,0.03)" }}
531
+ labelFormatter={(v) => useDaily ? new Date(v + "T00:00:00").toLocaleDateString() : v}
532
+ formatter={(value: number) => [value.toLocaleString(), undefined]}
533
+ />
534
+ <Legend
535
+ wrapperStyle={{ fontSize: 11 }}
536
+ iconType="circle"
537
+ iconSize={8}
538
+ />
539
+ <Bar
540
+ dataKey="input_tokens"
541
+ name="Input Tokens"
542
+ fill="#f97316"
543
+ radius={[2, 2, 0, 0]}
544
+ />
545
+ <Bar
546
+ dataKey="output_tokens"
547
+ name="Output Tokens"
548
+ fill="#ea580c"
549
+ radius={[2, 2, 0, 0]}
550
+ />
551
+ </BarChart>
552
+ </ResponsiveContainer>
553
+ </div>
554
+ </div>
555
+ );
556
+ })()}
557
+
374
558
  {/* Usage by Agent */}
375
559
  {usage.length > 0 && (
376
560
  <div className="mb-6">
@@ -0,0 +1,313 @@
1
+ import React, { useState, useEffect, useCallback, useMemo } from "react";
2
+ import { Chat, convertApiMessages } from "@apteva/apteva-kit";
3
+ import { useAgentActivity, useAuth, useProjects, useTelemetryContext } from "../../context";
4
+ import type { TelemetryEvent } from "../../context";
5
+ import type { Agent, Route } from "../../types";
6
+
7
+ interface Thread {
8
+ id: string;
9
+ title?: string;
10
+ created_at: string;
11
+ updated_at: string;
12
+ message_count?: number;
13
+ agent_id: string;
14
+ agent_name: string;
15
+ }
16
+
17
+ interface ThreadsPageProps {
18
+ agents: Agent[];
19
+ onNavigate?: (route: Route) => void;
20
+ }
21
+
22
+ export function ThreadsPage({ agents, onNavigate }: ThreadsPageProps) {
23
+ const { authFetch } = useAuth();
24
+ const { currentProjectId } = useProjects();
25
+ const { events: realtimeEvents, statusChangeCounter } = useTelemetryContext();
26
+
27
+ const [threads, setThreads] = useState<Thread[]>([]);
28
+ const [selectedThread, setSelectedThread] = useState<Thread | null>(null);
29
+ const [newChatAgent, setNewChatAgent] = useState<Agent | null>(null);
30
+ const [initialMessages, setInitialMessages] = useState<any[]>([]);
31
+ const [loadingThreads, setLoadingThreads] = useState(true);
32
+ const [loadingMessages, setLoadingMessages] = useState(false);
33
+ const [historicalActivities, setHistoricalActivities] = useState<TelemetryEvent[]>([]);
34
+ const [showAgentPicker, setShowAgentPicker] = useState(false);
35
+ const [newChatKey, setNewChatKey] = useState(0);
36
+
37
+ const filteredAgents = useMemo(() => {
38
+ if (currentProjectId === null) return agents;
39
+ if (currentProjectId === "unassigned") return agents.filter(a => !a.projectId);
40
+ return agents.filter(a => a.projectId === currentProjectId);
41
+ }, [agents, currentProjectId]);
42
+
43
+ const runningAgents = useMemo(() => filteredAgents.filter(a => a.status === "running"), [filteredAgents]);
44
+ const agentIds = useMemo(() => new Set(filteredAgents.map(a => a.id)), [filteredAgents]);
45
+
46
+ // Fetch consolidated threads
47
+ const fetchThreads = useCallback(async () => {
48
+ try {
49
+ const projectParam = currentProjectId ? `?project_id=${encodeURIComponent(currentProjectId)}` : "";
50
+ const [threadsRes, activityRes] = await Promise.all([
51
+ authFetch(`/api/threads${projectParam}`).catch(() => null),
52
+ authFetch(`/api/telemetry/events?type=thread_activity&limit=100${projectParam ? `&${projectParam}` : ""}`).catch(() => null),
53
+ ]);
54
+ if (threadsRes?.ok) {
55
+ const data = await threadsRes.json();
56
+ setThreads(data.threads || []);
57
+ }
58
+ if (activityRes?.ok) {
59
+ const data = await activityRes.json();
60
+ setHistoricalActivities(data.events || []);
61
+ }
62
+ } catch (e) {
63
+ console.error("Failed to fetch threads:", e);
64
+ } finally {
65
+ setLoadingThreads(false);
66
+ }
67
+ }, [authFetch, currentProjectId]);
68
+
69
+ useEffect(() => { fetchThreads(); }, [fetchThreads, statusChangeCounter]);
70
+
71
+ useEffect(() => {
72
+ const interval = setInterval(fetchThreads, 15000);
73
+ return () => clearInterval(interval);
74
+ }, [fetchThreads]);
75
+
76
+ // Open an existing thread
77
+ const openThread = useCallback(async (thread: Thread) => {
78
+ setNewChatAgent(null);
79
+ setLoadingMessages(true);
80
+ setSelectedThread(thread);
81
+ try {
82
+ const res = await authFetch(`/api/agents/${thread.agent_id}/threads/${thread.id}/messages`);
83
+ if (res.ok) {
84
+ const data = await res.json();
85
+ setInitialMessages(convertApiMessages(data.messages || []));
86
+ } else {
87
+ setInitialMessages([]);
88
+ }
89
+ } catch {
90
+ setInitialMessages([]);
91
+ }
92
+ setLoadingMessages(false);
93
+ }, [authFetch]);
94
+
95
+ // Start a new conversation with an agent
96
+ const startNewChat = (agent: Agent) => {
97
+ setSelectedThread(null);
98
+ setInitialMessages([]);
99
+ setNewChatAgent(agent);
100
+ setNewChatKey(k => k + 1);
101
+ setShowAgentPicker(false);
102
+ };
103
+
104
+ // Merge real-time + historical activity
105
+ const activities = useMemo(() => {
106
+ const realtimeThreadEvents = realtimeEvents.filter(e => e.type === "thread_activity" && !e.data?.parent_id);
107
+ const seen = new Set(realtimeThreadEvents.map(e => e.id));
108
+ const merged = [...realtimeThreadEvents];
109
+ for (const evt of historicalActivities) {
110
+ if (!seen.has(evt.id) && !evt.data?.parent_id) { merged.push(evt); seen.add(evt.id); }
111
+ }
112
+ return merged
113
+ .filter(e => agentIds.has(e.agent_id))
114
+ .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
115
+ .slice(0, 100);
116
+ }, [realtimeEvents, historicalActivities, agentIds]);
117
+
118
+ // Group activities by thread_id
119
+ const activityByThread = useMemo(() => {
120
+ const map = new Map<string, TelemetryEvent[]>();
121
+ for (const evt of activities) {
122
+ const tid = evt.thread_id || evt.data?.thread_id as string;
123
+ if (tid) {
124
+ if (!map.has(tid)) map.set(tid, []);
125
+ map.get(tid)!.push(evt);
126
+ }
127
+ }
128
+ return map;
129
+ }, [activities]);
130
+
131
+ const runningCount = runningAgents.length;
132
+
133
+ // What's currently shown in chat
134
+ const chatAgentId = selectedThread?.agent_id || newChatAgent?.id;
135
+ const chatAgentName = selectedThread?.agent_name || newChatAgent?.name;
136
+ const chatThreadId = selectedThread?.id;
137
+ const chatKey = selectedThread
138
+ ? `${selectedThread.agent_id}-${selectedThread.id}`
139
+ : newChatAgent
140
+ ? `new-${newChatAgent.id}-${newChatKey}`
141
+ : null;
142
+
143
+ return (
144
+ <div className="flex-1 flex flex-col overflow-hidden">
145
+ {/* Header */}
146
+ <div className="px-6 pt-6 pb-4 shrink-0">
147
+ <div className="flex items-center justify-between">
148
+ <h1 className="text-xl font-semibold">Threads</h1>
149
+ <span className="text-sm text-[#666]">
150
+ {threads.length} threads from {runningCount} running agents
151
+ </span>
152
+ </div>
153
+ </div>
154
+
155
+ {/* Messenger layout: 1/4 threads | 3/4 chat */}
156
+ <div className="flex-1 flex min-h-0 overflow-hidden">
157
+ {/* Thread list — 1/4 */}
158
+ <div className="w-1/4 min-w-[260px] max-w-[360px] flex flex-col overflow-hidden">
159
+ {/* New conversation button */}
160
+ <div className="p-2 shrink-0">
161
+ <div className="relative">
162
+ <button
163
+ onClick={() => setShowAgentPicker(!showAgentPicker)}
164
+ disabled={runningAgents.length === 0}
165
+ className="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg bg-[#f97316]/10 text-[#f97316] text-sm font-medium hover:bg-[#f97316]/20 transition disabled:opacity-30 disabled:cursor-not-allowed"
166
+ >
167
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
168
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
169
+ </svg>
170
+ New conversation
171
+ </button>
172
+
173
+ {/* Agent picker dropdown */}
174
+ {showAgentPicker && (
175
+ <>
176
+ <div className="fixed inset-0 z-40" onClick={() => setShowAgentPicker(false)} />
177
+ <div className="absolute top-full left-0 right-0 mt-1 bg-[#111] border border-[#222] rounded-lg shadow-xl z-50 max-h-60 overflow-auto">
178
+ {runningAgents.map(agent => (
179
+ <button
180
+ key={agent.id}
181
+ onClick={() => startNewChat(agent)}
182
+ className="w-full text-left px-3 py-2.5 hover:bg-[#1a1a1a] transition"
183
+ >
184
+ <p className="text-sm font-medium truncate">{agent.name}</p>
185
+ <p className="text-[10px] text-[#555]">{agent.provider} · {agent.model}</p>
186
+ </button>
187
+ ))}
188
+ </div>
189
+ </>
190
+ )}
191
+ </div>
192
+ </div>
193
+
194
+ <div className="flex-1 overflow-auto px-2 pb-2">
195
+ {loadingThreads ? (
196
+ <div className="p-6 text-center text-[#555] text-sm">Loading threads...</div>
197
+ ) : threads.length === 0 ? (
198
+ <div className="p-6 text-center text-[#555] text-sm">
199
+ <p>No threads yet</p>
200
+ <p className="mt-1 text-[#444]">Start a conversation or wait for agents</p>
201
+ </div>
202
+ ) : (
203
+ <div className="space-y-0.5">
204
+ {threads.map(thread => (
205
+ <ThreadRow
206
+ key={`${thread.agent_id}-${thread.id}`}
207
+ thread={thread}
208
+ selected={selectedThread?.id === thread.id && selectedThread?.agent_id === thread.agent_id}
209
+ activities={activityByThread.get(thread.id) || []}
210
+ onSelect={() => openThread(thread)}
211
+ />
212
+ ))}
213
+ </div>
214
+ )}
215
+ </div>
216
+ </div>
217
+
218
+ {/* Chat — 3/4 */}
219
+ <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
220
+ {chatAgentId && chatKey ? (
221
+ loadingMessages ? (
222
+ <div className="flex-1 flex items-center justify-center text-[#666]">Loading messages...</div>
223
+ ) : (
224
+ <Chat
225
+ key={chatKey}
226
+ agentId="default"
227
+ apiUrl={`/api/agents/${chatAgentId}`}
228
+ threadId={chatThreadId}
229
+ initialMessages={initialMessages}
230
+ placeholder={`Message ${chatAgentName}...`}
231
+ headerTitle={chatAgentName}
232
+ variant="terminal"
233
+ showHeader={true}
234
+ />
235
+ )
236
+ ) : (
237
+ <div className="flex-1 flex items-center justify-center">
238
+ <div className="text-center text-[#555]">
239
+ <svg className="w-12 h-12 mx-auto mb-3 text-[#333]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
240
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
241
+ </svg>
242
+ <p className="text-sm">Select a thread or start a new conversation</p>
243
+ <p className="text-xs text-[#444] mt-1">Chat with any running agent</p>
244
+ </div>
245
+ </div>
246
+ )}
247
+ </div>
248
+ </div>
249
+ </div>
250
+ );
251
+ }
252
+
253
+ // --- Thread Row ---
254
+
255
+ function ThreadRow({ thread, selected, activities, onSelect }: {
256
+ thread: Thread;
257
+ selected: boolean;
258
+ activities: TelemetryEvent[];
259
+ onSelect: () => void;
260
+ }) {
261
+ const { isActive } = useAgentActivity(thread.agent_id);
262
+ const latestActivity = activities[0];
263
+ const activityText = latestActivity?.data?.activity as string | undefined;
264
+
265
+ return (
266
+ <button
267
+ onClick={onSelect}
268
+ className={`w-full text-left px-3 py-2.5 rounded-lg transition ${
269
+ selected
270
+ ? "bg-[#f97316]/10"
271
+ : "hover:bg-[#151515]"
272
+ }`}
273
+ >
274
+ <div className="flex items-center justify-between gap-2 mb-1">
275
+ <span className="text-sm font-medium truncate">
276
+ {thread.title || `Thread ${thread.id.slice(0, 8)}`}
277
+ </span>
278
+ <span className="text-[10px] text-[#555] shrink-0">{timeAgo(thread.updated_at)}</span>
279
+ </div>
280
+ <div className="flex items-center gap-1.5">
281
+ <span
282
+ className={`w-1.5 h-1.5 rounded-full shrink-0 ${
283
+ isActive ? "bg-green-400 animate-pulse" : "bg-[#444]"
284
+ }`}
285
+ />
286
+ <span className="text-[11px] text-[#f97316]">{thread.agent_name}</span>
287
+ {thread.message_count != null && (
288
+ <>
289
+ <span className="text-[#333]">&middot;</span>
290
+ <span className="text-[10px] text-[#555]">{thread.message_count} msgs</span>
291
+ </>
292
+ )}
293
+ </div>
294
+ {activityText && (
295
+ <p className="text-[11px] text-[#555] truncate mt-1">{activityText}</p>
296
+ )}
297
+ </button>
298
+ );
299
+ }
300
+
301
+ // --- Helpers ---
302
+
303
+ function timeAgo(timestamp: string): string {
304
+ const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000);
305
+ if (seconds < 5) return "just now";
306
+ if (seconds < 60) return `${seconds}s ago`;
307
+ const minutes = Math.floor(seconds / 60);
308
+ if (minutes < 60) return `${minutes}m ago`;
309
+ const hours = Math.floor(minutes / 60);
310
+ if (hours < 24) return `${hours}h ago`;
311
+ const days = Math.floor(hours / 24);
312
+ return `${days}d ago`;
313
+ }
@@ -1,4 +1,4 @@
1
- import React, { createContext, useContext, useState, useEffect, useCallback, useRef, type ReactNode } from "react";
1
+ import React, { createContext, useContext, useState, useEffect, useCallback, useMemo, useRef, type ReactNode } from "react";
2
2
 
3
3
  interface User {
4
4
  id: string;
@@ -216,7 +216,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
216
216
  return () => clearInterval(refreshInterval);
217
217
  }, [accessToken, refreshTokenInternal]);
218
218
 
219
- const value: AuthContextValue = {
219
+ const value = useMemo<AuthContextValue>(() => ({
220
220
  user,
221
221
  isAuthenticated: !!user,
222
222
  isLoading,
@@ -230,7 +230,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
230
230
  refreshToken,
231
231
  checkAuth,
232
232
  authFetch,
233
- };
233
+ }), [user, isLoading, hasUsers, isDev, accessToken, onboardingComplete, setOnboardingComplete, login, logout, refreshToken, checkAuth, authFetch]);
234
234
 
235
235
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
236
236
  }
@@ -1,4 +1,4 @@
1
- import React, { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from "react";
1
+ import React, { createContext, useContext, useState, useEffect, useCallback, useMemo, type ReactNode } from "react";
2
2
  import { useAuth } from "./AuthContext";
3
3
 
4
4
  export interface Project {
@@ -189,7 +189,7 @@ export function ProjectProvider({ children }: ProjectProviderProps) {
189
189
  }
190
190
  }, [authLoading, projectsEnabled, refreshProjects]);
191
191
 
192
- const value: ProjectContextValue = {
192
+ const value = useMemo<ProjectContextValue>(() => ({
193
193
  projects,
194
194
  currentProjectId,
195
195
  currentProject,
@@ -203,7 +203,7 @@ export function ProjectProvider({ children }: ProjectProviderProps) {
203
203
  updateProject,
204
204
  deleteProject,
205
205
  refreshProjects,
206
- };
206
+ }), [projects, currentProjectId, currentProject, isLoading, error, unassignedCount, projectsEnabled, metaAgentEnabled, setCurrentProjectId, createProject, updateProject, deleteProject, refreshProjects]);
207
207
 
208
208
  return <ProjectContext.Provider value={value}>{children}</ProjectContext.Provider>;
209
209
  }