@yemi33/squad 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/LICENSE +21 -0
- package/README.md +483 -0
- package/TODO.md +60 -0
- package/agents/dallas/charter.md +55 -0
- package/agents/lambert/charter.md +66 -0
- package/agents/ralph/charter.md +44 -0
- package/agents/rebecca/charter.md +56 -0
- package/agents/ripley/charter.md +46 -0
- package/bin/squad.js +164 -0
- package/config.template.json +53 -0
- package/dashboard.html +1680 -0
- package/dashboard.js +886 -0
- package/docs/auto-discovery.md +414 -0
- package/docs/blog-first-successful-dispatch.md +127 -0
- package/docs/self-improvement.md +277 -0
- package/engine/ado-mcp-wrapper.js +49 -0
- package/engine/spawn-agent.js +98 -0
- package/engine.js +3416 -0
- package/package.json +46 -0
- package/playbooks/build-and-test.md +155 -0
- package/playbooks/explore.md +63 -0
- package/playbooks/fix.md +57 -0
- package/playbooks/implement.md +84 -0
- package/playbooks/plan-to-prd.md +74 -0
- package/playbooks/review.md +68 -0
- package/playbooks/test.md +75 -0
- package/playbooks/work-item.md +74 -0
- package/routing.md +29 -0
- package/skills/README.md +72 -0
- package/skills/ado-pr-status-fetch.md +18 -0
- package/squad.js +300 -0
- package/team.md +19 -0
package/dashboard.html
ADDED
|
@@ -0,0 +1,1680 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Squad Mission Control</title>
|
|
7
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>👽</text></svg>">
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #0d1117; --surface: #161b22; --surface2: #21262d; --border: #30363d;
|
|
11
|
+
--text: #e6edf3; --muted: #8b949e; --green: #3fb950; --yellow: #d29922;
|
|
12
|
+
--blue: #58a6ff; --purple: #bc8cff; --red: #f85149; --orange: #e3b341;
|
|
13
|
+
}
|
|
14
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
15
|
+
body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', system-ui, sans-serif; font-size: 14px; overflow-x: hidden; }
|
|
16
|
+
|
|
17
|
+
header {
|
|
18
|
+
background: var(--surface); border-bottom: 1px solid var(--border);
|
|
19
|
+
padding: 12px 24px; display: flex; align-items: center; justify-content: space-between;
|
|
20
|
+
position: sticky; top: 0; z-index: 100;
|
|
21
|
+
}
|
|
22
|
+
header h1 { font-size: 16px; font-weight: 600; color: var(--blue); letter-spacing: 0.5px; }
|
|
23
|
+
header h1 span { color: var(--muted); font-weight: 400; font-size: 13px; margin-left: 8px; }
|
|
24
|
+
.pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--green); display: inline-block; margin-right: 6px; animation: pulse 2s infinite; }
|
|
25
|
+
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} }
|
|
26
|
+
.timestamp { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
27
|
+
|
|
28
|
+
.layout { display: grid; grid-template-columns: 1fr 1fr; gap: 0; max-width: 100vw; overflow-x: hidden; }
|
|
29
|
+
section { padding: 20px 24px; border-bottom: 1px solid var(--border); overflow: hidden; min-width: 0; }
|
|
30
|
+
section:nth-child(odd) { border-right: 1px solid var(--border); }
|
|
31
|
+
section h2 { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); margin-bottom: 14px; display: flex; align-items: center; gap: 8px; }
|
|
32
|
+
section h2 .count { background: var(--surface2); border: 1px solid var(--border); border-radius: 10px; padding: 1px 7px; font-size: 11px; color: var(--text); }
|
|
33
|
+
|
|
34
|
+
.agents { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
35
|
+
.agent-card {
|
|
36
|
+
background: var(--surface2); border: 1px solid var(--border); border-radius: 8px;
|
|
37
|
+
padding: 12px 14px; transition: all 0.3s; cursor: pointer;
|
|
38
|
+
}
|
|
39
|
+
.agent-card:hover { border-color: var(--blue); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
|
|
40
|
+
.agent-card.working { border-color: var(--yellow); }
|
|
41
|
+
.agent-card.done { border-color: var(--green); }
|
|
42
|
+
.agent-card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
|
|
43
|
+
.agent-name { font-weight: 600; font-size: 14px; }
|
|
44
|
+
.agent-role { font-size: 11px; color: var(--muted); margin-bottom: 8px; }
|
|
45
|
+
.status-badge { font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
46
|
+
.status-badge.idle { background: var(--surface); color: var(--muted); border: 1px solid var(--border); }
|
|
47
|
+
.status-badge.working { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); animation: pulse 1.5s infinite; }
|
|
48
|
+
.status-badge.done { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
|
|
49
|
+
.agent-action { font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
|
50
|
+
.agent-card { min-width: 0; }
|
|
51
|
+
.agent-emoji { font-size: 20px; margin-right: 4px; }
|
|
52
|
+
.click-hint { font-size: 10px; color: var(--border); margin-top: 6px; }
|
|
53
|
+
|
|
54
|
+
.inbox-list { display: flex; flex-direction: column; gap: 8px; max-height: 320px; overflow-y: auto; }
|
|
55
|
+
|
|
56
|
+
/* PRD Progress */
|
|
57
|
+
.prd-progress-bar { width: 100%; height: 20px; background: var(--bg); border-radius: 10px; overflow: hidden; display: flex; margin-bottom: 12px; border: 1px solid var(--border); }
|
|
58
|
+
.prd-progress-bar .seg { height: 100%; transition: width 0.5s ease; }
|
|
59
|
+
.prd-progress-bar .seg.complete { background: var(--green); }
|
|
60
|
+
.prd-progress-bar .seg.pr-created { background: #2ea043; }
|
|
61
|
+
.prd-progress-bar .seg.in-progress { background: var(--yellow); }
|
|
62
|
+
.prd-progress-bar .seg.planned { background: var(--blue); opacity: 0.4; }
|
|
63
|
+
.prd-progress-bar .seg.missing { background: var(--border); }
|
|
64
|
+
.prd-progress-pct { font-size: 22px; font-weight: 700; color: var(--green); margin-bottom: 8px; }
|
|
65
|
+
.prd-progress-legend { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 12px; }
|
|
66
|
+
.prd-legend-item { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--muted); }
|
|
67
|
+
.prd-legend-dot { width: 10px; height: 10px; border-radius: 2px; }
|
|
68
|
+
.prd-legend-dot.complete { background: var(--green); }
|
|
69
|
+
.prd-legend-dot.pr-created { background: #2ea043; }
|
|
70
|
+
.prd-legend-dot.in-progress { background: var(--yellow); }
|
|
71
|
+
.prd-legend-dot.planned { background: var(--blue); opacity: 0.4; }
|
|
72
|
+
.prd-legend-dot.missing { background: var(--border); }
|
|
73
|
+
.prd-items-list { display: flex; flex-direction: column; gap: 4px; max-height: 200px; overflow-y: auto; }
|
|
74
|
+
.prd-item-row { display: flex; align-items: center; gap: 8px; padding: 5px 8px; border-radius: 4px; font-size: 11px; background: var(--surface2); border: 1px solid var(--border); }
|
|
75
|
+
.prd-item-id { font-family: Consolas, monospace; color: var(--blue); min-width: 36px; }
|
|
76
|
+
.prd-item-name { flex: 1; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
77
|
+
.prd-item-priority { font-size: 10px; padding: 1px 6px; border-radius: 8px; }
|
|
78
|
+
.prd-item-priority.high { background: rgba(248,81,73,0.15); color: var(--red); }
|
|
79
|
+
.prd-item-priority.medium { background: rgba(210,153,34,0.15); color: var(--yellow); }
|
|
80
|
+
.prd-item-priority.low { background: rgba(139,148,158,0.15); color: var(--muted); }
|
|
81
|
+
|
|
82
|
+
.inbox-item { background: var(--surface2); border: 1px solid var(--border); border-left: 3px solid var(--purple); border-radius: 4px; padding: 10px 12px; cursor: pointer; }
|
|
83
|
+
.inbox-item:hover { border-color: var(--blue); border-left-color: var(--blue); }
|
|
84
|
+
.inbox-name { font-weight: 500; font-size: 12px; color: var(--purple); margin-bottom: 4px; display: flex; justify-content: space-between; }
|
|
85
|
+
.inbox-preview { font-size: 11px; color: var(--muted); line-height: 1.5; max-height: 60px; overflow: hidden; }
|
|
86
|
+
|
|
87
|
+
.prd-panel, .pr-panel { grid-column: 1 / -1; border-bottom: 1px solid var(--border); overflow: visible; min-width: 0; }
|
|
88
|
+
.prd-inner { display: flex; gap: 16px; align-items: flex-start; }
|
|
89
|
+
.prd-stats { display: flex; gap: 16px; }
|
|
90
|
+
.prd-stat { text-align: center; background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: 12px 20px; }
|
|
91
|
+
.prd-stat-num { font-size: 28px; font-weight: 700; color: var(--blue); }
|
|
92
|
+
.prd-stat-num.green { color: var(--green); }
|
|
93
|
+
.prd-stat-num.red { color: var(--red); }
|
|
94
|
+
.prd-stat-num.orange { color: var(--orange); }
|
|
95
|
+
.prd-stat-label { font-size: 11px; color: var(--muted); margin-top: 2px; }
|
|
96
|
+
.prd-summary { flex: 1; font-size: 13px; color: var(--muted); line-height: 1.6; background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: 14px; }
|
|
97
|
+
.prd-pending { color: var(--yellow); font-style: italic; }
|
|
98
|
+
|
|
99
|
+
/* PR Tracker */
|
|
100
|
+
.pr-table-wrap { overflow-x: auto; }
|
|
101
|
+
.pr-table { width: 100%; border-collapse: collapse; font-size: 12px; table-layout: auto; }
|
|
102
|
+
.pr-table th:last-child, .pr-table td:last-child { width: 36px; min-width: 36px; text-align: center; }
|
|
103
|
+
.pr-table th { text-align: left; color: var(--muted); font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; padding: 8px 10px; border-bottom: 1px solid var(--border); }
|
|
104
|
+
.pr-table td { padding: 10px; border-bottom: 1px solid var(--border); vertical-align: middle; white-space: nowrap; }
|
|
105
|
+
.pr-table tr:last-child td { border-bottom: none; }
|
|
106
|
+
.pr-table tr:hover { background: var(--surface2); }
|
|
107
|
+
.pr-title { color: var(--blue); text-decoration: none; font-weight: 500; }
|
|
108
|
+
.pr-title:hover { text-decoration: underline; }
|
|
109
|
+
.pr-id { color: var(--muted); font-family: Consolas, monospace; font-size: 11px; }
|
|
110
|
+
.pr-agent { font-size: 11px; color: var(--text); }
|
|
111
|
+
.pr-branch { font-family: Consolas, monospace; font-size: 10px; color: var(--muted); background: var(--bg); padding: 2px 6px; border-radius: 3px; border: 1px solid var(--border); }
|
|
112
|
+
.pr-badge { font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; }
|
|
113
|
+
.pr-badge.draft { background: rgba(139,148,158,0.15); color: var(--muted); border: 1px solid var(--border); }
|
|
114
|
+
.pr-badge.active { background: rgba(88,166,255,0.15); color: var(--blue); border: 1px solid var(--blue); }
|
|
115
|
+
.pr-badge.approved { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
|
|
116
|
+
.pr-badge.rejected { background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid var(--red); }
|
|
117
|
+
.pr-badge.merged { background: rgba(188,140,255,0.15); color: var(--purple); border: 1px solid var(--purple); }
|
|
118
|
+
.pr-badge.building { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); animation: pulse 1.5s infinite; }
|
|
119
|
+
.pr-badge.build-pass { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
|
|
120
|
+
.pr-badge.build-fail { background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid var(--red); }
|
|
121
|
+
.pr-badge.no-build { background: var(--surface); color: var(--muted); border: 1px solid var(--border); }
|
|
122
|
+
.error-details-btn { font-size: 9px; padding: 1px 6px; margin-left: 4px; background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid var(--red); border-radius: 8px; cursor: pointer; font-weight: 600; text-transform: uppercase; letter-spacing: 0.3px; }
|
|
123
|
+
.error-details-btn:hover { background: rgba(248,81,73,0.3); }
|
|
124
|
+
.pr-empty { color: var(--muted); font-style: italic; font-size: 12px; padding: 12px 0; }
|
|
125
|
+
.pr-pager { display: flex; align-items: center; justify-content: space-between; margin-top: 10px; }
|
|
126
|
+
.pr-pager-btns { display: flex; gap: 6px; }
|
|
127
|
+
.pr-pager-btn {
|
|
128
|
+
background: var(--surface2); border: 1px solid var(--border); color: var(--muted);
|
|
129
|
+
font-size: 11px; padding: 4px 10px; border-radius: 4px; cursor: pointer; transition: all 0.2s;
|
|
130
|
+
}
|
|
131
|
+
.pr-pager-btn:hover { color: var(--text); border-color: var(--text); }
|
|
132
|
+
.pr-pager-btn.disabled { opacity: 0.3; pointer-events: none; }
|
|
133
|
+
.pr-pager-btn.see-all { color: var(--blue); border-color: var(--blue); }
|
|
134
|
+
.pr-pager-btn.see-all:hover { background: rgba(88,166,255,0.1); }
|
|
135
|
+
.pr-page-info { font-size: 11px; color: var(--muted); }
|
|
136
|
+
.pr-date { font-size: 11px; color: var(--muted); }
|
|
137
|
+
|
|
138
|
+
.archive-btn {
|
|
139
|
+
background: var(--surface2); border: 1px solid var(--border); color: var(--muted);
|
|
140
|
+
font-size: 11px; padding: 4px 10px; border-radius: 4px; cursor: pointer; transition: all 0.2s; margin-left: auto;
|
|
141
|
+
}
|
|
142
|
+
.archive-btn:hover { color: var(--purple); border-color: var(--purple); }
|
|
143
|
+
.archive-list { display: flex; flex-direction: column; gap: 8px; }
|
|
144
|
+
.archive-card { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 12px 14px; cursor: pointer; transition: all 0.2s; }
|
|
145
|
+
.archive-card:hover { border-color: var(--purple); }
|
|
146
|
+
.archive-card h4 { color: var(--purple); font-size: 13px; margin-bottom: 6px; }
|
|
147
|
+
.archive-card p { font-size: 11px; color: var(--muted); line-height: 1.5; }
|
|
148
|
+
.archive-detail-section { margin-bottom: 16px; }
|
|
149
|
+
.archive-detail-section h4 { color: var(--blue); font-size: 13px; margin-bottom: 8px; font-family: 'Segoe UI', sans-serif; }
|
|
150
|
+
.archive-feature { background: var(--surface2); border: 1px solid var(--border); border-radius: 4px; padding: 10px 12px; margin-bottom: 6px; }
|
|
151
|
+
.archive-feature .feat-id { font-family: Consolas, monospace; color: var(--blue); font-size: 11px; }
|
|
152
|
+
.archive-feature .feat-name { font-weight: 600; font-size: 12px; color: var(--text); margin: 4px 0; }
|
|
153
|
+
.archive-feature .feat-desc { font-size: 11px; color: var(--muted); line-height: 1.5; }
|
|
154
|
+
.archive-feature .feat-meta { font-size: 10px; color: var(--muted); margin-top: 6px; display: flex; gap: 12px; }
|
|
155
|
+
.archive-question { background: var(--surface2); border: 1px solid var(--border); border-left: 3px solid var(--orange); border-radius: 4px; padding: 10px 12px; margin-bottom: 6px; }
|
|
156
|
+
.archive-question .q-id { font-family: Consolas, monospace; color: var(--orange); font-size: 11px; }
|
|
157
|
+
.archive-question .q-text { font-size: 12px; color: var(--text); margin: 4px 0; }
|
|
158
|
+
.archive-question .q-context { font-size: 11px; color: var(--muted); line-height: 1.5; }
|
|
159
|
+
|
|
160
|
+
::-webkit-scrollbar { width: 6px; }
|
|
161
|
+
::-webkit-scrollbar-track { background: var(--bg); }
|
|
162
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
163
|
+
.empty { color: var(--muted); font-style: italic; font-size: 12px; padding: 8px 0; }
|
|
164
|
+
|
|
165
|
+
/* Command Center — Unified Input */
|
|
166
|
+
.cmd-center { grid-column: 1 / -1; overflow: visible !important; }
|
|
167
|
+
.cmd-input-wrap {
|
|
168
|
+
position: relative; display: flex; align-items: flex-start; gap: 0;
|
|
169
|
+
background: var(--bg); border: 2px solid var(--border); border-radius: 10px;
|
|
170
|
+
transition: border-color 0.2s, box-shadow 0.2s; padding: 0;
|
|
171
|
+
}
|
|
172
|
+
.cmd-input-wrap:focus-within { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(88,166,255,0.12); }
|
|
173
|
+
.cmd-input-wrap textarea {
|
|
174
|
+
flex: 1; background: transparent; border: none; color: var(--text);
|
|
175
|
+
padding: 14px 16px; font-size: 14px; font-family: inherit; resize: none;
|
|
176
|
+
outline: none; line-height: 1.5; min-height: 48px; max-height: 200px;
|
|
177
|
+
}
|
|
178
|
+
.cmd-input-wrap textarea::placeholder { color: var(--muted); }
|
|
179
|
+
.cmd-send-btn {
|
|
180
|
+
background: transparent; color: var(--blue); border: 1px solid var(--blue); padding: 10px 18px;
|
|
181
|
+
border-radius: 0 8px 8px 0; font-size: 13px; font-weight: 600; cursor: pointer;
|
|
182
|
+
transition: opacity 0.2s; white-space: nowrap; align-self: stretch; min-height: 48px;
|
|
183
|
+
display: flex; align-items: center; gap: 6px;
|
|
184
|
+
}
|
|
185
|
+
.cmd-send-btn:hover { background: rgba(88,166,255,0.1); }
|
|
186
|
+
.cmd-send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
187
|
+
.cmd-send-btn kbd { font-size: 10px; opacity: 0.7; font-family: inherit; }
|
|
188
|
+
|
|
189
|
+
/* Intent & meta chips */
|
|
190
|
+
.cmd-meta { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px; align-items: center; min-height: 0; }
|
|
191
|
+
.cmd-chip {
|
|
192
|
+
display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px;
|
|
193
|
+
border-radius: 12px; font-size: 11px; font-weight: 500; border: 1px solid var(--border);
|
|
194
|
+
background: var(--surface2); color: var(--muted); cursor: default; transition: all 0.2s;
|
|
195
|
+
}
|
|
196
|
+
.cmd-chip.intent { color: var(--blue); border-color: rgba(88,166,255,0.3); background: rgba(88,166,255,0.08); }
|
|
197
|
+
.cmd-chip.agent-chip { color: var(--purple); border-color: rgba(188,140,255,0.3); background: rgba(188,140,255,0.08); }
|
|
198
|
+
.cmd-chip.fanout { color: var(--orange); border-color: rgba(227,179,65,0.3); background: rgba(227,179,65,0.08); }
|
|
199
|
+
.cmd-chip.priority-high { color: var(--red); border-color: rgba(248,81,73,0.3); background: rgba(248,81,73,0.08); }
|
|
200
|
+
.cmd-chip.priority-medium { color: var(--yellow); border-color: rgba(210,153,34,0.3); background: rgba(210,153,34,0.08); }
|
|
201
|
+
.cmd-chip.priority-low { color: var(--muted); }
|
|
202
|
+
.cmd-chip.project-chip { color: var(--green); border-color: rgba(63,185,80,0.3); background: rgba(63,185,80,0.08); }
|
|
203
|
+
.cmd-chip .chip-x { cursor: pointer; opacity: 0.5; margin-left: 2px; font-size: 13px; }
|
|
204
|
+
.cmd-chip .chip-x:hover { opacity: 1; }
|
|
205
|
+
.cmd-chip select {
|
|
206
|
+
background: transparent; border: none; color: inherit; font-size: 11px;
|
|
207
|
+
font-weight: 500; font-family: inherit; cursor: pointer; outline: none;
|
|
208
|
+
-webkit-appearance: none; appearance: none; padding-right: 2px;
|
|
209
|
+
}
|
|
210
|
+
.cmd-chip select option { background: var(--surface); color: var(--text); }
|
|
211
|
+
|
|
212
|
+
/* @ mention autocomplete */
|
|
213
|
+
.cmd-mention-popup {
|
|
214
|
+
display: none; position: relative; left: 0; margin-top: 4px;
|
|
215
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
|
|
216
|
+
padding: 4px 0; min-width: 280px; box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
|
217
|
+
z-index: 200; max-height: 320px; overflow-y: auto;
|
|
218
|
+
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
|
|
219
|
+
}
|
|
220
|
+
.cmd-mention-popup::-webkit-scrollbar { width: 4px; }
|
|
221
|
+
.cmd-mention-popup::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
|
222
|
+
.cmd-mention-popup.visible { display: block; }
|
|
223
|
+
.cmd-mention-item {
|
|
224
|
+
padding: 8px 14px; font-size: 12px; cursor: pointer; display: flex;
|
|
225
|
+
align-items: center; gap: 8px; transition: background 0.15s;
|
|
226
|
+
}
|
|
227
|
+
.cmd-mention-item:hover { background: var(--surface2); }
|
|
228
|
+
.cmd-mention-item.active { background: rgba(88,166,255,0.15); border-left: 2px solid var(--blue); }
|
|
229
|
+
.cmd-mention-item .mention-emoji { font-size: 16px; }
|
|
230
|
+
.cmd-mention-item .mention-name { font-weight: 600; color: var(--purple); }
|
|
231
|
+
.cmd-mention-item .mention-role { color: var(--muted); font-size: 11px; }
|
|
232
|
+
|
|
233
|
+
/* Hints bar */
|
|
234
|
+
.cmd-hints {
|
|
235
|
+
display: flex; gap: 12px; margin-top: 2px; font-size: 10px; color: var(--muted);
|
|
236
|
+
letter-spacing: 0.2px; padding: 0 4px; overflow-x: auto; white-space: nowrap;
|
|
237
|
+
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
|
|
238
|
+
}
|
|
239
|
+
.cmd-hints::-webkit-scrollbar { height: 3px; }
|
|
240
|
+
.cmd-hints::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
241
|
+
.cmd-hints code { color: var(--blue); font-size: 10px; background: var(--surface2); padding: 1px 5px; border-radius: 3px; }
|
|
242
|
+
|
|
243
|
+
.cmd-toast {
|
|
244
|
+
display: none; padding: 8px 14px; border-radius: 4px; font-size: 12px;
|
|
245
|
+
margin-top: 10px; animation: fadeIn 0.3s;
|
|
246
|
+
}
|
|
247
|
+
.cmd-toast.success { display: block; background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
|
|
248
|
+
.cmd-toast.error { display: block; background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid var(--red); }
|
|
249
|
+
@keyframes fadeIn { from{opacity:0;transform:translateY(-4px)} to{opacity:1;transform:translateY(0)} }
|
|
250
|
+
|
|
251
|
+
/* Engine Status */
|
|
252
|
+
.engine-badge { padding: 4px 12px; border-radius: 12px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-right: 12px; }
|
|
253
|
+
.engine-badge.running { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid var(--green); }
|
|
254
|
+
.engine-badge.paused { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); }
|
|
255
|
+
.engine-badge.stopped { background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid var(--red); }
|
|
256
|
+
|
|
257
|
+
/* Dispatch Queue */
|
|
258
|
+
.dispatch-stats { display: flex; gap: 16px; margin-bottom: 14px; }
|
|
259
|
+
.dispatch-stat { text-align: center; background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: 10px 18px; }
|
|
260
|
+
.dispatch-stat-num { font-size: 24px; font-weight: 700; }
|
|
261
|
+
.dispatch-stat-num.blue { color: var(--blue); }
|
|
262
|
+
.dispatch-stat-num.green { color: var(--green); }
|
|
263
|
+
.dispatch-stat-num.yellow { color: var(--yellow); }
|
|
264
|
+
.dispatch-stat-num.muted { color: var(--muted); }
|
|
265
|
+
.dispatch-stat-label { font-size: 10px; color: var(--muted); margin-top: 2px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
266
|
+
.dispatch-list { display: flex; flex-direction: column; gap: 6px; }
|
|
267
|
+
.dispatch-item { display: flex; align-items: center; gap: 8px; padding: 8px 10px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; font-size: 12px; }
|
|
268
|
+
.dispatch-type { font-size: 10px; font-weight: 600; padding: 2px 8px; border-radius: 8px; text-transform: uppercase; white-space: nowrap; }
|
|
269
|
+
.dispatch-type.implement { background: rgba(88,166,255,0.15); color: var(--blue); }
|
|
270
|
+
.dispatch-type.review { background: rgba(188,140,255,0.15); color: var(--purple); }
|
|
271
|
+
.dispatch-type.fix { background: rgba(210,153,34,0.15); color: var(--yellow); }
|
|
272
|
+
.dispatch-type.analyze { background: rgba(63,185,80,0.15); color: var(--green); }
|
|
273
|
+
.dispatch-type.explore { background: rgba(139,148,158,0.15); color: var(--muted); }
|
|
274
|
+
.dispatch-type.test { background: rgba(227,179,65,0.15); color: var(--orange); }
|
|
275
|
+
.dispatch-type.manual { background: rgba(139,148,158,0.15); color: var(--muted); }
|
|
276
|
+
.dispatch-agent { font-weight: 600; color: var(--text); }
|
|
277
|
+
.dispatch-task { flex: 1; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
278
|
+
.dispatch-time { font-size: 10px; color: var(--border); }
|
|
279
|
+
|
|
280
|
+
/* Engine Log */
|
|
281
|
+
.log-list { max-height: 280px; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
|
|
282
|
+
.log-entry { font-size: 11px; padding: 4px 8px; border-bottom: 1px solid rgba(48,54,61,0.4); font-family: Consolas, monospace; }
|
|
283
|
+
.log-ts { color: var(--muted); }
|
|
284
|
+
.log-level-info { color: var(--blue); }
|
|
285
|
+
.log-level-warn { color: var(--yellow); }
|
|
286
|
+
.log-level-error { color: var(--red); }
|
|
287
|
+
|
|
288
|
+
/* Agent Detail Panel (slide-in) */
|
|
289
|
+
.detail-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 300; }
|
|
290
|
+
.detail-overlay.open { display: block; }
|
|
291
|
+
.detail-panel {
|
|
292
|
+
position: fixed; top: 0; right: 0; width: 55%; max-width: 750px; height: 100vh;
|
|
293
|
+
background: var(--surface); border-left: 1px solid var(--border);
|
|
294
|
+
z-index: 301; display: flex; flex-direction: column;
|
|
295
|
+
transform: translateX(100%); transition: transform 0.25s ease;
|
|
296
|
+
}
|
|
297
|
+
.detail-panel.open { transform: translateX(0); }
|
|
298
|
+
.detail-header {
|
|
299
|
+
padding: 16px 20px; border-bottom: 1px solid var(--border);
|
|
300
|
+
display: flex; justify-content: space-between; align-items: center; flex-shrink: 0;
|
|
301
|
+
}
|
|
302
|
+
.detail-header h3 { font-size: 16px; color: var(--blue); display: flex; align-items: center; gap: 8px; }
|
|
303
|
+
.detail-close { background: none; border: 1px solid var(--border); color: var(--muted); font-size: 12px; cursor: pointer; padding: 4px 12px; border-radius: 4px; }
|
|
304
|
+
.detail-close:hover { color: var(--text); border-color: var(--text); }
|
|
305
|
+
.detail-body { flex: 1; overflow-y: auto; padding: 0; }
|
|
306
|
+
.detail-tabs { display: flex; border-bottom: 1px solid var(--border); flex-shrink: 0; background: var(--bg); }
|
|
307
|
+
.detail-tab {
|
|
308
|
+
padding: 10px 16px; font-size: 12px; font-weight: 500; color: var(--muted);
|
|
309
|
+
cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.2s;
|
|
310
|
+
}
|
|
311
|
+
.detail-tab:hover { color: var(--text); }
|
|
312
|
+
.detail-tab.active { color: var(--blue); border-bottom-color: var(--blue); }
|
|
313
|
+
.detail-content {
|
|
314
|
+
padding: 16px 20px; font-size: 12px; line-height: 1.7;
|
|
315
|
+
white-space: pre-wrap; word-wrap: break-word; font-family: Consolas, monospace; color: var(--muted);
|
|
316
|
+
}
|
|
317
|
+
.detail-content h4 { color: var(--text); font-size: 13px; margin: 12px 0 6px 0; font-family: 'Segoe UI', sans-serif; }
|
|
318
|
+
.detail-content .section { margin-bottom: 16px; padding: 12px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; }
|
|
319
|
+
.status-line { display: flex; align-items: center; gap: 10px; padding: 10px 16px; background: var(--bg); border-bottom: 1px solid var(--border); font-size: 12px; }
|
|
320
|
+
|
|
321
|
+
/* Modal for inbox detail */
|
|
322
|
+
.modal-bg { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.75); z-index: 400; align-items: center; justify-content: center; }
|
|
323
|
+
.modal-bg.open { display: flex; }
|
|
324
|
+
.modal { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; width: 95%; max-width: 1100px; max-height: 80vh; display: flex; flex-direction: column; }
|
|
325
|
+
.modal-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
|
|
326
|
+
.modal-header h3 { font-size: 14px; color: var(--blue); }
|
|
327
|
+
.modal-header-actions { display: flex; align-items: center; gap: 8px; }
|
|
328
|
+
.modal-copy { background: var(--surface2); border: 1px solid var(--border); color: var(--muted); font-size: 11px; cursor: pointer; padding: 4px 10px; border-radius: 4px; display: flex; align-items: center; gap: 4px; transition: all 0.2s; }
|
|
329
|
+
.modal-copy:hover { color: var(--text); border-color: var(--text); }
|
|
330
|
+
.modal-copy.copied { color: var(--green); border-color: var(--green); }
|
|
331
|
+
.modal-close { background: none; border: none; color: var(--muted); font-size: 18px; cursor: pointer; padding: 4px 8px; }
|
|
332
|
+
.modal-close:hover { color: var(--text); }
|
|
333
|
+
.modal-body { padding: 16px 20px; overflow-y: auto; white-space: pre-wrap; font-size: 12px; line-height: 1.7; color: var(--muted); font-family: Consolas, monospace; }
|
|
334
|
+
|
|
335
|
+
/* Responsive: tablet / narrow window */
|
|
336
|
+
@media (max-width: 1100px) {
|
|
337
|
+
.layout { grid-template-columns: 1fr; }
|
|
338
|
+
section:nth-child(odd) { border-right: none; }
|
|
339
|
+
.prd-inner { flex-direction: column; }
|
|
340
|
+
.prd-stats { flex-wrap: wrap; }
|
|
341
|
+
.dispatch-stats { flex-wrap: wrap; }
|
|
342
|
+
.agents { grid-template-columns: 1fr 1fr; }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* Responsive: mobile / very narrow */
|
|
346
|
+
@media (max-width: 640px) {
|
|
347
|
+
header { padding: 10px 14px; flex-wrap: wrap; gap: 6px; }
|
|
348
|
+
header h1 { font-size: 14px; }
|
|
349
|
+
header h1 span { display: none; }
|
|
350
|
+
section { padding: 14px 12px; }
|
|
351
|
+
.agents { grid-template-columns: 1fr; }
|
|
352
|
+
.prd-stats { gap: 8px; }
|
|
353
|
+
.prd-stat { padding: 8px 12px; }
|
|
354
|
+
.dispatch-stats { gap: 8px; }
|
|
355
|
+
.dispatch-stat { padding: 8px 12px; flex: 1; min-width: 80px; }
|
|
356
|
+
.dispatch-stat-num { font-size: 18px; }
|
|
357
|
+
.pr-table { font-size: 11px; }
|
|
358
|
+
.pr-table td { padding: 8px 6px; }
|
|
359
|
+
.pr-table th { padding: 6px; font-size: 10px; }
|
|
360
|
+
.cmd-input-wrap { flex-direction: column; }
|
|
361
|
+
.cmd-send-btn { width: 100%; }
|
|
362
|
+
.cmd-hints { gap: 8px; }
|
|
363
|
+
.modal { width: 95vw; max-height: 80vh; }
|
|
364
|
+
#projects-bar { flex-wrap: wrap; padding: 6px 12px; }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* Ensure all tables scroll horizontally on narrow screens */
|
|
368
|
+
.pr-table-wrap, #pr-content, #completed-content, #work-items-content { overflow-x: auto; min-width: 0; }
|
|
369
|
+
</style>
|
|
370
|
+
</head>
|
|
371
|
+
<body>
|
|
372
|
+
|
|
373
|
+
<header>
|
|
374
|
+
<h1>Squad Mission Control <span id="header-projects">—</span></h1>
|
|
375
|
+
<div style="display:flex;align-items:center;gap:8px;">
|
|
376
|
+
<span class="engine-badge stopped" id="engine-badge">STOPPED</span>
|
|
377
|
+
<div class="timestamp" id="ts">—</div>
|
|
378
|
+
</div>
|
|
379
|
+
</header>
|
|
380
|
+
|
|
381
|
+
<div id="projects-bar" style="background:var(--surface);border-bottom:1px solid var(--border);padding:8px 24px;display:flex;align-items:center;gap:12px;font-size:12px;">
|
|
382
|
+
<span style="color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:0.5px;font-size:10px;">Projects</span>
|
|
383
|
+
<div id="projects-list" style="display:flex;gap:8px;flex-wrap:wrap;">—</div>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
<div class="layout">
|
|
387
|
+
<section class="cmd-center">
|
|
388
|
+
<h2>Command Center</h2>
|
|
389
|
+
<div class="cmd-input-wrap" id="cmd-input-wrap">
|
|
390
|
+
<textarea id="cmd-input" rows="1" placeholder='What do you need? e.g. "Fix the auth bug @dallas" or "/decide always use feature flags"'
|
|
391
|
+
oninput="cmdInputChanged()" onkeydown="cmdKeyDown(event)"></textarea>
|
|
392
|
+
<button class="cmd-send-btn" id="cmd-send-btn" onclick="cmdSubmit()">Send <kbd>Ctrl+Enter</kbd></button>
|
|
393
|
+
</div>
|
|
394
|
+
<div class="cmd-mention-popup" id="cmd-mention-popup"></div>
|
|
395
|
+
<div class="cmd-meta" id="cmd-meta"></div>
|
|
396
|
+
<div class="cmd-hints">
|
|
397
|
+
<span><code>@agent</code> assign</span>
|
|
398
|
+
<span><code>@everyone</code> fan-out</span>
|
|
399
|
+
<span><code>!high</code> / <code>!low</code> priority</span>
|
|
400
|
+
<span><code>/note</code> team note</span>
|
|
401
|
+
<span><code>/prd</code> PRD item</span>
|
|
402
|
+
<span><code>#project</code> target project</span>
|
|
403
|
+
</div>
|
|
404
|
+
<div class="cmd-toast" id="cmd-toast"></div>
|
|
405
|
+
</section>
|
|
406
|
+
|
|
407
|
+
<section>
|
|
408
|
+
<h2>Squad Members <span style="font-size:10px;color:var(--border);font-weight:400;text-transform:none;letter-spacing:0">click for details</span></h2>
|
|
409
|
+
<div class="agents" id="agents-grid">Loading...</div>
|
|
410
|
+
</section>
|
|
411
|
+
|
|
412
|
+
<section id="work-items-section" style="overflow:visible">
|
|
413
|
+
<h2>Work Items <span class="count" id="wi-count">0</span></h2>
|
|
414
|
+
<div id="work-items-content"><p class="empty">No work items. Add tasks via Command Center above.</p></div>
|
|
415
|
+
</section>
|
|
416
|
+
|
|
417
|
+
<section class="prd-panel" id="prd-section">
|
|
418
|
+
<h2>PRD <span class="count" id="prd-progress-count">0%</span> <span id="prd-badge"></span> <span id="archive-btns"></span></h2>
|
|
419
|
+
<div id="prd-content"><p class="prd-pending">No PRD found.</p></div>
|
|
420
|
+
<div id="prd-progress-content" style="margin-top:12px"></div>
|
|
421
|
+
</section>
|
|
422
|
+
|
|
423
|
+
<section class="pr-panel" id="pr-section">
|
|
424
|
+
<h2>Pull Requests <span class="count" id="pr-count">0</span></h2>
|
|
425
|
+
<div id="pr-content"><p class="pr-empty">No pull requests yet.</p></div>
|
|
426
|
+
</section>
|
|
427
|
+
|
|
428
|
+
<section>
|
|
429
|
+
<h2>Notes Inbox <span class="count" id="inbox-count">0</span></h2>
|
|
430
|
+
<div class="inbox-list" id="inbox-list">Loading...</div>
|
|
431
|
+
</section>
|
|
432
|
+
|
|
433
|
+
<section>
|
|
434
|
+
<h2>Team Notes</h2>
|
|
435
|
+
<div id="notes-list">Loading...</div>
|
|
436
|
+
</section>
|
|
437
|
+
|
|
438
|
+
<section>
|
|
439
|
+
<h2>Squad Skills <span class="count" id="skills-count">0</span></h2>
|
|
440
|
+
<div id="skills-list"><p class="empty">No skills yet. Agents create these when they discover repeatable workflows.</p></div>
|
|
441
|
+
</section>
|
|
442
|
+
|
|
443
|
+
<section>
|
|
444
|
+
<h2>Dispatch Queue</h2>
|
|
445
|
+
<div class="dispatch-stats" id="dispatch-stats"></div>
|
|
446
|
+
<div id="dispatch-active"></div>
|
|
447
|
+
<div id="dispatch-pending"></div>
|
|
448
|
+
</section>
|
|
449
|
+
|
|
450
|
+
<section>
|
|
451
|
+
<h2>Engine Log</h2>
|
|
452
|
+
<div class="log-list" id="engine-log">No log entries yet.</div>
|
|
453
|
+
</section>
|
|
454
|
+
|
|
455
|
+
<section>
|
|
456
|
+
<h2>Agent Metrics</h2>
|
|
457
|
+
<div id="metrics-content"><p class="empty">No metrics yet. Metrics appear after agents complete tasks.</p></div>
|
|
458
|
+
</section>
|
|
459
|
+
|
|
460
|
+
<section class="pr-panel" id="completed-section">
|
|
461
|
+
<h2>Recent Completions <span class="count" id="completed-count">0</span></h2>
|
|
462
|
+
<div id="completed-content"><p class="empty">No completed dispatches yet.</p></div>
|
|
463
|
+
</section>
|
|
464
|
+
</div>
|
|
465
|
+
|
|
466
|
+
<!-- Agent Detail Panel -->
|
|
467
|
+
<div class="detail-overlay" id="detail-overlay" onclick="closeDetail()"></div>
|
|
468
|
+
<div class="detail-panel" id="detail-panel">
|
|
469
|
+
<div class="detail-header">
|
|
470
|
+
<h3 id="detail-agent-name">—</h3>
|
|
471
|
+
<button class="detail-close" onclick="closeDetail()">ESC / Close</button>
|
|
472
|
+
</div>
|
|
473
|
+
<div class="status-line" id="detail-status-line">—</div>
|
|
474
|
+
<div class="detail-tabs" id="detail-tabs"></div>
|
|
475
|
+
<div class="detail-body">
|
|
476
|
+
<div class="detail-content" id="detail-content"></div>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
<!-- Modal for inbox detail -->
|
|
481
|
+
<div class="modal-bg" id="modal" onclick="if(event.target===this)closeModal()">
|
|
482
|
+
<div class="modal">
|
|
483
|
+
<div class="modal-header">
|
|
484
|
+
<h3 id="modal-title">—</h3>
|
|
485
|
+
<div class="modal-header-actions">
|
|
486
|
+
<button class="modal-copy" id="modal-copy-btn" onclick="copyModalContent()" title="Copy to clipboard"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25z"/></svg> Copy</button>
|
|
487
|
+
<button class="modal-close" onclick="closeModal()">X</button>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
<div class="modal-body" id="modal-body"></div>
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
|
|
494
|
+
<script>
|
|
495
|
+
let inboxData = [];
|
|
496
|
+
let agentData = [];
|
|
497
|
+
let currentAgentId = null;
|
|
498
|
+
let currentTab = 'thought-process';
|
|
499
|
+
|
|
500
|
+
function escHtml(s) {
|
|
501
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function statusColor(s) {
|
|
505
|
+
return s === 'working' ? 'working' : s === 'done' ? 'done' : '';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// -- Agent Detail Panel --
|
|
509
|
+
async function openAgentDetail(id) {
|
|
510
|
+
currentAgentId = id;
|
|
511
|
+
const agent = agentData.find(a => a.id === id);
|
|
512
|
+
currentTab = (agent?.status === 'working') ? 'live' : 'thought-process';
|
|
513
|
+
if (!agent) return;
|
|
514
|
+
|
|
515
|
+
document.getElementById('detail-agent-name').innerHTML =
|
|
516
|
+
'<span style="font-size:22px">' + agent.emoji + '</span> ' + agent.name + ' — ' + agent.role;
|
|
517
|
+
|
|
518
|
+
const badgeClass = agent.status;
|
|
519
|
+
document.getElementById('detail-status-line').innerHTML =
|
|
520
|
+
'<span class="status-badge ' + badgeClass + '">' + agent.status.toUpperCase() + '</span> ' +
|
|
521
|
+
'<span style="color:var(--muted)">' + escHtml(agent.lastAction) + '</span>';
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
const detail = await fetch('/api/agent/' + id).then(r => r.json());
|
|
525
|
+
renderDetailTabs(detail);
|
|
526
|
+
renderDetailContent(detail, currentTab);
|
|
527
|
+
} catch(e) {
|
|
528
|
+
document.getElementById('detail-content').textContent = 'Error loading agent detail: ' + e.message;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
document.getElementById('detail-overlay').classList.add('open');
|
|
532
|
+
document.getElementById('detail-panel').classList.add('open');
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function closeDetail() {
|
|
536
|
+
document.getElementById('detail-overlay').classList.remove('open');
|
|
537
|
+
document.getElementById('detail-panel').classList.remove('open');
|
|
538
|
+
currentAgentId = null;
|
|
539
|
+
stopLivePolling();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function renderDetailTabs(detail) {
|
|
543
|
+
const isWorking = detail.statusData?.status === 'working';
|
|
544
|
+
const tabs = [
|
|
545
|
+
...(isWorking ? [{ id: 'live', label: 'Live Output' }] : []),
|
|
546
|
+
{ id: 'thought-process', label: 'Thought Process' },
|
|
547
|
+
{ id: 'charter', label: 'Charter' },
|
|
548
|
+
{ id: 'history', label: 'History' },
|
|
549
|
+
{ id: 'output', label: 'Output Log' },
|
|
550
|
+
];
|
|
551
|
+
document.getElementById('detail-tabs').innerHTML = tabs.map(t =>
|
|
552
|
+
'<div class="detail-tab ' + (t.id === currentTab ? 'active' : '') + '" onclick="switchTab(\'' + t.id + '\')">' + t.label + '</div>'
|
|
553
|
+
).join('');
|
|
554
|
+
|
|
555
|
+
document.getElementById('detail-panel').dataset.detail = JSON.stringify(detail);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function switchTab(tabId) {
|
|
559
|
+
currentTab = tabId;
|
|
560
|
+
const detail = JSON.parse(document.getElementById('detail-panel').dataset.detail || '{}');
|
|
561
|
+
renderDetailTabs(detail);
|
|
562
|
+
renderDetailContent(detail, tabId);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function renderDetailContent(detail, tab) {
|
|
566
|
+
const el = document.getElementById('detail-content');
|
|
567
|
+
|
|
568
|
+
if (tab === 'thought-process') {
|
|
569
|
+
let html = '';
|
|
570
|
+
|
|
571
|
+
if (detail.statusData) {
|
|
572
|
+
html += '<h4>Current Status</h4><div class="section">';
|
|
573
|
+
html += 'Status: <span style="color:var(--' + (detail.statusData.status === 'working' ? 'yellow' : detail.statusData.status === 'done' ? 'green' : 'muted') + ')">' + (detail.statusData.status || 'idle').toUpperCase() + '</span>\n';
|
|
574
|
+
if (detail.statusData.task) html += 'Task: ' + escHtml(detail.statusData.task) + '\n';
|
|
575
|
+
if (detail.statusData.started_at) html += 'Started: ' + detail.statusData.started_at + '\n';
|
|
576
|
+
if (detail.statusData.completed_at) html += 'Completed: ' + detail.statusData.completed_at + '\n';
|
|
577
|
+
html += '</div>';
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (detail.inboxContents && detail.inboxContents.length > 0) {
|
|
581
|
+
html += '<h4>Notes & Findings (' + detail.inboxContents.length + ')</h4>';
|
|
582
|
+
detail.inboxContents.forEach(item => {
|
|
583
|
+
html += '<div class="section"><strong style="color:var(--purple)">' + escHtml(item.name) + '</strong>\n\n' + escHtml(item.content) + '</div>';
|
|
584
|
+
});
|
|
585
|
+
} else {
|
|
586
|
+
html += '<h4>Notes & Findings</h4><div class="section" style="color:var(--muted);font-style:italic">No notes or findings written yet.</div>';
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (detail.outputLog) {
|
|
590
|
+
html += '<h4>Latest Output</h4><div class="section">' + escHtml(detail.outputLog) + '</div>';
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
el.innerHTML = html;
|
|
594
|
+
} else if (tab === 'live') {
|
|
595
|
+
el.innerHTML = '<div class="section" id="live-output" style="max-height:60vh;overflow-y:auto;font-size:11px;line-height:1.6">Loading live output...</div>' +
|
|
596
|
+
'<div style="margin-top:8px;display:flex;gap:8px;align-items:center">' +
|
|
597
|
+
'<span class="pulse"></span><span style="font-size:11px;color:var(--green)">Auto-refreshing every 3s</span>' +
|
|
598
|
+
'<button class="pr-pager-btn" onclick="refreshLiveOutput()" style="font-size:10px">Refresh now</button>' +
|
|
599
|
+
'</div>';
|
|
600
|
+
startLivePolling();
|
|
601
|
+
} else if (tab === 'charter') {
|
|
602
|
+
el.innerHTML = '<div class="section">' + escHtml(detail.charter || 'No charter found.') + '</div>';
|
|
603
|
+
} else if (tab === 'history') {
|
|
604
|
+
el.innerHTML = '<div class="section">' + escHtml(detail.history || 'No history yet.') + '</div>';
|
|
605
|
+
} else if (tab === 'output') {
|
|
606
|
+
el.innerHTML = '<div class="section">' + escHtml(detail.outputLog || 'No output log. The coordinator will save agent output here when tasks complete.') + '</div>';
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
let livePollingInterval = null;
|
|
611
|
+
|
|
612
|
+
function startLivePolling() {
|
|
613
|
+
stopLivePolling();
|
|
614
|
+
refreshLiveOutput();
|
|
615
|
+
livePollingInterval = setInterval(refreshLiveOutput, 3000);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function stopLivePolling() {
|
|
619
|
+
if (livePollingInterval) { clearInterval(livePollingInterval); livePollingInterval = null; }
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async function refreshLiveOutput() {
|
|
623
|
+
if (!currentAgentId || currentTab !== 'live') { stopLivePolling(); return; }
|
|
624
|
+
try {
|
|
625
|
+
const text = await fetch('/api/agent/' + currentAgentId + '/live?tail=16384').then(r => r.text());
|
|
626
|
+
const el = document.getElementById('live-output');
|
|
627
|
+
if (el) {
|
|
628
|
+
const wasAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 50;
|
|
629
|
+
el.textContent = text;
|
|
630
|
+
if (wasAtBottom) el.scrollTop = el.scrollHeight;
|
|
631
|
+
}
|
|
632
|
+
} catch {}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// -- Main Renderers --
|
|
636
|
+
function renderAgents(agents) {
|
|
637
|
+
agentData = agents;
|
|
638
|
+
const grid = document.getElementById('agents-grid');
|
|
639
|
+
grid.innerHTML = agents.map(a => `
|
|
640
|
+
<div class="agent-card ${statusColor(a.status)}" onclick="openAgentDetail('${a.id}')">
|
|
641
|
+
<div class="agent-card-header">
|
|
642
|
+
<span class="agent-name"><span class="agent-emoji">${a.emoji}</span>${a.name}</span>
|
|
643
|
+
<span class="status-badge ${a.status}">${a.status}</span>
|
|
644
|
+
</div>
|
|
645
|
+
<div class="agent-role">${a.role}</div>
|
|
646
|
+
<div class="agent-action" title="${escHtml(a.lastAction)}">${escHtml(a.lastAction)}</div>
|
|
647
|
+
</div>
|
|
648
|
+
`).join('');
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function renderPrdProgress(prog) {
|
|
652
|
+
const el = document.getElementById('prd-progress-content');
|
|
653
|
+
const countEl = document.getElementById('prd-progress-count');
|
|
654
|
+
if (!prog) { el.innerHTML = ''; countEl.textContent = '—'; return; }
|
|
655
|
+
|
|
656
|
+
countEl.textContent = prog.donePercent + '%';
|
|
657
|
+
|
|
658
|
+
const pct = (n) => prog.total > 0 ? ((n / prog.total) * 100).toFixed(1) : 0;
|
|
659
|
+
|
|
660
|
+
const bar = '<div class="prd-progress-bar">' +
|
|
661
|
+
'<div class="seg complete" style="width:' + pct(prog.complete) + '%"></div>' +
|
|
662
|
+
'<div class="seg pr-created" style="width:' + pct(prog.prCreated) + '%"></div>' +
|
|
663
|
+
'<div class="seg in-progress" style="width:' + pct(prog.inProgress) + '%"></div>' +
|
|
664
|
+
'<div class="seg planned" style="width:' + pct(prog.planned) + '%"></div>' +
|
|
665
|
+
'<div class="seg missing" style="width:' + pct(prog.missing) + '%"></div>' +
|
|
666
|
+
'</div>';
|
|
667
|
+
|
|
668
|
+
const legend = '<div class="prd-progress-legend">' +
|
|
669
|
+
'<div class="prd-legend-item"><span class="prd-legend-dot complete"></span>Complete (' + prog.complete + ')</div>' +
|
|
670
|
+
'<div class="prd-legend-item"><span class="prd-legend-dot pr-created"></span>PR Created (' + prog.prCreated + ')</div>' +
|
|
671
|
+
'<div class="prd-legend-item"><span class="prd-legend-dot in-progress"></span>In Progress (' + prog.inProgress + ')</div>' +
|
|
672
|
+
'<div class="prd-legend-item"><span class="prd-legend-dot planned"></span>Planned (' + prog.planned + ')</div>' +
|
|
673
|
+
'<div class="prd-legend-item"><span class="prd-legend-dot missing"></span>Missing (' + prog.missing + ')</div>' +
|
|
674
|
+
'</div>';
|
|
675
|
+
|
|
676
|
+
const statusIcon = (s) => s === 'complete' ? 'OK' : s === 'pr-created' ? 'PR' : s === 'in-progress' ? 'WIP' : s === 'planned' ? 'PLAN' : '---';
|
|
677
|
+
|
|
678
|
+
const items = '<div class="prd-items-list">' +
|
|
679
|
+
(prog.items || []).map(i => {
|
|
680
|
+
const prLinks = (i.prs || []).map(pr =>
|
|
681
|
+
'<a class="pr-title" href="' + escHtml(pr.url || '#') + '" target="_blank" style="font-size:10px;margin-left:4px" title="' + escHtml(pr.title || '') + '">' + escHtml(pr.id) + '</a>'
|
|
682
|
+
).join(' ');
|
|
683
|
+
return '<div class="prd-item-row">' +
|
|
684
|
+
'<span>' + statusIcon(i.status) + '</span>' +
|
|
685
|
+
'<span class="prd-item-id">' + escHtml(i.id) + '</span>' +
|
|
686
|
+
'<span class="prd-item-name" title="' + escHtml(i.name) + '">' + escHtml(i.name) + '</span>' +
|
|
687
|
+
(prLinks ? '<span>' + prLinks + '</span>' : '') +
|
|
688
|
+
'<span class="prd-item-priority ' + (i.priority || '') + '">' + escHtml(i.priority || '') + '</span>' +
|
|
689
|
+
'</div>';
|
|
690
|
+
}).join('') +
|
|
691
|
+
'</div>';
|
|
692
|
+
|
|
693
|
+
el.innerHTML = bar + legend + items;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function renderInbox(inbox) {
|
|
697
|
+
inboxData = inbox;
|
|
698
|
+
const list = document.getElementById('inbox-list');
|
|
699
|
+
const count = document.getElementById('inbox-count');
|
|
700
|
+
count.textContent = inbox.length;
|
|
701
|
+
if (!inbox.length) { list.innerHTML = '<p class="empty">No messages yet.</p>'; return; }
|
|
702
|
+
list.innerHTML = inbox.map((item, i) => `
|
|
703
|
+
<div class="inbox-item">
|
|
704
|
+
<div class="inbox-name" onclick="openModal(${i})" style="cursor:pointer">
|
|
705
|
+
<span>${escHtml(item.name)}</span><span>${item.age}</span>
|
|
706
|
+
</div>
|
|
707
|
+
<div class="inbox-preview" onclick="openModal(${i})" style="cursor:pointer">${escHtml(item.content.slice(0,200))}</div>
|
|
708
|
+
<div style="display:flex;gap:6px;margin-top:6px">
|
|
709
|
+
<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="event.stopPropagation();persistInboxItem('${escHtml(item.name)}')">Persist as Note</button>
|
|
710
|
+
<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="event.stopPropagation();openInboxInExplorer('${escHtml(item.name)}')">Open in Explorer</button>
|
|
711
|
+
</div>
|
|
712
|
+
</div>
|
|
713
|
+
`).join('');
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function persistInboxItem(name) {
|
|
717
|
+
try {
|
|
718
|
+
const res = await fetch('/api/inbox/persist', {
|
|
719
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
720
|
+
body: JSON.stringify({ name })
|
|
721
|
+
});
|
|
722
|
+
const data = await res.json();
|
|
723
|
+
if (res.ok) {
|
|
724
|
+
refresh();
|
|
725
|
+
} else {
|
|
726
|
+
alert('Failed: ' + (data.error || 'unknown'));
|
|
727
|
+
}
|
|
728
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async function openInboxInExplorer(name) {
|
|
732
|
+
try {
|
|
733
|
+
await fetch('/api/inbox/open', {
|
|
734
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
735
|
+
body: JSON.stringify({ name })
|
|
736
|
+
});
|
|
737
|
+
} catch {}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function renderNotes(notes) {
|
|
741
|
+
const el = document.getElementById('notes-list');
|
|
742
|
+
if (!notes.length) { el.innerHTML = '<p class="empty">No team notes yet.</p>'; return; }
|
|
743
|
+
el.innerHTML = '<div style="display:flex;flex-direction:column;gap:6px">' +
|
|
744
|
+
notes.map(d => '<div style="font-size:12px;color:var(--text);padding:6px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;cursor:pointer" onclick="openNotesFile()">' + escHtml(d) + '</div>').join('') +
|
|
745
|
+
'</div>';
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async function openNotesFile() {
|
|
749
|
+
try {
|
|
750
|
+
const content = await fetch('/api/notes-full').then(r => r.text());
|
|
751
|
+
document.getElementById('modal-title').textContent = 'Team Notes (notes.md)';
|
|
752
|
+
document.getElementById('modal-body').textContent = content;
|
|
753
|
+
document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
|
|
754
|
+
document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
|
|
755
|
+
document.getElementById('modal').classList.add('open');
|
|
756
|
+
} catch {}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function renderPrd(prd) {
|
|
760
|
+
const section = document.getElementById('prd-content');
|
|
761
|
+
const badge = document.getElementById('prd-badge');
|
|
762
|
+
if (!prd) {
|
|
763
|
+
section.innerHTML = '<p class="prd-pending" style="margin-bottom:0">No PRD found.</p>';
|
|
764
|
+
badge.innerHTML = '';
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (prd.error) {
|
|
768
|
+
section.innerHTML = '<p style="color:var(--orange);margin-bottom:0">PRD file found but has parse errors.</p>';
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
badge.innerHTML = '<span style="color:var(--green);font-size:11px">' + prd.age + '</span>';
|
|
772
|
+
section.innerHTML =
|
|
773
|
+
'<div style="display:flex;gap:16px;align-items:center;flex-wrap:wrap">' +
|
|
774
|
+
'<div class="prd-stats" style="margin:0">' +
|
|
775
|
+
'<div class="prd-stat"><div class="prd-stat-num green">' + prd.existing + '</div><div class="prd-stat-label">Existing</div></div>' +
|
|
776
|
+
'<div class="prd-stat"><div class="prd-stat-num red">' + prd.missing + '</div><div class="prd-stat-label">Gaps</div></div>' +
|
|
777
|
+
'<div class="prd-stat"><div class="prd-stat-num orange">' + prd.questions + '</div><div class="prd-stat-label">Questions</div></div>' +
|
|
778
|
+
'</div>' +
|
|
779
|
+
(prd.summary ? '<div class="prd-summary" style="flex:1;min-width:200px">' + escHtml(prd.summary) + '</div>' : '') +
|
|
780
|
+
'</div>';
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
let allPrs = [];
|
|
784
|
+
let prPage = 0;
|
|
785
|
+
const PR_PER_PAGE = 3;
|
|
786
|
+
|
|
787
|
+
function prRow(pr) {
|
|
788
|
+
// Squad review (agent) state — separate from ADO human review
|
|
789
|
+
const sq = pr.squadReview || {};
|
|
790
|
+
const reviewSource = sq.status || pr.reviewStatus || 'pending';
|
|
791
|
+
const reviewClass = reviewSource === 'approved' ? 'approved' : (reviewSource === 'changes-requested' || reviewSource === 'rejected') ? 'rejected' : reviewSource === 'waiting' ? 'active' : 'draft';
|
|
792
|
+
const reviewLabel = sq.status ? sq.status + ' (squad)' : (pr.reviewStatus || 'pending');
|
|
793
|
+
const buildClass = pr.buildStatus === 'passing' ? 'build-pass' : pr.buildStatus === 'failing' ? 'build-fail' : pr.buildStatus === 'running' ? 'building' : 'no-build';
|
|
794
|
+
const buildLabel = pr.buildStatus || 'none';
|
|
795
|
+
const statusClass = pr.status === 'merged' ? 'merged' : pr.status === 'abandoned' ? 'rejected' : pr.status === 'active' ? 'active' : 'draft';
|
|
796
|
+
const statusLabel = pr.status || 'active';
|
|
797
|
+
const url = pr.url || '#';
|
|
798
|
+
const prId = pr.id || '—';
|
|
799
|
+
return '<tr>' +
|
|
800
|
+
'<td><span class="pr-id">' + escHtml(String(prId)) + '</span></td>' +
|
|
801
|
+
'<td><a class="pr-title" href="' + escHtml(url) + '" target="_blank">' + escHtml(pr.title || 'Untitled') + '</a></td>' +
|
|
802
|
+
'<td><span class="pr-agent">' + escHtml(pr.agent || '—') + '</span></td>' +
|
|
803
|
+
'<td><span class="pr-branch">' + escHtml(pr.branch || '—') + '</span></td>' +
|
|
804
|
+
'<td><span class="pr-badge ' + reviewClass + '">' + escHtml(reviewLabel) + '</span></td>' +
|
|
805
|
+
'<td>' + (sq.reviewer ? '<span class="pr-agent" title="' + escHtml(sq.note || '') + '">' + escHtml(sq.reviewer) + '</span>' : '<span style="color:var(--muted);font-size:11px">—</span>') + '</td>' +
|
|
806
|
+
'<td><span class="pr-badge ' + buildClass + '">' + escHtml(buildLabel) + '</span></td>' +
|
|
807
|
+
'<td><span class="pr-badge ' + statusClass + '">' + escHtml(statusLabel) + '</span></td>' +
|
|
808
|
+
'<td><span class="pr-date">' + escHtml(pr.created || '—') + '</span></td>' +
|
|
809
|
+
'</tr>';
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function prTableHtml(rows) {
|
|
813
|
+
return '<div class="pr-table-wrap"><table class="pr-table"><thead><tr>' +
|
|
814
|
+
'<th>PR</th><th>Title</th><th>Agent</th><th>Branch</th><th>Review</th><th>Signed Off By</th><th>Build</th><th>Status</th><th>Created</th><th></th>' +
|
|
815
|
+
'</tr></thead><tbody>' + rows + '</tbody></table></div>';
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function renderPrs(prs) {
|
|
819
|
+
allPrs = prs;
|
|
820
|
+
const el = document.getElementById('pr-content');
|
|
821
|
+
const count = document.getElementById('pr-count');
|
|
822
|
+
count.textContent = prs.length;
|
|
823
|
+
if (!prs.length) {
|
|
824
|
+
el.innerHTML = '<p class="pr-empty">No pull requests yet. PRs created by agents will appear here with review, build, and merge status.</p>';
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const totalPages = Math.ceil(prs.length / PR_PER_PAGE);
|
|
828
|
+
if (prPage >= totalPages) prPage = totalPages - 1;
|
|
829
|
+
const start = prPage * PR_PER_PAGE;
|
|
830
|
+
const pagePrs = prs.slice(start, start + PR_PER_PAGE);
|
|
831
|
+
const rows = pagePrs.map(prRow).join('');
|
|
832
|
+
|
|
833
|
+
let pager = '';
|
|
834
|
+
if (prs.length > PR_PER_PAGE) {
|
|
835
|
+
pager = '<div class="pr-pager">' +
|
|
836
|
+
'<span class="pr-page-info">Showing ' + (start+1) + ' to ' + Math.min(start+PR_PER_PAGE, prs.length) + ' of ' + prs.length + '</span>' +
|
|
837
|
+
'<div class="pr-pager-btns">' +
|
|
838
|
+
'<button class="pr-pager-btn ' + (prPage === 0 ? 'disabled' : '') + '" onclick="prPrev()">Prev</button>' +
|
|
839
|
+
'<button class="pr-pager-btn ' + (prPage >= totalPages-1 ? 'disabled' : '') + '" onclick="prNext()">Next</button>' +
|
|
840
|
+
'<button class="pr-pager-btn see-all" onclick="openAllPrs()">See all ' + prs.length + ' PRs</button>' +
|
|
841
|
+
'</div>' +
|
|
842
|
+
'</div>';
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
el.innerHTML = prTableHtml(rows) + pager;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function prPrev() { if (prPage > 0) { prPage--; renderPrs(allPrs); } }
|
|
849
|
+
function prNext() { const totalPages = Math.ceil(allPrs.length / PR_PER_PAGE); if (prPage < totalPages-1) { prPage++; renderPrs(allPrs); } }
|
|
850
|
+
|
|
851
|
+
function openAllPrs() {
|
|
852
|
+
document.getElementById('modal-title').textContent = 'All Pull Requests (' + allPrs.length + ')';
|
|
853
|
+
document.getElementById('modal-body').innerHTML = prTableHtml(allPrs.map(prRow).join(''));
|
|
854
|
+
document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
855
|
+
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
856
|
+
document.getElementById('modal').classList.add('open');
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function openModal(i) {
|
|
860
|
+
const item = inboxData[i];
|
|
861
|
+
if (!item) return;
|
|
862
|
+
document.getElementById('modal-title').textContent = item.name;
|
|
863
|
+
document.getElementById('modal-body').textContent = item.content;
|
|
864
|
+
document.getElementById('modal').classList.add('open');
|
|
865
|
+
}
|
|
866
|
+
function closeModal() { document.getElementById('modal').classList.remove('open'); }
|
|
867
|
+
|
|
868
|
+
document.addEventListener('keydown', e => {
|
|
869
|
+
if (e.key === 'Escape') { closeDetail(); closeModal(); }
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
let archivedPrds = [];
|
|
873
|
+
|
|
874
|
+
function renderArchiveButtons(archives) {
|
|
875
|
+
archivedPrds = archives;
|
|
876
|
+
const el = document.getElementById('archive-btns');
|
|
877
|
+
if (!archives.length) { el.innerHTML = ''; return; }
|
|
878
|
+
el.innerHTML = archives.map((a, i) =>
|
|
879
|
+
'<button class="archive-btn" onclick="openArchive(' + i + ')">Archived: ' + escHtml(a.version) + ' (' + a.total + ' items)</button>'
|
|
880
|
+
).join(' ');
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function openArchive(i) {
|
|
884
|
+
const a = archivedPrds[i];
|
|
885
|
+
if (!a) return;
|
|
886
|
+
|
|
887
|
+
document.getElementById('modal-title').textContent = 'Archived PRD — ' + a.version + ' (' + a.total + ' items)';
|
|
888
|
+
|
|
889
|
+
let html = '<div style="font-family:\'Segoe UI\',system-ui,sans-serif;white-space:normal">';
|
|
890
|
+
|
|
891
|
+
// Summary
|
|
892
|
+
if (a.summary) {
|
|
893
|
+
html += '<div class="archive-detail-section"><h4>Summary</h4><p style="font-size:12px;color:var(--muted);line-height:1.6">' + escHtml(a.summary) + '</p></div>';
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Existing features
|
|
897
|
+
if (a.existing_features.length) {
|
|
898
|
+
html += '<div class="archive-detail-section"><h4>Existing Features (' + a.existing_features.length + ')</h4>';
|
|
899
|
+
a.existing_features.forEach(f => {
|
|
900
|
+
html += '<div class="archive-feature">' +
|
|
901
|
+
'<span class="feat-id">' + escHtml(f.id) + '</span>' +
|
|
902
|
+
'<div class="feat-name">' + escHtml(f.name) + '</div>' +
|
|
903
|
+
'<div class="feat-desc">' + escHtml(f.description || '') + '</div>' +
|
|
904
|
+
'<div class="feat-meta">' +
|
|
905
|
+
(f.agent ? '<span>Agent: ' + escHtml(f.agent) + '</span>' : '') +
|
|
906
|
+
(f.status ? '<span>Status: ' + escHtml(f.status) + '</span>' : '') +
|
|
907
|
+
'</div>' +
|
|
908
|
+
'</div>';
|
|
909
|
+
});
|
|
910
|
+
html += '</div>';
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Missing features
|
|
914
|
+
if (a.missing_features.length) {
|
|
915
|
+
html += '<div class="archive-detail-section"><h4>Gap Items (' + a.missing_features.length + ')</h4>';
|
|
916
|
+
a.missing_features.forEach(f => {
|
|
917
|
+
const pClass = f.priority === 'high' ? 'high' : f.priority === 'medium' ? 'medium' : 'low';
|
|
918
|
+
html += '<div class="archive-feature">' +
|
|
919
|
+
'<span class="feat-id">' + escHtml(f.id) + '</span> ' +
|
|
920
|
+
'<span class="prd-item-priority ' + pClass + '">' + escHtml(f.priority || '') + '</span>' +
|
|
921
|
+
(f.status ? ' <span class="pr-badge ' + (f.status === 'complete' ? 'approved' : f.status === 'pr-created' ? 'active' : 'draft') + '" style="font-size:9px">' + escHtml(f.status) + '</span>' : '') +
|
|
922
|
+
'<div class="feat-name">' + escHtml(f.name) + '</div>' +
|
|
923
|
+
'<div class="feat-desc">' + escHtml(f.description || '') + '</div>' +
|
|
924
|
+
(f.rationale ? '<div class="feat-desc" style="margin-top:4px;color:var(--yellow)">Rationale: ' + escHtml(f.rationale) + '</div>' : '') +
|
|
925
|
+
'<div class="feat-meta">' +
|
|
926
|
+
(f.estimated_complexity ? '<span>Complexity: ' + escHtml(f.estimated_complexity) + '</span>' : '') +
|
|
927
|
+
(f.affected_areas ? '<span>Areas: ' + escHtml(f.affected_areas.join(', ')) + '</span>' : '') +
|
|
928
|
+
'</div>' +
|
|
929
|
+
'</div>';
|
|
930
|
+
});
|
|
931
|
+
html += '</div>';
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Open questions
|
|
935
|
+
if (a.open_questions.length) {
|
|
936
|
+
html += '<div class="archive-detail-section"><h4>Open Questions (' + a.open_questions.length + ')</h4>';
|
|
937
|
+
a.open_questions.forEach(q => {
|
|
938
|
+
html += '<div class="archive-question">' +
|
|
939
|
+
'<span class="q-id">' + escHtml(q.id) + '</span>' +
|
|
940
|
+
'<div class="q-text">' + escHtml(q.question) + '</div>' +
|
|
941
|
+
(q.context ? '<div class="q-context">' + escHtml(q.context) + '</div>' : '') +
|
|
942
|
+
'</div>';
|
|
943
|
+
});
|
|
944
|
+
html += '</div>';
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
html += '</div>';
|
|
948
|
+
|
|
949
|
+
document.getElementById('modal-body').innerHTML = html;
|
|
950
|
+
document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
951
|
+
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
952
|
+
document.getElementById('modal').classList.add('open');
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function renderEngineStatus(engine) {
|
|
956
|
+
const badge = document.getElementById('engine-badge');
|
|
957
|
+
const state = engine?.state || 'stopped';
|
|
958
|
+
badge.className = 'engine-badge ' + state;
|
|
959
|
+
badge.textContent = state.toUpperCase();
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function renderDispatch(dispatch) {
|
|
963
|
+
if (!dispatch) return;
|
|
964
|
+
|
|
965
|
+
// Stats
|
|
966
|
+
const stats = document.getElementById('dispatch-stats');
|
|
967
|
+
stats.innerHTML =
|
|
968
|
+
'<div class="dispatch-stat"><div class="dispatch-stat-num yellow">' + (dispatch.active || []).length + '</div><div class="dispatch-stat-label">Active</div></div>' +
|
|
969
|
+
'<div class="dispatch-stat"><div class="dispatch-stat-num blue">' + (dispatch.pending || []).length + '</div><div class="dispatch-stat-label">Pending</div></div>' +
|
|
970
|
+
'<div class="dispatch-stat"><div class="dispatch-stat-num green">' + (dispatch.completed || []).length + '</div><div class="dispatch-stat-label">Completed</div></div>';
|
|
971
|
+
|
|
972
|
+
// Active
|
|
973
|
+
const activeEl = document.getElementById('dispatch-active');
|
|
974
|
+
if ((dispatch.active || []).length > 0) {
|
|
975
|
+
activeEl.innerHTML = '<div style="font-size:11px;color:var(--green);margin-bottom:6px;font-weight:600">ACTIVE</div><div class="dispatch-list">' +
|
|
976
|
+
dispatch.active.map(d =>
|
|
977
|
+
'<div class="dispatch-item">' +
|
|
978
|
+
'<span class="dispatch-type ' + (d.type || '') + '">' + escHtml(d.type || '') + '</span>' +
|
|
979
|
+
'<span class="dispatch-agent">' + escHtml(d.agentName || d.agent || '') + '</span>' +
|
|
980
|
+
'<span class="dispatch-task" title="' + escHtml(d.task || '') + '">' + escHtml(d.task || '') + '</span>' +
|
|
981
|
+
'<span class="dispatch-time">' + shortTime(d.started_at) + '</span>' +
|
|
982
|
+
'</div>'
|
|
983
|
+
).join('') + '</div>';
|
|
984
|
+
} else {
|
|
985
|
+
activeEl.innerHTML = '<div style="color:var(--muted);font-size:11px;margin-bottom:8px">No active dispatches</div>';
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Pending
|
|
989
|
+
const pendingEl = document.getElementById('dispatch-pending');
|
|
990
|
+
if ((dispatch.pending || []).length > 0) {
|
|
991
|
+
pendingEl.innerHTML = '<div style="font-size:11px;color:var(--yellow);margin:8px 0 6px;font-weight:600">PENDING</div><div class="dispatch-list">' +
|
|
992
|
+
dispatch.pending.map(d =>
|
|
993
|
+
'<div class="dispatch-item">' +
|
|
994
|
+
'<span class="dispatch-type ' + (d.type || '') + '">' + escHtml(d.type || '') + '</span>' +
|
|
995
|
+
'<span class="dispatch-agent">' + escHtml(d.agentName || d.agent || '') + '</span>' +
|
|
996
|
+
'<span class="dispatch-task" title="' + escHtml(d.task || '') + '">' + escHtml(d.task || '') + '</span>' +
|
|
997
|
+
'</div>'
|
|
998
|
+
).join('') + '</div>';
|
|
999
|
+
} else {
|
|
1000
|
+
pendingEl.innerHTML = '';
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Completed
|
|
1004
|
+
const completedEl = document.getElementById('completed-content');
|
|
1005
|
+
const completedCount = document.getElementById('completed-count');
|
|
1006
|
+
const completed = (dispatch.completed || []).slice().reverse();
|
|
1007
|
+
completedCount.textContent = completed.length;
|
|
1008
|
+
|
|
1009
|
+
if (completed.length > 0) {
|
|
1010
|
+
completedEl.innerHTML = '<table class="pr-table"><thead><tr><th>ID</th><th>Type</th><th>Agent</th><th>Task</th><th>Result</th><th>Completed</th></tr></thead><tbody>' +
|
|
1011
|
+
completed.map(d => {
|
|
1012
|
+
const isError = d.result === 'error';
|
|
1013
|
+
const agentId = (d.agent || '').toLowerCase();
|
|
1014
|
+
const errorBtn = isError
|
|
1015
|
+
? ' <button class="error-details-btn" data-agent="' + escHtml(agentId) + '" data-reason="' + escHtml(d.reason || 'No reason recorded') + '" data-task="' + escHtml((d.task || '').slice(0, 100)) + '" onclick="showErrorDetails(this.dataset.agent, this.dataset.reason, this.dataset.task)" title="View error details">details</button>'
|
|
1016
|
+
: '';
|
|
1017
|
+
return '<tr>' +
|
|
1018
|
+
'<td style="font-family:Consolas;font-size:10px">' + escHtml((d.id || '').slice(0, 20)) + '</td>' +
|
|
1019
|
+
'<td><span class="dispatch-type ' + (d.type || '') + '">' + escHtml(d.type || '') + '</span></td>' +
|
|
1020
|
+
'<td>' + escHtml(d.agentName || d.agent || '') + '</td>' +
|
|
1021
|
+
'<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml((d.task || '').slice(0, 60)) + '</td>' +
|
|
1022
|
+
'<td style="color:' + (d.result === 'success' ? 'var(--green)' : 'var(--red)') + '">' + escHtml(d.result || '') + errorBtn + '</td>' +
|
|
1023
|
+
'<td class="pr-date">' + shortTime(d.completed_at) + '</td>' +
|
|
1024
|
+
'</tr>';
|
|
1025
|
+
}).join('') + '</tbody></table>';
|
|
1026
|
+
} else {
|
|
1027
|
+
completedEl.innerHTML = '<p class="empty">No completed dispatches yet.</p>';
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
async function showErrorDetails(agentId, reason, task) {
|
|
1032
|
+
document.getElementById('modal-title').textContent = 'Error: ' + task;
|
|
1033
|
+
document.getElementById('modal-body').textContent = 'Reason: ' + reason + '\n\nLoading agent output...';
|
|
1034
|
+
document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
|
|
1035
|
+
document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
|
|
1036
|
+
document.getElementById('modal').classList.add('open');
|
|
1037
|
+
|
|
1038
|
+
try {
|
|
1039
|
+
const output = await fetch('/api/agent/' + agentId + '/output').then(r => r.text());
|
|
1040
|
+
const lines = output.split('\n');
|
|
1041
|
+
const stderrIdx = lines.findIndex(l => l.startsWith('## stderr'));
|
|
1042
|
+
let summary = '';
|
|
1043
|
+
if (stderrIdx >= 0) {
|
|
1044
|
+
const stderr = lines.slice(stderrIdx + 1).join('\n').trim();
|
|
1045
|
+
if (stderr) summary = 'STDERR:\n' + stderr.slice(-2000);
|
|
1046
|
+
}
|
|
1047
|
+
if (!summary) summary = output.slice(-3000);
|
|
1048
|
+
document.getElementById('modal-body').textContent = 'Reason: ' + reason + '\n\n---\n\n' + summary;
|
|
1049
|
+
} catch {
|
|
1050
|
+
document.getElementById('modal-body').textContent = 'Reason: ' + reason + '\n\n(Could not load agent output)';
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function copyModalContent() {
|
|
1055
|
+
const body = document.getElementById('modal-body');
|
|
1056
|
+
const btn = document.getElementById('modal-copy-btn');
|
|
1057
|
+
navigator.clipboard.writeText(body.textContent).then(() => {
|
|
1058
|
+
btn.classList.add('copied');
|
|
1059
|
+
btn.innerHTML = '<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/></svg> Copied';
|
|
1060
|
+
setTimeout(() => {
|
|
1061
|
+
btn.classList.remove('copied');
|
|
1062
|
+
btn.innerHTML = '<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25z"/></svg> Copy';
|
|
1063
|
+
}, 2000);
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function renderEngineLog(log) {
|
|
1068
|
+
const el = document.getElementById('engine-log');
|
|
1069
|
+
if (!log || log.length === 0) {
|
|
1070
|
+
el.innerHTML = '<div class="empty">No log entries yet.</div>';
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
el.innerHTML = log.slice().reverse().map(e =>
|
|
1074
|
+
'<div class="log-entry">' +
|
|
1075
|
+
'<span class="log-ts">' + shortTime(e.timestamp) + '</span> ' +
|
|
1076
|
+
'<span class="log-level-' + (e.level || 'info') + '">[' + (e.level || 'info') + ']</span> ' +
|
|
1077
|
+
escHtml(e.message || '') +
|
|
1078
|
+
'</div>'
|
|
1079
|
+
).join('');
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function shortTime(t) {
|
|
1083
|
+
if (!t) return '';
|
|
1084
|
+
try { return new Date(t).toLocaleTimeString(); } catch { return t; }
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async function refresh() {
|
|
1088
|
+
try {
|
|
1089
|
+
const data = await fetch('/api/status').then(r => r.json());
|
|
1090
|
+
document.getElementById('ts').textContent = new Date(data.timestamp).toLocaleTimeString();
|
|
1091
|
+
renderAgents(data.agents);
|
|
1092
|
+
renderPrdProgress(data.prdProgress);
|
|
1093
|
+
renderInbox(data.inbox);
|
|
1094
|
+
cmdUpdateAgentList(data.agents);
|
|
1095
|
+
cmdUpdateProjectList(data.projects || []);
|
|
1096
|
+
renderNotes(data.notes);
|
|
1097
|
+
renderPrd(data.prd);
|
|
1098
|
+
renderPrs(data.pullRequests || []);
|
|
1099
|
+
renderArchiveButtons(data.archivedPrds || []);
|
|
1100
|
+
renderEngineStatus(data.engine);
|
|
1101
|
+
renderDispatch(data.dispatch);
|
|
1102
|
+
renderEngineLog(data.engineLog || []);
|
|
1103
|
+
renderProjects(data.projects || []);
|
|
1104
|
+
renderMetrics(data.metrics || {});
|
|
1105
|
+
renderWorkItems(data.workItems || []);
|
|
1106
|
+
renderSkills(data.skills || []);
|
|
1107
|
+
} catch(e) { console.error('refresh error', e); }
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
refresh();
|
|
1111
|
+
setInterval(refresh, 4000);
|
|
1112
|
+
|
|
1113
|
+
// -- Projects --
|
|
1114
|
+
function renderProjects(projects) {
|
|
1115
|
+
const header = document.getElementById('header-projects');
|
|
1116
|
+
const list = document.getElementById('projects-list');
|
|
1117
|
+
if (!projects.length) {
|
|
1118
|
+
header.textContent = 'No projects';
|
|
1119
|
+
list.innerHTML = '<span style="color:var(--muted);font-style:italic">No projects linked. Run: node squad.js add <dir></span>';
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
header.textContent = projects.map(p => p.name).join(' + ');
|
|
1123
|
+
list.innerHTML = projects.map(p =>
|
|
1124
|
+
'<span title="' + escHtml(p.description || p.path || '') + '" style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 10px;color:var(--blue);font-weight:500;cursor:help">' +
|
|
1125
|
+
escHtml(p.name) +
|
|
1126
|
+
(p.description ? '<span style="color:var(--muted);font-weight:400;margin-left:6px;font-size:10px">' + escHtml(p.description.slice(0, 60)) + (p.description.length > 60 ? '...' : '') + '</span>' : '') +
|
|
1127
|
+
'</span>'
|
|
1128
|
+
).join('');
|
|
1129
|
+
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// -- Squad Skills --
|
|
1133
|
+
function renderSkills(skills) {
|
|
1134
|
+
const el = document.getElementById('skills-list');
|
|
1135
|
+
const countEl = document.getElementById('skills-count');
|
|
1136
|
+
countEl.textContent = skills.length;
|
|
1137
|
+
if (!skills.length) {
|
|
1138
|
+
el.innerHTML = '<p class="empty">No skills yet. Agents create these when they discover repeatable workflows.</p>';
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
el.innerHTML = '<div style="display:flex;flex-direction:column;gap:6px">' +
|
|
1142
|
+
skills.map(r =>
|
|
1143
|
+
'<div class="inbox-item" onclick="openSkill(\'' + escHtml(r.file) + '\',\'' + escHtml(r.source || 'skills') + '\')" style="border-left-color:var(--green)">' +
|
|
1144
|
+
'<div class="inbox-name"><span style="color:var(--green);font-weight:600">' + escHtml(r.name) + '</span>' +
|
|
1145
|
+
'<span style="font-size:10px;color:var(--muted)">' + (r.scope === 'project' ? '📁 ' : '🔧 ') + escHtml(r.project || 'any') + ' · ' + escHtml(r.author || '') + ' · ' + escHtml(r.created || '') + '</span>' +
|
|
1146
|
+
'</div>' +
|
|
1147
|
+
(r.description ? '<div class="inbox-preview" style="color:var(--text)">' + escHtml(r.description) + '</div>' : '') +
|
|
1148
|
+
(r.trigger ? '<div class="inbox-preview" style="color:var(--muted);font-size:11px"><strong>When:</strong> ' + escHtml(r.trigger) + '</div>' : '') +
|
|
1149
|
+
'</div>'
|
|
1150
|
+
).join('') +
|
|
1151
|
+
'</div>';
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function openSkill(file, source) {
|
|
1155
|
+
fetch('/api/skill?file=' + encodeURIComponent(file) + '&source=' + encodeURIComponent(source || 'skills'))
|
|
1156
|
+
.then(r => r.text())
|
|
1157
|
+
.then(content => {
|
|
1158
|
+
document.getElementById('modal-title').textContent = file;
|
|
1159
|
+
document.getElementById('modal-body').textContent = content;
|
|
1160
|
+
document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
|
|
1161
|
+
document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
|
|
1162
|
+
document.getElementById('modal').classList.add('open');
|
|
1163
|
+
})
|
|
1164
|
+
.catch(() => {});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// -- Work Items --
|
|
1168
|
+
let allWorkItems = [];
|
|
1169
|
+
let wiPage = 0;
|
|
1170
|
+
const WI_PER_PAGE = 5;
|
|
1171
|
+
|
|
1172
|
+
function wiRow(item) {
|
|
1173
|
+
const statusBadge = (s) => {
|
|
1174
|
+
const cls = s === 'failed' ? 'rejected' : s === 'dispatched' ? 'building' : s === 'pending' || s === 'queued' ? 'active' : s === 'done' ? 'approved' : 'draft';
|
|
1175
|
+
return '<span class="pr-badge ' + cls + '">' + escHtml(s) + '</span>';
|
|
1176
|
+
};
|
|
1177
|
+
const typeBadge = (t) => '<span class="dispatch-type ' + (t || 'implement') + '">' + escHtml(t || 'implement') + '</span>';
|
|
1178
|
+
const priBadge = (p) => '<span class="prd-item-priority ' + (p || '') + '">' + escHtml(p || 'medium') + '</span>';
|
|
1179
|
+
const prLink = item._pr
|
|
1180
|
+
? '<a class="pr-title" href="' + escHtml(item._prUrl || '#') + '" target="_blank" style="font-size:10px">' + escHtml(item._pr) + '</a>'
|
|
1181
|
+
: '<span style="color:var(--muted)">—</span>';
|
|
1182
|
+
return '<tr>' +
|
|
1183
|
+
'<td><span class="pr-id">' + escHtml(item.id || '') + '</span></td>' +
|
|
1184
|
+
'<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escHtml(item.description || item.title || '') + '">' + escHtml(item.title || '') + '</td>' +
|
|
1185
|
+
'<td><span style="font-size:10px;color:var(--muted)">' + escHtml(item._source || '') + '</span>' +
|
|
1186
|
+
(item.scope === 'fan-out' ? ' <span class="pr-badge ' + (item.status === 'done' || item.status === 'failed' ? 'draft' : 'building') + '" style="font-size:8px">fan-out</span>' : '') + '</td>' +
|
|
1187
|
+
'<td>' + typeBadge(item.type) + '</td>' +
|
|
1188
|
+
'<td>' + priBadge(item.priority) + '</td>' +
|
|
1189
|
+
'<td>' + statusBadge(item.status || 'pending') +
|
|
1190
|
+
(item.status === 'failed' ? ' <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--yellow);border-color:var(--yellow);margin-left:4px" onclick="event.stopPropagation();retryWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')">Retry</button>' : '') +
|
|
1191
|
+
'</td>' +
|
|
1192
|
+
'<td>' +
|
|
1193
|
+
(item.completedAgents && item.completedAgents.length > 0
|
|
1194
|
+
? '<span class="pr-agent">' + escHtml(item.completedAgents.join(', ')) + '</span>'
|
|
1195
|
+
: '<span class="pr-agent">' + escHtml(item.dispatched_to || '—') + '</span>') +
|
|
1196
|
+
(item.failReason ? '<span style="display:block;font-size:9px;color:var(--red)" title="' + escHtml(item.failReason) + '">' + escHtml(item.failReason.slice(0, 30)) + '</span>' : '') +
|
|
1197
|
+
'</td>' +
|
|
1198
|
+
'<td>' + prLink + '</td>' +
|
|
1199
|
+
'<td><span class="pr-date">' + shortTime(item.created) + '</span></td>' +
|
|
1200
|
+
'<td><button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--red);border-color:var(--red)" onclick="event.stopPropagation();deleteWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')" title="Delete work item and kill agent">✕</button></td>' +
|
|
1201
|
+
'</tr>';
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function renderWorkItems(items) {
|
|
1205
|
+
allWorkItems = items;
|
|
1206
|
+
const el = document.getElementById('work-items-content');
|
|
1207
|
+
const countEl = document.getElementById('wi-count');
|
|
1208
|
+
countEl.textContent = items.length;
|
|
1209
|
+
if (!items.length) {
|
|
1210
|
+
el.innerHTML = '<p class="empty">No work items. Add tasks via Command Center above.</p>';
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const totalPages = Math.ceil(items.length / WI_PER_PAGE);
|
|
1215
|
+
if (wiPage >= totalPages) wiPage = totalPages - 1;
|
|
1216
|
+
const start = wiPage * WI_PER_PAGE;
|
|
1217
|
+
const pageItems = items.slice(start, start + WI_PER_PAGE);
|
|
1218
|
+
|
|
1219
|
+
let html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Source</th><th>Type</th><th>Priority</th><th>Status</th><th>Agent</th><th>PR</th><th>Created</th><th></th></tr></thead><tbody>';
|
|
1220
|
+
html += pageItems.map(wiRow).join('');
|
|
1221
|
+
html += '</tbody></table></div>';
|
|
1222
|
+
|
|
1223
|
+
if (items.length > WI_PER_PAGE) {
|
|
1224
|
+
html += '<div class="pr-pager">' +
|
|
1225
|
+
'<span class="pr-page-info">Showing ' + (start+1) + ' to ' + Math.min(start+WI_PER_PAGE, items.length) + ' of ' + items.length + '</span>' +
|
|
1226
|
+
'<div class="pr-pager-btns">' +
|
|
1227
|
+
'<button class="pr-pager-btn ' + (wiPage === 0 ? 'disabled' : '') + '" onclick="wiPrev()">Prev</button>' +
|
|
1228
|
+
'<button class="pr-pager-btn ' + (wiPage >= totalPages-1 ? 'disabled' : '') + '" onclick="wiNext()">Next</button>' +
|
|
1229
|
+
'<button class="pr-pager-btn see-all" onclick="openAllWorkItems()">See all ' + items.length + '</button>' +
|
|
1230
|
+
'</div>' +
|
|
1231
|
+
'</div>';
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
el.innerHTML = html;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
async function retryWorkItem(id, source) {
|
|
1238
|
+
try {
|
|
1239
|
+
const res = await fetch('/api/work-items/retry', {
|
|
1240
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1241
|
+
body: JSON.stringify({ id, source: source || undefined })
|
|
1242
|
+
});
|
|
1243
|
+
if (res.ok) { refresh(); } else {
|
|
1244
|
+
const d = await res.json();
|
|
1245
|
+
alert('Retry failed: ' + (d.error || 'unknown'));
|
|
1246
|
+
}
|
|
1247
|
+
} catch (e) { alert('Retry error: ' + e.message); }
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
async function deleteWorkItem(id, source) {
|
|
1251
|
+
if (!confirm('Delete work item ' + id + '? This will kill any running agent and remove all dispatch history.')) return;
|
|
1252
|
+
try {
|
|
1253
|
+
const res = await fetch('/api/work-items/delete', {
|
|
1254
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1255
|
+
body: JSON.stringify({ id, source: source || undefined })
|
|
1256
|
+
});
|
|
1257
|
+
if (res.ok) { refresh(); } else {
|
|
1258
|
+
const d = await res.json();
|
|
1259
|
+
alert('Delete failed: ' + (d.error || 'unknown'));
|
|
1260
|
+
}
|
|
1261
|
+
} catch (e) { alert('Delete error: ' + e.message); }
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function wiPrev() { if (wiPage > 0) { wiPage--; renderWorkItems(allWorkItems); } }
|
|
1265
|
+
function wiNext() { const tp = Math.ceil(allWorkItems.length / WI_PER_PAGE); if (wiPage < tp-1) { wiPage++; renderWorkItems(allWorkItems); } }
|
|
1266
|
+
|
|
1267
|
+
function openAllWorkItems() {
|
|
1268
|
+
document.getElementById('modal-title').textContent = 'All Work Items (' + allWorkItems.length + ')';
|
|
1269
|
+
const html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>ID</th><th>Title</th><th>Source</th><th>Type</th><th>Priority</th><th>Status</th><th>Agent</th><th>PR</th><th>Created</th><th></th></tr></thead><tbody>' +
|
|
1270
|
+
allWorkItems.map(wiRow).join('') + '</tbody></table></div>';
|
|
1271
|
+
document.getElementById('modal-body').innerHTML = html;
|
|
1272
|
+
document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
1273
|
+
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
1274
|
+
document.getElementById('modal').classList.add('open');
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// -- Metrics --
|
|
1278
|
+
function renderMetrics(metrics) {
|
|
1279
|
+
const el = document.getElementById('metrics-content');
|
|
1280
|
+
const agents = Object.entries(metrics);
|
|
1281
|
+
if (agents.length === 0) {
|
|
1282
|
+
el.innerHTML = '<p class="empty">No metrics yet. Metrics appear after agents complete tasks.</p>';
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
let html = '<table class="pr-table"><thead><tr><th>Agent</th><th>Done</th><th>Errors</th><th>PRs</th><th>Approved</th><th>Rejected</th><th>Rate</th><th>Reviews</th></tr></thead><tbody>';
|
|
1286
|
+
for (const [id, m] of agents) {
|
|
1287
|
+
const rate = m.prsCreated > 0 ? Math.round((m.prsApproved / m.prsCreated) * 100) + '%' : '-';
|
|
1288
|
+
const rateColor = m.prsCreated > 0 ? (m.prsApproved / m.prsCreated >= 0.7 ? 'var(--green)' : 'var(--red)') : 'var(--muted)';
|
|
1289
|
+
html += '<tr>' +
|
|
1290
|
+
'<td style="font-weight:600">' + escHtml(id) + '</td>' +
|
|
1291
|
+
'<td style="color:var(--green)">' + (m.tasksCompleted || 0) + '</td>' +
|
|
1292
|
+
'<td style="color:' + (m.tasksErrored > 0 ? 'var(--red)' : 'var(--muted)') + '">' + (m.tasksErrored || 0) + '</td>' +
|
|
1293
|
+
'<td>' + (m.prsCreated || 0) + '</td>' +
|
|
1294
|
+
'<td style="color:var(--green)">' + (m.prsApproved || 0) + '</td>' +
|
|
1295
|
+
'<td style="color:' + (m.prsRejected > 0 ? 'var(--red)' : 'var(--muted)') + '">' + (m.prsRejected || 0) + '</td>' +
|
|
1296
|
+
'<td style="color:' + rateColor + ';font-weight:600">' + rate + '</td>' +
|
|
1297
|
+
'<td>' + (m.reviewsDone || 0) + '</td>' +
|
|
1298
|
+
'</tr>';
|
|
1299
|
+
}
|
|
1300
|
+
html += '</tbody></table>';
|
|
1301
|
+
el.innerHTML = html;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// -- Command Center (Unified Input) --
|
|
1305
|
+
let cmdAgents = []; // [{id, name, emoji, role}]
|
|
1306
|
+
let cmdProjects = []; // [{name, description}]
|
|
1307
|
+
let cmdMentionIdx = -1; // active mention autocomplete index
|
|
1308
|
+
|
|
1309
|
+
function cmdUpdateAgentList(agents) {
|
|
1310
|
+
cmdAgents = (agents || []).map(a => ({ id: a.id, name: a.name, emoji: a.emoji, role: a.role }));
|
|
1311
|
+
}
|
|
1312
|
+
function cmdUpdateProjectList(projects) {
|
|
1313
|
+
cmdProjects = (projects || []).map(p => ({ name: p.name, description: p.description || '' }));
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function showToast(id, msg, ok) {
|
|
1317
|
+
const el = document.getElementById(id);
|
|
1318
|
+
el.className = 'cmd-toast ' + (ok ? 'success' : 'error');
|
|
1319
|
+
el.textContent = msg;
|
|
1320
|
+
setTimeout(() => { el.className = 'cmd-toast'; }, 4000);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function detectWorkItemType(text) {
|
|
1324
|
+
const t = text.toLowerCase();
|
|
1325
|
+
const patterns = [
|
|
1326
|
+
{ type: 'explore', words: ['explore', 'investigate', 'understand', 'analyze', 'audit', 'document', 'architecture', 'how does', 'what is', 'look into', 'research', 'survey', 'map out', 'codebase'] },
|
|
1327
|
+
{ type: 'fix', words: ['fix', 'bug', 'broken', 'crash', 'error', 'issue', 'patch', 'repair', 'resolve', 'regression', 'failing', 'doesn\'t work', 'not working'] },
|
|
1328
|
+
{ type: 'review', words: ['review', 'code review', 'check pr', 'look at pr', 'audit code', 'inspect'] },
|
|
1329
|
+
{ type: 'test', words: ['test', 'write tests', 'add tests', 'unit test', 'e2e test', 'coverage', 'testing', 'build', 'run locally', 'localhost', 'start the', 'spin up', 'verify', 'check if it works'] },
|
|
1330
|
+
];
|
|
1331
|
+
for (const { type, words } of patterns) {
|
|
1332
|
+
if (words.some(w => t.includes(w))) return type;
|
|
1333
|
+
}
|
|
1334
|
+
return 'implement';
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// Parse the unified input into structured intent
|
|
1338
|
+
function cmdParseInput(raw) {
|
|
1339
|
+
let text = raw.trim();
|
|
1340
|
+
const result = {
|
|
1341
|
+
intent: 'work-item', // 'work-item' | 'note' | 'prd'
|
|
1342
|
+
agents: [], // assigned agent IDs
|
|
1343
|
+
fanout: false,
|
|
1344
|
+
priority: 'medium',
|
|
1345
|
+
project: '',
|
|
1346
|
+
title: '',
|
|
1347
|
+
description: '',
|
|
1348
|
+
type: '', // work item type (auto-detected)
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
// Detect /decide or /note prefix
|
|
1352
|
+
if (/^\/decide\b/i.test(text) || /^\/note\b/i.test(text)) {
|
|
1353
|
+
result.intent = 'note';
|
|
1354
|
+
text = text.replace(/^\/decide\s*/i, '');
|
|
1355
|
+
} else if (/^\/prd\b/i.test(text)) {
|
|
1356
|
+
result.intent = 'prd';
|
|
1357
|
+
text = text.replace(/^\/prd\s*/i, '');
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// Extract @mentions
|
|
1361
|
+
const mentionRe = /@(\w+)/g;
|
|
1362
|
+
let m;
|
|
1363
|
+
while ((m = mentionRe.exec(text)) !== null) {
|
|
1364
|
+
const name = m[1].toLowerCase();
|
|
1365
|
+
if (name === 'everyone' || name === 'all') {
|
|
1366
|
+
result.fanout = true;
|
|
1367
|
+
} else {
|
|
1368
|
+
const agent = cmdAgents.find(a => a.id === name || a.name.toLowerCase() === name);
|
|
1369
|
+
if (agent && !result.agents.includes(agent.id)) result.agents.push(agent.id);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
// Remove @mentions from text
|
|
1373
|
+
text = text.replace(/@\w+/g, '').trim();
|
|
1374
|
+
|
|
1375
|
+
// Extract !priority
|
|
1376
|
+
if (/!high\b/i.test(text)) { result.priority = 'high'; text = text.replace(/!high\b/i, '').trim(); }
|
|
1377
|
+
else if (/!low\b/i.test(text)) { result.priority = 'low'; text = text.replace(/!low\b/i, '').trim(); }
|
|
1378
|
+
else if (/!urgent\b/i.test(text)) { result.priority = 'high'; text = text.replace(/!urgent\b/i, '').trim(); }
|
|
1379
|
+
|
|
1380
|
+
// Extract #project
|
|
1381
|
+
const projRe = /#(\S+)/g;
|
|
1382
|
+
while ((m = projRe.exec(text)) !== null) {
|
|
1383
|
+
const pname = m[1];
|
|
1384
|
+
const proj = cmdProjects.find(p => p.name.toLowerCase() === pname.toLowerCase());
|
|
1385
|
+
if (proj) result.project = proj.name;
|
|
1386
|
+
}
|
|
1387
|
+
text = text.replace(/#\S+/g, '').trim();
|
|
1388
|
+
|
|
1389
|
+
// Clean up extra whitespace
|
|
1390
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
1391
|
+
|
|
1392
|
+
// Split first line as title, rest as description
|
|
1393
|
+
const lines = text.split('\n');
|
|
1394
|
+
result.title = lines[0] || '';
|
|
1395
|
+
result.description = lines.slice(1).join('\n').trim();
|
|
1396
|
+
|
|
1397
|
+
// Auto-detect work type
|
|
1398
|
+
result.type = detectWorkItemType(result.title + ' ' + result.description);
|
|
1399
|
+
|
|
1400
|
+
return result;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// Render the parsed meta chips below input
|
|
1404
|
+
function cmdRenderMeta() {
|
|
1405
|
+
const el = document.getElementById('cmd-meta');
|
|
1406
|
+
const input = document.getElementById('cmd-input').value;
|
|
1407
|
+
if (!input.trim()) { el.innerHTML = ''; return; }
|
|
1408
|
+
|
|
1409
|
+
const parsed = cmdParseInput(input);
|
|
1410
|
+
let chips = [];
|
|
1411
|
+
|
|
1412
|
+
// Intent chip
|
|
1413
|
+
const intentLabels = { 'work-item': 'Work Item', 'note': 'Note', 'prd': 'PRD Item' };
|
|
1414
|
+
chips.push('<span class="cmd-chip intent">' + intentLabels[parsed.intent] + '</span>');
|
|
1415
|
+
|
|
1416
|
+
// Type chip (only for work items)
|
|
1417
|
+
if (parsed.intent === 'work-item') {
|
|
1418
|
+
chips.push('<span class="cmd-chip">' + parsed.type + '</span>');
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// Priority chip
|
|
1422
|
+
chips.push('<span class="cmd-chip priority-' + parsed.priority + '">' + parsed.priority + ' priority</span>');
|
|
1423
|
+
|
|
1424
|
+
// Agent chips
|
|
1425
|
+
if (parsed.fanout) {
|
|
1426
|
+
chips.push('<span class="cmd-chip fanout">@everyone (fan-out)</span>');
|
|
1427
|
+
}
|
|
1428
|
+
for (const agentId of parsed.agents) {
|
|
1429
|
+
const agent = cmdAgents.find(a => a.id === agentId);
|
|
1430
|
+
if (agent) {
|
|
1431
|
+
chips.push('<span class="cmd-chip agent-chip">' + agent.emoji + ' @' + agent.name + '</span>');
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// Project chip
|
|
1436
|
+
if (parsed.project) {
|
|
1437
|
+
chips.push('<span class="cmd-chip project-chip">#' + escHtml(parsed.project) + '</span>');
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
el.innerHTML = chips.join('');
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// Autocomplete popup (shared for @mentions and #projects)
|
|
1444
|
+
let cmdPopupMode = ''; // '@' or '#'
|
|
1445
|
+
|
|
1446
|
+
function cmdShowMentions(query) {
|
|
1447
|
+
cmdPopupMode = '@';
|
|
1448
|
+
const popup = document.getElementById('cmd-mention-popup');
|
|
1449
|
+
const q = query.toLowerCase();
|
|
1450
|
+
let items = [];
|
|
1451
|
+
|
|
1452
|
+
// Always show @everyone option
|
|
1453
|
+
items.push({ id: 'everyone', name: 'everyone', emoji: '📢', role: 'Fan-out to all agents' });
|
|
1454
|
+
|
|
1455
|
+
for (const a of cmdAgents) {
|
|
1456
|
+
if (!q || a.id.includes(q) || a.name.toLowerCase().includes(q) || a.role.toLowerCase().includes(q)) {
|
|
1457
|
+
items.push(a);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
if (items.length === 0) { popup.classList.remove('visible'); return; }
|
|
1462
|
+
|
|
1463
|
+
cmdMentionIdx = 0;
|
|
1464
|
+
popup.innerHTML = items.map((a, i) =>
|
|
1465
|
+
'<div class="cmd-mention-item' + (i === 0 ? ' active' : '') + '" data-id="' + a.id + '" onclick="cmdInsertPopupItem(\'' + escHtml(a.id) + '\')">' +
|
|
1466
|
+
'<span class="mention-emoji">' + a.emoji + '</span>' +
|
|
1467
|
+
'<span class="mention-name">@' + escHtml(a.name) + '</span>' +
|
|
1468
|
+
'<span class="mention-role">' + escHtml(a.role) + '</span>' +
|
|
1469
|
+
'</div>'
|
|
1470
|
+
).join('');
|
|
1471
|
+
popup.classList.add('visible');
|
|
1472
|
+
popup.scrollTop = 0;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function cmdShowProjects(query) {
|
|
1476
|
+
cmdPopupMode = '#';
|
|
1477
|
+
const popup = document.getElementById('cmd-mention-popup');
|
|
1478
|
+
const q = query.toLowerCase();
|
|
1479
|
+
let items = cmdProjects.filter(p => !q || p.name.toLowerCase().includes(q));
|
|
1480
|
+
|
|
1481
|
+
if (items.length === 0) { popup.classList.remove('visible'); return; }
|
|
1482
|
+
|
|
1483
|
+
cmdMentionIdx = 0;
|
|
1484
|
+
popup.innerHTML = items.map((p, i) =>
|
|
1485
|
+
'<div class="cmd-mention-item' + (i === 0 ? ' active' : '') + '" data-id="' + escHtml(p.name) + '" onclick="cmdInsertPopupItem(\'' + escHtml(p.name) + '\')">' +
|
|
1486
|
+
'<span class="mention-emoji">📁</span>' +
|
|
1487
|
+
'<span class="mention-name" style="color:var(--green)">#' + escHtml(p.name) + '</span>' +
|
|
1488
|
+
'<span class="mention-role">' + escHtml(p.description.slice(0, 50)) + (p.description.length > 50 ? '...' : '') + '</span>' +
|
|
1489
|
+
'</div>'
|
|
1490
|
+
).join('');
|
|
1491
|
+
popup.classList.add('visible');
|
|
1492
|
+
popup.scrollTop = 0;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function cmdHidePopup() {
|
|
1496
|
+
document.getElementById('cmd-mention-popup').classList.remove('visible');
|
|
1497
|
+
cmdMentionIdx = -1;
|
|
1498
|
+
cmdPopupMode = '';
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function cmdInsertPopupItem(id) {
|
|
1502
|
+
const input = document.getElementById('cmd-input');
|
|
1503
|
+
const val = input.value;
|
|
1504
|
+
const cursor = input.selectionStart;
|
|
1505
|
+
const before = val.slice(0, cursor);
|
|
1506
|
+
const trigger = cmdPopupMode; // '@' or '#'
|
|
1507
|
+
const triggerIdx = before.lastIndexOf(trigger);
|
|
1508
|
+
if (triggerIdx === -1) return;
|
|
1509
|
+
const after = val.slice(cursor);
|
|
1510
|
+
input.value = before.slice(0, triggerIdx) + trigger + id + ' ' + after;
|
|
1511
|
+
input.focus();
|
|
1512
|
+
const newPos = triggerIdx + id.length + 2;
|
|
1513
|
+
input.setSelectionRange(newPos, newPos);
|
|
1514
|
+
cmdHidePopup();
|
|
1515
|
+
cmdRenderMeta();
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// Auto-resize textarea
|
|
1519
|
+
function cmdAutoResize() {
|
|
1520
|
+
const ta = document.getElementById('cmd-input');
|
|
1521
|
+
ta.style.height = 'auto';
|
|
1522
|
+
ta.style.height = Math.min(ta.scrollHeight, 200) + 'px';
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function cmdInputChanged() {
|
|
1526
|
+
cmdAutoResize();
|
|
1527
|
+
cmdRenderMeta();
|
|
1528
|
+
|
|
1529
|
+
// Check for @ mention or # project trigger
|
|
1530
|
+
const input = document.getElementById('cmd-input');
|
|
1531
|
+
const cursor = input.selectionStart;
|
|
1532
|
+
const before = input.value.slice(0, cursor);
|
|
1533
|
+
const atMatch = before.match(/@(\w*)$/);
|
|
1534
|
+
const hashMatch = before.match(/#(\w*)$/);
|
|
1535
|
+
if (atMatch) {
|
|
1536
|
+
cmdShowMentions(atMatch[1]);
|
|
1537
|
+
} else if (hashMatch) {
|
|
1538
|
+
cmdShowProjects(hashMatch[1]);
|
|
1539
|
+
} else {
|
|
1540
|
+
cmdHidePopup();
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function cmdKeyDown(e) {
|
|
1545
|
+
const popup = document.getElementById('cmd-mention-popup');
|
|
1546
|
+
const isPopupVisible = popup.classList.contains('visible');
|
|
1547
|
+
|
|
1548
|
+
// Navigate mention popup with arrow keys
|
|
1549
|
+
if (isPopupVisible) {
|
|
1550
|
+
const items = popup.querySelectorAll('.cmd-mention-item');
|
|
1551
|
+
if (items.length === 0) return;
|
|
1552
|
+
|
|
1553
|
+
if (e.key === 'ArrowDown' || (e.key === 'Tab' && !e.shiftKey)) {
|
|
1554
|
+
e.preventDefault();
|
|
1555
|
+
e.stopPropagation();
|
|
1556
|
+
cmdMentionIdx = (cmdMentionIdx + 1) % items.length;
|
|
1557
|
+
items.forEach((el, i) => el.classList.toggle('active', i === cmdMentionIdx));
|
|
1558
|
+
items[cmdMentionIdx]?.scrollIntoView({ block: 'nearest' });
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
if (e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey)) {
|
|
1562
|
+
e.preventDefault();
|
|
1563
|
+
e.stopPropagation();
|
|
1564
|
+
cmdMentionIdx = (cmdMentionIdx - 1 + items.length) % items.length;
|
|
1565
|
+
items.forEach((el, i) => el.classList.toggle('active', i === cmdMentionIdx));
|
|
1566
|
+
items[cmdMentionIdx]?.scrollIntoView({ block: 'nearest' });
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
if (e.key === 'Enter' && !e.ctrlKey) {
|
|
1570
|
+
e.preventDefault();
|
|
1571
|
+
e.stopPropagation();
|
|
1572
|
+
const active = items[cmdMentionIdx >= 0 ? cmdMentionIdx : 0];
|
|
1573
|
+
if (active) cmdInsertPopupItem(active.dataset.id);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
if (e.key === 'Escape') {
|
|
1577
|
+
e.preventDefault();
|
|
1578
|
+
cmdHidePopup();
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// Ctrl+Enter to submit
|
|
1584
|
+
if (e.key === 'Enter' && e.ctrlKey) {
|
|
1585
|
+
e.preventDefault();
|
|
1586
|
+
cmdSubmit();
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// Unified submit
|
|
1591
|
+
async function cmdSubmit() {
|
|
1592
|
+
const input = document.getElementById('cmd-input');
|
|
1593
|
+
const raw = input.value.trim();
|
|
1594
|
+
if (!raw) return showToast('cmd-toast', 'Type something first', false);
|
|
1595
|
+
|
|
1596
|
+
const parsed = cmdParseInput(raw);
|
|
1597
|
+
const btn = document.getElementById('cmd-send-btn');
|
|
1598
|
+
btn.disabled = true;
|
|
1599
|
+
|
|
1600
|
+
try {
|
|
1601
|
+
if (parsed.intent === 'note') {
|
|
1602
|
+
await cmdSubmitNote(parsed);
|
|
1603
|
+
} else if (parsed.intent === 'prd') {
|
|
1604
|
+
await cmdSubmitPrd(parsed);
|
|
1605
|
+
} else {
|
|
1606
|
+
await cmdSubmitWorkItem(parsed);
|
|
1607
|
+
}
|
|
1608
|
+
// Clear on success
|
|
1609
|
+
input.value = '';
|
|
1610
|
+
cmdAutoResize();
|
|
1611
|
+
cmdRenderMeta();
|
|
1612
|
+
} catch (e) {
|
|
1613
|
+
showToast('cmd-toast', 'Error: ' + e.message, false);
|
|
1614
|
+
} finally {
|
|
1615
|
+
btn.disabled = false;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
async function cmdSubmitWorkItem(parsed) {
|
|
1620
|
+
const body = {
|
|
1621
|
+
title: parsed.title,
|
|
1622
|
+
type: parsed.type,
|
|
1623
|
+
priority: parsed.priority,
|
|
1624
|
+
description: parsed.description,
|
|
1625
|
+
};
|
|
1626
|
+
if (parsed.project) body.project = parsed.project;
|
|
1627
|
+
if (parsed.fanout) body.scope = 'fan-out';
|
|
1628
|
+
if (parsed.agents.length === 1) body.agent = parsed.agents[0];
|
|
1629
|
+
// For multiple agent mentions without @everyone, treat as fan-out with agent list
|
|
1630
|
+
if (parsed.agents.length > 1) {
|
|
1631
|
+
body.scope = 'fan-out';
|
|
1632
|
+
body.agents = parsed.agents;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
const res = await fetch('/api/work-items', {
|
|
1636
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1637
|
+
body: JSON.stringify(body),
|
|
1638
|
+
});
|
|
1639
|
+
const data = await res.json();
|
|
1640
|
+
if (!res.ok) throw new Error(data.error || 'Failed to add');
|
|
1641
|
+
const agentLabel = parsed.fanout ? ' (fan-out)' : parsed.agents.length ? ' → ' + parsed.agents.join(', ') : '';
|
|
1642
|
+
showToast('cmd-toast', 'Work item ' + data.id + ' created' + agentLabel, true);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
async function cmdSubmitNote(parsed) {
|
|
1646
|
+
const res = await fetch('/api/notes', {
|
|
1647
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1648
|
+
body: JSON.stringify({
|
|
1649
|
+
title: parsed.title,
|
|
1650
|
+
author: '',
|
|
1651
|
+
what: parsed.title + (parsed.description ? '\n' + parsed.description : ''),
|
|
1652
|
+
why: '',
|
|
1653
|
+
}),
|
|
1654
|
+
});
|
|
1655
|
+
const data = await res.json();
|
|
1656
|
+
if (!res.ok) throw new Error(data.error || 'Failed to add');
|
|
1657
|
+
showToast('cmd-toast', 'Note added', true);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
async function cmdSubmitPrd(parsed) {
|
|
1661
|
+
// Auto-generate PRD ID
|
|
1662
|
+
const id = 'M' + String(Date.now()).slice(-4);
|
|
1663
|
+
const res = await fetch('/api/prd-items', {
|
|
1664
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1665
|
+
body: JSON.stringify({
|
|
1666
|
+
id,
|
|
1667
|
+
name: parsed.title,
|
|
1668
|
+
description: parsed.description || parsed.title,
|
|
1669
|
+
priority: parsed.priority,
|
|
1670
|
+
estimated_complexity: 'medium',
|
|
1671
|
+
rationale: '',
|
|
1672
|
+
}),
|
|
1673
|
+
});
|
|
1674
|
+
const data = await res.json();
|
|
1675
|
+
if (!res.ok) throw new Error(data.error || 'Failed to add');
|
|
1676
|
+
showToast('cmd-toast', 'PRD item ' + (data.id || id) + ' added', true);
|
|
1677
|
+
}
|
|
1678
|
+
</script>
|
|
1679
|
+
</body>
|
|
1680
|
+
</html>
|