@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,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, "<")
|
|
16
|
+
.replace(/>/g, ">")
|
|
17
|
+
.replace(/"/g, """)
|
|
18
|
+
.replace(/'/g, "'");
|
|
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
|
+
}
|