@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,256 @@
|
|
|
1
|
+
// components/gateway-chat.js — GatewayChat embedded component
|
|
2
|
+
// Reusable gateway chat panel extracted from views/gateway.js.
|
|
3
|
+
// Shows terminal output + chat input when gateway is running,
|
|
4
|
+
// or a "Start Gateway" prompt when stopped.
|
|
5
|
+
|
|
6
|
+
import { html, useCallback, useEffect, useRef, useState } from "../lib/preact-setup.js";
|
|
7
|
+
|
|
8
|
+
const POLL_INTERVAL_MS = 2500;
|
|
9
|
+
|
|
10
|
+
export function GatewayChat({ onStatusChange }) {
|
|
11
|
+
const [running, setRunning] = useState(false);
|
|
12
|
+
const [tmuxSession, setTmuxSession] = useState(null);
|
|
13
|
+
const [statusLoading, setStatusLoading] = useState(true);
|
|
14
|
+
const [actionLoading, setActionLoading] = useState(false);
|
|
15
|
+
const [actionError, setActionError] = useState("");
|
|
16
|
+
const [text, setText] = useState("");
|
|
17
|
+
const [sendError, setSendError] = useState("");
|
|
18
|
+
const [sending, setSending] = useState(false);
|
|
19
|
+
const [termOutput, setTermOutput] = useState("");
|
|
20
|
+
const pollRef = useRef(null);
|
|
21
|
+
const outputRef = useRef(null);
|
|
22
|
+
|
|
23
|
+
const setRunningWithCallback = useCallback(
|
|
24
|
+
(value) => {
|
|
25
|
+
setRunning(value);
|
|
26
|
+
if (onStatusChange) onStatusChange(value);
|
|
27
|
+
},
|
|
28
|
+
[onStatusChange],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const fetchStatus = useCallback(async () => {
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch("/api/gateway/status");
|
|
34
|
+
if (res.ok) {
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
setRunningWithCallback(data.running);
|
|
37
|
+
setTmuxSession(data.tmuxSession ?? null);
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// ignore
|
|
41
|
+
} finally {
|
|
42
|
+
setStatusLoading(false);
|
|
43
|
+
}
|
|
44
|
+
}, [setRunningWithCallback]);
|
|
45
|
+
|
|
46
|
+
const fetchTermOutput = useCallback(async () => {
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch("/api/terminal/capture?agent=gateway&lines=100");
|
|
49
|
+
if (res.ok) {
|
|
50
|
+
const data = await res.json();
|
|
51
|
+
setTermOutput(typeof data.output === "string" ? data.output : "");
|
|
52
|
+
if (outputRef.current) {
|
|
53
|
+
outputRef.current.scrollTop = outputRef.current.scrollHeight;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// ignore
|
|
58
|
+
}
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
fetchStatus();
|
|
63
|
+
}, [fetchStatus]);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (running) {
|
|
67
|
+
fetchTermOutput();
|
|
68
|
+
pollRef.current = setInterval(fetchTermOutput, POLL_INTERVAL_MS);
|
|
69
|
+
} else {
|
|
70
|
+
if (pollRef.current) {
|
|
71
|
+
clearInterval(pollRef.current);
|
|
72
|
+
pollRef.current = null;
|
|
73
|
+
}
|
|
74
|
+
setTermOutput("");
|
|
75
|
+
}
|
|
76
|
+
return () => {
|
|
77
|
+
if (pollRef.current) {
|
|
78
|
+
clearInterval(pollRef.current);
|
|
79
|
+
pollRef.current = null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}, [running, fetchTermOutput]);
|
|
83
|
+
|
|
84
|
+
const handleStart = useCallback(async () => {
|
|
85
|
+
setActionLoading(true);
|
|
86
|
+
setActionError("");
|
|
87
|
+
try {
|
|
88
|
+
const res = await fetch("/api/gateway/start", {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: { "Content-Type": "application/json" },
|
|
91
|
+
body: JSON.stringify({}),
|
|
92
|
+
});
|
|
93
|
+
if (res.ok) {
|
|
94
|
+
await fetchStatus();
|
|
95
|
+
} else {
|
|
96
|
+
const err = await res.json().catch(() => ({}));
|
|
97
|
+
setActionError(err.error || "Failed to start gateway");
|
|
98
|
+
}
|
|
99
|
+
} catch (e) {
|
|
100
|
+
setActionError(e.message || "Failed to start gateway");
|
|
101
|
+
} finally {
|
|
102
|
+
setActionLoading(false);
|
|
103
|
+
}
|
|
104
|
+
}, [fetchStatus]);
|
|
105
|
+
|
|
106
|
+
const handleStop = useCallback(async () => {
|
|
107
|
+
setActionLoading(true);
|
|
108
|
+
setActionError("");
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch("/api/gateway/stop", {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: { "Content-Type": "application/json" },
|
|
113
|
+
body: JSON.stringify({}),
|
|
114
|
+
});
|
|
115
|
+
if (res.ok) {
|
|
116
|
+
setRunningWithCallback(false);
|
|
117
|
+
setTmuxSession(null);
|
|
118
|
+
} else {
|
|
119
|
+
const err = await res.json().catch(() => ({}));
|
|
120
|
+
setActionError(err.error || "Failed to stop gateway");
|
|
121
|
+
}
|
|
122
|
+
} catch (e) {
|
|
123
|
+
setActionError(e.message || "Failed to stop gateway");
|
|
124
|
+
} finally {
|
|
125
|
+
setActionLoading(false);
|
|
126
|
+
}
|
|
127
|
+
}, [setRunningWithCallback]);
|
|
128
|
+
|
|
129
|
+
const handleSend = useCallback(async () => {
|
|
130
|
+
const trimmed = text.trim();
|
|
131
|
+
if (!trimmed) return;
|
|
132
|
+
setSendError("");
|
|
133
|
+
setSending(true);
|
|
134
|
+
try {
|
|
135
|
+
const res = await fetch("/api/gateway/chat", {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: { "Content-Type": "application/json" },
|
|
138
|
+
body: JSON.stringify({ text: trimmed }),
|
|
139
|
+
});
|
|
140
|
+
if (res.ok) {
|
|
141
|
+
setText("");
|
|
142
|
+
setTimeout(fetchTermOutput, 600);
|
|
143
|
+
} else {
|
|
144
|
+
const err = await res.json().catch(() => ({}));
|
|
145
|
+
setSendError(err.error || "Send failed");
|
|
146
|
+
}
|
|
147
|
+
} catch (e) {
|
|
148
|
+
setSendError(e.message || "Send failed");
|
|
149
|
+
} finally {
|
|
150
|
+
setSending(false);
|
|
151
|
+
}
|
|
152
|
+
}, [text, fetchTermOutput]);
|
|
153
|
+
|
|
154
|
+
const handleKeyDown = useCallback(
|
|
155
|
+
(e) => {
|
|
156
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
handleSend();
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
[handleSend],
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
if (statusLoading) {
|
|
165
|
+
return html`
|
|
166
|
+
<div class="flex items-center justify-center h-full bg-[#0f0f0f] text-[#555] text-sm">
|
|
167
|
+
Checking gateway status...
|
|
168
|
+
</div>
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return html`
|
|
173
|
+
<div class="flex flex-col h-full">
|
|
174
|
+
|
|
175
|
+
<!-- Header bar -->
|
|
176
|
+
<div class="flex items-center gap-3 px-4 py-3 border-b border-[#2a2a2a] bg-[#1a1a1a] shrink-0">
|
|
177
|
+
<span class="text-sm font-semibold text-[#e5e5e5]">Gateway</span>
|
|
178
|
+
<span
|
|
179
|
+
class=${`w-2 h-2 rounded-full shrink-0 ${running ? "bg-green-500" : "bg-[#444]"}`}
|
|
180
|
+
title=${running ? "Running" : "Stopped"}
|
|
181
|
+
></span>
|
|
182
|
+
<span class=${`text-xs ${running ? "text-green-400" : "text-[#555]"}`}>
|
|
183
|
+
${running ? (tmuxSession ? `Running (${tmuxSession})` : "Running") : "Stopped"}
|
|
184
|
+
</span>
|
|
185
|
+
<span class="flex-1"></span>
|
|
186
|
+
${
|
|
187
|
+
running
|
|
188
|
+
? html`<button
|
|
189
|
+
onClick=${handleStop}
|
|
190
|
+
disabled=${actionLoading}
|
|
191
|
+
class="text-xs px-3 py-1 rounded border border-[#444] text-[#e5e5e5] bg-transparent hover:bg-[#2a2a2a] disabled:opacity-50 cursor-pointer"
|
|
192
|
+
>
|
|
193
|
+
${actionLoading ? "Stopping..." : "Stop"}
|
|
194
|
+
</button>`
|
|
195
|
+
: html`<button
|
|
196
|
+
onClick=${handleStart}
|
|
197
|
+
disabled=${actionLoading}
|
|
198
|
+
class="text-xs px-3 py-1 rounded bg-[#E64415] hover:bg-[#cc3d12] text-white disabled:opacity-50 cursor-pointer border-none"
|
|
199
|
+
>
|
|
200
|
+
${actionLoading ? "Starting..." : "Start"}
|
|
201
|
+
</button>`
|
|
202
|
+
}
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
${actionError && html`<div class="px-4 py-2 text-xs text-red-400 bg-[#1a0a0a] border-b border-[#3a1a1a]">${actionError}</div>`}
|
|
206
|
+
|
|
207
|
+
${
|
|
208
|
+
!running
|
|
209
|
+
? html`
|
|
210
|
+
<div class="flex flex-col items-center justify-center flex-1 gap-3 text-[#555]">
|
|
211
|
+
<p class="text-sm">Gateway is not running.</p>
|
|
212
|
+
<button
|
|
213
|
+
onClick=${handleStart}
|
|
214
|
+
disabled=${actionLoading}
|
|
215
|
+
class="text-sm px-4 py-2 rounded bg-[#E64415] hover:bg-[#cc3d12] text-white disabled:opacity-50 cursor-pointer border-none"
|
|
216
|
+
>
|
|
217
|
+
${actionLoading ? "Starting..." : "Start Gateway"}
|
|
218
|
+
</button>
|
|
219
|
+
</div>
|
|
220
|
+
`
|
|
221
|
+
: html`
|
|
222
|
+
<!-- Terminal output area -->
|
|
223
|
+
<div
|
|
224
|
+
ref=${outputRef}
|
|
225
|
+
class="flex-1 overflow-y-auto p-4 font-mono text-xs text-[#e5e5e5] whitespace-pre-wrap min-h-0"
|
|
226
|
+
style="background:#0a0a0a"
|
|
227
|
+
>
|
|
228
|
+
${termOutput || html`<span class="text-[#555]">No output yet...</span>`}
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<!-- Chat input -->
|
|
232
|
+
<div class="border-t border-[#2a2a2a] p-3 shrink-0">
|
|
233
|
+
<div class="flex gap-2 items-end">
|
|
234
|
+
<textarea
|
|
235
|
+
placeholder="Send text to gateway... (Ctrl+Enter to send)"
|
|
236
|
+
rows="2"
|
|
237
|
+
value=${text}
|
|
238
|
+
onInput=${(e) => setText(e.target.value)}
|
|
239
|
+
onKeyDown=${handleKeyDown}
|
|
240
|
+
class="flex-1 bg-[#1a1a1a] border border-[#2a2a2a] rounded px-2 py-1 text-sm text-[#e5e5e5] placeholder-[#666] outline-none focus:border-[#E64415] resize-none"
|
|
241
|
+
/>
|
|
242
|
+
<button
|
|
243
|
+
onClick=${handleSend}
|
|
244
|
+
disabled=${sending || !text.trim()}
|
|
245
|
+
class="bg-[#E64415] hover:bg-[#cc3d12] disabled:opacity-50 text-white text-sm px-3 py-1 rounded cursor-pointer border-none self-end"
|
|
246
|
+
>
|
|
247
|
+
${sending ? "..." : "Send"}
|
|
248
|
+
</button>
|
|
249
|
+
</div>
|
|
250
|
+
${sendError && html`<span class="text-xs text-red-400 mt-1 block">${sendError}</span>`}
|
|
251
|
+
</div>
|
|
252
|
+
`
|
|
253
|
+
}
|
|
254
|
+
</div>
|
|
255
|
+
`;
|
|
256
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// IssueCard — reusable kanban card component
|
|
2
|
+
// Used by views/issues.js
|
|
3
|
+
|
|
4
|
+
import { html } from "../lib/preact-setup.js";
|
|
5
|
+
|
|
6
|
+
// Maps priority number → left border color (hex, for inline style)
|
|
7
|
+
const priorityBorderColors = {
|
|
8
|
+
0: "#ef4444",
|
|
9
|
+
1: "#f97316",
|
|
10
|
+
2: "#eab308",
|
|
11
|
+
3: "#3b82f6",
|
|
12
|
+
4: "#6b7280",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function timeAgo(isoString) {
|
|
16
|
+
if (!isoString) return "";
|
|
17
|
+
const diff = Date.now() - new Date(isoString).getTime();
|
|
18
|
+
if (diff < 0) return "just now";
|
|
19
|
+
const s = Math.floor(diff / 1000);
|
|
20
|
+
if (s < 60) return `${s}s ago`;
|
|
21
|
+
const m = Math.floor(s / 60);
|
|
22
|
+
if (m < 60) return `${m}m ago`;
|
|
23
|
+
const hh = Math.floor(m / 60);
|
|
24
|
+
if (hh < 24) return `${hh}h ago`;
|
|
25
|
+
return `${Math.floor(hh / 24)}d ago`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function truncate(str, maxLen) {
|
|
29
|
+
if (!str) return "";
|
|
30
|
+
return str.length <= maxLen ? str : `${str.slice(0, maxLen - 3)}...`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function IssueCard({ issue, children }) {
|
|
34
|
+
const borderColor = priorityBorderColors[issue.priority] ?? "#6b7280";
|
|
35
|
+
const hasBlockedBy = Array.isArray(issue.blockedBy) && issue.blockedBy.length > 0;
|
|
36
|
+
const isClosed = issue.status === "closed";
|
|
37
|
+
|
|
38
|
+
function handleClick() {
|
|
39
|
+
location.hash = `task/${issue.id}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return html`
|
|
43
|
+
<div
|
|
44
|
+
class=${`bg-[#1a1a1a] border border-[#2a2a2a] border-l-4 rounded-sm p-3 cursor-pointer hover:border-[#3a3a3a] hover:bg-[#222]${isClosed ? " opacity-50" : ""}`}
|
|
45
|
+
style=${{ borderLeftColor: borderColor }}
|
|
46
|
+
onClick=${handleClick}
|
|
47
|
+
>
|
|
48
|
+
<div class="flex items-start justify-between gap-2 mb-1">
|
|
49
|
+
<span class="flex items-center gap-1">
|
|
50
|
+
${hasBlockedBy ? html`<span class="text-xs">⚠️</span>` : null}
|
|
51
|
+
<span class=${`text-xs font-mono${hasBlockedBy ? " text-red-400" : " text-[#999]"}`}>${issue.id || ""}</span>
|
|
52
|
+
${isClosed ? html`<span class="text-xs bg-green-900/40 text-green-400 rounded px-1">Closed</span>` : null}
|
|
53
|
+
</span>
|
|
54
|
+
${issue.priority != null ? html`<span class="text-[#999] text-xs">P${issue.priority}</span>` : null}
|
|
55
|
+
</div>
|
|
56
|
+
<div class=${`text-[#e5e5e5] font-medium text-sm mb-1${isClosed ? " line-through" : ""}`}>
|
|
57
|
+
${truncate(issue.title || "", 60)}
|
|
58
|
+
</div>
|
|
59
|
+
${
|
|
60
|
+
isClosed && issue.closeReason
|
|
61
|
+
? html`
|
|
62
|
+
<div class="text-[#666] text-xs mb-1 italic">
|
|
63
|
+
${truncate(issue.closeReason, 80)}
|
|
64
|
+
</div>
|
|
65
|
+
`
|
|
66
|
+
: null
|
|
67
|
+
}
|
|
68
|
+
${
|
|
69
|
+
issue.description
|
|
70
|
+
? html`
|
|
71
|
+
<div class="text-[#999] text-xs mb-2 leading-relaxed">
|
|
72
|
+
${truncate(issue.description, 120)}
|
|
73
|
+
</div>
|
|
74
|
+
`
|
|
75
|
+
: null
|
|
76
|
+
}
|
|
77
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
78
|
+
${issue.type ? html`<span class="text-xs bg-[#2a2a2a] rounded px-1 text-[#999]">${issue.type}</span>` : null}
|
|
79
|
+
${issue.assignee ? html`<span class="text-[#999] text-xs">${issue.assignee}</span>` : null}
|
|
80
|
+
${issue.createdAt ? html`<span class="text-[#999] text-xs">${timeAgo(issue.createdAt)}</span>` : null}
|
|
81
|
+
</div>
|
|
82
|
+
${
|
|
83
|
+
hasBlockedBy
|
|
84
|
+
? html`
|
|
85
|
+
<div class="mt-1 text-xs text-red-500">
|
|
86
|
+
blocked by: ${issue.blockedBy.join(", ")}
|
|
87
|
+
</div>
|
|
88
|
+
`
|
|
89
|
+
: null
|
|
90
|
+
}
|
|
91
|
+
${children}
|
|
92
|
+
</div>
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default IssueCard;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Legio Web UI — Layout components
|
|
2
|
+
// App shell: NavBar, WsIndicator, Layout wrapper.
|
|
3
|
+
// No npm dependencies — uses CDN imports. Served as a static ES module.
|
|
4
|
+
|
|
5
|
+
import htm from "https://esm.sh/htm@latest";
|
|
6
|
+
import { h } from "https://esm.sh/preact@latest";
|
|
7
|
+
|
|
8
|
+
const html = htm.bind(h);
|
|
9
|
+
|
|
10
|
+
const NAV_LINKS = [
|
|
11
|
+
{ label: "Chat", hash: "/" },
|
|
12
|
+
{ label: "Dashboard", hash: "dashboard" },
|
|
13
|
+
{ label: "Events", hash: "events" },
|
|
14
|
+
{ label: "Costs", hash: "costs" },
|
|
15
|
+
{ label: "Issues", hash: "issues" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* WsIndicator — shows WebSocket connection state.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} props
|
|
22
|
+
* @param {boolean} props.connected - Whether the WebSocket is connected
|
|
23
|
+
*/
|
|
24
|
+
export function WsIndicator({ connected }) {
|
|
25
|
+
return html`
|
|
26
|
+
<div class="flex items-center gap-1.5 text-xs">
|
|
27
|
+
<span
|
|
28
|
+
class=${`w-2 h-2 rounded-full ${connected ? "bg-green-500" : "bg-red-500"}`}
|
|
29
|
+
></span>
|
|
30
|
+
<span class=${connected ? "text-green-400" : "text-red-400"}>
|
|
31
|
+
${connected ? "connected" : "disconnected"}
|
|
32
|
+
</span>
|
|
33
|
+
</div>
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* NavBar — horizontal top navigation bar.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} props
|
|
41
|
+
* @param {string} props.currentView - Active view name (matches NAV_LINKS hash)
|
|
42
|
+
* @param {boolean} props.wsConnected - WebSocket connection state
|
|
43
|
+
*/
|
|
44
|
+
export function NavBar({ currentView, wsConnected }) {
|
|
45
|
+
return html`
|
|
46
|
+
<nav class="bg-[#0f0f0f] border-b border-[#2a2a2a] px-4 h-12 flex items-center justify-between">
|
|
47
|
+
<div class="flex items-center gap-6">
|
|
48
|
+
<span class="font-bold text-[#e5e5e5] text-sm tracking-wide">legio</span>
|
|
49
|
+
<div class="flex items-center gap-0">
|
|
50
|
+
${NAV_LINKS.map(
|
|
51
|
+
(link) => html`
|
|
52
|
+
<a
|
|
53
|
+
key=${link.hash}
|
|
54
|
+
href=${`#${link.hash}`}
|
|
55
|
+
class=${[
|
|
56
|
+
"px-3 h-12 flex items-center text-sm transition-colors",
|
|
57
|
+
currentView === link.hash
|
|
58
|
+
? "text-[#E64415] border-b-2 border-[#E64415]"
|
|
59
|
+
: "text-[#999] hover:text-[#e5e5e5]",
|
|
60
|
+
].join(" ")}
|
|
61
|
+
>
|
|
62
|
+
${link.label}
|
|
63
|
+
</a>
|
|
64
|
+
`,
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<${WsIndicator} connected=${wsConnected} />
|
|
69
|
+
</nav>
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Layout — app shell wrapping children with NavBar on top.
|
|
75
|
+
*
|
|
76
|
+
* @param {object} props
|
|
77
|
+
* @param {string} props.currentView - Active view name passed to NavBar
|
|
78
|
+
* @param {boolean} props.wsConnected - WebSocket connection state
|
|
79
|
+
* @param {*} props.children - Page content
|
|
80
|
+
*/
|
|
81
|
+
export function Layout({ currentView, wsConnected, children }) {
|
|
82
|
+
return html`
|
|
83
|
+
<div class="min-h-screen bg-[#0f0f0f] text-[#e5e5e5]">
|
|
84
|
+
<${NavBar} currentView=${currentView} wsConnected=${wsConnected} />
|
|
85
|
+
<main class="max-w-7xl mx-auto px-4 py-6">${children}</main>
|
|
86
|
+
</div>
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Legio Web UI — MessageBubble + ActivityCard components
|
|
2
|
+
// Conversational-style message rendering with agent capability color coding.
|
|
3
|
+
// No npm dependencies — uses shared preact-setup.js for version consistency.
|
|
4
|
+
|
|
5
|
+
import { renderMarkdown } from "../lib/markdown.js";
|
|
6
|
+
import { html } from "../lib/preact-setup.js";
|
|
7
|
+
import { agentColor, timeAgo } from "../lib/utils.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* MessageBubble — renders a single mail message as a conversational bubble.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} props
|
|
13
|
+
* @param {object} props.msg - Message object { from, to, body, subject, createdAt, type, priority, read }
|
|
14
|
+
* @param {string} [props.capability] - Agent capability for color coding ("coordinator", "builder", etc.)
|
|
15
|
+
* @param {boolean} [props.isUser] - True for user-sent messages (right-aligned, accent color)
|
|
16
|
+
* @param {boolean} [props.showName] - Show sender name + avatar + timestamp header (default true)
|
|
17
|
+
* @param {boolean} [props.compact] - Tighter padding for grouped messages (default false)
|
|
18
|
+
*/
|
|
19
|
+
export function MessageBubble({
|
|
20
|
+
msg,
|
|
21
|
+
capability,
|
|
22
|
+
isUser = false,
|
|
23
|
+
showName = true,
|
|
24
|
+
compact = false,
|
|
25
|
+
}) {
|
|
26
|
+
const colors = isUser
|
|
27
|
+
? {
|
|
28
|
+
bg: "bg-[#E64415]/10",
|
|
29
|
+
border: "border-[#E64415]",
|
|
30
|
+
text: "text-[#E64415]",
|
|
31
|
+
dot: "bg-[#E64415]",
|
|
32
|
+
avatar: "💬",
|
|
33
|
+
}
|
|
34
|
+
: agentColor(capability);
|
|
35
|
+
|
|
36
|
+
// Priority overrides border color
|
|
37
|
+
const borderColor =
|
|
38
|
+
msg.priority === "urgent"
|
|
39
|
+
? "border-red-500"
|
|
40
|
+
: msg.priority === "high"
|
|
41
|
+
? "border-orange-500"
|
|
42
|
+
: colors.border;
|
|
43
|
+
|
|
44
|
+
const bubbleClasses = [
|
|
45
|
+
"max-w-[80%]",
|
|
46
|
+
"border border-[#2a2a2a] border-l-2",
|
|
47
|
+
borderColor,
|
|
48
|
+
colors.bg,
|
|
49
|
+
"rounded-sm",
|
|
50
|
+
compact ? "py-1 px-3 mb-0.5" : "p-3 mb-2",
|
|
51
|
+
isUser ? "ml-auto" : "",
|
|
52
|
+
]
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.join(" ");
|
|
55
|
+
|
|
56
|
+
return html`
|
|
57
|
+
<div class=${bubbleClasses}>
|
|
58
|
+
${
|
|
59
|
+
showName &&
|
|
60
|
+
html`<div class="flex items-center gap-1.5 mb-1">
|
|
61
|
+
<span class="text-base leading-none flex-shrink-0">${colors.avatar}</span>
|
|
62
|
+
<span class=${`text-xs font-semibold ${colors.text}`}>${msg.from || ""}</span>
|
|
63
|
+
<span class="text-xs text-[#555]">${timeAgo(msg.createdAt)}</span>
|
|
64
|
+
</div>`
|
|
65
|
+
}
|
|
66
|
+
<div class="text-sm text-[#e5e5e5] break-words chat-markdown"
|
|
67
|
+
dangerouslySetInnerHTML=${{ __html: renderMarkdown(msg.body) }}></div>
|
|
68
|
+
</div>
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generate a human-readable summary for a mail-type activity message.
|
|
74
|
+
*/
|
|
75
|
+
function activitySummary(event) {
|
|
76
|
+
if (event.summary) return event.summary;
|
|
77
|
+
switch (event.type) {
|
|
78
|
+
case "dispatch":
|
|
79
|
+
return `Dispatched ${event.to || ""} for ${event.subject || ""}`;
|
|
80
|
+
case "worker_done":
|
|
81
|
+
return `${event.from || event.agent || ""} completed work`;
|
|
82
|
+
case "merge_ready":
|
|
83
|
+
return `${event.from || event.agent || ""} ready to merge`;
|
|
84
|
+
case "merged":
|
|
85
|
+
return `Branch merged: ${event.subject || ""}`;
|
|
86
|
+
case "merge_failed":
|
|
87
|
+
return `Merge failed: ${event.subject || ""}`;
|
|
88
|
+
case "spawned":
|
|
89
|
+
return `Agent spawned: ${event.agent || ""}`;
|
|
90
|
+
case "state_change":
|
|
91
|
+
return `${event.agent || ""}: ${event.from || ""} → ${event.to || ""}`;
|
|
92
|
+
case "removed":
|
|
93
|
+
return `Agent removed: ${event.agent || ""}`;
|
|
94
|
+
default:
|
|
95
|
+
return event.subject || event.type || "";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* ActivityCard — compact centered card for agent activity events.
|
|
101
|
+
*
|
|
102
|
+
* @param {object} props
|
|
103
|
+
* @param {object} props.event - Activity event or mail message with activity type
|
|
104
|
+
* @param {string} [props.capability] - Agent capability for color coding
|
|
105
|
+
*/
|
|
106
|
+
export function ActivityCard({ event, capability }) {
|
|
107
|
+
const colors = agentColor(capability ?? event.capability);
|
|
108
|
+
const summary = activitySummary(event);
|
|
109
|
+
const ts = event.timestamp || event.createdAt;
|
|
110
|
+
|
|
111
|
+
return html`
|
|
112
|
+
<div
|
|
113
|
+
class="mx-auto max-w-[70%] flex items-center gap-1.5 px-3 py-1 mb-1 rounded bg-[#1a1a1a] border border-[#2a2a2a]"
|
|
114
|
+
>
|
|
115
|
+
<span class="text-xs leading-none flex-shrink-0">${colors.avatar}</span>
|
|
116
|
+
<span class="text-xs text-[#666] truncate">${summary}</span>
|
|
117
|
+
<span class="text-xs text-[#444] flex-shrink-0 ml-auto">${timeAgo(ts)}</span>
|
|
118
|
+
</div>
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Legio Web UI — StatCard component
|
|
2
|
+
// Metric display card: label + large value + optional subtitle.
|
|
3
|
+
// Used on the dashboard view. No npm dependencies — uses CDN imports.
|
|
4
|
+
|
|
5
|
+
import htm from "https://esm.sh/htm@latest";
|
|
6
|
+
import { h } from "https://esm.sh/preact@latest";
|
|
7
|
+
|
|
8
|
+
const html = htm.bind(h);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* StatCard — displays a single metric with label, value, and optional subtitle.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} props
|
|
14
|
+
* @param {string} props.label - Short label shown above the value (uppercase)
|
|
15
|
+
* @param {string|number} props.value - The main metric value to display
|
|
16
|
+
* @param {string} [props.subtitle] - Optional supplementary text below the value
|
|
17
|
+
*/
|
|
18
|
+
export function StatCard({ label, value, subtitle }) {
|
|
19
|
+
return html`
|
|
20
|
+
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-4">
|
|
21
|
+
<div class="text-xs uppercase text-gray-500 tracking-wide mb-1">${label}</div>
|
|
22
|
+
<div class="text-2xl font-bold text-[#e5e5e5]">${value}</div>
|
|
23
|
+
${subtitle && html`<div class="text-sm text-gray-400 mt-1">${subtitle}</div>`}
|
|
24
|
+
</div>
|
|
25
|
+
`;
|
|
26
|
+
}
|