@yemi33/minions 0.1.2100 → 0.1.2102
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/dashboard/slim/body.html +251 -0
- package/dashboard/slim/js/chat.js +329 -0
- package/dashboard/slim/js/command-send.js +190 -0
- package/dashboard/slim/js/helpers.js +26 -0
- package/dashboard/slim/js/history.js +442 -0
- package/dashboard/slim/js/link-pr.js +83 -0
- package/dashboard/slim/js/members.js +138 -0
- package/dashboard/slim/js/modals-tiles.js +161 -0
- package/dashboard/slim/js/projects.js +209 -0
- package/dashboard/slim/js/settings.js +130 -0
- package/dashboard/slim/js/status.js +122 -0
- package/dashboard/slim/layout.html +21 -0
- package/dashboard/slim/styles.css +1128 -0
- package/dashboard-build.js +33 -1
- package/dashboard.js +11 -10
- package/docs/pr-review-fix-loop.md +2 -2
- package/engine/ado.js +21 -0
- package/engine/github.js +21 -0
- package/engine/lifecycle.js +35 -0
- package/engine/shared.js +9 -11
- package/package.json +1 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
function describeAction(action) {
|
|
2
|
+
if (!action || !action.type) return 'action';
|
|
3
|
+
var t = action.type;
|
|
4
|
+
var label = action.title || action.id || action.file || action.agent || '';
|
|
5
|
+
return label ? t + ': ' + label : t;
|
|
6
|
+
}
|
|
7
|
+
function renderActionResults(actions, results) {
|
|
8
|
+
if (!actions || !actions.length) return;
|
|
9
|
+
for (var i = 0; i < actions.length; i++) {
|
|
10
|
+
var action = actions[i];
|
|
11
|
+
var r = (results && results[i]) || null;
|
|
12
|
+
var label = describeAction(action);
|
|
13
|
+
var severity = 'ok';
|
|
14
|
+
var symbol = '✓';
|
|
15
|
+
var detail = '';
|
|
16
|
+
if (r && r.error) { severity = 'err'; symbol = '✗'; detail = ' — ' + r.error; }
|
|
17
|
+
else if (r && r.warning) { severity = 'warn'; symbol = 'ℹ'; detail = ' — ' + r.warning; }
|
|
18
|
+
else if (r && r.duplicate) { severity = 'warn'; symbol = '↺'; detail = ' (already exists)'; }
|
|
19
|
+
var text = symbol + ' ' + label + detail;
|
|
20
|
+
appendActionStatus(severity, text);
|
|
21
|
+
messages.push({ role: 'action', severity: severity, text: text });
|
|
22
|
+
}
|
|
23
|
+
saveState();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function sendMessage() {
|
|
27
|
+
if (sending) return;
|
|
28
|
+
var text = inputEl.value.trim();
|
|
29
|
+
if (!text) return;
|
|
30
|
+
|
|
31
|
+
appendBubble('user', text);
|
|
32
|
+
messages.push({ role: 'user', text: text });
|
|
33
|
+
saveState();
|
|
34
|
+
|
|
35
|
+
inputEl.value = '';
|
|
36
|
+
inputEl.style.height = 'auto';
|
|
37
|
+
setSending(true);
|
|
38
|
+
|
|
39
|
+
var stream = buildStreamBubble();
|
|
40
|
+
var streamedText = '';
|
|
41
|
+
abortController = new AbortController();
|
|
42
|
+
var userAborted = false;
|
|
43
|
+
var timeoutSignal = null;
|
|
44
|
+
try { timeoutSignal = AbortSignal.timeout(STREAM_TIMEOUT_MS); } catch (_e) { /* old browsers */ }
|
|
45
|
+
var signal = timeoutSignal && AbortSignal.any
|
|
46
|
+
? AbortSignal.any([abortController.signal, timeoutSignal])
|
|
47
|
+
: abortController.signal;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
var res = await fetch('/api/command-center/stream', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({ message: text, tabId: tabId, sessionId: sessionId, currentProject: currentProject || undefined }),
|
|
54
|
+
signal: signal,
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
var errBody = await res.text().catch(function() { return ''; });
|
|
58
|
+
throw new Error('HTTP ' + res.status + (errBody ? ': ' + errBody.slice(0, 200) : ''));
|
|
59
|
+
}
|
|
60
|
+
if (!res.body || !res.body.getReader) {
|
|
61
|
+
throw new Error('Streaming not supported by this browser.');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
var reader = res.body.getReader();
|
|
65
|
+
var decoder = new TextDecoder();
|
|
66
|
+
var buf = '';
|
|
67
|
+
var doneEvt = null;
|
|
68
|
+
var errorEvt = null;
|
|
69
|
+
|
|
70
|
+
while (true) {
|
|
71
|
+
var r = await reader.read();
|
|
72
|
+
if (r.done) break;
|
|
73
|
+
buf += decoder.decode(r.value, { stream: true });
|
|
74
|
+
var lines = buf.split('\n');
|
|
75
|
+
buf = lines.pop();
|
|
76
|
+
for (var i = 0; i < lines.length; i++) {
|
|
77
|
+
var line = lines[i];
|
|
78
|
+
if (!line.startsWith('data: ')) continue;
|
|
79
|
+
var evt;
|
|
80
|
+
try { evt = JSON.parse(line.slice(6)); } catch (_e) { continue; }
|
|
81
|
+
if (evt.type === 'chunk') {
|
|
82
|
+
streamedText = mergeStreamText(streamedText, evt.text || '');
|
|
83
|
+
stream.setText(streamedText);
|
|
84
|
+
} else if (evt.type === 'tool') {
|
|
85
|
+
stream.addTool(evt.name, evt.input || {});
|
|
86
|
+
} else if (evt.type === 'heartbeat') {
|
|
87
|
+
/* server keep-alive — ignored on purpose */
|
|
88
|
+
} else if (evt.type === 'done') {
|
|
89
|
+
doneEvt = evt;
|
|
90
|
+
} else if (evt.type === 'error') {
|
|
91
|
+
errorEvt = evt;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (errorEvt) {
|
|
97
|
+
stream.replaceWithError(errorEvt.error || 'Error');
|
|
98
|
+
messages.push({ role: 'error', text: errorEvt.error || 'Error' });
|
|
99
|
+
saveState();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (doneEvt) {
|
|
104
|
+
var finalText = mergeStreamText(streamedText, doneEvt.text || '');
|
|
105
|
+
stream.finalize(finalText || '(no response)');
|
|
106
|
+
messages.push({ role: 'assistant', text: finalText || '(no response)', toolCalls: stream.getToolList() });
|
|
107
|
+
if (doneEvt.sessionId !== undefined) sessionId = doneEvt.sessionId || null;
|
|
108
|
+
saveState();
|
|
109
|
+
if (doneEvt.actionParseError) {
|
|
110
|
+
var pe = '⚠️ Actions block emitted but JSON could not be parsed — no actions executed.';
|
|
111
|
+
appendActionStatus('err', pe);
|
|
112
|
+
messages.push({ role: 'action', severity: 'err', text: pe });
|
|
113
|
+
saveState();
|
|
114
|
+
}
|
|
115
|
+
if (doneEvt.actions && doneEvt.actions.length > 0) {
|
|
116
|
+
renderActionResults(doneEvt.actions, doneEvt.actionResults || []);
|
|
117
|
+
}
|
|
118
|
+
// Refresh status + history opportunistically — actions probably
|
|
119
|
+
// changed something.
|
|
120
|
+
scheduleStatusRefresh(800);
|
|
121
|
+
} else {
|
|
122
|
+
stream.finalize(streamedText || '(stream ended before completion)');
|
|
123
|
+
messages.push({ role: 'assistant', text: streamedText || '(stream ended before completion)', toolCalls: stream.getToolList() });
|
|
124
|
+
saveState();
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
if (e && (e.name === 'AbortError' || /aborted/i.test(String(e.message || '')))) {
|
|
128
|
+
userAborted = true;
|
|
129
|
+
if (streamedText) {
|
|
130
|
+
stream.finalize(streamedText);
|
|
131
|
+
messages.push({ role: 'assistant', text: streamedText, toolCalls: stream.getToolList() });
|
|
132
|
+
} else {
|
|
133
|
+
stream.replaceWithError('Stopped.');
|
|
134
|
+
messages.push({ role: 'error', text: 'Stopped.' });
|
|
135
|
+
}
|
|
136
|
+
saveState();
|
|
137
|
+
} else {
|
|
138
|
+
var msg = 'Send failed: ' + (e && e.message ? e.message : e);
|
|
139
|
+
stream.replaceWithError(msg);
|
|
140
|
+
messages.push({ role: 'error', text: msg });
|
|
141
|
+
saveState();
|
|
142
|
+
}
|
|
143
|
+
} finally {
|
|
144
|
+
setSending(false);
|
|
145
|
+
abortController = null;
|
|
146
|
+
if (!userAborted) inputEl.focus();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function abortInFlight() {
|
|
151
|
+
if (!sending || !abortController) return;
|
|
152
|
+
try {
|
|
153
|
+
fetch('/api/command-center/abort', {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: { 'Content-Type': 'application/json' },
|
|
156
|
+
body: JSON.stringify({ tabId: tabId }),
|
|
157
|
+
}).catch(function() {});
|
|
158
|
+
} catch (_e) { /* ignore */ }
|
|
159
|
+
try { abortController.abort(); } catch (_e) { /* ignore */ }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function slimChatNew() {
|
|
163
|
+
if (sending) abortInFlight();
|
|
164
|
+
sessionId = null;
|
|
165
|
+
messages = [];
|
|
166
|
+
saveState();
|
|
167
|
+
rerenderHistory();
|
|
168
|
+
try {
|
|
169
|
+
fetch('/api/cc-sessions/' + encodeURIComponent(tabId), { method: 'DELETE' }).catch(function() {});
|
|
170
|
+
} catch (_e) { /* ignore */ }
|
|
171
|
+
inputEl.focus();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
var newChatBtn = document.getElementById('slim-new-chat-btn');
|
|
175
|
+
if (newChatBtn) newChatBtn.addEventListener('click', slimChatNew);
|
|
176
|
+
sendBtn.addEventListener('click', sendMessage);
|
|
177
|
+
stopBtn.addEventListener('click', abortInFlight);
|
|
178
|
+
inputEl.addEventListener('keydown', function(ev) {
|
|
179
|
+
if (ev.key === 'Enter' && !ev.shiftKey) {
|
|
180
|
+
ev.preventDefault();
|
|
181
|
+
sendMessage();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
inputEl.addEventListener('input', function() {
|
|
185
|
+
inputEl.style.height = 'auto';
|
|
186
|
+
inputEl.style.height = Math.min(inputEl.scrollHeight, 130) + 'px';
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
inputEl.focus();
|
|
190
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
|
|
2
|
+
// ── Helpers ────────────────────────────────────────────────────
|
|
3
|
+
function escHtml(s) {
|
|
4
|
+
return String(s == null ? '' : s)
|
|
5
|
+
.replace(/&/g, '&')
|
|
6
|
+
.replace(/</g, '<')
|
|
7
|
+
.replace(/>/g, '>')
|
|
8
|
+
.replace(/"/g, '"')
|
|
9
|
+
.replace(/'/g, ''');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function relTime(iso) {
|
|
13
|
+
if (!iso) return '';
|
|
14
|
+
var t = typeof iso === 'number' ? iso : Date.parse(iso);
|
|
15
|
+
if (isNaN(t)) return '';
|
|
16
|
+
var s = Math.floor((Date.now() - t) / 1000);
|
|
17
|
+
if (s < 0) s = 0;
|
|
18
|
+
if (s < 60) return s + 's ago';
|
|
19
|
+
var m = Math.floor(s / 60);
|
|
20
|
+
if (m < 60) return m + 'm ago';
|
|
21
|
+
var h = Math.floor(m / 60);
|
|
22
|
+
if (h < 24) return h + 'h ago';
|
|
23
|
+
var d = Math.floor(h / 24);
|
|
24
|
+
return d + 'd ago';
|
|
25
|
+
}
|
|
26
|
+
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
// Completions Card helpers (W-betterHistory) — see the CSS block of the
|
|
2
|
+
// same name in dashboard/slim.html for the component layout reference.
|
|
3
|
+
// ─────────────────────────────────────────────────
|
|
4
|
+
// The type chip is a single emoji glyph. Map covers every value the
|
|
5
|
+
// engine can emit (14 WORK_TYPE constants + the synthetic 'qa-validate'
|
|
6
|
+
// routed via routing.md). Unknown types fall back to a generic marker
|
|
7
|
+
// and still get the raw type name in the hover tooltip.
|
|
8
|
+
var COMPLETION_TYPE_EMOJI = {
|
|
9
|
+
'implement': '🔧',
|
|
10
|
+
'implement:large': '🛠️',
|
|
11
|
+
'fix': '🐛',
|
|
12
|
+
'review': '👀',
|
|
13
|
+
'verify': '✅',
|
|
14
|
+
'test': '🧪',
|
|
15
|
+
'plan': '📋',
|
|
16
|
+
'plan-to-prd': '📐',
|
|
17
|
+
'decompose': '🪓',
|
|
18
|
+
'meeting': '💬',
|
|
19
|
+
'explore': '🔍',
|
|
20
|
+
'ask': '❓',
|
|
21
|
+
'docs': '📝',
|
|
22
|
+
'setup': '⚙️',
|
|
23
|
+
'qa-validate': '🎯',
|
|
24
|
+
};
|
|
25
|
+
function typeEmoji(t) {
|
|
26
|
+
if (!t) return '•';
|
|
27
|
+
return COMPLETION_TYPE_EMOJI[t] || '•';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Pick the title for a completion card. Uses the dispatch description
|
|
31
|
+
// (c.task) — that's what the agent was actually told to do — falling back
|
|
32
|
+
// to the linked WI title only when no task description is present.
|
|
33
|
+
function completionTitle(c) {
|
|
34
|
+
var item = (c.meta && c.meta.item) || {};
|
|
35
|
+
return c.task || item.title || c.title || c.workItemTitle || c.id || '(untitled)';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Status: 'ok' (success) | 'fail' (error) | 'warn' (partial managed-spawn).
|
|
39
|
+
function completionStatus(c) {
|
|
40
|
+
if (c._managedSpawnPartial) return 'warn';
|
|
41
|
+
if (c.result === 'success' || c.status === 'done') return 'ok';
|
|
42
|
+
return 'fail';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Build one lineage chip with a fixed kind label + value (or "-" when
|
|
46
|
+
// absent). DOM-built (no innerHTML) so the SEC-03 baseline stays clean.
|
|
47
|
+
function buildLineageChip(kindLabel, value, tooltip) {
|
|
48
|
+
var chip = document.createElement('span');
|
|
49
|
+
chip.className = 'completions-card-chip';
|
|
50
|
+
chip.title = tooltip || kindLabel;
|
|
51
|
+
var labelEl = document.createElement('span');
|
|
52
|
+
labelEl.className = 'completions-card-chip-label';
|
|
53
|
+
labelEl.textContent = kindLabel + ':';
|
|
54
|
+
var valueEl = document.createElement('span');
|
|
55
|
+
valueEl.className = 'completions-card-chip-value';
|
|
56
|
+
valueEl.textContent = value == null || value === '' ? '-' : String(value);
|
|
57
|
+
chip.appendChild(labelEl);
|
|
58
|
+
chip.appendChild(valueEl);
|
|
59
|
+
return chip;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Render a single completion card (DOM node). Returns the .history-item
|
|
63
|
+
// element with attached click handlers. Used by renderHistoryFeed.
|
|
64
|
+
function renderCompletionCard(c) {
|
|
65
|
+
var status = completionStatus(c);
|
|
66
|
+
var ts = c.completed_at || c.completedAt || c.endedAt || c.started_at || c.startedAt || c._ts;
|
|
67
|
+
var title = completionTitle(c);
|
|
68
|
+
var item = (c.meta && c.meta.item) || {};
|
|
69
|
+
var pr = (c.meta && c.meta.pr) || null;
|
|
70
|
+
|
|
71
|
+
var card = document.createElement('div');
|
|
72
|
+
card.className = 'history-item completions-card kind-' + (status === 'fail' ? 'failure' : 'completion');
|
|
73
|
+
card.setAttribute('role', 'button');
|
|
74
|
+
card.setAttribute('tabindex', '0');
|
|
75
|
+
card.title = title;
|
|
76
|
+
|
|
77
|
+
// Left rail: two rows. Top 70% = type emoji alone (prominent);
|
|
78
|
+
// bottom 30% = `<status-icon> · <relative-time>`, all in muted gray.
|
|
79
|
+
var rail = document.createElement('div');
|
|
80
|
+
rail.className = 'completions-card-rail';
|
|
81
|
+
var railTop = document.createElement('div');
|
|
82
|
+
railTop.className = 'completions-card-rail-top';
|
|
83
|
+
var chip = document.createElement('div');
|
|
84
|
+
chip.className = 'completions-card-type-chip';
|
|
85
|
+
chip.textContent = typeEmoji(c.type);
|
|
86
|
+
if (c.type) chip.title = c.type;
|
|
87
|
+
railTop.appendChild(chip);
|
|
88
|
+
|
|
89
|
+
var railBottom = document.createElement('div');
|
|
90
|
+
railBottom.className = 'completions-card-rail-bottom';
|
|
91
|
+
var icon = document.createElement('span');
|
|
92
|
+
icon.className = 'completions-card-status-icon ' + status;
|
|
93
|
+
icon.textContent = status === 'ok' ? '✓' : (status === 'warn' ? '⚠' : '✕');
|
|
94
|
+
icon.title = status === 'ok' ? 'Success' : (status === 'warn' ? 'Partial' : 'Failure');
|
|
95
|
+
var sep = document.createElement('span');
|
|
96
|
+
sep.className = 'completions-card-rail-sep';
|
|
97
|
+
sep.textContent = '·';
|
|
98
|
+
sep.setAttribute('aria-hidden', 'true');
|
|
99
|
+
var when = document.createElement('span');
|
|
100
|
+
when.className = 'completions-card-time';
|
|
101
|
+
when.textContent = ts ? relTime(ts) : '';
|
|
102
|
+
if (ts) when.title = new Date(ts).toLocaleString();
|
|
103
|
+
railBottom.appendChild(icon);
|
|
104
|
+
railBottom.appendChild(sep);
|
|
105
|
+
railBottom.appendChild(when);
|
|
106
|
+
rail.appendChild(railTop);
|
|
107
|
+
rail.appendChild(railBottom);
|
|
108
|
+
|
|
109
|
+
// Body-top — title + optional failure line. Lives in grid cell (col 2,
|
|
110
|
+
// row 1); rail-top centers vertically against this block via subgrid.
|
|
111
|
+
var bodyTop = document.createElement('div');
|
|
112
|
+
bodyTop.className = 'completions-card-body-top';
|
|
113
|
+
var titleEl = document.createElement('div');
|
|
114
|
+
titleEl.className = 'completions-card-title';
|
|
115
|
+
titleEl.textContent = title;
|
|
116
|
+
bodyTop.appendChild(titleEl);
|
|
117
|
+
|
|
118
|
+
if (status === 'fail' && (c.failureClass || c.reason)) {
|
|
119
|
+
var failLine = document.createElement('div');
|
|
120
|
+
failLine.className = 'completions-card-fail-line';
|
|
121
|
+
var truncatedReason = c.reason ? String(c.reason).slice(0, 120) : '';
|
|
122
|
+
if (c.failureClass) {
|
|
123
|
+
var fc = document.createElement('span');
|
|
124
|
+
fc.className = 'completions-card-fail-class';
|
|
125
|
+
fc.textContent = c.failureClass;
|
|
126
|
+
failLine.appendChild(fc);
|
|
127
|
+
if (truncatedReason) failLine.appendChild(document.createTextNode(' — ' + truncatedReason));
|
|
128
|
+
} else {
|
|
129
|
+
failLine.textContent = truncatedReason;
|
|
130
|
+
}
|
|
131
|
+
if (c.reason && c.reason.length > 120) failLine.title = c.reason;
|
|
132
|
+
bodyTop.appendChild(failLine);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Bottom row: fixed 3-column lineage grid (WI / Plan / PR). Lives in
|
|
136
|
+
// grid cell (col 2, row 2); rail-bottom sits in the same parent row
|
|
137
|
+
// so the status·time line aligns with this chip row.
|
|
138
|
+
var lineage = document.createElement('div');
|
|
139
|
+
lineage.className = 'completions-card-lineage';
|
|
140
|
+
lineage.appendChild(buildLineageChip('WI', item.id || null,
|
|
141
|
+
item.id ? 'Work item: ' + item.id + (item.title ? ' — ' + item.title : '') : 'Work item: none'));
|
|
142
|
+
var planValue = item.sourcePlan ? String(item.sourcePlan).replace(/\.(md|json)$/i, '') : null;
|
|
143
|
+
lineage.appendChild(buildLineageChip('Plan', planValue,
|
|
144
|
+
item.sourcePlan ? 'Source plan: ' + item.sourcePlan : 'Source plan: none'));
|
|
145
|
+
var prRef = pr && (pr.id || pr.number) ? String(pr.id || pr.number) : null;
|
|
146
|
+
var prChip = buildLineageChip('PR', prRef ? '!' + prRef : null,
|
|
147
|
+
prRef ? 'Pull request: !' + prRef + (pr.url ? ' (' + pr.url + ')' : '') : 'Pull request: none');
|
|
148
|
+
if (prRef && pr && pr.url) {
|
|
149
|
+
prChip.classList.add('completions-card-chip-pr');
|
|
150
|
+
prChip.addEventListener('click', function(ev) {
|
|
151
|
+
ev.stopPropagation();
|
|
152
|
+
window.open(pr.url, '_blank', 'noopener');
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
lineage.appendChild(prChip);
|
|
156
|
+
|
|
157
|
+
// Three direct children of the 2x2 card grid: rail (spans both rows
|
|
158
|
+
// via subgrid), bodyTop (cell 2,1), lineage (cell 2,2).
|
|
159
|
+
card.appendChild(rail);
|
|
160
|
+
card.appendChild(bodyTop);
|
|
161
|
+
card.appendChild(lineage);
|
|
162
|
+
|
|
163
|
+
// Whole-card click → completion details modal.
|
|
164
|
+
card.addEventListener('click', function() { showCompletionDetails(c); });
|
|
165
|
+
card.addEventListener('keydown', function(ev) {
|
|
166
|
+
if (ev.key === 'Enter' || ev.key === ' ') {
|
|
167
|
+
ev.preventDefault();
|
|
168
|
+
showCompletionDetails(c);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return card;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Modal: full task, resultSummary, linked WI, completion report contents.
|
|
176
|
+
// Closes on backdrop click or × button (wired below).
|
|
177
|
+
var _completionReportCache = {};
|
|
178
|
+
function showCompletionDetails(c) {
|
|
179
|
+
var modal = document.getElementById('slim-completions-modal');
|
|
180
|
+
var body = document.getElementById('slim-completions-body');
|
|
181
|
+
var titleEl = document.getElementById('slim-completions-title');
|
|
182
|
+
if (!modal || !body) return;
|
|
183
|
+
|
|
184
|
+
var status = completionStatus(c);
|
|
185
|
+
var statusText = status === 'ok' ? 'Success' : (status === 'warn' ? 'Partial' : 'Failure');
|
|
186
|
+
titleEl.textContent = statusText + ' · ' + (c.type || 'completion');
|
|
187
|
+
|
|
188
|
+
// Build modal body as DOM nodes (no innerHTML — keeps SEC-03 baseline happy).
|
|
189
|
+
body.replaceChildren();
|
|
190
|
+
|
|
191
|
+
var addSection = function(label, valueNode) {
|
|
192
|
+
var section = document.createElement('div');
|
|
193
|
+
section.className = 'completions-modal-section';
|
|
194
|
+
var lab = document.createElement('div');
|
|
195
|
+
lab.className = 'completions-modal-label';
|
|
196
|
+
lab.textContent = label;
|
|
197
|
+
section.appendChild(lab);
|
|
198
|
+
section.appendChild(valueNode);
|
|
199
|
+
body.appendChild(section);
|
|
200
|
+
};
|
|
201
|
+
var textValue = function(text) {
|
|
202
|
+
var v = document.createElement('div');
|
|
203
|
+
v.className = 'completions-modal-value';
|
|
204
|
+
v.textContent = text;
|
|
205
|
+
return v;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
var title = completionTitle(c);
|
|
209
|
+
if (title) addSection('Task', textValue(title));
|
|
210
|
+
|
|
211
|
+
var taskFull = c.task && c.task !== title ? c.task : null;
|
|
212
|
+
if (taskFull) addSection('Dispatch description', textValue(taskFull));
|
|
213
|
+
|
|
214
|
+
var agentLine = (c.agentName || c.agent || '?') +
|
|
215
|
+
(c.agentRole ? ' (' + c.agentRole + ')' : '') +
|
|
216
|
+
(c.runtimeName ? ' · ' + c.runtimeName : '');
|
|
217
|
+
addSection('Agent', textValue(agentLine));
|
|
218
|
+
|
|
219
|
+
var startedAt = c.started_at || c.startedAt;
|
|
220
|
+
var completedAt = c.completed_at || c.completedAt;
|
|
221
|
+
if (startedAt || completedAt) {
|
|
222
|
+
var lines = [];
|
|
223
|
+
if (startedAt) lines.push('Started: ' + new Date(startedAt).toLocaleString());
|
|
224
|
+
if (completedAt) lines.push('Completed: ' + new Date(completedAt).toLocaleString());
|
|
225
|
+
if (startedAt && completedAt) {
|
|
226
|
+
var dur = Date.parse(completedAt) - Date.parse(startedAt);
|
|
227
|
+
if (dur > 0) lines.push('Duration: ' + formatDuration(dur));
|
|
228
|
+
}
|
|
229
|
+
addSection('Timing', textValue(lines.join('\n')));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (status === 'fail' && (c.failureClass || c.reason)) {
|
|
233
|
+
var failBlock = c.failureClass ? (c.failureClass + (c.reason ? '\n\n' + c.reason : '')) : c.reason;
|
|
234
|
+
addSection('Failure', textValue(failBlock));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (c.resultSummary) addSection('Result summary', textValue(c.resultSummary));
|
|
238
|
+
|
|
239
|
+
// Lineage: WI, source plan, PR.
|
|
240
|
+
var item = (c.meta && c.meta.item) || {};
|
|
241
|
+
var pr = (c.meta && c.meta.pr) || null;
|
|
242
|
+
if (item.id || item.sourcePlan || pr) {
|
|
243
|
+
var lineageWrap = document.createElement('div');
|
|
244
|
+
lineageWrap.className = 'completions-modal-value';
|
|
245
|
+
var lines2 = [];
|
|
246
|
+
if (item.id) lines2.push('Work item: #' + item.id + (item.title ? ' — ' + item.title : ''));
|
|
247
|
+
if (item.sourcePlan) lines2.push('Source plan: ' + item.sourcePlan);
|
|
248
|
+
if (pr) {
|
|
249
|
+
var prLine = 'PR: !' + (pr.id || pr.number || '?');
|
|
250
|
+
if (pr.url) prLine += ' (' + pr.url + ')';
|
|
251
|
+
if (pr.branch) prLine += '\n branch: ' + pr.branch;
|
|
252
|
+
if (pr.buildStatus) prLine += '\n build: ' + pr.buildStatus;
|
|
253
|
+
if (pr.reviewStatus) prLine += '\n review: ' + pr.reviewStatus;
|
|
254
|
+
lines2.push(prLine);
|
|
255
|
+
}
|
|
256
|
+
lineageWrap.textContent = lines2.join('\n');
|
|
257
|
+
addSection('Lineage', lineageWrap);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Completion report: lazy-fetch from meta.completionReportPath when present.
|
|
261
|
+
var reportPath = c.meta && c.meta.completionReportPath;
|
|
262
|
+
if (reportPath) {
|
|
263
|
+
var reportSection = document.createElement('div');
|
|
264
|
+
reportSection.className = 'completions-modal-section';
|
|
265
|
+
var reportLab = document.createElement('div');
|
|
266
|
+
reportLab.className = 'completions-modal-label';
|
|
267
|
+
reportLab.textContent = 'Completion report';
|
|
268
|
+
reportSection.appendChild(reportLab);
|
|
269
|
+
var reportBox = document.createElement('div');
|
|
270
|
+
reportBox.className = 'completions-modal-report';
|
|
271
|
+
reportBox.textContent = 'Loading…';
|
|
272
|
+
reportSection.appendChild(reportBox);
|
|
273
|
+
body.appendChild(reportSection);
|
|
274
|
+
var cacheKey = c.id || reportPath;
|
|
275
|
+
if (_completionReportCache[cacheKey]) {
|
|
276
|
+
reportBox.textContent = _completionReportCache[cacheKey];
|
|
277
|
+
} else if (!c.id) {
|
|
278
|
+
reportBox.textContent = '(report unavailable — no dispatch id)';
|
|
279
|
+
} else {
|
|
280
|
+
fetch('/api/dispatch/' + encodeURIComponent(c.id) + '/completion-report', {
|
|
281
|
+
headers: { 'Accept': 'application/json' },
|
|
282
|
+
}).then(function(r) {
|
|
283
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
284
|
+
return r.json();
|
|
285
|
+
}).then(function(payload) {
|
|
286
|
+
var pretty = JSON.stringify(payload, null, 2);
|
|
287
|
+
_completionReportCache[cacheKey] = pretty;
|
|
288
|
+
reportBox.textContent = pretty;
|
|
289
|
+
}).catch(function() {
|
|
290
|
+
reportBox.textContent = '(report unavailable — ' + reportPath + ')';
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
modal.classList.add('open');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function formatDuration(ms) {
|
|
299
|
+
if (ms < 1000) return ms + 'ms';
|
|
300
|
+
var s = Math.floor(ms / 1000);
|
|
301
|
+
if (s < 60) return s + 's';
|
|
302
|
+
var m = Math.floor(s / 60);
|
|
303
|
+
var rs = s % 60;
|
|
304
|
+
if (m < 60) return m + 'm ' + rs + 's';
|
|
305
|
+
var h = Math.floor(m / 60);
|
|
306
|
+
var rm = m % 60;
|
|
307
|
+
return h + 'h ' + rm + 'm';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Bind completions-modal close handlers once (shared helper above).
|
|
311
|
+
bindModalClose('slim-completions-modal', 'slim-completions-close');
|
|
312
|
+
|
|
313
|
+
function renderHistoryFeed(input) {
|
|
314
|
+
var listEl = document.getElementById('history-list');
|
|
315
|
+
if (!listEl) return;
|
|
316
|
+
var events = [];
|
|
317
|
+
|
|
318
|
+
// Active dispatches (blue) — at the top so users know what's running.
|
|
319
|
+
(input.active || []).forEach(function(d) {
|
|
320
|
+
var ts = d.startedAt || d.dispatchedAt || d.createdAt || d._ts;
|
|
321
|
+
events.push({
|
|
322
|
+
kind: 'dispatch',
|
|
323
|
+
kindLabel: 'Dispatch',
|
|
324
|
+
title: describeDispatch(d),
|
|
325
|
+
meta: 'started ' + relTime(ts) + (d.workItemId ? ' · ' + d.workItemId : ''),
|
|
326
|
+
ts: ts ? Date.parse(ts) || Date.now() : Date.now(),
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Recent completions (green or red) — last 12 most recent first.
|
|
331
|
+
// Carry the raw record on the event so the new card renderer has access
|
|
332
|
+
// to lineage / failure / report fields without re-flattening.
|
|
333
|
+
(input.completed || []).slice().reverse().slice(0, 12).forEach(function(c) {
|
|
334
|
+
var ok = completionStatus(c) === 'ok';
|
|
335
|
+
var ts = c.completed_at || c.completedAt || c.endedAt || c.started_at || c.startedAt || c._ts;
|
|
336
|
+
events.push({
|
|
337
|
+
kind: ok ? 'completion' : 'failure',
|
|
338
|
+
raw: c,
|
|
339
|
+
ts: ts ? Date.parse(ts) || 0 : 0,
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// PR events (amber) — shows PRs we're tracking, anchored on updatedAt.
|
|
344
|
+
// `metaUrl` carries the optional "open PR" link so the renderer can build
|
|
345
|
+
// it as a real <a> node instead of splicing raw HTML into innerHTML.
|
|
346
|
+
(input.prs || []).slice(0, 6).forEach(function(p) {
|
|
347
|
+
var ts = p.updatedAt || p.createdAt;
|
|
348
|
+
events.push({
|
|
349
|
+
kind: 'pr',
|
|
350
|
+
kindLabel: 'PR',
|
|
351
|
+
title: '#' + (p.prNumber || p.id || '?') + ' — ' + (p.title || '(untitled PR)'),
|
|
352
|
+
meta: (p.buildStatus || 'no-build') + ' · ' + (p.reviewStatus || 'no-review'),
|
|
353
|
+
metaUrl: p.url || null,
|
|
354
|
+
ts: ts ? Date.parse(ts) || 0 : 0,
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
events.sort(function(a, b) { return (b.ts || 0) - (a.ts || 0); });
|
|
359
|
+
events = events.slice(0, 20);
|
|
360
|
+
|
|
361
|
+
if (events.length === 0) {
|
|
362
|
+
var empty = document.createElement('div');
|
|
363
|
+
empty.className = 'history-empty';
|
|
364
|
+
empty.textContent = 'Nothing has happened yet — Command Center will fill this in.';
|
|
365
|
+
listEl.replaceChildren(empty);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
// Build the feed as DOM nodes so SEC-03's .innerHTML ratchet stays happy
|
|
369
|
+
// (test/unit.test.js DYNAMIC_INNERHTML_BASELINE). Completion / failure
|
|
370
|
+
// events use renderCompletionCard (meta-rail layout); dispatch / PR
|
|
371
|
+
// events keep the legacy .history-head + .history-title + .history-meta
|
|
372
|
+
// shape since the user-facing redesign is scoped to completions for now.
|
|
373
|
+
var frag = document.createDocumentFragment();
|
|
374
|
+
for (var i = 0; i < events.length; i++) {
|
|
375
|
+
var e = events[i];
|
|
376
|
+
if ((e.kind === 'completion' || e.kind === 'failure') && e.raw) {
|
|
377
|
+
frag.appendChild(renderCompletionCard(e.raw));
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
var item = document.createElement('div');
|
|
381
|
+
item.className = 'history-item kind-' + e.kind;
|
|
382
|
+
var head = document.createElement('div');
|
|
383
|
+
head.className = 'history-head';
|
|
384
|
+
var kind = document.createElement('span');
|
|
385
|
+
kind.className = 'history-kind';
|
|
386
|
+
kind.textContent = e.kindLabel;
|
|
387
|
+
var when = document.createElement('span');
|
|
388
|
+
when.className = 'history-time';
|
|
389
|
+
when.textContent = e.ts ? relTime(e.ts) : '';
|
|
390
|
+
head.appendChild(kind);
|
|
391
|
+
head.appendChild(when);
|
|
392
|
+
var titleEl = document.createElement('div');
|
|
393
|
+
titleEl.className = 'history-title';
|
|
394
|
+
titleEl.textContent = e.title;
|
|
395
|
+
var metaEl = document.createElement('div');
|
|
396
|
+
metaEl.className = 'history-meta';
|
|
397
|
+
metaEl.appendChild(document.createTextNode(e.meta || ''));
|
|
398
|
+
if (e.metaUrl) {
|
|
399
|
+
metaEl.appendChild(document.createTextNode(' · '));
|
|
400
|
+
var a = document.createElement('a');
|
|
401
|
+
a.href = e.metaUrl;
|
|
402
|
+
a.target = '_blank';
|
|
403
|
+
a.rel = 'noopener';
|
|
404
|
+
a.textContent = 'open';
|
|
405
|
+
metaEl.appendChild(a);
|
|
406
|
+
}
|
|
407
|
+
item.appendChild(head);
|
|
408
|
+
item.appendChild(titleEl);
|
|
409
|
+
item.appendChild(metaEl);
|
|
410
|
+
frag.appendChild(item);
|
|
411
|
+
}
|
|
412
|
+
listEl.replaceChildren(frag);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function pollStatusOnce() {
|
|
416
|
+
try {
|
|
417
|
+
var res = await fetch('/api/status', { headers: { 'Accept': 'application/json' } });
|
|
418
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
419
|
+
var data = await res.json();
|
|
420
|
+
applyStatus(data);
|
|
421
|
+
} catch (e) {
|
|
422
|
+
// Soft failure — leave previous values, surface in stamp
|
|
423
|
+
var stamp = document.getElementById('cockpit-stamp');
|
|
424
|
+
if (stamp) stamp.textContent = 'status fetch failed: ' + (e.message || e);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function startStatusPolling() {
|
|
429
|
+
pollStatusOnce();
|
|
430
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
431
|
+
pollTimer = setInterval(pollStatusOnce, STATUS_POLL_MS);
|
|
432
|
+
}
|
|
433
|
+
// Pause polling when the tab is hidden; resume when it's visible again.
|
|
434
|
+
// Saves us 12 fetches/min on background tabs.
|
|
435
|
+
document.addEventListener('visibilitychange', function() {
|
|
436
|
+
if (document.hidden) {
|
|
437
|
+
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
|
438
|
+
} else {
|
|
439
|
+
startStatusPolling();
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
startStatusPolling();
|