@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,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;
|