@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,422 @@
1
+ // views/task-detail.js — Task detail view with Overview, Agents, Communication tabs
2
+ // Exports TaskDetailView (Preact component)
3
+
4
+ import { html, useCallback, useEffect, useRef, useState } from "../lib/preact-setup.js";
5
+
6
+ // ── Utilities ──────────────────────────────────────────────────────────────
7
+
8
+ function timeAgo(isoString) {
9
+ if (!isoString) return "";
10
+ const diff = Date.now() - new Date(isoString).getTime();
11
+ if (diff < 0) return "just now";
12
+ const s = Math.floor(diff / 1000);
13
+ if (s < 60) return `${s}s ago`;
14
+ const m = Math.floor(s / 60);
15
+ if (m < 60) return `${m}m ago`;
16
+ const hh = Math.floor(m / 60);
17
+ if (hh < 24) return `${hh}h ago`;
18
+ return `${Math.floor(hh / 24)}d ago`;
19
+ }
20
+
21
+ function formatDate(isoString) {
22
+ if (!isoString) return "—";
23
+ const d = new Date(isoString);
24
+ return `${d.toLocaleDateString()} ${d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`;
25
+ }
26
+
27
+ function formatDuration(ms) {
28
+ if (ms == null || ms < 0) return "—";
29
+ if (ms < 1000) return "< 1s";
30
+ const s = Math.floor(ms / 1000);
31
+ const m = Math.floor(s / 60);
32
+ const hh = Math.floor(m / 60);
33
+ if (hh > 0) return `${hh}h ${m % 60}m`;
34
+ if (m > 0) return `${m}m ${s % 60}s`;
35
+ return `${s}s`;
36
+ }
37
+
38
+ // ── Badge helpers ──────────────────────────────────────────────────────────
39
+
40
+ function statusBadge(status) {
41
+ const cfg = {
42
+ open: "text-blue-400 bg-blue-400/10",
43
+ in_progress: "text-yellow-400 bg-yellow-400/10",
44
+ blocked: "text-red-400 bg-red-400/10",
45
+ closed: "text-green-400 bg-green-400/10",
46
+ };
47
+ const cls = cfg[status] ?? "text-[#999] bg-[#2a2a2a]";
48
+ return html`<span class=${`text-xs px-2 py-0.5 rounded-sm font-medium ${cls}`}>${status ?? "unknown"}</span>`;
49
+ }
50
+
51
+ function priorityBadge(priority) {
52
+ if (priority == null) return null;
53
+ const colors = [
54
+ "text-red-400",
55
+ "text-orange-400",
56
+ "text-yellow-400",
57
+ "text-blue-400",
58
+ "text-[#888]",
59
+ ];
60
+ const cls = colors[priority] ?? "text-[#888]";
61
+ return html`<span class=${`text-xs font-mono ${cls}`}>P${priority}</span>`;
62
+ }
63
+
64
+ function mailTypeBadge(type) {
65
+ const cfg = {
66
+ status: "text-blue-400 bg-blue-400/10",
67
+ question: "text-yellow-400 bg-yellow-400/10",
68
+ result: "text-green-400 bg-green-400/10",
69
+ error: "text-red-400 bg-red-400/10",
70
+ worker_done: "text-green-400 bg-green-400/10",
71
+ dispatch: "text-[#999] bg-[#2a2a2a]",
72
+ };
73
+ const cls = cfg[type] ?? "text-[#999] bg-[#2a2a2a]";
74
+ return html`<span class=${`text-xs px-2 py-0.5 rounded-sm ${cls}`}>${type ?? "—"}</span>`;
75
+ }
76
+
77
+ const agentStateBadgeClasses = {
78
+ working: "text-green-500 bg-green-500/10",
79
+ booting: "text-yellow-500 bg-yellow-500/10",
80
+ stalled: "text-red-500 bg-red-500/10",
81
+ zombie: "text-gray-500 bg-gray-500/10",
82
+ completed: "text-blue-500 bg-blue-500/10",
83
+ };
84
+
85
+ // ── Tab definitions ────────────────────────────────────────────────────────
86
+
87
+ const TABS = ["Overview", "Agents", "Communication"];
88
+
89
+ // ── Sub-components ─────────────────────────────────────────────────────────
90
+
91
+ function MetaCard({ label, children }) {
92
+ return html`
93
+ <div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-3">
94
+ <div class="text-[#999] text-xs mb-1">${label}</div>
95
+ <div class="text-[#e5e5e5] text-sm">${children}</div>
96
+ </div>
97
+ `;
98
+ }
99
+
100
+ function OverviewTab({ issue }) {
101
+ const blockedBy = Array.isArray(issue.blockedBy) ? issue.blockedBy : [];
102
+
103
+ return html`
104
+ <div>
105
+ <!-- Description -->
106
+ <h3 class="text-sm font-semibold text-[#999] uppercase tracking-wide mb-2">Description</h3>
107
+ <div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-4 mb-6">
108
+ ${
109
+ issue.description
110
+ ? html`<p class="text-[#e5e5e5] text-sm leading-relaxed whitespace-pre-wrap">${issue.description}</p>`
111
+ : html`<p class="text-[#555] text-sm italic">No description</p>`
112
+ }
113
+ </div>
114
+
115
+ <!-- Metadata cards -->
116
+ <h3 class="text-sm font-semibold text-[#999] uppercase tracking-wide mb-2">Metadata</h3>
117
+ <div class="grid grid-cols-2 gap-2 mb-6 sm:grid-cols-3">
118
+ <${MetaCard} label="Status">${statusBadge(issue.status)}</${MetaCard}>
119
+ <${MetaCard} label="Priority">${priorityBadge(issue.priority) ?? html`<span class="text-[#555]">—</span>`}</${MetaCard}>
120
+ <${MetaCard} label="Type">
121
+ ${issue.type ? html`<span class="bg-[#2a2a2a] rounded px-1 text-[#999] text-xs">${issue.type}</span>` : html`<span class="text-[#555]">—</span>`}
122
+ </${MetaCard}>
123
+ <${MetaCard} label="Assignee">
124
+ <span class="text-[#e5e5e5]">${issue.assignee ?? "—"}</span>
125
+ </${MetaCard}>
126
+ <${MetaCard} label="Owner">
127
+ <span class="text-[#e5e5e5]">${issue.owner ?? "—"}</span>
128
+ </${MetaCard}>
129
+ <${MetaCard} label="Created">
130
+ <span class="text-[#e5e5e5]">${formatDate(issue.createdAt)}</span>
131
+ </${MetaCard}>
132
+ ${
133
+ issue.closedAt
134
+ ? html`<${MetaCard} label="Closed"><span class="text-[#e5e5e5]">${formatDate(issue.closedAt)}</${MetaCard}>`
135
+ : null
136
+ }
137
+ </div>
138
+
139
+ <!-- Blocked by -->
140
+ ${
141
+ blockedBy.length > 0
142
+ ? html`
143
+ <h3 class="text-sm font-semibold text-[#999] uppercase tracking-wide mb-2">Blocked By</h3>
144
+ <div class="flex flex-wrap gap-2 mb-6">
145
+ ${blockedBy.map(
146
+ (id) => html`
147
+ <a
148
+ key=${id}
149
+ href=${`#task/${id}`}
150
+ class="font-mono text-xs bg-red-900/20 text-red-400 border border-red-900/40 rounded px-2 py-1 hover:bg-red-900/30"
151
+ >${id}</a>
152
+ `,
153
+ )}
154
+ </div>
155
+ `
156
+ : null
157
+ }
158
+ </div>
159
+ `;
160
+ }
161
+
162
+ function AgentsTab({ agents, taskId }) {
163
+ const filtered = agents.filter((a) => {
164
+ const session = a.session || a;
165
+ return (session.beadId && session.beadId === taskId) || session.agentName?.includes(taskId);
166
+ });
167
+
168
+ // Sort: active states first, then completed
169
+ const sorted = [...filtered].sort((a, b) => {
170
+ const stateOrder = { working: 0, booting: 1, stalled: 2, zombie: 3, completed: 4 };
171
+ const sa = a.session || a;
172
+ const sb = b.session || b;
173
+ return (stateOrder[sa.state] ?? 5) - (stateOrder[sb.state] ?? 5);
174
+ });
175
+
176
+ if (sorted.length === 0) {
177
+ return html`
178
+ <div class="flex items-center justify-center h-32 text-[#555] text-sm">
179
+ No agents found for task ${taskId}
180
+ </div>
181
+ `;
182
+ }
183
+
184
+ return html`
185
+ <div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm overflow-x-auto">
186
+ <table class="w-full text-sm">
187
+ <thead>
188
+ <tr class="border-b border-[#2a2a2a]">
189
+ <th class="text-left text-[#999] text-xs px-3 py-2 font-medium">Agent</th>
190
+ <th class="text-left text-[#999] text-xs px-3 py-2 font-medium">Capability</th>
191
+ <th class="text-left text-[#999] text-xs px-3 py-2 font-medium">State</th>
192
+ <th class="text-left text-[#999] text-xs px-3 py-2 font-medium">Duration</th>
193
+ <th class="text-left text-[#999] text-xs px-3 py-2 font-medium">Branch</th>
194
+ </tr>
195
+ </thead>
196
+ <tbody>
197
+ ${sorted.map((a) => {
198
+ const session = a.session || a;
199
+ const dur = session.startedAt
200
+ ? Date.now() - new Date(session.startedAt).getTime()
201
+ : null;
202
+ const badgeCls =
203
+ agentStateBadgeClasses[session.state] ?? "text-gray-500 bg-gray-500/10";
204
+ return html`
205
+ <tr key=${session.agentName} class="border-b border-[#2a2a2a] last:border-0">
206
+ <td class="px-3 py-2">
207
+ <a
208
+ href=${`#inspect/${session.agentName ?? ""}`}
209
+ class="font-mono text-[#E64415] hover:text-[#ff6633] text-xs"
210
+ >${session.agentName ?? "—"}</a>
211
+ </td>
212
+ <td class="px-3 py-2 text-[#999] text-xs">${session.capability ?? "—"}</td>
213
+ <td class="px-3 py-2">
214
+ <span class=${`text-xs px-2 py-0.5 rounded-sm ${badgeCls}`}>${session.state ?? "—"}</span>
215
+ </td>
216
+ <td class="px-3 py-2 text-[#999] text-xs font-mono">${dur != null ? formatDuration(dur) : "—"}</td>
217
+ <td class="px-3 py-2 font-mono text-[#999] text-xs">${session.branchName ?? "—"}</td>
218
+ </tr>
219
+ `;
220
+ })}
221
+ </tbody>
222
+ </table>
223
+ </div>
224
+ `;
225
+ }
226
+
227
+ function CommunicationTab({ mail, taskId }) {
228
+ const [expandedId, setExpandedId] = useState(null);
229
+
230
+ const filtered = mail.filter((m) => {
231
+ const subject = (m.subject ?? "").toLowerCase();
232
+ const body = (m.body ?? "").toLowerCase();
233
+ const id = taskId.toLowerCase();
234
+ return subject.includes(id) || body.includes(id);
235
+ });
236
+
237
+ // Sort chronologically (oldest first)
238
+ const sorted = [...filtered].sort((a, b) => {
239
+ const ta = a.createdAt ?? a.sentAt ?? "";
240
+ const tb = b.createdAt ?? b.sentAt ?? "";
241
+ return ta < tb ? -1 : ta > tb ? 1 : 0;
242
+ });
243
+
244
+ if (sorted.length === 0) {
245
+ return html`
246
+ <div class="flex items-center justify-center h-32 text-[#555] text-sm">
247
+ No messages found for task ${taskId}
248
+ </div>
249
+ `;
250
+ }
251
+
252
+ return html`
253
+ <div class="flex flex-col gap-1">
254
+ ${sorted.map((msg) => {
255
+ const msgId = msg.id ?? msg.messageId ?? Math.random().toString();
256
+ const isExpanded = expandedId === msgId;
257
+ return html`
258
+ <div
259
+ key=${msgId}
260
+ class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm overflow-hidden"
261
+ >
262
+ <div
263
+ class="flex items-center gap-3 px-3 py-2 cursor-pointer hover:bg-[#222]"
264
+ onClick=${() => setExpandedId(isExpanded ? null : msgId)}
265
+ >
266
+ <span class="text-[#555] text-xs font-mono shrink-0 w-20">
267
+ ${timeAgo(msg.createdAt ?? msg.sentAt)}
268
+ </span>
269
+ ${mailTypeBadge(msg.type)}
270
+ <span class="text-[#999] text-xs shrink-0">
271
+ <span class="text-[#666]">from</span> ${msg.from ?? "—"}
272
+ <span class="text-[#666]"> to</span> ${msg.to ?? "—"}
273
+ </span>
274
+ <span class="text-[#e5e5e5] text-sm flex-1 truncate">${msg.subject ?? ""}</span>
275
+ <span class="text-[#555] text-xs shrink-0">${isExpanded ? "▲" : "▼"}</span>
276
+ </div>
277
+ ${
278
+ isExpanded
279
+ ? html`
280
+ <div class="border-t border-[#2a2a2a] px-3 py-3">
281
+ <pre class="text-[#e5e5e5] text-xs leading-relaxed whitespace-pre-wrap break-words font-mono m-0">${msg.body ?? ""}</pre>
282
+ </div>
283
+ `
284
+ : null
285
+ }
286
+ </div>
287
+ `;
288
+ })}
289
+ </div>
290
+ `;
291
+ }
292
+
293
+ // ── Main view ──────────────────────────────────────────────────────────────
294
+
295
+ export function TaskDetailView({ taskId }) {
296
+ const [issue, setIssue] = useState(null);
297
+ const [agents, setAgents] = useState([]);
298
+ const [mail, setMail] = useState([]);
299
+ const [loading, setLoading] = useState(true);
300
+ const [error, setError] = useState(null);
301
+ const [activeTab, setActiveTab] = useState("Overview");
302
+ const intervalRef = useRef(null);
303
+
304
+ const fetchAll = useCallback(async () => {
305
+ if (!taskId) return;
306
+ try {
307
+ const [issueRes, agentsRes, mailRes] = await Promise.all([
308
+ fetch(`/api/issues/${encodeURIComponent(taskId)}`).then((r) => {
309
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
310
+ return r.json();
311
+ }),
312
+ fetch("/api/agents").then((r) => (r.ok ? r.json() : [])),
313
+ fetch("/api/mail").then((r) => {
314
+ if (!r.ok) return [];
315
+ return r.json().then((d) => (Array.isArray(d) ? d : (d?.recent ?? [])));
316
+ }),
317
+ ]);
318
+ setIssue(issueRes);
319
+ setAgents(Array.isArray(agentsRes) ? agentsRes : []);
320
+ setMail(Array.isArray(mailRes) ? mailRes : []);
321
+ setLoading(false);
322
+ setError(null);
323
+ } catch (e) {
324
+ setError(String(e));
325
+ setLoading(false);
326
+ }
327
+ }, [taskId]);
328
+
329
+ useEffect(() => {
330
+ if (!taskId) return;
331
+ setLoading(true);
332
+ setError(null);
333
+ fetchAll();
334
+ intervalRef.current = setInterval(fetchAll, 5000);
335
+ return () => {
336
+ if (intervalRef.current) clearInterval(intervalRef.current);
337
+ };
338
+ }, [taskId, fetchAll]);
339
+
340
+ if (!taskId) {
341
+ return html`
342
+ <div class="flex items-center justify-center h-64 text-[#555]">No task selected</div>
343
+ `;
344
+ }
345
+
346
+ if (loading) {
347
+ return html`
348
+ <div class="flex items-center justify-center h-64 text-[#999]">Loading ${taskId}…</div>
349
+ `;
350
+ }
351
+
352
+ if (error) {
353
+ return html`
354
+ <div class="p-4">
355
+ <a href="#tasks" class="text-[#E64415] text-sm hover:underline">← Back to Tasks</a>
356
+ <div class="mt-4 text-red-500 text-sm">Failed to load task: ${error}</div>
357
+ </div>
358
+ `;
359
+ }
360
+
361
+ if (!issue) return null;
362
+
363
+ const hasBlockedBy = Array.isArray(issue.blockedBy) && issue.blockedBy.length > 0;
364
+
365
+ return html`
366
+ <div class="p-4 text-[#e5e5e5]">
367
+ <!-- Back link -->
368
+ <div class="mb-4">
369
+ <a href="#tasks" class="text-[#999] text-sm hover:text-[#ccc] transition-colors">← Back to Tasks</a>
370
+ </div>
371
+
372
+ <!-- Header -->
373
+ <div class="mb-4">
374
+ <div class="flex items-center gap-3 flex-wrap mb-2">
375
+ <span class="font-mono text-[#999] text-sm">${issue.id ?? taskId}</span>
376
+ ${statusBadge(issue.status)}
377
+ ${priorityBadge(issue.priority)}
378
+ ${issue.type ? html`<span class="text-xs bg-[#2a2a2a] rounded px-1 text-[#999]">${issue.type}</span>` : null}
379
+ </div>
380
+ <h1 class="text-xl font-semibold text-[#e5e5e5] mb-2">${issue.title ?? taskId}</h1>
381
+ <div class="flex items-center gap-4 text-[#999] text-sm flex-wrap">
382
+ ${issue.assignee ? html`<span>Assignee: ${issue.assignee}</span>` : null}
383
+ ${issue.owner ? html`<span>Owner: ${issue.owner}</span>` : null}
384
+ ${issue.createdAt ? html`<span>Created ${timeAgo(issue.createdAt)}</span>` : null}
385
+ ${hasBlockedBy ? html`<span class="text-red-400">⚠ blocked</span>` : null}
386
+ </div>
387
+ ${
388
+ issue.closeReason
389
+ ? html`
390
+ <div class="mt-2 text-[#666] text-sm italic">Close reason: ${issue.closeReason}</div>
391
+ `
392
+ : null
393
+ }
394
+ </div>
395
+
396
+ <!-- Tabs -->
397
+ <div class="flex gap-1 mb-4 border-b border-[#2a2a2a]">
398
+ ${TABS.map(
399
+ (tab) => html`
400
+ <button
401
+ key=${tab}
402
+ onClick=${() => setActiveTab(tab)}
403
+ class=${
404
+ "px-4 py-2 text-sm font-medium border-b-2 transition-colors " +
405
+ (activeTab === tab
406
+ ? "text-white border-[#E64415]"
407
+ : "text-[#888] border-transparent hover:text-[#ccc]")
408
+ }
409
+ >${tab}</button>
410
+ `,
411
+ )}
412
+ </div>
413
+
414
+ <!-- Tab content -->
415
+ ${activeTab === "Overview" ? html`<${OverviewTab} issue=${issue} />` : null}
416
+ ${activeTab === "Agents" ? html`<${AgentsTab} agents=${agents} taskId=${taskId} />` : null}
417
+ ${activeTab === "Communication" ? html`<${CommunicationTab} mail=${mail} taskId=${taskId} />` : null}
418
+ </div>
419
+ `;
420
+ }
421
+
422
+ export default TaskDetailView;