@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.
Files changed (219) hide show
  1. package/CHANGELOG.md +422 -0
  2. package/LICENSE +21 -0
  3. package/README.md +555 -0
  4. package/agents/builder.md +141 -0
  5. package/agents/coordinator.md +351 -0
  6. package/agents/cto.md +196 -0
  7. package/agents/gateway.md +276 -0
  8. package/agents/lead.md +281 -0
  9. package/agents/merger.md +156 -0
  10. package/agents/monitor.md +212 -0
  11. package/agents/reviewer.md +142 -0
  12. package/agents/scout.md +131 -0
  13. package/agents/supervisor.md +416 -0
  14. package/bin/legio.mjs +38 -0
  15. package/package.json +77 -0
  16. package/src/agents/checkpoint.test.ts +88 -0
  17. package/src/agents/checkpoint.ts +102 -0
  18. package/src/agents/hooks-deployer.test.ts +1820 -0
  19. package/src/agents/hooks-deployer.ts +574 -0
  20. package/src/agents/identity.test.ts +614 -0
  21. package/src/agents/identity.ts +385 -0
  22. package/src/agents/lifecycle.test.ts +202 -0
  23. package/src/agents/lifecycle.ts +184 -0
  24. package/src/agents/manifest.test.ts +558 -0
  25. package/src/agents/manifest.ts +297 -0
  26. package/src/agents/overlay.test.ts +592 -0
  27. package/src/agents/overlay.ts +316 -0
  28. package/src/beads/client.test.ts +210 -0
  29. package/src/beads/client.ts +227 -0
  30. package/src/beads/molecules.test.ts +320 -0
  31. package/src/beads/molecules.ts +209 -0
  32. package/src/commands/agents.test.ts +325 -0
  33. package/src/commands/agents.ts +286 -0
  34. package/src/commands/clean.test.ts +730 -0
  35. package/src/commands/clean.ts +653 -0
  36. package/src/commands/completions.test.ts +346 -0
  37. package/src/commands/completions.ts +950 -0
  38. package/src/commands/coordinator.test.ts +1524 -0
  39. package/src/commands/coordinator.ts +880 -0
  40. package/src/commands/costs.test.ts +1015 -0
  41. package/src/commands/costs.ts +473 -0
  42. package/src/commands/dashboard.test.ts +94 -0
  43. package/src/commands/dashboard.ts +607 -0
  44. package/src/commands/doctor.test.ts +295 -0
  45. package/src/commands/doctor.ts +213 -0
  46. package/src/commands/down.test.ts +308 -0
  47. package/src/commands/down.ts +124 -0
  48. package/src/commands/errors.test.ts +648 -0
  49. package/src/commands/errors.ts +255 -0
  50. package/src/commands/feed.test.ts +579 -0
  51. package/src/commands/feed.ts +368 -0
  52. package/src/commands/gateway.test.ts +698 -0
  53. package/src/commands/gateway.ts +419 -0
  54. package/src/commands/group.test.ts +262 -0
  55. package/src/commands/group.ts +539 -0
  56. package/src/commands/hooks.test.ts +292 -0
  57. package/src/commands/hooks.ts +210 -0
  58. package/src/commands/init.test.ts +211 -0
  59. package/src/commands/init.ts +622 -0
  60. package/src/commands/inspect.test.ts +670 -0
  61. package/src/commands/inspect.ts +455 -0
  62. package/src/commands/log.test.ts +1556 -0
  63. package/src/commands/log.ts +752 -0
  64. package/src/commands/logs.test.ts +379 -0
  65. package/src/commands/logs.ts +544 -0
  66. package/src/commands/mail.test.ts +1726 -0
  67. package/src/commands/mail.ts +926 -0
  68. package/src/commands/merge.test.ts +676 -0
  69. package/src/commands/merge.ts +374 -0
  70. package/src/commands/metrics.test.ts +444 -0
  71. package/src/commands/metrics.ts +150 -0
  72. package/src/commands/monitor.test.ts +151 -0
  73. package/src/commands/monitor.ts +394 -0
  74. package/src/commands/nudge.test.ts +230 -0
  75. package/src/commands/nudge.ts +373 -0
  76. package/src/commands/prime.test.ts +467 -0
  77. package/src/commands/prime.ts +386 -0
  78. package/src/commands/replay.test.ts +742 -0
  79. package/src/commands/replay.ts +367 -0
  80. package/src/commands/run.test.ts +443 -0
  81. package/src/commands/run.ts +365 -0
  82. package/src/commands/server.test.ts +626 -0
  83. package/src/commands/server.ts +298 -0
  84. package/src/commands/sling.test.ts +810 -0
  85. package/src/commands/sling.ts +700 -0
  86. package/src/commands/spec.test.ts +206 -0
  87. package/src/commands/spec.ts +171 -0
  88. package/src/commands/status.test.ts +276 -0
  89. package/src/commands/status.ts +339 -0
  90. package/src/commands/stop.test.ts +357 -0
  91. package/src/commands/stop.ts +119 -0
  92. package/src/commands/supervisor.test.ts +186 -0
  93. package/src/commands/supervisor.ts +544 -0
  94. package/src/commands/trace.test.ts +746 -0
  95. package/src/commands/trace.ts +332 -0
  96. package/src/commands/up.test.ts +597 -0
  97. package/src/commands/up.ts +275 -0
  98. package/src/commands/watch.test.ts +152 -0
  99. package/src/commands/watch.ts +238 -0
  100. package/src/commands/worktree.test.ts +648 -0
  101. package/src/commands/worktree.ts +266 -0
  102. package/src/config.test.ts +496 -0
  103. package/src/config.ts +616 -0
  104. package/src/doctor/agents.test.ts +448 -0
  105. package/src/doctor/agents.ts +396 -0
  106. package/src/doctor/config-check.test.ts +184 -0
  107. package/src/doctor/config-check.ts +185 -0
  108. package/src/doctor/consistency.test.ts +645 -0
  109. package/src/doctor/consistency.ts +294 -0
  110. package/src/doctor/databases.test.ts +284 -0
  111. package/src/doctor/databases.ts +211 -0
  112. package/src/doctor/dependencies.test.ts +150 -0
  113. package/src/doctor/dependencies.ts +179 -0
  114. package/src/doctor/logs.test.ts +244 -0
  115. package/src/doctor/logs.ts +295 -0
  116. package/src/doctor/merge-queue.test.ts +210 -0
  117. package/src/doctor/merge-queue.ts +144 -0
  118. package/src/doctor/structure.test.ts +285 -0
  119. package/src/doctor/structure.ts +195 -0
  120. package/src/doctor/types.ts +37 -0
  121. package/src/doctor/version.test.ts +130 -0
  122. package/src/doctor/version.ts +131 -0
  123. package/src/e2e/chat-flow.test.ts +346 -0
  124. package/src/e2e/init-sling-lifecycle.test.ts +288 -0
  125. package/src/errors.test.ts +21 -0
  126. package/src/errors.ts +246 -0
  127. package/src/events/store.test.ts +660 -0
  128. package/src/events/store.ts +344 -0
  129. package/src/events/tool-filter.test.ts +330 -0
  130. package/src/events/tool-filter.ts +126 -0
  131. package/src/global-setup.ts +14 -0
  132. package/src/index.ts +339 -0
  133. package/src/insights/analyzer.test.ts +466 -0
  134. package/src/insights/analyzer.ts +203 -0
  135. package/src/logging/color.test.ts +118 -0
  136. package/src/logging/color.ts +71 -0
  137. package/src/logging/logger.test.ts +812 -0
  138. package/src/logging/logger.ts +266 -0
  139. package/src/logging/reporter.test.ts +258 -0
  140. package/src/logging/reporter.ts +109 -0
  141. package/src/logging/sanitizer.test.ts +190 -0
  142. package/src/logging/sanitizer.ts +57 -0
  143. package/src/mail/broadcast.test.ts +203 -0
  144. package/src/mail/broadcast.ts +92 -0
  145. package/src/mail/client.test.ts +873 -0
  146. package/src/mail/client.ts +236 -0
  147. package/src/mail/store.test.ts +815 -0
  148. package/src/mail/store.ts +402 -0
  149. package/src/merge/queue.test.ts +449 -0
  150. package/src/merge/queue.ts +262 -0
  151. package/src/merge/resolver.test.ts +1453 -0
  152. package/src/merge/resolver.ts +759 -0
  153. package/src/metrics/store.test.ts +1167 -0
  154. package/src/metrics/store.ts +511 -0
  155. package/src/metrics/summary.test.ts +397 -0
  156. package/src/metrics/summary.ts +178 -0
  157. package/src/metrics/transcript.test.ts +643 -0
  158. package/src/metrics/transcript.ts +351 -0
  159. package/src/mulch/client.test.ts +547 -0
  160. package/src/mulch/client.ts +416 -0
  161. package/src/server/audit-store.test.ts +384 -0
  162. package/src/server/audit-store.ts +257 -0
  163. package/src/server/headless.test.ts +180 -0
  164. package/src/server/headless.ts +151 -0
  165. package/src/server/index.test.ts +241 -0
  166. package/src/server/index.ts +317 -0
  167. package/src/server/public/app.js +187 -0
  168. package/src/server/public/apple-touch-icon.png +0 -0
  169. package/src/server/public/components/agent-badge.js +37 -0
  170. package/src/server/public/components/data-table.js +114 -0
  171. package/src/server/public/components/gateway-chat.js +256 -0
  172. package/src/server/public/components/issue-card.js +96 -0
  173. package/src/server/public/components/layout.js +88 -0
  174. package/src/server/public/components/message-bubble.js +120 -0
  175. package/src/server/public/components/stat-card.js +26 -0
  176. package/src/server/public/components/terminal-panel.js +140 -0
  177. package/src/server/public/favicon-16.png +0 -0
  178. package/src/server/public/favicon-32.png +0 -0
  179. package/src/server/public/favicon.ico +0 -0
  180. package/src/server/public/favicon.png +0 -0
  181. package/src/server/public/index.html +64 -0
  182. package/src/server/public/lib/api.js +35 -0
  183. package/src/server/public/lib/markdown.js +8 -0
  184. package/src/server/public/lib/preact-setup.js +8 -0
  185. package/src/server/public/lib/state.js +99 -0
  186. package/src/server/public/lib/utils.js +309 -0
  187. package/src/server/public/lib/ws.js +79 -0
  188. package/src/server/public/views/chat.js +983 -0
  189. package/src/server/public/views/costs.js +692 -0
  190. package/src/server/public/views/dashboard.js +781 -0
  191. package/src/server/public/views/gateway-chat.js +622 -0
  192. package/src/server/public/views/inspect.js +399 -0
  193. package/src/server/public/views/issues.js +470 -0
  194. package/src/server/public/views/setup.js +94 -0
  195. package/src/server/public/views/task-detail.js +422 -0
  196. package/src/server/routes.test.ts +3816 -0
  197. package/src/server/routes.ts +1964 -0
  198. package/src/server/websocket.test.ts +288 -0
  199. package/src/server/websocket.ts +196 -0
  200. package/src/sessions/compat.test.ts +109 -0
  201. package/src/sessions/compat.ts +17 -0
  202. package/src/sessions/store.test.ts +969 -0
  203. package/src/sessions/store.ts +480 -0
  204. package/src/test-helpers.test.ts +97 -0
  205. package/src/test-helpers.ts +143 -0
  206. package/src/types.ts +708 -0
  207. package/src/watchdog/daemon.test.ts +1233 -0
  208. package/src/watchdog/daemon.ts +533 -0
  209. package/src/watchdog/health.test.ts +371 -0
  210. package/src/watchdog/health.ts +248 -0
  211. package/src/watchdog/triage.test.ts +162 -0
  212. package/src/watchdog/triage.ts +193 -0
  213. package/src/worktree/manager.test.ts +444 -0
  214. package/src/worktree/manager.ts +224 -0
  215. package/src/worktree/tmux.test.ts +1238 -0
  216. package/src/worktree/tmux.ts +644 -0
  217. package/templates/CLAUDE.md.tmpl +89 -0
  218. package/templates/hooks.json.tmpl +132 -0
  219. 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
+ }