@katyella/legio 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.
- package/CHANGELOG.md +422 -0
- package/LICENSE +21 -0
- package/README.md +555 -0
- package/agents/builder.md +141 -0
- package/agents/coordinator.md +351 -0
- package/agents/cto.md +196 -0
- package/agents/gateway.md +276 -0
- package/agents/lead.md +281 -0
- package/agents/merger.md +156 -0
- package/agents/monitor.md +212 -0
- package/agents/reviewer.md +142 -0
- package/agents/scout.md +131 -0
- package/agents/supervisor.md +416 -0
- package/bin/legio.mjs +38 -0
- package/package.json +77 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +102 -0
- package/src/agents/hooks-deployer.test.ts +1820 -0
- package/src/agents/hooks-deployer.ts +574 -0
- package/src/agents/identity.test.ts +614 -0
- package/src/agents/identity.ts +385 -0
- package/src/agents/lifecycle.test.ts +202 -0
- package/src/agents/lifecycle.ts +184 -0
- package/src/agents/manifest.test.ts +558 -0
- package/src/agents/manifest.ts +297 -0
- package/src/agents/overlay.test.ts +592 -0
- package/src/agents/overlay.ts +316 -0
- package/src/beads/client.test.ts +210 -0
- package/src/beads/client.ts +227 -0
- package/src/beads/molecules.test.ts +320 -0
- package/src/beads/molecules.ts +209 -0
- package/src/commands/agents.test.ts +325 -0
- package/src/commands/agents.ts +286 -0
- package/src/commands/clean.test.ts +730 -0
- package/src/commands/clean.ts +653 -0
- package/src/commands/completions.test.ts +346 -0
- package/src/commands/completions.ts +950 -0
- package/src/commands/coordinator.test.ts +1524 -0
- package/src/commands/coordinator.ts +880 -0
- package/src/commands/costs.test.ts +1015 -0
- package/src/commands/costs.ts +473 -0
- package/src/commands/dashboard.test.ts +94 -0
- package/src/commands/dashboard.ts +607 -0
- package/src/commands/doctor.test.ts +295 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/down.test.ts +308 -0
- package/src/commands/down.ts +124 -0
- package/src/commands/errors.test.ts +648 -0
- package/src/commands/errors.ts +255 -0
- package/src/commands/feed.test.ts +579 -0
- package/src/commands/feed.ts +368 -0
- package/src/commands/gateway.test.ts +698 -0
- package/src/commands/gateway.ts +419 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +539 -0
- package/src/commands/hooks.test.ts +292 -0
- package/src/commands/hooks.ts +210 -0
- package/src/commands/init.test.ts +211 -0
- package/src/commands/init.ts +622 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +455 -0
- package/src/commands/log.test.ts +1556 -0
- package/src/commands/log.ts +752 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +544 -0
- package/src/commands/mail.test.ts +1726 -0
- package/src/commands/mail.ts +926 -0
- package/src/commands/merge.test.ts +676 -0
- package/src/commands/merge.ts +374 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +150 -0
- package/src/commands/monitor.test.ts +151 -0
- package/src/commands/monitor.ts +394 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +373 -0
- package/src/commands/prime.test.ts +467 -0
- package/src/commands/prime.ts +386 -0
- package/src/commands/replay.test.ts +742 -0
- package/src/commands/replay.ts +367 -0
- package/src/commands/run.test.ts +443 -0
- package/src/commands/run.ts +365 -0
- package/src/commands/server.test.ts +626 -0
- package/src/commands/server.ts +298 -0
- package/src/commands/sling.test.ts +810 -0
- package/src/commands/sling.ts +700 -0
- package/src/commands/spec.test.ts +206 -0
- package/src/commands/spec.ts +171 -0
- package/src/commands/status.test.ts +276 -0
- package/src/commands/status.ts +339 -0
- package/src/commands/stop.test.ts +357 -0
- package/src/commands/stop.ts +119 -0
- package/src/commands/supervisor.test.ts +186 -0
- package/src/commands/supervisor.ts +544 -0
- package/src/commands/trace.test.ts +746 -0
- package/src/commands/trace.ts +332 -0
- package/src/commands/up.test.ts +597 -0
- package/src/commands/up.ts +275 -0
- package/src/commands/watch.test.ts +152 -0
- package/src/commands/watch.ts +238 -0
- package/src/commands/worktree.test.ts +648 -0
- package/src/commands/worktree.ts +266 -0
- package/src/config.test.ts +496 -0
- package/src/config.ts +616 -0
- package/src/doctor/agents.test.ts +448 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +184 -0
- package/src/doctor/config-check.ts +185 -0
- package/src/doctor/consistency.test.ts +645 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +284 -0
- package/src/doctor/databases.ts +211 -0
- package/src/doctor/dependencies.test.ts +150 -0
- package/src/doctor/dependencies.ts +179 -0
- package/src/doctor/logs.test.ts +244 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +210 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +285 -0
- package/src/doctor/structure.ts +195 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +130 -0
- package/src/doctor/version.ts +131 -0
- package/src/e2e/chat-flow.test.ts +346 -0
- package/src/e2e/init-sling-lifecycle.test.ts +288 -0
- package/src/errors.test.ts +21 -0
- package/src/errors.ts +246 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +344 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/global-setup.ts +14 -0
- package/src/index.ts +339 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +118 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +812 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +258 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +873 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +815 -0
- package/src/mail/store.ts +402 -0
- package/src/merge/queue.test.ts +449 -0
- package/src/merge/queue.ts +262 -0
- package/src/merge/resolver.test.ts +1453 -0
- package/src/merge/resolver.ts +759 -0
- package/src/metrics/store.test.ts +1167 -0
- package/src/metrics/store.ts +511 -0
- package/src/metrics/summary.test.ts +397 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +643 -0
- package/src/metrics/transcript.ts +351 -0
- package/src/mulch/client.test.ts +547 -0
- package/src/mulch/client.ts +416 -0
- package/src/server/audit-store.test.ts +384 -0
- package/src/server/audit-store.ts +257 -0
- package/src/server/headless.test.ts +180 -0
- package/src/server/headless.ts +151 -0
- package/src/server/index.test.ts +241 -0
- package/src/server/index.ts +317 -0
- package/src/server/public/app.js +187 -0
- package/src/server/public/apple-touch-icon.png +0 -0
- package/src/server/public/components/agent-badge.js +37 -0
- package/src/server/public/components/data-table.js +114 -0
- package/src/server/public/components/gateway-chat.js +256 -0
- package/src/server/public/components/issue-card.js +96 -0
- package/src/server/public/components/layout.js +88 -0
- package/src/server/public/components/message-bubble.js +120 -0
- package/src/server/public/components/stat-card.js +26 -0
- package/src/server/public/components/terminal-panel.js +140 -0
- package/src/server/public/favicon-16.png +0 -0
- package/src/server/public/favicon-32.png +0 -0
- package/src/server/public/favicon.ico +0 -0
- package/src/server/public/favicon.png +0 -0
- package/src/server/public/index.html +64 -0
- package/src/server/public/lib/api.js +35 -0
- package/src/server/public/lib/markdown.js +8 -0
- package/src/server/public/lib/preact-setup.js +8 -0
- package/src/server/public/lib/state.js +99 -0
- package/src/server/public/lib/utils.js +309 -0
- package/src/server/public/lib/ws.js +79 -0
- package/src/server/public/views/chat.js +983 -0
- package/src/server/public/views/costs.js +692 -0
- package/src/server/public/views/dashboard.js +781 -0
- package/src/server/public/views/gateway-chat.js +622 -0
- package/src/server/public/views/inspect.js +399 -0
- package/src/server/public/views/issues.js +470 -0
- package/src/server/public/views/setup.js +94 -0
- package/src/server/public/views/task-detail.js +422 -0
- package/src/server/routes.test.ts +3816 -0
- package/src/server/routes.ts +1964 -0
- package/src/server/websocket.test.ts +288 -0
- package/src/server/websocket.ts +196 -0
- package/src/sessions/compat.test.ts +109 -0
- package/src/sessions/compat.ts +17 -0
- package/src/sessions/store.test.ts +969 -0
- package/src/sessions/store.ts +480 -0
- package/src/test-helpers.test.ts +97 -0
- package/src/test-helpers.ts +143 -0
- package/src/types.ts +708 -0
- package/src/watchdog/daemon.test.ts +1233 -0
- package/src/watchdog/daemon.ts +533 -0
- package/src/watchdog/health.test.ts +371 -0
- package/src/watchdog/health.ts +248 -0
- package/src/watchdog/triage.test.ts +162 -0
- package/src/watchdog/triage.ts +193 -0
- package/src/worktree/manager.test.ts +444 -0
- package/src/worktree/manager.ts +224 -0
- package/src/worktree/tmux.test.ts +1238 -0
- package/src/worktree/tmux.ts +644 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +132 -0
- package/templates/overlay.md.tmpl +79 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
// dashboard.js — Unified Dashboard View (Preact+HTM)
|
|
2
|
+
// Two-panel: Left = Coordinator Chat (~58%), Right = Sidebar (MetricsStrip + AgentRoster + MailFeed)
|
|
3
|
+
// Merges former CommandView and DashboardView into a single unified page.
|
|
4
|
+
|
|
5
|
+
import { fetchJson, postJson } from "../lib/api.js";
|
|
6
|
+
import { html, useCallback, useEffect, useState } from "../lib/preact-setup.js";
|
|
7
|
+
import { appState } from "../lib/state.js";
|
|
8
|
+
import { agentColor, stateColor, stateIcon, timeAgo } from "../lib/utils.js";
|
|
9
|
+
import { GatewayChat } from "./gateway-chat.js";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Constants
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
// Type badge Tailwind classes for ActivityTimeline
|
|
16
|
+
const TYPE_COLORS = {
|
|
17
|
+
session_start: "bg-green-900/50 text-green-400",
|
|
18
|
+
session_end: "bg-[#333] text-[#999]",
|
|
19
|
+
mail_sent: "bg-purple-900/50 text-purple-400",
|
|
20
|
+
mail_received: "bg-purple-900/50 text-purple-400",
|
|
21
|
+
mail: "bg-purple-900/50 text-purple-400",
|
|
22
|
+
error: "bg-red-900/50 text-red-400",
|
|
23
|
+
system: "bg-[#333] text-[#999]",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Type badge Tailwind classes for MailFeed
|
|
27
|
+
const MAIL_TYPE_COLORS = {
|
|
28
|
+
result: "bg-green-900/50 text-green-400",
|
|
29
|
+
worker_done: "bg-green-900/50 text-green-400",
|
|
30
|
+
merged: "bg-green-900/50 text-green-400",
|
|
31
|
+
status: "bg-blue-900/50 text-blue-400",
|
|
32
|
+
dispatch: "bg-blue-900/50 text-blue-400",
|
|
33
|
+
assign: "bg-blue-900/50 text-blue-400",
|
|
34
|
+
question: "bg-yellow-900/50 text-yellow-400",
|
|
35
|
+
merge_ready: "bg-yellow-900/50 text-yellow-400",
|
|
36
|
+
error: "bg-red-900/50 text-red-400",
|
|
37
|
+
merge_failed: "bg-red-900/50 text-red-400",
|
|
38
|
+
escalation: "bg-red-900/50 text-red-400",
|
|
39
|
+
health_check: "bg-[#333] text-[#999]",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Helpers
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
function buildEventSummary(e) {
|
|
47
|
+
switch (e.eventType) {
|
|
48
|
+
case "session_start":
|
|
49
|
+
return `${e.agentName} session started`;
|
|
50
|
+
case "session_end":
|
|
51
|
+
return `${e.agentName} session ended`;
|
|
52
|
+
case "mail_sent":
|
|
53
|
+
return `Mail sent by ${e.agentName}`;
|
|
54
|
+
case "mail_received":
|
|
55
|
+
return `Mail received by ${e.agentName}`;
|
|
56
|
+
case "error": {
|
|
57
|
+
try {
|
|
58
|
+
return `Error: ${JSON.parse(e.data || "{}").message || "unknown"}`;
|
|
59
|
+
} catch {
|
|
60
|
+
return "Error";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
default:
|
|
64
|
+
return e.eventType;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function typeBadgeClass(type) {
|
|
69
|
+
return TYPE_COLORS[type] ?? "bg-[#333] text-[#999]";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// MetricsStrip
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
function MetricsStrip({ agents, status }) {
|
|
77
|
+
const totalSessions = agents.length;
|
|
78
|
+
const activeCount = agents.filter((a) => a.state === "working" || a.state === "booting").length;
|
|
79
|
+
const completedCount = agents.filter((a) => a.state === "completed").length;
|
|
80
|
+
const unreadMail = status?.unreadMailCount ?? 0;
|
|
81
|
+
const pendingMerges = status?.mergeQueueCount ?? 0;
|
|
82
|
+
|
|
83
|
+
const stats = [
|
|
84
|
+
{ label: "Sessions", value: totalSessions },
|
|
85
|
+
{ label: "Active", value: activeCount },
|
|
86
|
+
{ label: "Completed", value: completedCount },
|
|
87
|
+
{ label: "Unread Mail", value: unreadMail },
|
|
88
|
+
{ label: "Pending Merges", value: pendingMerges },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
return html`
|
|
92
|
+
<div class="bg-[#1a1a1a] border-b border-[#2a2a2a] shrink-0">
|
|
93
|
+
<div class="border-b border-[#2a2a2a] px-3 py-1.5 text-xs font-bold uppercase tracking-wider text-gray-400">
|
|
94
|
+
Metrics
|
|
95
|
+
</div>
|
|
96
|
+
<div class="flex flex-wrap gap-4 px-3 py-2">
|
|
97
|
+
${stats.map(
|
|
98
|
+
({ label, value }) => html`
|
|
99
|
+
<span key=${label} class="text-xs text-gray-400">
|
|
100
|
+
${label}:${" "}<strong class="text-white">${value}</strong>
|
|
101
|
+
</span>
|
|
102
|
+
`,
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// MailFeed
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
function MailFeed({ mail }) {
|
|
114
|
+
const [activeFilters, setActiveFilters] = useState(new Set());
|
|
115
|
+
const [expandedId, setExpandedId] = useState(null);
|
|
116
|
+
|
|
117
|
+
const sorted = [...mail]
|
|
118
|
+
.filter((m) => m.audience !== "human")
|
|
119
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
120
|
+
.slice(0, 50);
|
|
121
|
+
|
|
122
|
+
const filtered =
|
|
123
|
+
activeFilters.size === 0 ? sorted : sorted.filter((m) => activeFilters.has(m.type));
|
|
124
|
+
|
|
125
|
+
const toggleExpand = useCallback((id) => {
|
|
126
|
+
setExpandedId((prev) => (prev === id ? null : id));
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
const allTypes = Object.keys(MAIL_TYPE_COLORS);
|
|
130
|
+
|
|
131
|
+
const toggleFilter = useCallback((type) => {
|
|
132
|
+
setActiveFilters((prev) => {
|
|
133
|
+
const next = new Set(prev);
|
|
134
|
+
if (next.has(type)) {
|
|
135
|
+
next.delete(type);
|
|
136
|
+
} else {
|
|
137
|
+
next.add(type);
|
|
138
|
+
}
|
|
139
|
+
return next;
|
|
140
|
+
});
|
|
141
|
+
}, []);
|
|
142
|
+
|
|
143
|
+
const clearFilters = useCallback(() => {
|
|
144
|
+
setActiveFilters(new Set());
|
|
145
|
+
}, []);
|
|
146
|
+
|
|
147
|
+
return html`
|
|
148
|
+
<div class="bg-[#1a1a1a] border-t border-[#2a2a2a] shrink-0">
|
|
149
|
+
<div class="border-b border-[#2a2a2a] px-3 py-1.5 text-xs font-bold uppercase tracking-wider text-gray-400">
|
|
150
|
+
Mail Feed
|
|
151
|
+
</div>
|
|
152
|
+
<!-- Filter chips -->
|
|
153
|
+
<div class="flex flex-wrap gap-1 px-2 py-1.5 border-b border-[#2a2a2a]">
|
|
154
|
+
<button
|
|
155
|
+
onClick=${clearFilters}
|
|
156
|
+
class=${`px-1.5 py-0.5 rounded text-xs font-mono cursor-pointer border-none ${activeFilters.size === 0 ? "bg-white/20 text-white" : "bg-[#2a2a2a] text-[#666]"}`}
|
|
157
|
+
>
|
|
158
|
+
All
|
|
159
|
+
</button>
|
|
160
|
+
${allTypes.map(
|
|
161
|
+
(type) => html`
|
|
162
|
+
<button
|
|
163
|
+
key=${type}
|
|
164
|
+
onClick=${() => toggleFilter(type)}
|
|
165
|
+
class=${`px-1.5 py-0.5 rounded text-xs font-mono cursor-pointer border-none ${activeFilters.has(type) ? (MAIL_TYPE_COLORS[type] ?? "bg-[#333] text-[#999]") : "bg-[#2a2a2a] text-[#666]"}`}
|
|
166
|
+
>
|
|
167
|
+
${type}
|
|
168
|
+
</button>
|
|
169
|
+
`,
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
<div class="overflow-y-auto max-h-[30vh] p-2 space-y-0.5">
|
|
173
|
+
${
|
|
174
|
+
filtered.length === 0
|
|
175
|
+
? html`<div class="px-2 py-4 text-center text-gray-500 text-xs">No recent mail</div>`
|
|
176
|
+
: filtered.map((m) => {
|
|
177
|
+
const isExpanded = expandedId === m.id;
|
|
178
|
+
let parsedPayload = null;
|
|
179
|
+
if (m.payload) {
|
|
180
|
+
try {
|
|
181
|
+
parsedPayload =
|
|
182
|
+
typeof m.payload === "string" ? JSON.parse(m.payload) : m.payload;
|
|
183
|
+
} catch {
|
|
184
|
+
parsedPayload = m.payload;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return html`
|
|
188
|
+
<div
|
|
189
|
+
key=${m.id}
|
|
190
|
+
class="rounded-sm ${isExpanded ? "bg-white/5 border border-[#2a2a2a]" : "border border-transparent"}"
|
|
191
|
+
>
|
|
192
|
+
<div
|
|
193
|
+
class="flex items-center gap-1.5 px-2 py-1 text-xs cursor-pointer hover:bg-white/5 rounded-sm"
|
|
194
|
+
onClick=${() => toggleExpand(m.id)}
|
|
195
|
+
>
|
|
196
|
+
<span
|
|
197
|
+
class=${`px-1 rounded font-mono flex-shrink-0 ${MAIL_TYPE_COLORS[m.type] ?? "bg-[#333] text-[#999]"}`}
|
|
198
|
+
>
|
|
199
|
+
${m.type || "mail"}
|
|
200
|
+
</span>
|
|
201
|
+
<span class="text-[#777] flex-shrink-0 truncate max-w-[5rem]">${m.from}</span>
|
|
202
|
+
<span class="text-[#444] flex-shrink-0">\u2192</span>
|
|
203
|
+
<span class="text-[#777] flex-shrink-0 truncate max-w-[5rem]">${m.to}</span>
|
|
204
|
+
<span class="flex-1 truncate text-[#999] min-w-0">${m.subject || ""}</span>
|
|
205
|
+
<span class="text-[#444] flex-shrink-0 ml-auto">${timeAgo(m.createdAt)}</span>
|
|
206
|
+
<span class="text-[#444] flex-shrink-0 ml-1">${isExpanded ? "\u25B2" : "\u25BC"}</span>
|
|
207
|
+
</div>
|
|
208
|
+
${
|
|
209
|
+
isExpanded
|
|
210
|
+
? html`
|
|
211
|
+
<div class="px-2 pb-2 text-xs border-t border-[#2a2a2a] mt-0.5 pt-1.5 space-y-1">
|
|
212
|
+
${
|
|
213
|
+
m.priority && m.priority !== "normal"
|
|
214
|
+
? html`
|
|
215
|
+
<div class="flex gap-1.5">
|
|
216
|
+
<span class="text-[#555] flex-shrink-0">priority:</span>
|
|
217
|
+
<span class="text-yellow-400 font-mono">${m.priority}</span>
|
|
218
|
+
</div>
|
|
219
|
+
`
|
|
220
|
+
: null
|
|
221
|
+
}
|
|
222
|
+
${
|
|
223
|
+
m.body
|
|
224
|
+
? html`
|
|
225
|
+
<div>
|
|
226
|
+
<div class="text-[#555] mb-0.5">body:</div>
|
|
227
|
+
<div class="text-[#aaa] whitespace-pre-wrap break-words font-mono bg-[#111] rounded px-2 py-1 max-h-[10rem] overflow-y-auto">
|
|
228
|
+
${m.body}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
`
|
|
232
|
+
: null
|
|
233
|
+
}
|
|
234
|
+
${
|
|
235
|
+
parsedPayload
|
|
236
|
+
? html`
|
|
237
|
+
<div>
|
|
238
|
+
<div class="text-[#555] mb-0.5">payload:</div>
|
|
239
|
+
<div class="text-[#aaa] whitespace-pre-wrap break-words font-mono bg-[#111] rounded px-2 py-1 max-h-[10rem] overflow-y-auto">
|
|
240
|
+
${JSON.stringify(parsedPayload, null, 2)}
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
`
|
|
244
|
+
: null
|
|
245
|
+
}
|
|
246
|
+
</div>
|
|
247
|
+
`
|
|
248
|
+
: null
|
|
249
|
+
}
|
|
250
|
+
</div>
|
|
251
|
+
`;
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// AgentRoster
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
const STATE_ORDER = { working: 0, booting: 1, stalled: 2, zombie: 3, completed: 4 };
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Build a depth-annotated ordered list from a flat agents array.
|
|
267
|
+
* Agents whose parentAgent is absent or not in the list are roots (depth 0).
|
|
268
|
+
* Children are placed immediately after their parent, sorted by state.
|
|
269
|
+
*/
|
|
270
|
+
function buildAgentHierarchy(agents) {
|
|
271
|
+
const agentNames = new Set(agents.map((a) => a.agentName));
|
|
272
|
+
const byParent = new Map();
|
|
273
|
+
const roots = [];
|
|
274
|
+
|
|
275
|
+
for (const agent of agents) {
|
|
276
|
+
const parent = agent.parentAgent;
|
|
277
|
+
if (!parent || !agentNames.has(parent)) {
|
|
278
|
+
roots.push(agent);
|
|
279
|
+
} else {
|
|
280
|
+
if (!byParent.has(parent)) byParent.set(parent, []);
|
|
281
|
+
byParent.get(parent).push(agent);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function sortByState(arr) {
|
|
286
|
+
return [...arr].sort((a, b) => {
|
|
287
|
+
const ao = STATE_ORDER[a.state] ?? 99;
|
|
288
|
+
const bo = STATE_ORDER[b.state] ?? 99;
|
|
289
|
+
if (ao !== bo) return ao - bo;
|
|
290
|
+
return (a.agentName ?? "").localeCompare(b.agentName ?? "");
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const result = [];
|
|
295
|
+
function walk(agent, depth) {
|
|
296
|
+
result.push({ agent, depth });
|
|
297
|
+
const children = sortByState(byParent.get(agent.agentName) ?? []);
|
|
298
|
+
for (const child of children) {
|
|
299
|
+
walk(child, depth + 1);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
for (const root of sortByState(roots)) {
|
|
303
|
+
walk(root, 0);
|
|
304
|
+
}
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function AgentRoster({ agents, mail, events }) {
|
|
309
|
+
const [expandedAgent, setExpandedAgent] = useState(null);
|
|
310
|
+
|
|
311
|
+
const ordered = buildAgentHierarchy(agents);
|
|
312
|
+
|
|
313
|
+
const activeCount = agents.filter((a) => a.state === "working" || a.state === "booting").length;
|
|
314
|
+
|
|
315
|
+
const toggleExpand = useCallback((name) => {
|
|
316
|
+
setExpandedAgent((prev) => (prev === name ? null : name));
|
|
317
|
+
}, []);
|
|
318
|
+
|
|
319
|
+
return html`
|
|
320
|
+
<div class="flex flex-col flex-1 min-h-0">
|
|
321
|
+
<!-- Header -->
|
|
322
|
+
<div class="px-3 py-2 border-b border-[#2a2a2a] shrink-0">
|
|
323
|
+
<span class="text-sm font-medium text-[#e5e5e5]">Agents</span>
|
|
324
|
+
<span class="ml-2 text-xs text-[#555]">${activeCount} active / ${agents.length} total</span>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<!-- Agent list -->
|
|
328
|
+
<div class="flex-1 overflow-y-auto min-h-0 p-2">
|
|
329
|
+
${
|
|
330
|
+
ordered.length === 0
|
|
331
|
+
? html`
|
|
332
|
+
<div class="flex items-center justify-center h-full text-[#666] text-sm">
|
|
333
|
+
No agents yet
|
|
334
|
+
</div>
|
|
335
|
+
`
|
|
336
|
+
: ordered.map(({ agent, depth }) => {
|
|
337
|
+
const isExpanded = expandedAgent === agent.agentName;
|
|
338
|
+
const colors = agentColor(agent.capability);
|
|
339
|
+
const icon = stateIcon(agent.state);
|
|
340
|
+
const iconColor = stateColor(agent.state);
|
|
341
|
+
|
|
342
|
+
// Filter mail for this agent
|
|
343
|
+
const agentMail = mail
|
|
344
|
+
.filter((m) => m.from === agent.agentName || m.to === agent.agentName)
|
|
345
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
346
|
+
.slice(0, 5);
|
|
347
|
+
|
|
348
|
+
// Filter events for this agent
|
|
349
|
+
const agentEvents = events.filter((e) => e.agent === agent.agentName).slice(0, 5);
|
|
350
|
+
|
|
351
|
+
return html`
|
|
352
|
+
<div
|
|
353
|
+
key=${agent.agentName}
|
|
354
|
+
class="mb-1 rounded border border-[#2a2a2a] bg-[#1a1a1a] overflow-hidden"
|
|
355
|
+
style=${{ marginLeft: `${depth * 16}px` }}
|
|
356
|
+
>
|
|
357
|
+
<!-- Row -->
|
|
358
|
+
<div
|
|
359
|
+
class="flex items-center gap-2 px-3 py-2 cursor-pointer hover:border-[#3a3a3a] hover:bg-[#222]"
|
|
360
|
+
onClick=${() => toggleExpand(agent.agentName)}
|
|
361
|
+
>
|
|
362
|
+
<span class="text-base leading-none flex-shrink-0">${colors.avatar}</span>
|
|
363
|
+
<span class=${`text-xs leading-none flex-shrink-0 ${iconColor}`}>${icon}</span>
|
|
364
|
+
<span class="text-sm text-[#e5e5e5] truncate flex-1 min-w-0">
|
|
365
|
+
${agent.agentName}
|
|
366
|
+
</span>
|
|
367
|
+
${
|
|
368
|
+
agent.beadId
|
|
369
|
+
? html`<span class="text-xs font-mono text-[#666] flex-shrink-0">
|
|
370
|
+
${agent.beadId}
|
|
371
|
+
</span>`
|
|
372
|
+
: null
|
|
373
|
+
}
|
|
374
|
+
<a
|
|
375
|
+
href=${`#inspect/${agent.agentName}`}
|
|
376
|
+
class="text-xs text-blue-400 hover:text-blue-300 flex-shrink-0 px-1.5 py-0.5 rounded bg-blue-900/20 hover:bg-blue-900/40 no-underline"
|
|
377
|
+
onClick=${(e) => e.stopPropagation()}
|
|
378
|
+
>
|
|
379
|
+
Details
|
|
380
|
+
</a>
|
|
381
|
+
<span class="text-xs text-[#444] flex-shrink-0">
|
|
382
|
+
${isExpanded ? "\u25B2" : "\u25BC"}
|
|
383
|
+
</span>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
<!-- Expanded detail -->
|
|
387
|
+
${
|
|
388
|
+
isExpanded
|
|
389
|
+
? html`
|
|
390
|
+
<div class="border-t border-[#2a2a2a] px-3 py-2 text-xs">
|
|
391
|
+
<!-- Meta badges -->
|
|
392
|
+
<div class="flex flex-wrap gap-1.5 mb-2">
|
|
393
|
+
<span
|
|
394
|
+
class=${`px-1.5 py-0.5 rounded font-mono ${colors.bg} ${colors.text} border ${colors.border}`}
|
|
395
|
+
>
|
|
396
|
+
${agent.capability || "unknown"}
|
|
397
|
+
</span>
|
|
398
|
+
<span
|
|
399
|
+
class=${`px-1.5 py-0.5 rounded font-mono ${iconColor}`}
|
|
400
|
+
>
|
|
401
|
+
${icon} ${agent.state}
|
|
402
|
+
</span>
|
|
403
|
+
${
|
|
404
|
+
agent.beadId
|
|
405
|
+
? html`<span class="font-mono text-[#666]">${agent.beadId}</span>`
|
|
406
|
+
: null
|
|
407
|
+
}
|
|
408
|
+
${
|
|
409
|
+
agent.startedAt
|
|
410
|
+
? html`<span class="text-[#555]">started ${timeAgo(agent.startedAt)}</span>`
|
|
411
|
+
: null
|
|
412
|
+
}
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<!-- Drill-down link -->
|
|
416
|
+
<div class="mb-2">
|
|
417
|
+
<a
|
|
418
|
+
href=${`#inspect/${agent.agentName}`}
|
|
419
|
+
class="text-xs text-blue-400 hover:text-blue-300"
|
|
420
|
+
>
|
|
421
|
+
View Details
|
|
422
|
+
</a>
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
<!-- Recent Mail -->
|
|
426
|
+
<div class="mb-2">
|
|
427
|
+
<div class="text-[#555] mb-1">Recent Mail</div>
|
|
428
|
+
${
|
|
429
|
+
agentMail.length === 0
|
|
430
|
+
? null
|
|
431
|
+
: agentMail.map(
|
|
432
|
+
(m) => html`
|
|
433
|
+
<div
|
|
434
|
+
key=${m.id}
|
|
435
|
+
class="flex items-center gap-1.5 py-0.5 border-b border-[#1f1f1f]"
|
|
436
|
+
>
|
|
437
|
+
<span class="text-[#444] flex-shrink-0">
|
|
438
|
+
${m.from === agent.agentName ? "\u2192" : "\u2190"}
|
|
439
|
+
</span>
|
|
440
|
+
<span class="text-[#666] flex-shrink-0 truncate max-w-[6rem]">
|
|
441
|
+
${m.from === agent.agentName ? m.to : m.from}
|
|
442
|
+
</span>
|
|
443
|
+
<span class="flex-1 truncate text-[#999] min-w-0">
|
|
444
|
+
${m.subject || m.body || ""}
|
|
445
|
+
</span>
|
|
446
|
+
<span class="text-[#444] flex-shrink-0 ml-auto">
|
|
447
|
+
${timeAgo(m.createdAt)}
|
|
448
|
+
</span>
|
|
449
|
+
</div>
|
|
450
|
+
`,
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
<!-- Recent Events -->
|
|
456
|
+
<div>
|
|
457
|
+
<div class="text-[#555] mb-1">Recent Events</div>
|
|
458
|
+
${
|
|
459
|
+
agentEvents.length === 0
|
|
460
|
+
? null
|
|
461
|
+
: agentEvents.map(
|
|
462
|
+
(ev) => html`
|
|
463
|
+
<div
|
|
464
|
+
key=${ev.id}
|
|
465
|
+
class="flex items-center gap-1.5 py-0.5 border-b border-[#1f1f1f]"
|
|
466
|
+
>
|
|
467
|
+
<span
|
|
468
|
+
class=${`px-1 rounded font-mono flex-shrink-0 ${typeBadgeClass(ev.type)}`}
|
|
469
|
+
>
|
|
470
|
+
${ev.type || "unknown"}
|
|
471
|
+
</span>
|
|
472
|
+
<span class="flex-1 truncate text-[#999] min-w-0">
|
|
473
|
+
${ev.summary || ""}
|
|
474
|
+
</span>
|
|
475
|
+
<span class="text-[#444] flex-shrink-0 ml-auto">
|
|
476
|
+
${timeAgo(ev.createdAt)}
|
|
477
|
+
</span>
|
|
478
|
+
</div>
|
|
479
|
+
`,
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
</div>
|
|
483
|
+
|
|
484
|
+
${
|
|
485
|
+
agentMail.length === 0 && agentEvents.length === 0
|
|
486
|
+
? html`<div class="text-[#444] text-center py-1">No recent activity</div>`
|
|
487
|
+
: null
|
|
488
|
+
}
|
|
489
|
+
</div>
|
|
490
|
+
`
|
|
491
|
+
: null
|
|
492
|
+
}
|
|
493
|
+
</div>
|
|
494
|
+
`;
|
|
495
|
+
})
|
|
496
|
+
}
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
// CoordinatorBar
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
|
|
506
|
+
function CoordinatorBar() {
|
|
507
|
+
const [coordStatus, setCoordStatus] = useState(null); // null = unknown, true = running, false = stopped
|
|
508
|
+
const [loading, setLoading] = useState(false); // start/stop in-flight
|
|
509
|
+
const [gwStatus, setGwStatus] = useState(null); // null = unknown, true = running, false = stopped
|
|
510
|
+
const [gwLoading, setGwLoading] = useState(false);
|
|
511
|
+
const [error, setError] = useState(null);
|
|
512
|
+
|
|
513
|
+
const poll = useCallback(async (cancelled) => {
|
|
514
|
+
try {
|
|
515
|
+
const data = await fetchJson("/api/coordinator/status");
|
|
516
|
+
if (!cancelled) setCoordStatus(data?.running === true);
|
|
517
|
+
} catch (_err) {
|
|
518
|
+
// non-fatal — leave status as unknown
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
const gwData = await fetchJson("/api/gateway/status");
|
|
522
|
+
if (!cancelled) setGwStatus(gwData?.running === true);
|
|
523
|
+
} catch (_err) {
|
|
524
|
+
/* non-fatal */
|
|
525
|
+
}
|
|
526
|
+
}, []);
|
|
527
|
+
|
|
528
|
+
useEffect(() => {
|
|
529
|
+
let cancelled = false;
|
|
530
|
+
poll(cancelled);
|
|
531
|
+
const interval = setInterval(() => poll(cancelled), 5000);
|
|
532
|
+
return () => {
|
|
533
|
+
cancelled = true;
|
|
534
|
+
clearInterval(interval);
|
|
535
|
+
};
|
|
536
|
+
}, [poll]);
|
|
537
|
+
|
|
538
|
+
// Auto-clear error after 5s
|
|
539
|
+
useEffect(() => {
|
|
540
|
+
if (!error) return;
|
|
541
|
+
const timer = setTimeout(() => setError(null), 5000);
|
|
542
|
+
return () => clearTimeout(timer);
|
|
543
|
+
}, [error]);
|
|
544
|
+
|
|
545
|
+
const handleStart = useCallback(async () => {
|
|
546
|
+
setLoading(true);
|
|
547
|
+
setError(null);
|
|
548
|
+
try {
|
|
549
|
+
await postJson("/api/coordinator/start", {});
|
|
550
|
+
await poll(false);
|
|
551
|
+
} catch (err) {
|
|
552
|
+
setError(err?.message ?? "Failed to start coordinator");
|
|
553
|
+
} finally {
|
|
554
|
+
setLoading(false);
|
|
555
|
+
}
|
|
556
|
+
}, [poll]);
|
|
557
|
+
|
|
558
|
+
const handleStop = useCallback(async () => {
|
|
559
|
+
setLoading(true);
|
|
560
|
+
setError(null);
|
|
561
|
+
try {
|
|
562
|
+
await postJson("/api/coordinator/stop", {});
|
|
563
|
+
await poll(false);
|
|
564
|
+
} catch (err) {
|
|
565
|
+
setError(err?.message ?? "Failed to stop coordinator");
|
|
566
|
+
} finally {
|
|
567
|
+
setLoading(false);
|
|
568
|
+
}
|
|
569
|
+
}, [poll]);
|
|
570
|
+
|
|
571
|
+
const handleGwStart = useCallback(async () => {
|
|
572
|
+
setGwLoading(true);
|
|
573
|
+
setError(null);
|
|
574
|
+
try {
|
|
575
|
+
await postJson("/api/gateway/start", {});
|
|
576
|
+
await poll(false);
|
|
577
|
+
} catch (err) {
|
|
578
|
+
setError(err?.message ?? "Failed to start gateway");
|
|
579
|
+
} finally {
|
|
580
|
+
setGwLoading(false);
|
|
581
|
+
}
|
|
582
|
+
}, [poll]);
|
|
583
|
+
|
|
584
|
+
const handleGwStop = useCallback(async () => {
|
|
585
|
+
setGwLoading(true);
|
|
586
|
+
setError(null);
|
|
587
|
+
try {
|
|
588
|
+
await postJson("/api/gateway/stop", {});
|
|
589
|
+
await poll(false);
|
|
590
|
+
} catch (err) {
|
|
591
|
+
setError(err?.message ?? "Failed to stop gateway");
|
|
592
|
+
} finally {
|
|
593
|
+
setGwLoading(false);
|
|
594
|
+
}
|
|
595
|
+
}, [poll]);
|
|
596
|
+
|
|
597
|
+
const isRunning = coordStatus === true;
|
|
598
|
+
const isStopped = coordStatus === false;
|
|
599
|
+
const isUnknown = coordStatus === null;
|
|
600
|
+
|
|
601
|
+
const dotColor = isRunning ? "bg-green-500" : isStopped ? "bg-[#666]" : "bg-[#666]";
|
|
602
|
+
const statusText = isRunning ? "Running" : isStopped ? "Stopped" : "Unknown";
|
|
603
|
+
|
|
604
|
+
const gwIsRunning = gwStatus === true;
|
|
605
|
+
const gwIsStopped = gwStatus === false;
|
|
606
|
+
const gwIsUnknown = gwStatus === null;
|
|
607
|
+
|
|
608
|
+
const gwDotColor = gwIsRunning ? "bg-green-500" : "bg-[#666]";
|
|
609
|
+
const gwStatusText = gwIsRunning ? "Running" : gwIsStopped ? "Stopped" : "Unknown";
|
|
610
|
+
|
|
611
|
+
return html`
|
|
612
|
+
<div class="flex items-center gap-3 px-3 py-2 bg-[#1a1a1a] border-b border-[#2a2a2a] shrink-0">
|
|
613
|
+
<div class="flex items-center gap-2">
|
|
614
|
+
<span class="text-xs text-[#666] uppercase tracking-wide">Coordinator</span>
|
|
615
|
+
<div class="flex items-center gap-1">
|
|
616
|
+
<div class="w-2 h-2 rounded-full ${dotColor}"></div>
|
|
617
|
+
<span class="text-sm text-[#e5e5e5]">${statusText}</span>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
<div class="flex items-center gap-2">
|
|
621
|
+
<button
|
|
622
|
+
onClick=${handleStart}
|
|
623
|
+
disabled=${loading || isRunning}
|
|
624
|
+
class="bg-[#E64415] hover:bg-[#cc3d12] disabled:opacity-50 text-white text-sm px-3 py-1 rounded cursor-pointer border-none"
|
|
625
|
+
>
|
|
626
|
+
${loading && !isRunning ? "\u2026" : "Start"}
|
|
627
|
+
</button>
|
|
628
|
+
<button
|
|
629
|
+
onClick=${handleStop}
|
|
630
|
+
disabled=${loading || isStopped || isUnknown}
|
|
631
|
+
class="bg-[#E64415] hover:bg-[#cc3d12] disabled:opacity-50 text-white text-sm px-3 py-1 rounded cursor-pointer border-none"
|
|
632
|
+
>
|
|
633
|
+
${loading && isRunning ? "\u2026" : "Stop"}
|
|
634
|
+
</button>
|
|
635
|
+
</div>
|
|
636
|
+
<div class="border-l border-[#2a2a2a] pl-3 ml-1 flex items-center gap-2">
|
|
637
|
+
<span class="text-xs text-[#666] uppercase tracking-wide">Gateway</span>
|
|
638
|
+
<div class="flex items-center gap-1">
|
|
639
|
+
<div class="w-2 h-2 rounded-full ${gwDotColor}"></div>
|
|
640
|
+
<span class="text-sm text-[#e5e5e5]">${gwStatusText}</span>
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
<div class="flex items-center gap-2">
|
|
644
|
+
<button
|
|
645
|
+
onClick=${handleGwStart}
|
|
646
|
+
disabled=${gwLoading || gwIsRunning}
|
|
647
|
+
class="bg-[#E64415] hover:bg-[#cc3d12] disabled:opacity-50 text-white text-sm px-3 py-1 rounded cursor-pointer border-none"
|
|
648
|
+
>
|
|
649
|
+
${gwLoading && !gwIsRunning ? "\u2026" : "Start"}
|
|
650
|
+
</button>
|
|
651
|
+
<button
|
|
652
|
+
onClick=${handleGwStop}
|
|
653
|
+
disabled=${gwLoading || gwIsStopped || gwIsUnknown}
|
|
654
|
+
class="bg-[#E64415] hover:bg-[#cc3d12] disabled:opacity-50 text-white text-sm px-3 py-1 rounded cursor-pointer border-none"
|
|
655
|
+
>
|
|
656
|
+
${gwLoading && gwIsRunning ? "\u2026" : "Stop"}
|
|
657
|
+
</button>
|
|
658
|
+
</div>
|
|
659
|
+
${error ? html`<span class="text-xs text-red-400">${error}</span>` : null}
|
|
660
|
+
</div>
|
|
661
|
+
`;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ---------------------------------------------------------------------------
|
|
665
|
+
// DashboardView — main export
|
|
666
|
+
// ---------------------------------------------------------------------------
|
|
667
|
+
|
|
668
|
+
const NOISE_EVENT_TYPES = new Set(["tool_start", "tool_end"]);
|
|
669
|
+
|
|
670
|
+
export function DashboardView() {
|
|
671
|
+
const [activityEvents, setActivityEvents] = useState([]);
|
|
672
|
+
const [mail, setMail] = useState([]);
|
|
673
|
+
const [_coordRunning, setCoordRunning] = useState(false);
|
|
674
|
+
const [gwRunning, setGwRunning] = useState(false);
|
|
675
|
+
|
|
676
|
+
// Poll event store every 5s
|
|
677
|
+
useEffect(() => {
|
|
678
|
+
let cancelled = false;
|
|
679
|
+
|
|
680
|
+
async function fetchActivity() {
|
|
681
|
+
try {
|
|
682
|
+
const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
|
683
|
+
const data = await fetchJson(`/api/events?since=${encodeURIComponent(since)}&limit=200`);
|
|
684
|
+
if (!cancelled) {
|
|
685
|
+
const rawEvents = Array.isArray(data) ? data : (data?.events ?? []);
|
|
686
|
+
const filteredEvents = rawEvents
|
|
687
|
+
.filter((e) => !NOISE_EVENT_TYPES.has(e.eventType))
|
|
688
|
+
.map((e) => ({
|
|
689
|
+
id: `evt-${e.id}`,
|
|
690
|
+
type: e.eventType,
|
|
691
|
+
agent: e.agentName,
|
|
692
|
+
summary: buildEventSummary(e),
|
|
693
|
+
detail: e.data,
|
|
694
|
+
createdAt: e.createdAt,
|
|
695
|
+
}));
|
|
696
|
+
setActivityEvents(filteredEvents);
|
|
697
|
+
}
|
|
698
|
+
} catch (_err) {
|
|
699
|
+
// non-fatal
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
fetchActivity();
|
|
704
|
+
const interval = setInterval(fetchActivity, 5000);
|
|
705
|
+
return () => {
|
|
706
|
+
cancelled = true;
|
|
707
|
+
clearInterval(interval);
|
|
708
|
+
};
|
|
709
|
+
}, []);
|
|
710
|
+
|
|
711
|
+
// Poll mail every 5s
|
|
712
|
+
useEffect(() => {
|
|
713
|
+
let cancelled = false;
|
|
714
|
+
async function fetchMail() {
|
|
715
|
+
try {
|
|
716
|
+
const data = await fetchJson("/api/mail");
|
|
717
|
+
if (!cancelled) {
|
|
718
|
+
setMail(Array.isArray(data) ? data : (data?.recent ?? []));
|
|
719
|
+
}
|
|
720
|
+
} catch (_err) {
|
|
721
|
+
// non-fatal
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
fetchMail();
|
|
725
|
+
const interval = setInterval(fetchMail, 5000);
|
|
726
|
+
return () => {
|
|
727
|
+
cancelled = true;
|
|
728
|
+
clearInterval(interval);
|
|
729
|
+
};
|
|
730
|
+
}, []);
|
|
731
|
+
|
|
732
|
+
// Poll coordinator/gateway status for chat routing
|
|
733
|
+
useEffect(() => {
|
|
734
|
+
let cancelled = false;
|
|
735
|
+
async function fetchStatuses() {
|
|
736
|
+
try {
|
|
737
|
+
const [coordData, gwData] = await Promise.all([
|
|
738
|
+
fetchJson("/api/coordinator/status").catch(() => null),
|
|
739
|
+
fetchJson("/api/gateway/status").catch(() => null),
|
|
740
|
+
]);
|
|
741
|
+
if (!cancelled) {
|
|
742
|
+
setCoordRunning(coordData?.running === true);
|
|
743
|
+
setGwRunning(gwData?.running === true);
|
|
744
|
+
}
|
|
745
|
+
} catch (_err) {
|
|
746
|
+
// non-fatal
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
fetchStatuses();
|
|
750
|
+
const interval = setInterval(fetchStatuses, 5000);
|
|
751
|
+
return () => {
|
|
752
|
+
cancelled = true;
|
|
753
|
+
clearInterval(interval);
|
|
754
|
+
};
|
|
755
|
+
}, []);
|
|
756
|
+
|
|
757
|
+
const agents = appState.agents.value;
|
|
758
|
+
const status = appState.status.value;
|
|
759
|
+
|
|
760
|
+
return html`
|
|
761
|
+
<div class="flex flex-col h-full bg-[#0f0f0f] min-h-0">
|
|
762
|
+
<${CoordinatorBar} />
|
|
763
|
+
<div class="flex flex-1 min-h-0">
|
|
764
|
+
<!-- Coordinator Chat (left, ~58%) -->
|
|
765
|
+
<div
|
|
766
|
+
class="flex flex-col min-h-0 overflow-hidden border-r border-[#2a2a2a]"
|
|
767
|
+
style="flex: 58 1 0%"
|
|
768
|
+
>
|
|
769
|
+
<${GatewayChat} gwRunning=${gwRunning} />
|
|
770
|
+
</div>
|
|
771
|
+
|
|
772
|
+
<!-- Sidebar (right, ~42%): MetricsStrip + AgentRoster + MailFeed -->
|
|
773
|
+
<div class="flex flex-col min-h-0 overflow-hidden" style="flex: 42 1 0%">
|
|
774
|
+
<${MetricsStrip} agents=${agents} status=${status} />
|
|
775
|
+
<${AgentRoster} agents=${agents} mail=${mail} events=${activityEvents} />
|
|
776
|
+
<${MailFeed} mail=${mail} />
|
|
777
|
+
</div>
|
|
778
|
+
</div>
|
|
779
|
+
</div>
|
|
780
|
+
`;
|
|
781
|
+
}
|