agmon 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,233 @@
1
+ import { Container, Graphics, Text } from "pixi.js";
2
+
3
+ // One glyph per tool type. Each is drawn inside a ±10 box so a badge can swap
4
+ // icons without relayout. Only the tool *type* is shown — never arguments.
5
+
6
+ const S = { cap: "round", join: "round" };
7
+
8
+ export const TOOL_TYPES = {
9
+ bash: {
10
+ label: "Bash",
11
+ color: 0x34d399,
12
+ draw(g, c) {
13
+ g.moveTo(-8, -6).lineTo(-2, 0).lineTo(-8, 6).stroke({ width: 2.4, color: c, ...S });
14
+ g.moveTo(1, 7).lineTo(8, 7).stroke({ width: 2.4, color: c, ...S });
15
+ },
16
+ },
17
+ read: {
18
+ label: "Read",
19
+ color: 0x60a5fa,
20
+ draw(g, c) {
21
+ g.moveTo(-6, -9).lineTo(3, -9).lineTo(7, -5).lineTo(7, 9).lineTo(-6, 9).closePath()
22
+ .stroke({ width: 2, color: c, ...S });
23
+ g.moveTo(-3, -1).lineTo(4, -1).stroke({ width: 1.8, color: c, ...S });
24
+ g.moveTo(-3, 3).lineTo(4, 3).stroke({ width: 1.8, color: c, ...S });
25
+ g.moveTo(-3, 6.5).lineTo(1, 6.5).stroke({ width: 1.8, color: c, ...S });
26
+ },
27
+ },
28
+ edit: {
29
+ label: "Edit",
30
+ color: 0xfbbf24,
31
+ draw(g, c) {
32
+ g.moveTo(-7, 8).lineTo(-6, 3).lineTo(4, -7).lineTo(8, -3).lineTo(-2, 7).closePath()
33
+ .stroke({ width: 2, color: c, ...S });
34
+ g.moveTo(1, -4).lineTo(5, 0).stroke({ width: 1.8, color: c, ...S });
35
+ g.moveTo(-8, 9.5).lineTo(-1, 9.5).stroke({ width: 1.8, color: c, ...S });
36
+ },
37
+ },
38
+ search: {
39
+ label: "Search",
40
+ color: 0xf472b6,
41
+ draw(g, c) {
42
+ g.circle(-2, -2, 6).stroke({ width: 2.2, color: c, ...S });
43
+ g.moveTo(2.5, 2.5).lineTo(8, 8).stroke({ width: 2.6, color: c, ...S });
44
+ },
45
+ },
46
+ web: {
47
+ label: "Web",
48
+ color: 0x22d3ee,
49
+ draw(g, c) {
50
+ g.circle(0, 0, 8.5).stroke({ width: 2, color: c, ...S });
51
+ g.ellipse(0, 0, 3.6, 8.5).stroke({ width: 1.6, color: c, ...S });
52
+ g.moveTo(-8.5, 0).lineTo(8.5, 0).stroke({ width: 1.6, color: c, ...S });
53
+ },
54
+ },
55
+ mcp: {
56
+ label: "MCP",
57
+ color: 0xa78bfa,
58
+ draw(g, c) {
59
+ g.moveTo(-4, -9).lineTo(-4, -3).stroke({ width: 2.4, color: c, ...S });
60
+ g.moveTo(4, -9).lineTo(4, -3).stroke({ width: 2.4, color: c, ...S });
61
+ g.roundRect(-8, -3, 16, 9, 3).stroke({ width: 2, color: c, ...S });
62
+ g.moveTo(0, 6).lineTo(0, 10).stroke({ width: 2.4, color: c, ...S });
63
+ },
64
+ },
65
+ agent: {
66
+ label: "Subagent",
67
+ color: 0xfb923c,
68
+ draw(g, c) {
69
+ g.moveTo(0, -4).lineTo(-6, 6).stroke({ width: 1.8, color: c, ...S });
70
+ g.moveTo(0, -4).lineTo(6, 6).stroke({ width: 1.8, color: c, ...S });
71
+ g.circle(0, -5, 3.6).fill({ color: c });
72
+ g.circle(-6.5, 7, 3).fill({ color: c });
73
+ g.circle(6.5, 7, 3).fill({ color: c });
74
+ },
75
+ },
76
+ todo: {
77
+ label: "Todo",
78
+ color: 0x94a3b8,
79
+ draw(g, c) {
80
+ g.moveTo(-8, -5).lineTo(-6, -3).lineTo(-3, -7).stroke({ width: 1.8, color: c, ...S });
81
+ g.moveTo(1, -5).lineTo(8, -5).stroke({ width: 1.8, color: c, ...S });
82
+ g.moveTo(-8, 5).lineTo(-6, 7).lineTo(-3, 3).stroke({ width: 1.8, color: c, ...S });
83
+ g.moveTo(1, 5).lineTo(8, 5).stroke({ width: 1.8, color: c, ...S });
84
+ },
85
+ },
86
+ gitnexus: {
87
+ label: "GitNexus",
88
+ color: 0xc084fc, // purple-400
89
+ draw(g, c) {
90
+ // Knowledge graph: central hub connected to 3 orbiting nodes
91
+ g.moveTo(0, 0).lineTo(-6, -6).stroke({ width: 1.5, color: c, ...S });
92
+ g.moveTo(0, 0).lineTo(6, -5).stroke({ width: 1.5, color: c, ...S });
93
+ g.moveTo(0, 0).lineTo(0, 7).stroke({ width: 1.5, color: c, ...S });
94
+ g.circle(0, 0, 3.2).fill({ color: c });
95
+ g.circle(-6, -6, 2.2).fill({ color: c });
96
+ g.circle(6, -5, 2.2).fill({ color: c });
97
+ g.circle(0, 7, 2.2).fill({ color: c });
98
+ },
99
+ },
100
+ browser: {
101
+ label: "Browser",
102
+ color: 0x06b6d4, // cyan-500
103
+ draw(g, c) {
104
+ // Browser window with top tab bar
105
+ g.roundRect(-8, -7, 16, 14, 2.5).stroke({ width: 1.8, color: c, ...S });
106
+ g.moveTo(-8, -2).lineTo(8, -2).stroke({ width: 1.4, color: c, ...S });
107
+ g.circle(-5, -4.5, 1).fill({ color: c });
108
+ g.circle(-2, -4.5, 1).fill({ color: c });
109
+ },
110
+ },
111
+ git: {
112
+ label: "Git",
113
+ color: 0xf97316, // orange-500
114
+ draw(g, c) {
115
+ // Git branching tree
116
+ g.moveTo(-4, -7).lineTo(-4, 7).stroke({ width: 2, color: c, ...S });
117
+ g.moveTo(-4, 2).arc(0, 2, 4, Math.PI, 0).lineTo(4, -3).stroke({ width: 1.8, color: c, ...S });
118
+ g.circle(-4, -6, 2.4).fill({ color: c });
119
+ g.circle(-4, 6, 2.4).fill({ color: c });
120
+ g.circle(4, -3, 2.4).fill({ color: c });
121
+ },
122
+ },
123
+ docker: {
124
+ label: "Docker",
125
+ color: 0x38bdf8, // light blue
126
+ draw(g, c) {
127
+ // Container blocks stacked
128
+ g.roundRect(-7, 1, 6, 5, 1).fill({ color: c, alpha: 0.8 }).stroke({ width: 1.2, color: c, ...S });
129
+ g.roundRect(1, 1, 6, 5, 1).fill({ color: c, alpha: 0.8 }).stroke({ width: 1.2, color: c, ...S });
130
+ g.roundRect(-3, -6, 6, 5, 1).fill({ color: c, alpha: 0.8 }).stroke({ width: 1.2, color: c, ...S });
131
+ },
132
+ },
133
+ };
134
+
135
+ // Request-status glyphs share the badge machinery with tools: lifecycle states,
136
+ // the four failure reasons, and fallback.
137
+ export const STATUS_TYPES = {
138
+ pending: {
139
+ label: "Queued",
140
+ color: 0xf59e0b,
141
+ draw(g, c) {
142
+ for (const x of [-7, 0, 7]) g.circle(x, 1, 2.6).fill({ color: c });
143
+ },
144
+ },
145
+ done: {
146
+ label: "Done",
147
+ color: 0x34d399,
148
+ draw(g, c) {
149
+ g.moveTo(-7, 0).lineTo(-2, 6).lineTo(8, -7).stroke({ width: 3, color: c, ...S });
150
+ },
151
+ },
152
+ rate_limited: {
153
+ label: "Rate limit",
154
+ color: 0xef4444,
155
+ draw(g, c) {
156
+ g.moveTo(-6, -9).lineTo(6, -9).lineTo(-6, 9).lineTo(6, 9).closePath()
157
+ .stroke({ width: 2, color: c, ...S });
158
+ g.moveTo(-3, 6.5).lineTo(3, 6.5).stroke({ width: 2.2, color: c, ...S });
159
+ },
160
+ },
161
+ upstream_timeout: {
162
+ label: "Timeout",
163
+ color: 0xef4444,
164
+ draw(g, c) {
165
+ g.circle(0, 0, 8.5).stroke({ width: 2, color: c, ...S });
166
+ g.moveTo(0, -5).lineTo(0, 0).lineTo(4.5, 3).stroke({ width: 2, color: c, ...S });
167
+ },
168
+ },
169
+ quota_exhausted: {
170
+ label: "Quota",
171
+ color: 0xef4444,
172
+ draw(g, c) {
173
+ g.roundRect(-8, -5, 14, 10, 2).stroke({ width: 2, color: c, ...S });
174
+ g.rect(6, -2, 3, 4).fill({ color: c });
175
+ g.rect(-6, -3, 2.6, 6).fill({ color: c });
176
+ },
177
+ },
178
+ auth_refresh_failed: {
179
+ label: "Auth",
180
+ color: 0xef4444,
181
+ draw(g, c) {
182
+ g.circle(-4, 0, 4.2).stroke({ width: 2, color: c, ...S });
183
+ g.moveTo(0, 0).lineTo(8, 0).stroke({ width: 2, color: c, ...S });
184
+ g.moveTo(5, 0).lineTo(5, 4).stroke({ width: 2, color: c, ...S });
185
+ g.moveTo(-9, -8).lineTo(9, 8).stroke({ width: 2.2, color: 0xfbbf24, ...S });
186
+ },
187
+ },
188
+ fallback: {
189
+ label: "Fallback",
190
+ color: 0xfbbf24,
191
+ draw(g, c) {
192
+ g.moveTo(-8, -4).lineTo(7, -4).stroke({ width: 2.2, color: c, ...S });
193
+ g.moveTo(3, -8).lineTo(8, -4).lineTo(3, 0).stroke({ width: 2.2, color: c, ...S });
194
+ g.moveTo(8, 5).lineTo(-7, 5).stroke({ width: 2.2, color: c, ...S });
195
+ g.moveTo(-3, 1).lineTo(-8, 5).lineTo(-3, 9).stroke({ width: 2.2, color: c, ...S });
196
+ },
197
+ },
198
+ };
199
+
200
+ export const TOOL_KEYS = Object.keys(TOOL_TYPES);
201
+ export const STATUS_KEYS = ["pending", "done", "rate_limited", "upstream_timeout", "quota_exhausted", "auth_refresh_failed", "fallback"];
202
+
203
+ /** A round badge: dark disc, coloured ring, the tool's glyph, optional name. */
204
+ export function createToolBadge(tool, { size = 1, showLabel = true, label: labelOverride } = {}) {
205
+ const def = TOOL_TYPES[tool] || STATUS_TYPES[tool] || TOOL_TYPES.bash;
206
+ const root = new Container();
207
+
208
+ const halo = new Graphics();
209
+ halo.circle(0, 0, 21).fill({ color: def.color, alpha: 0.18 });
210
+
211
+ const disc = new Graphics();
212
+ disc.circle(0, 0, 15).fill({ color: 0x0f141c });
213
+ disc.circle(0, 0, 15).stroke({ width: 2, color: def.color, alpha: 0.95 });
214
+
215
+ const glyph = new Graphics();
216
+ def.draw(glyph, def.color);
217
+ glyph.scale.set(0.95);
218
+
219
+ root.addChild(halo, disc, glyph);
220
+
221
+ if (showLabel) {
222
+ const label = new Text({
223
+ text: labelOverride ?? def.label,
224
+ style: { fontFamily: "Inter, system-ui, sans-serif", fontSize: 9, fill: def.color, fontWeight: "800" },
225
+ });
226
+ label.anchor.set(0.5, 0);
227
+ label.y = 18;
228
+ root.addChild(label);
229
+ }
230
+
231
+ root.scale.set(size);
232
+ return { root, halo, tool };
233
+ }
@@ -0,0 +1,171 @@
1
+ export const MOCK_PRESETS = {
2
+ idle: { agents: 2, errorRatio: 0 },
3
+ busy: { agents: 8, errorRatio: 0 },
4
+ storm: { agents: 30, errorRatio: 0.15 },
5
+ errors: { agents: 10, errorRatio: 0.6 },
6
+ cases: { agents: 16, errorRatio: 0, cases: true },
7
+ };
8
+
9
+ const CLIENTS = [
10
+ { account: "Claude Code · main", icon: "terminal" },
11
+ { account: "Claude Code · review", icon: "rate_review" },
12
+ { account: "Antigravity · agent", icon: "smart_toy" },
13
+ { account: "Antigravity · dev", icon: "construction" },
14
+ { account: "Cursor · workspace", icon: "edit_note" },
15
+ { account: "OpenCode · build", icon: "science" },
16
+ { account: "Aider", icon: "smart_toy" },
17
+ { account: "Continue · dev", icon: "play_arrow" },
18
+ ];
19
+
20
+ const PROVIDER_MODELS = [
21
+ { provider: "claude", model: "claude-3-7-sonnet" },
22
+ { provider: "gemini", model: "gemini-2.5-pro" },
23
+ { provider: "openai", model: "gpt-4o" },
24
+ { provider: "minimax", model: "minimax-m2" },
25
+ { provider: "deepseek", model: "deepseek-v3" },
26
+ ];
27
+
28
+ const ERROR_REASONS = [
29
+ "rate_limited",
30
+ "upstream_timeout",
31
+ "quota_exhausted",
32
+ "auth_refresh_failed",
33
+ ];
34
+
35
+ export const MOCK_TOOL_TYPES = ["bash", "read", "edit", "search", "web", "mcp", "agent", "gitnexus", "browser", "git", "docker"];
36
+
37
+ const CASE_DECK = [
38
+ { name: "sleeping", state: "done", ageMs: 38000 },
39
+ { name: "happy", state: "done", ageMs: 9000, cost: 1.42 },
40
+ { name: "pending", state: "pending", ageMs: 2500, tools: ["todo"] },
41
+ { name: "gitnexus", state: "streaming", ageMs: 14000, tools: ["gitnexus", "read"] },
42
+ { name: "docker", state: "streaming", ageMs: 15000, tools: ["docker", "bash"] },
43
+ { name: "browser", state: "streaming", ageMs: 16000, tools: ["browser", "web"] },
44
+ { name: "git+agent", state: "streaming", ageMs: 12000, tools: ["git", "agent"], concurrent: 4 },
45
+ { name: "rate_limited", state: "error", ageMs: 6000, error: "rate_limited" },
46
+ { name: "upstream_timeout", state: "error", ageMs: 30000, error: "upstream_timeout" },
47
+ { name: "quota_exhausted", state: "error", ageMs: 8000, error: "quota_exhausted" },
48
+ { name: "auth_refresh_failed", state: "error", ageMs: 5000, error: "auth_refresh_failed" },
49
+ { name: "fallback-1", state: "streaming", ageMs: 13000, tools: ["bash"], fallbackShift: 3 },
50
+ { name: "fallback-2", state: "streaming", ageMs: 14000, tools: ["browser", "mcp"], fallbackShift: 2 },
51
+ { name: "slow", state: "streaming", ageMs: 36000, tools: ["docker"] },
52
+ { name: "queue+cached", state: "streaming", ageMs: 10000, tools: ["read", "search"], concurrent: 5, cachedBoost: true },
53
+ { name: "expensive", state: "done", ageMs: 20000, cost: 7.35 },
54
+ ];
55
+
56
+ function createRandom(seed) {
57
+ let s = seed >>> 0 || 1;
58
+ return () => {
59
+ s ^= s << 13;
60
+ s ^= s >>> 17;
61
+ s ^= s << 5;
62
+ s >>>= 0;
63
+ return s / 4294967296;
64
+ };
65
+ }
66
+
67
+ function pick(rand, list) {
68
+ return list[Math.floor(rand() * list.length) % list.length];
69
+ }
70
+
71
+ function accountTarget(index) {
72
+ return PROVIDER_MODELS[index % PROVIDER_MODELS.length];
73
+ }
74
+
75
+ function randomTokens(rand) {
76
+ return {
77
+ input: Math.round(400 + rand() * 14000),
78
+ output: Math.round(80 + rand() * 2900),
79
+ cached: Math.round(rand() * 6000),
80
+ };
81
+ }
82
+
83
+ export function generateMockTraces({ count = 8, errorRatio = 0, seed = 42, now = Date.now(), cases = false } = {}) {
84
+ const rand = createRandom(seed);
85
+ const traces = [];
86
+
87
+ for (let i = 0; i < count; i += 1) {
88
+ const client = CLIENTS[i % CLIENTS.length];
89
+ const cycle = Math.floor(i / CLIENTS.length) + 1;
90
+ const account = cycle > 1 ? `${client.account} ${cycle}` : client.account;
91
+ const connectionId = `mock-conn-${i}`;
92
+ const target = accountTarget(i);
93
+ const deck = cases ? CASE_DECK[i % CASE_DECK.length] : null;
94
+ const randAge = Math.floor(rand() * 40000);
95
+ const randErr = rand() < errorRatio;
96
+ const ageMs = deck ? deck.ageMs : randAge;
97
+ const startedAt = now - ageMs;
98
+ const isError = deck ? deck.state === "error" : randErr;
99
+
100
+ const ageRatio = ageMs / 40000;
101
+ let state;
102
+ if (deck) state = deck.state;
103
+ else if (isError) state = "error";
104
+ else if (ageRatio < 0.25) state = "pending";
105
+ else if (ageRatio < 0.6) state = "streaming";
106
+ else state = "done";
107
+
108
+ const tokens = randomTokens(rand);
109
+ const toolRand = createRandom(seed * 7919 + i * 104729 + 13);
110
+ const toolCount = state === "done" ? 0 : 1 + Math.floor(toolRand() * 3);
111
+ const toolPick = [];
112
+ for (let k = 0; k < toolCount; k += 1) {
113
+ toolPick.push(MOCK_TOOL_TYPES[Math.floor(toolRand() * MOCK_TOOL_TYPES.length)]);
114
+ }
115
+ const toolNames = deck?.tools ?? (state === "error" ? [] : toolPick);
116
+ const toolCalls = [];
117
+ for (const tool of toolNames) {
118
+ if (!toolCalls.some((c) => c.tool === tool)) {
119
+ toolCalls.push({ tool, state: toolCalls.length === 0 ? "running" : "queued" });
120
+ }
121
+ }
122
+
123
+ const wantsFallback = deck ? !!deck.fallbackShift : toolRand() < 0.28;
124
+ const fallbackProvider = PROVIDER_MODELS[(i + (deck?.fallbackShift ?? 3)) % PROVIDER_MODELS.length].provider;
125
+ const fallback =
126
+ wantsFallback && (state === "pending" || state === "streaming") && fallbackProvider !== target.provider
127
+ ? { from: fallbackProvider }
128
+ : null;
129
+
130
+ const concurrent = deck?.concurrent ?? (state === "done" || state === "error" ? 1 : 1 + Math.floor(toolRand() * 3));
131
+ if (deck?.cachedBoost) tokens.cached = 14000;
132
+
133
+ traces.push({
134
+ traceId: `mock-${i}-${Math.floor(rand() * 1e6).toString(36)}`,
135
+ cli: "mock",
136
+ connectionId,
137
+ account,
138
+ model: target.model,
139
+ provider: target.provider,
140
+ state,
141
+ startedAt,
142
+ elapsedMs: ageMs,
143
+ tokens,
144
+ cost: deck?.cost ?? Math.round((tokens.input * 0.000015 + tokens.output * 0.00006) * 100) / 100,
145
+ status: isError ? "error" : "200",
146
+ error: isError ? deck?.error ?? pick(rand, ERROR_REASONS) : null,
147
+ clientIcon: client.icon,
148
+ toolCalls,
149
+ tools: toolNames,
150
+ activeTool: toolNames[0] || null,
151
+ currentCommand: toolNames[0] ? `${toolNames[0]} task running...` : null,
152
+ fallback,
153
+ concurrent,
154
+ });
155
+ }
156
+
157
+ return traces;
158
+ }
159
+
160
+ export function mockProviderDescriptors(traces) {
161
+ const seen = new Map();
162
+ for (const trace of traces) {
163
+ if (trace.provider && !seen.has(trace.provider)) {
164
+ seen.set(trace.provider, {
165
+ provider: trace.provider,
166
+ name: trace.provider.charAt(0).toUpperCase() + trace.provider.slice(1),
167
+ });
168
+ }
169
+ }
170
+ return [...seen.values()];
171
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Standard Trace Contract for Agent Factory.
3
+ * All watchers normalize their session logs into this uniform schema.
4
+ */
5
+
6
+ export function normalizeTrace(raw) {
7
+ const tokens = raw.tokens || {};
8
+ return {
9
+ traceId: String(raw.traceId || `trace-${Math.random().toString(36).slice(2, 9)}`),
10
+ cli: raw.cli || "agent", // "antigravity" | "claude" | "cursor" | etc.
11
+ connectionId: String(raw.connectionId || "Agent Desk"),
12
+ account: String(raw.account || "Default Workspace"),
13
+ model: String(raw.model || "Unknown Model"),
14
+ provider: String(raw.provider || "anthropic").toLowerCase(),
15
+ state: ["streaming", "pending", "done", "error"].includes(raw.state) ? raw.state : "streaming",
16
+ startedAt: typeof raw.startedAt === "number" ? raw.startedAt : Date.now(),
17
+ elapsedMs: typeof raw.elapsedMs === "number" ? raw.elapsedMs : 0,
18
+ tokens: {
19
+ input: Number(tokens.input || 0),
20
+ output: Number(tokens.output || 0),
21
+ cached: Number(tokens.cached || 0),
22
+ },
23
+ cost: Number(raw.cost || 0),
24
+ status: String(raw.status || "200"),
25
+ tools: Array.isArray(raw.tools) ? raw.tools : [],
26
+ activeTool: raw.activeTool || null,
27
+ currentCommand: raw.currentCommand || null,
28
+ error: raw.error ? String(raw.error) : null,
29
+ };
30
+ }
31
+
32
+ export function extractProviders(traces) {
33
+ const seen = new Set();
34
+ const list = [];
35
+ for (const t of traces) {
36
+ const p = (t.provider || "other").toLowerCase();
37
+ if (!seen.has(p)) {
38
+ seen.add(p);
39
+ list.push({
40
+ provider: p,
41
+ name: p.charAt(0).toUpperCase() + p.slice(1),
42
+ });
43
+ }
44
+ }
45
+ return list;
46
+ }
@@ -0,0 +1,142 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { generateMockTraces, mockProviderDescriptors, MOCK_PRESETS } from "./mockTraces";
5
+
6
+ export function useFactoryTraces({ mode = "live", preset = "cases" } = {}) {
7
+ const [traces, setTraces] = useState([]);
8
+ const [providers, setProviders] = useState([]);
9
+ const [connected, setConnected] = useState(false);
10
+ const [isRefreshing, setIsRefreshing] = useState(false);
11
+ const [lastUpdated, setLastUpdated] = useState(null);
12
+
13
+ const eventSourceRef = useRef(null);
14
+ const watchdogTimerRef = useRef(null);
15
+
16
+ // Reconnection trigger
17
+ const [connectionEpoch, setConnectionEpoch] = useState(0);
18
+
19
+ const refresh = useCallback(() => {
20
+ setIsRefreshing(true);
21
+ if (eventSourceRef.current) {
22
+ eventSourceRef.current.close();
23
+ eventSourceRef.current = null;
24
+ }
25
+ setConnectionEpoch((prev) => prev + 1);
26
+ setTimeout(() => setIsRefreshing(false), 500);
27
+ }, []);
28
+
29
+ // 1. Mock Mode
30
+ useEffect(() => {
31
+ if (mode !== "mock") return undefined;
32
+
33
+ const cfg = MOCK_PRESETS[preset] || MOCK_PRESETS.cases;
34
+ const updateMock = () => {
35
+ const next = generateMockTraces({ count: cfg.agents, cases: !!cfg.cases, errorRatio: cfg.errorRatio });
36
+ setTraces(next);
37
+ setProviders(mockProviderDescriptors(next));
38
+ setConnected(true);
39
+ setLastUpdated(Date.now());
40
+ };
41
+
42
+ updateMock();
43
+ const interval = setInterval(updateMock, 3000);
44
+ return () => clearInterval(interval);
45
+ }, [mode, preset, connectionEpoch]);
46
+
47
+ // 2. Live Mode (SSE + Wake-up Watchdog)
48
+ useEffect(() => {
49
+ if (mode !== "live") return undefined;
50
+
51
+ let cancelled = false;
52
+
53
+ const resetWatchdog = () => {
54
+ if (watchdogTimerRef.current) clearTimeout(watchdogTimerRef.current);
55
+ // If no packet arrives for 7s, force reconnect
56
+ watchdogTimerRef.current = setTimeout(() => {
57
+ if (!cancelled) {
58
+ console.warn("[SSE] Watchdog timeout, reconnecting...");
59
+ setConnectionEpoch((prev) => prev + 1);
60
+ }
61
+ }, 7000);
62
+ };
63
+
64
+ try {
65
+ const es = new EventSource("/api/traces/stream?source=live");
66
+ eventSourceRef.current = es;
67
+
68
+ es.onopen = () => {
69
+ if (!cancelled) {
70
+ setConnected(true);
71
+ resetWatchdog();
72
+ }
73
+ };
74
+
75
+ es.onmessage = (event) => {
76
+ if (cancelled || !event?.data) return;
77
+ try {
78
+ const payload = JSON.parse(event.data);
79
+ if (Array.isArray(payload.traces)) {
80
+ setTraces(payload.traces);
81
+ }
82
+ if (Array.isArray(payload.providers)) {
83
+ setProviders(payload.providers);
84
+ }
85
+ setLastUpdated(Date.now());
86
+ resetWatchdog();
87
+ } catch {
88
+ // ignore ping
89
+ }
90
+ };
91
+
92
+ es.onerror = () => {
93
+ if (!cancelled) {
94
+ setConnected(false);
95
+ }
96
+ };
97
+ } catch {
98
+ setConnected(false);
99
+ }
100
+
101
+ return () => {
102
+ cancelled = true;
103
+ if (watchdogTimerRef.current) clearTimeout(watchdogTimerRef.current);
104
+ if (eventSourceRef.current) {
105
+ eventSourceRef.current.close();
106
+ eventSourceRef.current = null;
107
+ }
108
+ };
109
+ }, [mode, connectionEpoch]);
110
+
111
+ // 3. Auto-reconnect when laptop wakes up or tab gains focus
112
+ useEffect(() => {
113
+ if (mode !== "live") return undefined;
114
+
115
+ const handleWakeup = () => {
116
+ if (document.visibilityState === "visible") {
117
+ console.log("[AgentFactory] Tab active / wake up, refreshing telemetry...");
118
+ refresh();
119
+ }
120
+ };
121
+
122
+ window.addEventListener("visibilitychange", handleWakeup);
123
+ window.addEventListener("online", handleWakeup);
124
+ window.addEventListener("focus", handleWakeup);
125
+
126
+ return () => {
127
+ window.removeEventListener("visibilitychange", handleWakeup);
128
+ window.removeEventListener("online", handleWakeup);
129
+ window.removeEventListener("focus", handleWakeup);
130
+ };
131
+ }, [mode, refresh]);
132
+
133
+ return {
134
+ traces,
135
+ providers,
136
+ connected,
137
+ live: mode === "live",
138
+ isRefreshing,
139
+ refresh,
140
+ lastUpdated,
141
+ };
142
+ }