@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,470 @@
1
+ // views/issues.js — Kanban board for beads issues
2
+ // Exports IssuesView (Preact component) and sets window.renderIssues (legacy shim)
3
+
4
+ import { IssueCard } from "../components/issue-card.js";
5
+ import { fetchJson, postJson } from "../lib/api.js";
6
+ import { html, useCallback, useEffect, useRef, useState } from "../lib/preact-setup.js";
7
+ import { appState } from "../lib/state.js";
8
+
9
+ // ── Helpers ────────────────────────────────────────────────────────────────
10
+
11
+ function escapeHtml(str) {
12
+ if (str == null) return "";
13
+ return String(str)
14
+ .replace(/&/g, "&")
15
+ .replace(/</g, "&lt;")
16
+ .replace(/>/g, "&gt;")
17
+ .replace(/"/g, "&quot;")
18
+ .replace(/'/g, "&#39;");
19
+ }
20
+
21
+ function truncate(str, maxLen) {
22
+ if (!str) return "";
23
+ return str.length <= maxLen ? str : `${str.slice(0, maxLen - 3)}...`;
24
+ }
25
+
26
+ // Priority border colors (hex) for the inline-style approach used by shim
27
+ const priorityBorderHex = {
28
+ 0: "#ef4444",
29
+ 1: "#f97316",
30
+ 2: "#eab308",
31
+ 3: "#3b82f6",
32
+ 4: "#6b7280",
33
+ };
34
+
35
+ // Separate issues into the 4 kanban columns
36
+ function categorize(issues) {
37
+ const open = [];
38
+ const inProgress = [];
39
+ const blocked = [];
40
+ const closed = [];
41
+ for (const issue of issues) {
42
+ const status = issue.status || "";
43
+ const hasBlockers = Array.isArray(issue.blockedBy) && issue.blockedBy.length > 0;
44
+ if (status === "in_progress") inProgress.push(issue);
45
+ else if (status === "closed") closed.push(issue);
46
+ else if (status === "blocked") blocked.push(issue);
47
+ else if (status === "open" && hasBlockers) blocked.push(issue);
48
+ else open.push(issue);
49
+ }
50
+ return { open, inProgress, blocked, closed };
51
+ }
52
+
53
+ // ── Search helper ──────────────────────────────────────────────────────────
54
+
55
+ function matchSearch(issue, query) {
56
+ if (!query) return true;
57
+ const q = query.toLowerCase();
58
+ return (
59
+ (issue.id ?? "").toLowerCase().includes(q) ||
60
+ (issue.title ?? "").toLowerCase().includes(q) ||
61
+ (issue.description ?? "").toLowerCase().includes(q) ||
62
+ (issue.status ?? "").toLowerCase().includes(q) ||
63
+ (issue.priority != null && `p${issue.priority}`.includes(q))
64
+ );
65
+ }
66
+
67
+ // ── Preact sub-component: DispatchableCard ─────────────────────────────────
68
+
69
+ function DispatchableCard({ issue }) {
70
+ const [dispatching, setDispatching] = useState(false);
71
+ const [dispatched, setDispatched] = useState(false);
72
+ const [dispatchError, setDispatchError] = useState(null);
73
+ const [closeConfirm, setCloseConfirm] = useState(false);
74
+ const [closing, setClosing] = useState(false);
75
+ const [closeError, setCloseError] = useState(null);
76
+
77
+ const isDispatchable = issue.status === "open" || issue.status === "in_progress";
78
+
79
+ const handleDispatch = useCallback(
80
+ async (e) => {
81
+ e.stopPropagation();
82
+ if (dispatching || dispatched) return;
83
+ setDispatching(true);
84
+ setDispatchError(null);
85
+ try {
86
+ await postJson(`/api/issues/${issue.id}/dispatch`, {});
87
+ setDispatched(true);
88
+ } catch (err) {
89
+ setDispatchError(err.message || "Dispatch failed");
90
+ } finally {
91
+ setDispatching(false);
92
+ }
93
+ },
94
+ [issue.id, dispatching, dispatched],
95
+ );
96
+
97
+ const handleClose = useCallback(
98
+ async (e) => {
99
+ e.stopPropagation();
100
+ if (closing) return;
101
+ if (!closeConfirm) {
102
+ setCloseConfirm(true);
103
+ return;
104
+ }
105
+ setClosing(true);
106
+ setCloseError(null);
107
+ try {
108
+ await postJson(`/api/issues/${issue.id}/close`, {});
109
+ appState.issues.value = appState.issues.value.map((i) =>
110
+ i.id === issue.id ? { ...i, status: "closed" } : i,
111
+ );
112
+ } catch (err) {
113
+ setCloseError(err.message || "Close failed");
114
+ setCloseConfirm(false);
115
+ } finally {
116
+ setClosing(false);
117
+ }
118
+ },
119
+ [issue.id, closing, closeConfirm],
120
+ );
121
+
122
+ return html`
123
+ <${IssueCard} issue=${issue}>
124
+ ${
125
+ isDispatchable
126
+ ? html`
127
+ <div class="border-t border-[#2a2a2a] mt-2 pt-2 flex items-center gap-2">
128
+ <button
129
+ onClick=${handleDispatch}
130
+ disabled=${dispatching || dispatched}
131
+ class=${
132
+ dispatched
133
+ ? "px-2 py-1 text-xs rounded-sm border border-green-700 text-green-400 bg-green-900/20 cursor-default"
134
+ : dispatching
135
+ ? "px-2 py-1 text-xs rounded-sm border border-[#444] text-[#999] cursor-wait"
136
+ : "px-2 py-1 text-xs rounded-sm border border-[#E64415] text-[#E64415] hover:bg-[#E64415]/10"
137
+ }
138
+ >
139
+ ${dispatched ? "✓ Dispatched" : dispatching ? "Dispatching…" : "Dispatch"}
140
+ </button>
141
+ <button
142
+ onClick=${handleClose}
143
+ disabled=${closing}
144
+ class=${
145
+ closing
146
+ ? "px-2 py-1 text-xs rounded-sm border border-[#444] text-[#999] cursor-wait"
147
+ : "px-2 py-1 text-xs rounded-sm border border-red-700 text-red-400 hover:bg-red-900/20"
148
+ }
149
+ >
150
+ ${closing ? "Closing…" : closeConfirm ? "Confirm?" : "Close"}
151
+ </button>
152
+ ${dispatchError ? html`<span class="text-red-400 text-xs">${dispatchError}</span>` : null}
153
+ ${closeError ? html`<span class="text-red-400 text-xs">${closeError}</span>` : null}
154
+ </div>
155
+ `
156
+ : null
157
+ }
158
+ <//>
159
+ `;
160
+ }
161
+
162
+ // ── Preact sub-component: SkeletonCard ─────────────────────────────────────
163
+
164
+ function SkeletonCard() {
165
+ return html`
166
+ <div class="bg-[#1a1a1a] border border-[#2a2a2a] border-l-4 rounded-sm p-3" style="border-left-color: #2a2a2a">
167
+ <div class="flex items-start justify-between gap-2 mb-1">
168
+ <div class="bg-[#2a2a2a] rounded h-3 w-16" style="animation: shimmer 1.5s infinite" />
169
+ <div class="bg-[#2a2a2a] rounded h-3 w-6" style="animation: shimmer 1.5s infinite" />
170
+ </div>
171
+ <div class="bg-[#2a2a2a] rounded h-4 w-3/4 mb-1" style="animation: shimmer 1.5s infinite" />
172
+ <div class="bg-[#2a2a2a] rounded h-3 w-1/2" style="animation: shimmer 1.5s infinite" />
173
+ </div>
174
+ `;
175
+ }
176
+
177
+ // ── Preact sub-component: SkeletonColumn ───────────────────────────────────
178
+
179
+ function SkeletonColumn({ title, borderClass, count }) {
180
+ return html`
181
+ <div class="flex-1 min-w-[240px] flex flex-col">
182
+ <div class=${`border-t-2 ${borderClass} bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm px-3 py-2 mb-2 flex items-center gap-2`}>
183
+ <span class="text-[#e5e5e5] text-sm font-medium">${title}</span>
184
+ <span class="bg-[#2a2a2a] text-[#999] text-xs rounded-full px-2">—</span>
185
+ </div>
186
+ <div class="flex flex-col gap-2">
187
+ ${Array.from({ length: count }, (_, i) => html`<${SkeletonCard} key=${i} />`)}
188
+ </div>
189
+ </div>
190
+ `;
191
+ }
192
+
193
+ // ── Preact sub-component: Column ───────────────────────────────────────────
194
+
195
+ function Column({ title, issues, borderClass }) {
196
+ return html`
197
+ <div class="flex-1 min-w-[240px] flex flex-col">
198
+ <div class=${`border-t-2 ${borderClass} bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm px-3 py-2 mb-2 flex items-center gap-2`}>
199
+ <span class="text-[#e5e5e5] text-sm font-medium">${title}</span>
200
+ <span class="bg-[#2a2a2a] text-[#999] text-xs rounded-full px-2">${issues.length}</span>
201
+ </div>
202
+ <div class="flex flex-col gap-2">
203
+ ${
204
+ issues.length === 0
205
+ ? html`<div class="text-[#999] text-sm text-center py-4">No issues</div>`
206
+ : issues.map((issue) => html`<${DispatchableCard} key=${issue.id} issue=${issue} />`)
207
+ }
208
+ </div>
209
+ </div>
210
+ `;
211
+ }
212
+
213
+ // ── Preact component: IssuesView ───────────────────────────────────────────
214
+
215
+ export function IssuesView() {
216
+ // null = show all priorities
217
+ const [priorityFilter, setPriorityFilter] = useState(null);
218
+ const [showClosed, setShowClosed] = useState(true);
219
+ const [searchInput, setSearchInput] = useState("");
220
+ const [searchText, setSearchText] = useState("");
221
+ const debounceRef = useRef(null);
222
+
223
+ // Read from signal (establishes subscription if auto-tracking works)
224
+ const signalIssues = appState.issues.value;
225
+
226
+ // Also fetch on mount as fallback
227
+ const [fetchedIssues, setFetchedIssues] = useState(null);
228
+
229
+ useEffect(() => {
230
+ let cancelled = false;
231
+ const fetchIssues = () => {
232
+ fetchJson("/api/issues")
233
+ .then((data) => {
234
+ if (!cancelled) {
235
+ setFetchedIssues(data ?? []);
236
+ // Update signal so other consumers see the data
237
+ appState.issues.value = data ?? [];
238
+ }
239
+ })
240
+ .catch(() => {
241
+ if (!cancelled) setFetchedIssues([]);
242
+ });
243
+ };
244
+ fetchIssues();
245
+ const interval = setInterval(fetchIssues, 5000);
246
+ return () => {
247
+ cancelled = true;
248
+ clearInterval(interval);
249
+ };
250
+ }, []);
251
+
252
+ // Debounce search input
253
+ const handleSearchChange = useCallback((e) => {
254
+ const val = e.target.value;
255
+ setSearchInput(val);
256
+ if (debounceRef.current) clearTimeout(debounceRef.current);
257
+ debounceRef.current = setTimeout(() => {
258
+ setSearchText(val);
259
+ }, 200);
260
+ }, []);
261
+
262
+ // Prefer signal (non-empty) over fetched data
263
+ const issues = signalIssues && signalIssues.length > 0 ? signalIssues : (fetchedIssues ?? []);
264
+
265
+ // Show skeleton while waiting for first fetch
266
+ if (fetchedIssues === null && (!signalIssues || signalIssues.length === 0)) {
267
+ return html`
268
+ <div class="p-4">
269
+ <style>
270
+ @keyframes shimmer {
271
+ 0% { opacity: 0.3; }
272
+ 50% { opacity: 0.6; }
273
+ 100% { opacity: 0.3; }
274
+ }
275
+ </style>
276
+ <div class="flex gap-4 overflow-x-auto pb-4">
277
+ <${SkeletonColumn} title="Open" borderClass="border-blue-500" count=${3} />
278
+ <${SkeletonColumn} title="In Progress" borderClass="border-yellow-500" count=${2} />
279
+ <${SkeletonColumn} title="Blocked" borderClass="border-red-500" count=${1} />
280
+ <${SkeletonColumn} title="Closed" borderClass="border-green-500" count=${2} />
281
+ </div>
282
+ </div>
283
+ `;
284
+ }
285
+
286
+ const afterSearch = searchText ? issues.filter((i) => matchSearch(i, searchText)) : issues;
287
+
288
+ const filtered =
289
+ priorityFilter == null ? afterSearch : afterSearch.filter((i) => i.priority === priorityFilter);
290
+
291
+ const visibleIssues = showClosed ? filtered : filtered.filter((i) => i.status !== "closed");
292
+ const { open, inProgress, blocked, closed } = categorize(visibleIssues);
293
+ closed.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
294
+
295
+ const filterButtons = [null, 0, 1, 2, 3, 4];
296
+
297
+ return html`
298
+ <div class="p-4">
299
+ <!-- Search input -->
300
+ <div class="mb-3">
301
+ <input
302
+ type="text"
303
+ placeholder="Search issues…"
304
+ value=${searchInput}
305
+ onInput=${handleSearchChange}
306
+ class="w-full max-w-sm bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm px-3 py-1.5 text-sm text-[#e5e5e5] placeholder-[#555] focus:outline-none focus:border-[#444]"
307
+ />
308
+ </div>
309
+ <!-- Priority filter bar -->
310
+ <div class="flex items-center gap-2 mb-4">
311
+ ${filterButtons.map((p) => {
312
+ const active = priorityFilter === p;
313
+ const label = p == null ? "All" : `P${p}`;
314
+ return html`
315
+ <button
316
+ key=${label}
317
+ class=${
318
+ active
319
+ ? "px-2 py-1 text-xs rounded-sm border border-[#E64415] text-[#E64415] bg-[#E64415]/10"
320
+ : "px-2 py-1 text-xs rounded-sm border border-[#2a2a2a] text-[#999] hover:border-[#444]"
321
+ }
322
+ onClick=${() => setPriorityFilter(p)}
323
+ >
324
+ ${label}
325
+ </button>
326
+ `;
327
+ })}
328
+ <div class="ml-2 pl-2 border-l border-[#2a2a2a]">
329
+ <button
330
+ class=${
331
+ showClosed
332
+ ? "px-2 py-1 text-xs rounded-sm border border-green-700 text-green-400 bg-green-900/20"
333
+ : "px-2 py-1 text-xs rounded-sm border border-[#2a2a2a] text-[#999] hover:border-[#444]"
334
+ }
335
+ onClick=${() => setShowClosed(!showClosed)}
336
+ >
337
+ ${showClosed ? "Hide Closed" : "Show Closed"}
338
+ </button>
339
+ </div>
340
+ </div>
341
+
342
+ <!-- Kanban board -->
343
+ <div class="flex gap-4 overflow-x-auto pb-4">
344
+ <${Column} title="Open" issues=${open} borderClass="border-blue-500" />
345
+ <${Column} title="In Progress" issues=${inProgress} borderClass="border-yellow-500" />
346
+ <${Column} title="Blocked" issues=${blocked} borderClass="border-red-500" />
347
+ <${Column} title="Closed" issues=${closed} borderClass="border-green-500" />
348
+ </div>
349
+ </div>
350
+ `;
351
+ }
352
+
353
+ // ── Legacy global shim for the existing app.js router ─────────────────────
354
+ // Uses innerHTML to render the kanban board without requiring a Preact root.
355
+
356
+ function renderIssueCardHtml(issue) {
357
+ const borderColor = priorityBorderHex[issue.priority] ?? "#6b7280";
358
+ const hasBlockedBy = Array.isArray(issue.blockedBy) && issue.blockedBy.length > 0;
359
+ const isClosed = issue.status === "closed";
360
+ const opacityClass = isClosed ? " opacity-50" : "";
361
+ const idColorClass = hasBlockedBy ? "text-red-400" : "text-[#999]";
362
+ const blockedIcon = hasBlockedBy ? `<span class="text-xs">⚠️</span> ` : "";
363
+ const closedBadge = isClosed
364
+ ? `<span class="text-xs bg-green-900/40 text-green-400 rounded px-1 ml-1">Closed</span>`
365
+ : "";
366
+ const titleClass = isClosed ? "line-through" : "";
367
+ const closeReasonHtml =
368
+ isClosed && issue.closeReason
369
+ ? `<div class="text-[#666] text-xs mb-1 italic">${escapeHtml(truncate(issue.closeReason, 80))}</div>`
370
+ : "";
371
+ return `
372
+ <div class="bg-[#1a1a1a] border border-[#2a2a2a] border-l-4 rounded-sm p-3${opacityClass}" style="border-left-color: ${borderColor}">
373
+ <div class="flex items-start justify-between gap-2 mb-1">
374
+ <span class="flex items-center gap-1">
375
+ ${blockedIcon}<span class="${idColorClass} text-xs font-mono">${escapeHtml(issue.id || "")}</span>${closedBadge}
376
+ </span>
377
+ ${issue.priority != null ? `<span class="text-[#999] text-xs">P${issue.priority}</span>` : ""}
378
+ </div>
379
+ <div class="text-[#e5e5e5] font-medium text-sm mb-1 ${titleClass}">${escapeHtml(truncate(issue.title || "", 60))}</div>
380
+ ${closeReasonHtml}
381
+ ${issue.description ? `<div class="text-[#999] text-xs mb-2 leading-relaxed">${escapeHtml(truncate(issue.description, 120))}</div>` : ""}
382
+ <div class="flex items-center gap-2 flex-wrap">
383
+ ${issue.type ? `<span class="text-xs bg-[#2a2a2a] rounded px-1 text-[#999]">${escapeHtml(issue.type)}</span>` : ""}
384
+ ${issue.assignee ? `<span class="text-[#999] text-xs">${escapeHtml(issue.assignee)}</span>` : ""}
385
+ </div>
386
+ ${hasBlockedBy ? `<div class="mt-1 text-xs text-red-500">blocked by: ${escapeHtml(issue.blockedBy.join(", "))}</div>` : ""}
387
+ </div>`;
388
+ }
389
+
390
+ function renderColumnHtml(title, issues, borderClass) {
391
+ const cards =
392
+ issues.length === 0
393
+ ? `<div class="text-[#999] text-sm text-center py-4">No issues</div>`
394
+ : issues.map(renderIssueCardHtml).join("");
395
+ return `
396
+ <div class="flex-1 min-w-[240px]">
397
+ <div class="border-t-2 ${borderClass} bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm px-3 py-2 mb-2 flex items-center gap-2">
398
+ <span class="text-[#e5e5e5] text-sm font-medium">${escapeHtml(title)}</span>
399
+ <span class="bg-[#2a2a2a] text-[#999] text-xs rounded-full px-2">${issues.length}</span>
400
+ </div>
401
+ <div class="flex flex-col gap-2">${cards}</div>
402
+ </div>`;
403
+ }
404
+
405
+ window.renderIssues = (appState, el) => {
406
+ const issues = appState.issues || [];
407
+ const priorityFilter = el.dataset.priorityFilter || "all";
408
+ const showClosed = el.dataset.showClosed !== "false";
409
+
410
+ const filtered =
411
+ priorityFilter === "all" ? issues : issues.filter((i) => String(i.priority) === priorityFilter);
412
+
413
+ const visibleIssues = showClosed ? filtered : filtered.filter((i) => i.status !== "closed");
414
+ const { open, inProgress, blocked, closed } = categorize(visibleIssues);
415
+ closed.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
416
+
417
+ const filterButtons = [
418
+ { key: "all", label: "All" },
419
+ { key: "0", label: "P0" },
420
+ { key: "1", label: "P1" },
421
+ { key: "2", label: "P2" },
422
+ { key: "3", label: "P3" },
423
+ { key: "4", label: "P4" },
424
+ ];
425
+
426
+ const filterBtnsHtml = filterButtons
427
+ .map(({ key, label }) => {
428
+ const active = priorityFilter === key;
429
+ const cls = active
430
+ ? "border-[#E64415] text-[#E64415] bg-[#E64415]/10"
431
+ : "border-[#2a2a2a] text-[#999]";
432
+ return `<button class="px-2 py-1 text-xs rounded-sm border ${cls}" data-priority="${escapeHtml(key)}">${escapeHtml(label)}</button>`;
433
+ })
434
+ .join("");
435
+
436
+ const closedToggleCls = showClosed
437
+ ? "border-green-700 text-green-400 bg-green-900/20"
438
+ : "border-[#2a2a2a] text-[#999]";
439
+ const closedToggleLabel = showClosed ? "Hide Closed" : "Show Closed";
440
+ const closedToggleHtml = `<div class="ml-2 pl-2 border-l border-[#2a2a2a]"><button class="px-2 py-1 text-xs rounded-sm border ${closedToggleCls}" data-toggle-closed="true">${closedToggleLabel}</button></div>`;
441
+
442
+ const columnsHtml = [
443
+ renderColumnHtml("Open", open, "border-blue-500"),
444
+ renderColumnHtml("In Progress", inProgress, "border-yellow-500"),
445
+ renderColumnHtml("Blocked", blocked, "border-red-500"),
446
+ renderColumnHtml("Closed", closed, "border-green-500"),
447
+ ].join("");
448
+
449
+ el.innerHTML = `
450
+ <div class="p-4">
451
+ <div class="flex items-center gap-2 mb-4">${filterBtnsHtml}${closedToggleHtml}</div>
452
+ <div class="flex gap-4 overflow-x-auto pb-4">${columnsHtml}</div>
453
+ </div>`;
454
+
455
+ // Wire up filter button click handlers
456
+ el.querySelectorAll("button[data-priority]").forEach((btn) => {
457
+ btn.addEventListener("click", () => {
458
+ el.dataset.priorityFilter = btn.getAttribute("data-priority") || "all";
459
+ window.renderIssues(appState, el);
460
+ });
461
+ });
462
+
463
+ // Wire up show/hide closed toggle
464
+ el.querySelectorAll("button[data-toggle-closed]").forEach((btn) => {
465
+ btn.addEventListener("click", () => {
466
+ el.dataset.showClosed = showClosed ? "false" : "true";
467
+ window.renderIssues(appState, el);
468
+ });
469
+ });
470
+ };
@@ -0,0 +1,94 @@
1
+ // Legio Web UI — SetupView component
2
+ // Preact + HTM component for the setup wizard (shown when .legio/ is not initialized).
3
+ // No npm dependencies — uses importmap bare specifiers. Served as a static ES module.
4
+
5
+ import { postJson } from "../lib/api.js";
6
+ import { html, useCallback, useState } from "../lib/preact-setup.js";
7
+
8
+ export function SetupView({ onInitialized, projectRoot }) {
9
+ const [status, setStatus] = useState("idle"); // idle | loading | success | error
10
+ const [error, setError] = useState(null);
11
+
12
+ const handleInit = useCallback(async () => {
13
+ setStatus("loading");
14
+ setError(null);
15
+ try {
16
+ const result = await postJson("/api/setup/init", {});
17
+ if (result.success) {
18
+ setStatus("success");
19
+ setTimeout(() => {
20
+ onInitialized?.();
21
+ }, 1000);
22
+ } else {
23
+ setStatus("error");
24
+ setError(result.error ?? "Unknown error");
25
+ }
26
+ } catch (err) {
27
+ setStatus("error");
28
+ setError(err.message ?? "Failed to initialize");
29
+ }
30
+ }, [onInitialized]);
31
+
32
+ return html`
33
+ <div class="flex items-center justify-center h-screen bg-[#0f0f0f]">
34
+ <div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-lg p-8 w-full max-w-md mx-4">
35
+ <div class="flex items-center gap-3 mb-6">
36
+ <div class="w-8 h-8 bg-[#E64415] rounded flex items-center justify-center shrink-0">
37
+ <span class="text-white font-bold text-sm">L</span>
38
+ </div>
39
+ <h1 class="text-xl font-semibold text-[#e5e5e5]">Legio Setup</h1>
40
+ </div>
41
+
42
+ <p class="text-[#888] text-sm mb-4">
43
+ This project has not been initialized with Legio yet. Run setup to create
44
+ the <code class="text-[#ccc] bg-[#0f0f0f] px-1 rounded">.legio/</code> directory
45
+ and configuration.
46
+ </p>
47
+
48
+ ${
49
+ projectRoot
50
+ ? html`
51
+ <div class="mb-6 p-3 bg-[#0f0f0f] border border-[#2a2a2a] rounded">
52
+ <span class="text-[#555] text-xs uppercase tracking-wider font-medium">Project Root</span>
53
+ <p class="text-[#ccc] text-xs font-mono mt-1 break-all">${projectRoot}</p>
54
+ </div>
55
+ `
56
+ : null
57
+ }
58
+
59
+ ${
60
+ status === "success"
61
+ ? html`
62
+ <div class="text-green-400 text-sm py-3 text-center">
63
+ ✓ Project initialized successfully. Loading dashboard...
64
+ </div>
65
+ `
66
+ : html`
67
+ <button
68
+ onClick=${handleInit}
69
+ disabled=${status === "loading"}
70
+ class=${
71
+ "w-full py-2.5 px-4 rounded text-sm font-medium transition-colors " +
72
+ (status === "loading"
73
+ ? "bg-[#333] text-[#666] cursor-not-allowed"
74
+ : "bg-[#E64415] hover:bg-[#cc3a12] text-white cursor-pointer")
75
+ }
76
+ >
77
+ ${status === "loading" ? "Initializing..." : "Initialize Project"}
78
+ </button>
79
+ `
80
+ }
81
+
82
+ ${
83
+ status === "error" && error
84
+ ? html`
85
+ <div class="mt-3 p-3 bg-[#2a1010] border border-[#5a2020] rounded text-red-400 text-xs font-mono whitespace-pre-wrap break-all">
86
+ ${error}
87
+ </div>
88
+ `
89
+ : null
90
+ }
91
+ </div>
92
+ </div>
93
+ `;
94
+ }