@yemi33/minions 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/dashboard/js/command-center.js +377 -0
- package/dashboard/js/command-history.js +70 -0
- package/dashboard/js/command-input.js +268 -0
- package/dashboard/js/command-parser.js +129 -0
- package/dashboard/js/detail-panel.js +98 -0
- package/dashboard/js/live-stream.js +69 -0
- package/dashboard/js/modal-qa.js +268 -0
- package/dashboard/js/modal.js +131 -0
- package/dashboard/js/refresh.js +59 -0
- package/dashboard/js/render-agents.js +17 -0
- package/dashboard/js/render-dispatch.js +148 -0
- package/dashboard/js/render-inbox.js +126 -0
- package/dashboard/js/render-kb.js +107 -0
- package/dashboard/js/render-other.js +181 -0
- package/dashboard/js/render-plans.js +304 -0
- package/dashboard/js/render-prd.js +469 -0
- package/dashboard/js/render-prs.js +94 -0
- package/dashboard/js/render-schedules.js +158 -0
- package/dashboard/js/render-skills.js +89 -0
- package/dashboard/js/render-work-items.js +219 -0
- package/dashboard/js/settings.js +135 -0
- package/dashboard/js/state.js +84 -0
- package/dashboard/js/utils.js +39 -0
- package/dashboard/layout.html +123 -0
- package/dashboard/pages/engine.html +12 -0
- package/dashboard/pages/home.html +31 -0
- package/dashboard/pages/inbox.html +17 -0
- package/dashboard/pages/plans.html +4 -0
- package/dashboard/pages/prd.html +5 -0
- package/dashboard/pages/prs.html +4 -0
- package/dashboard/pages/schedule.html +10 -0
- package/dashboard/pages/work.html +5 -0
- package/dashboard/styles.css +598 -0
- package/dashboard-build.js +51 -0
- package/dashboard.js +44 -1
- package/package.json +1 -1
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// modal-qa.js — Modal Q&A (document chat) functions extracted from dashboard.html
|
|
2
|
+
|
|
3
|
+
let _modalDocContext = { title: '', content: '', selection: '' };
|
|
4
|
+
let _modalFilePath = null; // file path for steering (null = read-only Q&A only)
|
|
5
|
+
let _modalOriginalPlan = null; // tracks original plan file when editing a forked version
|
|
6
|
+
|
|
7
|
+
function showModalQa() {
|
|
8
|
+
document.getElementById('modal-qa').style.display = '';
|
|
9
|
+
}
|
|
10
|
+
let _qaHistory = []; // multi-turn conversation history [{role:'user',text:''},{role:'assistant',text:''}]
|
|
11
|
+
let _qaProcessing = false; // true while waiting for Haiku response
|
|
12
|
+
let _qaQueue = []; // queued messages while processing
|
|
13
|
+
let _qaSessionKey = ''; // key for current conversation (title or filePath)
|
|
14
|
+
const _qaSessions = new Map(); // persist conversations across modal open/close {key → {history, threadHtml}}
|
|
15
|
+
// Restore from localStorage
|
|
16
|
+
try {
|
|
17
|
+
const saved = JSON.parse(localStorage.getItem('qa-sessions') || '{}');
|
|
18
|
+
for (const [k, v] of Object.entries(saved)) _qaSessions.set(k, v);
|
|
19
|
+
} catch {}
|
|
20
|
+
function _saveQaSessions() {
|
|
21
|
+
try {
|
|
22
|
+
const obj = {};
|
|
23
|
+
// Only persist last 10 sessions, cap threadHtml at 50KB each
|
|
24
|
+
const entries = [..._qaSessions.entries()].slice(-10);
|
|
25
|
+
for (const [k, v] of entries) obj[k] = { ...v, threadHtml: (v.threadHtml || '').slice(0, 50000) };
|
|
26
|
+
localStorage.setItem('qa-sessions', JSON.stringify(obj));
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function modalAskAboutSelection() {
|
|
31
|
+
document.getElementById('ask-selection-btn').style.display = 'none';
|
|
32
|
+
|
|
33
|
+
// If the modal isn't open but we have a selection (from detail panel), open modal for Q&A
|
|
34
|
+
const modal = document.getElementById('modal');
|
|
35
|
+
if (!modal.classList.contains('open')) {
|
|
36
|
+
document.getElementById('modal-title').textContent = 'Q&A: ' + (_modalDocContext.title || 'Document');
|
|
37
|
+
document.getElementById('modal-body').textContent = _modalDocContext.content.slice(0, 3000) + (_modalDocContext.content.length > 3000 ? '\n\n...(truncated for display)' : '');
|
|
38
|
+
modal.classList.add('open');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Show the selection pill
|
|
42
|
+
const pill = document.getElementById('modal-qa-pill');
|
|
43
|
+
const pillText = document.getElementById('modal-qa-pill-text');
|
|
44
|
+
const sel = _modalDocContext.selection || '';
|
|
45
|
+
if (sel) {
|
|
46
|
+
pillText.textContent = sel.slice(0, 80) + (sel.length > 80 ? '...' : '');
|
|
47
|
+
pill.style.display = 'flex';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const input = document.getElementById('modal-qa-input');
|
|
51
|
+
input.value = '';
|
|
52
|
+
input.placeholder = 'What do you want to know about this?';
|
|
53
|
+
input.focus();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function clearQaSelection() {
|
|
57
|
+
_modalDocContext.selection = '';
|
|
58
|
+
document.getElementById('modal-qa-pill').style.display = 'none';
|
|
59
|
+
document.getElementById('modal-qa-input').placeholder = 'Ask about this document (or select text first)...';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
function _initQaSession() {
|
|
64
|
+
var key = _modalFilePath || _modalDocContext.title || '';
|
|
65
|
+
if (!key || _qaSessionKey === key) return;
|
|
66
|
+
_qaSessionKey = key;
|
|
67
|
+
// Clear notification badge on the source card when reopening
|
|
68
|
+
const card = findCardForFile(_modalFilePath);
|
|
69
|
+
if (card) clearNotifBadge(card);
|
|
70
|
+
var prior = _qaSessions.get(key);
|
|
71
|
+
if (prior) {
|
|
72
|
+
_qaHistory = prior.history;
|
|
73
|
+
document.getElementById('modal-qa-thread').innerHTML = prior.threadHtml;
|
|
74
|
+
if (prior.docContext) {
|
|
75
|
+
// Preserve freshly-fetched content and title — prior session may have stale/empty content
|
|
76
|
+
const freshContent = _modalDocContext.content;
|
|
77
|
+
const freshTitle = _modalDocContext.title;
|
|
78
|
+
_modalDocContext = Object.assign({}, prior.docContext, {
|
|
79
|
+
selection: _modalDocContext.selection,
|
|
80
|
+
content: freshContent || prior.docContext.content || '',
|
|
81
|
+
title: freshTitle || prior.docContext.title || '',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (prior.filePath) { _modalFilePath = prior.filePath; showModalQa(); }
|
|
85
|
+
document.getElementById('qa-clear-btn').style.display = 'block';
|
|
86
|
+
} else {
|
|
87
|
+
_qaHistory = [];
|
|
88
|
+
document.getElementById('modal-qa-thread').innerHTML = '';
|
|
89
|
+
document.getElementById('qa-clear-btn').style.display = 'none';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function clearQaConversation() {
|
|
94
|
+
_qaHistory = [];
|
|
95
|
+
_qaQueue = [];
|
|
96
|
+
_qaProcessing = false;
|
|
97
|
+
document.getElementById('modal-qa-thread').innerHTML = '';
|
|
98
|
+
document.getElementById('qa-clear-btn').style.display = 'none';
|
|
99
|
+
if (_qaSessionKey) _qaSessions.delete(_qaSessionKey);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function modalSend() {
|
|
103
|
+
var input = document.getElementById('modal-qa-input');
|
|
104
|
+
var message = input.value.trim();
|
|
105
|
+
if (!message) return;
|
|
106
|
+
|
|
107
|
+
if (!_modalDocContext.content) {
|
|
108
|
+
var body = document.getElementById('modal-body');
|
|
109
|
+
if (body) {
|
|
110
|
+
_modalDocContext.content = body.textContent || body.innerText || '';
|
|
111
|
+
_modalDocContext.title = document.getElementById('modal-title')?.textContent || '';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!_modalDocContext.content) {
|
|
115
|
+
showToast('cmd-toast', 'No document content', false);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_initQaSession();
|
|
120
|
+
document.getElementById('qa-clear-btn').style.display = 'block';
|
|
121
|
+
|
|
122
|
+
var thread = document.getElementById('modal-qa-thread');
|
|
123
|
+
const selection = _modalDocContext.selection || '';
|
|
124
|
+
|
|
125
|
+
// Show message in thread immediately
|
|
126
|
+
let qHtml = '<div class="modal-qa-q">' + escHtml(message);
|
|
127
|
+
if (selection) {
|
|
128
|
+
qHtml += '<span class="selection-ref">Re: "' + escHtml(selection.slice(0, 100)) + ((selection.length > 100) ? '...' : '') + '"</span>';
|
|
129
|
+
}
|
|
130
|
+
qHtml += '</div>';
|
|
131
|
+
thread.innerHTML += qHtml;
|
|
132
|
+
thread.scrollTop = thread.scrollHeight;
|
|
133
|
+
|
|
134
|
+
// Clear input immediately so user can type next message
|
|
135
|
+
input.value = '';
|
|
136
|
+
_modalDocContext.selection = '';
|
|
137
|
+
document.getElementById('modal-qa-pill').style.display = 'none';
|
|
138
|
+
|
|
139
|
+
if (_qaProcessing) {
|
|
140
|
+
// Queue the message — show it as "queued" in the thread
|
|
141
|
+
_qaQueue.push({ message, selection });
|
|
142
|
+
const preview = message.split(/\s+/).slice(0, 6).join(' ') + (message.split(/\s+/).length > 6 ? '...' : '');
|
|
143
|
+
thread.innerHTML += '<div class="modal-qa-loading" style="color:var(--muted);font-size:10px">Queued: "' + escHtml(preview) + '" — will send after current response</div>';
|
|
144
|
+
thread.scrollTop = thread.scrollHeight;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_processQaMessage(message, selection);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function _processQaMessage(message, selection) {
|
|
152
|
+
const thread = document.getElementById('modal-qa-thread');
|
|
153
|
+
const btn = document.getElementById('modal-send-btn');
|
|
154
|
+
_qaProcessing = true;
|
|
155
|
+
|
|
156
|
+
// Capture state now — closeModal may null these while we're awaiting
|
|
157
|
+
const capturedFilePath = _modalFilePath;
|
|
158
|
+
const capturedDocContext = { ..._modalDocContext };
|
|
159
|
+
|
|
160
|
+
const loadingId = 'chat-loading-' + Date.now();
|
|
161
|
+
const qaQueueBadge = _qaQueue.length > 0 ? ' <span style="font-size:9px;color:var(--muted);background:var(--surface);padding:1px 5px;border-radius:8px;border:1px solid var(--border)">+' + _qaQueue.length + ' queued</span>' : '';
|
|
162
|
+
thread.innerHTML += '<div class="modal-qa-loading" id="' + loadingId + '">' +
|
|
163
|
+
'<div class="dot-pulse"><span></span><span></span><span></span></div> ' +
|
|
164
|
+
'<span id="' + loadingId + '-text">Thinking...</span> ' +
|
|
165
|
+
'<span id="' + loadingId + '-time" style="font-size:10px;color:var(--muted)"></span>' +
|
|
166
|
+
qaQueueBadge + '</div>';
|
|
167
|
+
thread.scrollTop = thread.scrollHeight;
|
|
168
|
+
|
|
169
|
+
const isPlanEdit = _modalFilePath && _modalFilePath.match(/^plans\/.*\.md$/);
|
|
170
|
+
const qaStartTime = Date.now();
|
|
171
|
+
const qaPhases = isPlanEdit
|
|
172
|
+
? [[0,'Reading plan...'],[3000,'Analyzing structure...'],[8000,'Researching context...'],[15000,'Drafting revisions...'],[30000,'Writing updated plan...'],[60000,'Still working (large document)...'],[120000,'Deep edit in progress...'],[300000,'Almost there...']]
|
|
173
|
+
: [[0,'Thinking...'],[3000,'Reading document...'],[8000,'Analyzing...'],[20000,'Still working...'],[60000,'Taking a while...']];
|
|
174
|
+
const qaTimer = setInterval(() => {
|
|
175
|
+
const elapsed = Date.now() - qaStartTime;
|
|
176
|
+
const timeEl = document.getElementById(loadingId + '-time');
|
|
177
|
+
const textEl = document.getElementById(loadingId + '-text');
|
|
178
|
+
if (timeEl) timeEl.textContent = Math.floor(elapsed / 1000) + 's';
|
|
179
|
+
if (textEl) { for (let i = qaPhases.length - 1; i >= 0; i--) { if (elapsed >= qaPhases[i][0]) { textEl.textContent = qaPhases[i][1]; break; } } }
|
|
180
|
+
}, 500);
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const res = await fetch('/api/doc-chat', {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: { 'Content-Type': 'application/json' },
|
|
186
|
+
body: JSON.stringify({
|
|
187
|
+
message,
|
|
188
|
+
document: capturedDocContext.content,
|
|
189
|
+
title: capturedDocContext.title,
|
|
190
|
+
selection: selection,
|
|
191
|
+
filePath: capturedFilePath || null,
|
|
192
|
+
}),
|
|
193
|
+
});
|
|
194
|
+
const data = await res.json();
|
|
195
|
+
clearInterval(qaTimer);
|
|
196
|
+
const loadingEl = document.getElementById(loadingId);
|
|
197
|
+
if (loadingEl) loadingEl.remove();
|
|
198
|
+
|
|
199
|
+
if (data.ok) {
|
|
200
|
+
const borderColor = data.edited ? 'var(--green)' : 'var(--blue)';
|
|
201
|
+
const suffix = data.edited ? '\n\n\u2713 Document saved.' : '';
|
|
202
|
+
const qaElapsed = Math.round((Date.now() - qaStartTime) / 1000);
|
|
203
|
+
const qaTimeLabel = '<div style="font-size:9px;color:var(--muted);margin-top:4px;text-align:right">' + qaElapsed + 's</div>';
|
|
204
|
+
thread.innerHTML += '<div class="modal-qa-a" style="border-left-color:' + borderColor + '">' + llmCopyBtn() + escHtml(data.answer + suffix) + qaTimeLabel + '</div>';
|
|
205
|
+
|
|
206
|
+
// Track conversation history
|
|
207
|
+
_qaHistory.push({ role: 'user', text: message });
|
|
208
|
+
_qaHistory.push({ role: 'assistant', text: data.answer });
|
|
209
|
+
|
|
210
|
+
// Execute any CC actions (dispatch, note, etc.)
|
|
211
|
+
if (data.actions && data.actions.length > 0) {
|
|
212
|
+
for (const action of data.actions) { await ccExecuteAction(action); }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Refresh modal body if document was edited
|
|
216
|
+
if (data.edited && data.content) {
|
|
217
|
+
const display = data.content.replace(/^---[\s\S]*?---\n*/m, '');
|
|
218
|
+
document.getElementById('modal-body').textContent = display;
|
|
219
|
+
_modalDocContext.content = display;
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
const qaElapsedErr = Math.round((Date.now() - qaStartTime) / 1000);
|
|
223
|
+
thread.innerHTML += '<div class="modal-qa-a" style="color:var(--red)">Error: ' + escHtml(data.error || 'Failed') + '<div style="font-size:9px;color:var(--muted);margin-top:4px;text-align:right">' + qaElapsedErr + 's</div></div>';
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
clearInterval(qaTimer);
|
|
227
|
+
const loadingEl = document.getElementById(loadingId);
|
|
228
|
+
if (loadingEl) loadingEl.remove();
|
|
229
|
+
const qaElapsedExc = Math.round((Date.now() - qaStartTime) / 1000);
|
|
230
|
+
thread.innerHTML += '<div class="modal-qa-a" style="color:var(--red)">Error: ' + escHtml(e.message) + '<div style="font-size:9px;color:var(--muted);margin-top:4px;text-align:right">' + qaElapsedExc + 's</div></div>';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
_qaProcessing = false;
|
|
234
|
+
thread.scrollTop = thread.scrollHeight;
|
|
235
|
+
|
|
236
|
+
// Save session (persists even if modal was closed during processing)
|
|
237
|
+
const modalIsOpen = document.getElementById('modal').classList.contains('open');
|
|
238
|
+
if (_qaSessionKey) {
|
|
239
|
+
// Use captured values if closeModal nulled the globals during processing
|
|
240
|
+
const sessionFilePath = _modalFilePath || capturedFilePath;
|
|
241
|
+
const sessionDocContext = _modalDocContext.title ? { ..._modalDocContext } : { ...capturedDocContext, ..._modalDocContext, title: capturedDocContext.title };
|
|
242
|
+
_qaSessions.set(_qaSessionKey, {
|
|
243
|
+
history: _qaHistory,
|
|
244
|
+
threadHtml: thread.innerHTML,
|
|
245
|
+
docContext: sessionDocContext,
|
|
246
|
+
filePath: sessionFilePath,
|
|
247
|
+
});
|
|
248
|
+
_saveQaSessions();
|
|
249
|
+
// If modal was closed while processing, show notification badge on source card
|
|
250
|
+
if (!modalIsOpen) {
|
|
251
|
+
const card = findCardForFile(sessionFilePath);
|
|
252
|
+
if (card) showNotifBadge(card);
|
|
253
|
+
_qaSessionKey = '';
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Process next queued message
|
|
258
|
+
if (_qaQueue.length > 0) {
|
|
259
|
+
const next = _qaQueue.shift();
|
|
260
|
+
const queuedEls = thread.querySelectorAll('.modal-qa-loading');
|
|
261
|
+
for (const el of queuedEls) {
|
|
262
|
+
if (el.textContent.includes('Queued')) { el.remove(); break; }
|
|
263
|
+
}
|
|
264
|
+
_processQaMessage(next.message, next.selection);
|
|
265
|
+
} else if (modalIsOpen) {
|
|
266
|
+
document.getElementById('modal-qa-input')?.focus();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// modal.js — Modal and notification badge functions extracted from dashboard.html
|
|
2
|
+
|
|
3
|
+
function closeModal() {
|
|
4
|
+
const modalEl = document.querySelector('#modal .modal');
|
|
5
|
+
if (modalEl) modalEl.classList.remove('modal-wide');
|
|
6
|
+
document.getElementById('modal').classList.remove('open');
|
|
7
|
+
// Hide Q&A section (only shown for document modals)
|
|
8
|
+
document.getElementById('modal-qa').style.display = 'none';
|
|
9
|
+
// Remove settings save button if present
|
|
10
|
+
const settingsBtn = document.getElementById('modal-settings-save');
|
|
11
|
+
if (settingsBtn) settingsBtn.remove();
|
|
12
|
+
// Save Q&A session for this document (persist across modal open/close)
|
|
13
|
+
if (_qaSessionKey && (_qaHistory.length > 0 || _qaQueue.length > 0)) {
|
|
14
|
+
_qaSessions.set(_qaSessionKey, {
|
|
15
|
+
history: _qaHistory,
|
|
16
|
+
threadHtml: document.getElementById('modal-qa-thread').innerHTML,
|
|
17
|
+
docContext: { ..._modalDocContext },
|
|
18
|
+
filePath: _modalFilePath,
|
|
19
|
+
});
|
|
20
|
+
_saveQaSessions();
|
|
21
|
+
}
|
|
22
|
+
// If still processing, show animated badge on the source card
|
|
23
|
+
if (_qaProcessing && _modalFilePath) {
|
|
24
|
+
const card = findCardForFile(_modalFilePath);
|
|
25
|
+
if (card) showNotifBadge(card, 'processing');
|
|
26
|
+
}
|
|
27
|
+
// Reset UI state but don't kill processing/queue — they run in background
|
|
28
|
+
_modalDocContext = { title: '', content: '', selection: '' };
|
|
29
|
+
// Keep session key alive if processing is in flight — result will save when it completes
|
|
30
|
+
if (!_qaProcessing) _qaSessionKey = '';
|
|
31
|
+
document.getElementById('modal-qa-input').value = '';
|
|
32
|
+
document.getElementById('modal-qa-input').placeholder = 'Ask about this document (or select text first)...';
|
|
33
|
+
document.getElementById('modal-qa-pill').style.display = 'none';
|
|
34
|
+
document.getElementById('ask-selection-btn').style.display = 'none';
|
|
35
|
+
// Clear edit/steer state
|
|
36
|
+
_modalEditable = null;
|
|
37
|
+
_modalFilePath = null;
|
|
38
|
+
_modalOriginalPlan = null;
|
|
39
|
+
// steer btn removed — unified send
|
|
40
|
+
const body = document.getElementById('modal-body');
|
|
41
|
+
body.contentEditable = 'false';
|
|
42
|
+
body.style.border = '';
|
|
43
|
+
body.style.padding = '';
|
|
44
|
+
document.getElementById('modal-edit-btn').style.display = 'none';
|
|
45
|
+
document.getElementById('modal-save-btn').style.display = 'none';
|
|
46
|
+
document.getElementById('modal-cancel-edit-btn').style.display = 'none';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function copyModalContent() {
|
|
50
|
+
const body = document.getElementById('modal-body');
|
|
51
|
+
const btn = document.getElementById('modal-copy-btn');
|
|
52
|
+
navigator.clipboard.writeText(body.textContent).then(() => {
|
|
53
|
+
btn.classList.add('copied');
|
|
54
|
+
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';
|
|
55
|
+
setTimeout(() => {
|
|
56
|
+
btn.classList.remove('copied');
|
|
57
|
+
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';
|
|
58
|
+
}, 2000);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Notification Badges ──────────────────────────────────────────────────────
|
|
63
|
+
// Show a red dot on a card/button when a background response arrives
|
|
64
|
+
// _activeBadges tracks badges by filePath so they survive DOM re-renders
|
|
65
|
+
const _activeBadges = new Map(); // filePath → state ('done' | 'processing')
|
|
66
|
+
|
|
67
|
+
function showNotifBadge(targetEl, state) {
|
|
68
|
+
if (!targetEl) return;
|
|
69
|
+
clearNotifBadge(targetEl);
|
|
70
|
+
targetEl.style.position = 'relative';
|
|
71
|
+
const badge = document.createElement('span');
|
|
72
|
+
badge.className = 'notif-badge ' + (state || 'done');
|
|
73
|
+
if (state === 'processing') {
|
|
74
|
+
badge.innerHTML = '<span></span><span></span><span></span>';
|
|
75
|
+
}
|
|
76
|
+
targetEl.appendChild(badge);
|
|
77
|
+
// Track by data-file so badge can be restored after DOM re-render
|
|
78
|
+
const fileKey = targetEl.getAttribute('data-file');
|
|
79
|
+
if (fileKey) _activeBadges.set(fileKey, state || 'done');
|
|
80
|
+
}
|
|
81
|
+
function clearNotifBadge(targetEl) {
|
|
82
|
+
if (!targetEl) return;
|
|
83
|
+
const dot = targetEl.querySelector('.notif-badge');
|
|
84
|
+
if (dot) dot.remove();
|
|
85
|
+
const fileKey = targetEl.getAttribute('data-file');
|
|
86
|
+
if (fileKey) _activeBadges.delete(fileKey);
|
|
87
|
+
}
|
|
88
|
+
// Re-apply tracked badges after DOM re-renders (called after renderInbox, renderPlans, renderKnowledgeBase)
|
|
89
|
+
function restoreNotifBadges() {
|
|
90
|
+
for (const [filePath, state] of _activeBadges) {
|
|
91
|
+
const card = findCardForFile(filePath);
|
|
92
|
+
if (card && !card.querySelector('.notif-badge')) {
|
|
93
|
+
card.style.position = 'relative';
|
|
94
|
+
const badge = document.createElement('span');
|
|
95
|
+
badge.className = 'notif-badge ' + state;
|
|
96
|
+
if (state === 'processing') {
|
|
97
|
+
badge.innerHTML = '<span></span><span></span><span></span>';
|
|
98
|
+
}
|
|
99
|
+
card.appendChild(badge);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Find the plan/KB/inbox card that matches a filePath
|
|
104
|
+
function findCardForFile(filePath) {
|
|
105
|
+
if (!filePath) return null;
|
|
106
|
+
// Direct match by data-file attribute
|
|
107
|
+
var card = document.querySelector('[data-file="' + CSS.escape(filePath) + '"]');
|
|
108
|
+
if (card) return card;
|
|
109
|
+
// Plan cards may have data-file="plans/x.json" but filePath="prd/x.json" (PRD variant)
|
|
110
|
+
if (filePath.startsWith('prd/')) {
|
|
111
|
+
card = document.querySelector('[data-file="' + CSS.escape('plans/' + filePath.slice(4)) + '"]');
|
|
112
|
+
if (card) return card;
|
|
113
|
+
}
|
|
114
|
+
// Archived items: badge the "View Archives" button
|
|
115
|
+
if (filePath.includes('archive')) {
|
|
116
|
+
if (filePath.startsWith('prd/') || filePath.startsWith('plans/')) {
|
|
117
|
+
card = document.querySelector('[data-file="prd-archives"]') || document.querySelector('[data-file="plan-archives"]');
|
|
118
|
+
if (card) return card;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function renderArchiveButtons(archives) {
|
|
125
|
+
archivedPrds = archives;
|
|
126
|
+
const el = document.getElementById('archive-btns');
|
|
127
|
+
if (!archives.length) { el.innerHTML = ''; return; }
|
|
128
|
+
el.innerHTML = archives.map((a, i) =>
|
|
129
|
+
'<button class="archive-btn" onclick="openArchive(' + i + ')">Archived: ' + escHtml(a.version) + ' (' + a.total + ' items)</button>'
|
|
130
|
+
).join(' ');
|
|
131
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// refresh.js — Main refresh loop and initialization extracted from dashboard.html
|
|
2
|
+
|
|
3
|
+
async function refresh() {
|
|
4
|
+
try {
|
|
5
|
+
const data = await fetch('/api/status').then(r => r.json());
|
|
6
|
+
// Detect fresh install — clear stale browser state if install ID changed
|
|
7
|
+
if (data.installId) {
|
|
8
|
+
const prev = localStorage.getItem('minions-install-id');
|
|
9
|
+
if (prev && prev !== data.installId) {
|
|
10
|
+
localStorage.clear();
|
|
11
|
+
console.log('Minions: fresh install detected, cleared browser state');
|
|
12
|
+
}
|
|
13
|
+
localStorage.setItem('minions-install-id', data.installId);
|
|
14
|
+
}
|
|
15
|
+
document.getElementById('ts').textContent = new Date(data.timestamp).toLocaleTimeString();
|
|
16
|
+
const engineState = (data.engine && data.engine.state) ? data.engine.state : 'stopped';
|
|
17
|
+
document.getElementById('setup-banner').style.display = (!data.initialized && engineState !== 'stopped') ? 'block' : 'none';
|
|
18
|
+
renderAgents(data.agents);
|
|
19
|
+
renderPrdProgress(data.prdProgress);
|
|
20
|
+
_cachePrdItems(data.prdProgress);
|
|
21
|
+
renderInbox(data.inbox);
|
|
22
|
+
cmdUpdateAgentList(data.agents);
|
|
23
|
+
cmdUpdateProjectList(data.projects || []);
|
|
24
|
+
renderNotes(data.notes);
|
|
25
|
+
renderPrd(data.prd, data.prdProgress);
|
|
26
|
+
renderPrs(data.pullRequests || []);
|
|
27
|
+
renderArchiveButtons(data.archivedPrds || []);
|
|
28
|
+
renderEngineStatus(data.engine);
|
|
29
|
+
renderDispatch(data.dispatch);
|
|
30
|
+
window._lastDispatch = data.dispatch;
|
|
31
|
+
window._lastWorkItems = data.workItems || [];
|
|
32
|
+
window._lastStatus = data;
|
|
33
|
+
prunePrdRequeueState(window._lastWorkItems);
|
|
34
|
+
renderEngineLog(data.engineLog || []);
|
|
35
|
+
renderProjects(data.projects || []);
|
|
36
|
+
renderMetrics(data.metrics || {});
|
|
37
|
+
renderWorkItems(data.workItems || []);
|
|
38
|
+
renderSkills(data.skills || []);
|
|
39
|
+
renderMcpServers(data.mcpServers || []);
|
|
40
|
+
renderSchedules(data.schedules || []);
|
|
41
|
+
// Update sidebar counts
|
|
42
|
+
const swi = document.getElementById('sidebar-wi');
|
|
43
|
+
if (swi) swi.textContent = (data.workItems || []).length || '';
|
|
44
|
+
const spr = document.getElementById('sidebar-pr');
|
|
45
|
+
if (spr) spr.textContent = (data.pullRequests || []).length || '';
|
|
46
|
+
// Refresh KB and plans less frequently (every 3rd cycle = ~12s)
|
|
47
|
+
if (!window._kbRefreshCount) window._kbRefreshCount = 0;
|
|
48
|
+
if (window._kbRefreshCount++ % 3 === 0) { refreshKnowledgeBase(); refreshPlans(); }
|
|
49
|
+
} catch(e) { console.error('refresh error', e); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
refresh();
|
|
53
|
+
setInterval(refresh, 4000);
|
|
54
|
+
|
|
55
|
+
// Wire sidebar navigation
|
|
56
|
+
document.querySelectorAll('.sidebar-link').forEach(link => {
|
|
57
|
+
link.addEventListener('click', e => { e.preventDefault(); switchPage(link.dataset.page); });
|
|
58
|
+
});
|
|
59
|
+
switchPage(currentPage);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// dashboard/js/render-agents.js — Agent grid rendering extracted from dashboard.html
|
|
2
|
+
|
|
3
|
+
function renderAgents(agents) {
|
|
4
|
+
agentData = agents;
|
|
5
|
+
const grid = document.getElementById('agents-grid');
|
|
6
|
+
grid.innerHTML = agents.map(a => `
|
|
7
|
+
<div class="agent-card ${statusColor(a.status)}" onclick="openAgentDetail('${a.id}')">
|
|
8
|
+
<div class="agent-card-header">
|
|
9
|
+
<span class="agent-name"><span class="agent-emoji">${a.emoji}</span>${a.name}</span>
|
|
10
|
+
<span class="status-badge ${a.status}">${a.status}</span>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="agent-role">${a.role}</div>
|
|
13
|
+
<div class="agent-action" title="${escHtml(a.lastAction)}">${escHtml(a.lastAction)}</div>
|
|
14
|
+
${a.resultSummary ? `<div class="agent-result" title="${escHtml(a.resultSummary)}">${escHtml(a.resultSummary.slice(0, 200))}${a.resultSummary.length > 200 ? '...' : ''}</div>` : ''}
|
|
15
|
+
</div>
|
|
16
|
+
`).join('');
|
|
17
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// dashboard/js/render-dispatch.js — Engine status, dispatch, and log rendering extracted from dashboard.html
|
|
2
|
+
|
|
3
|
+
function renderEngineStatus(engine) {
|
|
4
|
+
const badge = document.getElementById('engine-badge');
|
|
5
|
+
let state = engine?.state || 'stopped';
|
|
6
|
+
let staleMs = 0;
|
|
7
|
+
|
|
8
|
+
// Detect stale engine — says running but heartbeat is old (>2 min)
|
|
9
|
+
if (state === 'running' && engine?.heartbeat) {
|
|
10
|
+
staleMs = Date.now() - engine.heartbeat;
|
|
11
|
+
if (staleMs > 120000) {
|
|
12
|
+
state = 'stale';
|
|
13
|
+
}
|
|
14
|
+
} else if (state === 'running' && !engine?.heartbeat) {
|
|
15
|
+
// Running but no heartbeat yet — engine just started or old version
|
|
16
|
+
state = 'running';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
badge.className = 'engine-badge ' + (state === 'stale' ? 'stopped' : state);
|
|
20
|
+
badge.textContent = state === 'stale' ? 'STALE' : state.toUpperCase();
|
|
21
|
+
badge.title = state === 'stale'
|
|
22
|
+
? 'Engine claims running but heartbeat is stale (>2min). It may have crashed. Run: node engine.js start'
|
|
23
|
+
: state === 'stopped' ? 'Engine is stopped. Run: node engine.js start' : '';
|
|
24
|
+
renderEngineAlert(state, staleMs);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function renderEngineAlert(state, staleMs) {
|
|
28
|
+
const el = document.getElementById('engine-alert');
|
|
29
|
+
if (!el) return;
|
|
30
|
+
if (state !== 'stale') {
|
|
31
|
+
el.style.display = 'none';
|
|
32
|
+
el.innerHTML = '';
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const mins = Math.max(1, Math.round(staleMs / 60000));
|
|
36
|
+
el.innerHTML =
|
|
37
|
+
'<span class="engine-alert-msg">⚠️ Engine heartbeat is stale (' + mins + 'm old). Dispatch may be stuck.</span>' +
|
|
38
|
+
'<span class="engine-alert-action" id="engine-alert-restart">Restart engine</span>';
|
|
39
|
+
document.getElementById('engine-alert-restart').onclick = async function() {
|
|
40
|
+
this.classList.add('clicked');
|
|
41
|
+
this.textContent = 'Restarting...';
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch('/api/engine/restart', { method: 'POST' });
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
if (data.ok) {
|
|
46
|
+
this.textContent = 'Restarted (PID ' + data.pid + ')';
|
|
47
|
+
setTimeout(() => refresh(), 3000);
|
|
48
|
+
} else {
|
|
49
|
+
this.textContent = 'Failed: ' + (data.error || 'unknown');
|
|
50
|
+
this.classList.remove('clicked');
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
this.textContent = 'Failed: ' + e.message;
|
|
54
|
+
this.classList.remove('clicked');
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
el.style.display = 'flex';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function renderDispatch(dispatch) {
|
|
61
|
+
if (!dispatch) return;
|
|
62
|
+
|
|
63
|
+
// Stats
|
|
64
|
+
const stats = document.getElementById('dispatch-stats');
|
|
65
|
+
stats.innerHTML =
|
|
66
|
+
'<div class="dispatch-stat"><div class="dispatch-stat-num yellow">' + (dispatch.active || []).length + '</div><div class="dispatch-stat-label">Active</div></div>' +
|
|
67
|
+
'<div class="dispatch-stat"><div class="dispatch-stat-num blue">' + (dispatch.pending || []).length + '</div><div class="dispatch-stat-label">Pending</div></div>' +
|
|
68
|
+
'<div class="dispatch-stat"><div class="dispatch-stat-num green">' + (dispatch.completed || []).length + '</div><div class="dispatch-stat-label">Completed</div></div>';
|
|
69
|
+
|
|
70
|
+
// Active
|
|
71
|
+
const activeEl = document.getElementById('dispatch-active');
|
|
72
|
+
if ((dispatch.active || []).length > 0) {
|
|
73
|
+
activeEl.innerHTML = '<div style="font-size:11px;color:var(--green);margin-bottom:6px;font-weight:600">ACTIVE</div><div class="dispatch-list">' +
|
|
74
|
+
dispatch.active.map(d =>
|
|
75
|
+
'<div class="dispatch-item">' +
|
|
76
|
+
'<span class="dispatch-type ' + (d.type || '') + '">' + escHtml(d.type || '') + '</span>' +
|
|
77
|
+
'<span class="dispatch-agent">' + escHtml(d.agentName || d.agent || '') + '</span>' +
|
|
78
|
+
'<span class="dispatch-task" title="' + escHtml(d.task || '') + '">' + escHtml(d.task || '') + '</span>' +
|
|
79
|
+
'<span class="dispatch-time">' + shortTime(d.started_at) + '</span>' +
|
|
80
|
+
'</div>'
|
|
81
|
+
).join('') + '</div>';
|
|
82
|
+
} else {
|
|
83
|
+
activeEl.innerHTML = '<div style="color:var(--muted);font-size:11px;margin-bottom:8px">No active dispatches</div>';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Pending
|
|
87
|
+
const pendingEl = document.getElementById('dispatch-pending');
|
|
88
|
+
if ((dispatch.pending || []).length > 0) {
|
|
89
|
+
pendingEl.innerHTML = '<div style="font-size:11px;color:var(--yellow);margin:8px 0 6px;font-weight:600">PENDING</div><div class="dispatch-list">' +
|
|
90
|
+
dispatch.pending.map(d =>
|
|
91
|
+
'<div class="dispatch-item">' +
|
|
92
|
+
'<span class="dispatch-type ' + (d.type || '') + '">' + escHtml(d.type || '') + '</span>' +
|
|
93
|
+
'<span class="dispatch-agent">' + escHtml(d.agentName || d.agent || '') + '</span>' +
|
|
94
|
+
'<span class="dispatch-task" title="' + escHtml(d.task || '') + '">' + escHtml(d.task || '') + '</span>' +
|
|
95
|
+
(d.skipReason ? '<span style="font-size:9px;color:var(--muted);margin-left:6px" title="' + escHtml(d.skipReason) + '">' + escHtml(d.skipReason.replace(/_/g, ' ')) + '</span>' : '') +
|
|
96
|
+
'</div>'
|
|
97
|
+
).join('') + '</div>';
|
|
98
|
+
} else {
|
|
99
|
+
pendingEl.innerHTML = '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Completed
|
|
103
|
+
const completedEl = document.getElementById('completed-content');
|
|
104
|
+
const completedCount = document.getElementById('completed-count');
|
|
105
|
+
const completed = (dispatch.completed || []).slice().reverse();
|
|
106
|
+
completedCount.textContent = completed.length;
|
|
107
|
+
|
|
108
|
+
if (completed.length > 0) {
|
|
109
|
+
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>' +
|
|
110
|
+
completed.map(d => {
|
|
111
|
+
const isError = d.result === 'error';
|
|
112
|
+
const agentId = (d.agent || '').toLowerCase();
|
|
113
|
+
const errorBtn = isError
|
|
114
|
+
? ' <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>'
|
|
115
|
+
: '';
|
|
116
|
+
return '<tr>' +
|
|
117
|
+
'<td style="font-family:Consolas;font-size:10px" title="' + escHtml(d.id || '') + '">' + escHtml(d.id || '') + '</td>' +
|
|
118
|
+
'<td><span class="dispatch-type ' + (d.type || '') + '">' + escHtml(d.type || '') + '</span></td>' +
|
|
119
|
+
'<td>' + escHtml(d.agentName || d.agent || '') + '</td>' +
|
|
120
|
+
'<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml((d.task || '').slice(0, 60)) + '</td>' +
|
|
121
|
+
'<td style="color:' + (d.result === 'success' ? 'var(--green)' : 'var(--red)') + '">' + escHtml(d.result || '') + errorBtn + '</td>' +
|
|
122
|
+
'<td class="pr-date">' + shortTime(d.completed_at) + '</td>' +
|
|
123
|
+
'</tr>';
|
|
124
|
+
}).join('') + '</tbody></table>';
|
|
125
|
+
} else {
|
|
126
|
+
completedEl.innerHTML = '<p class="empty">No completed dispatches yet.</p>';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderEngineLog(log) {
|
|
131
|
+
const el = document.getElementById('engine-log');
|
|
132
|
+
if (!log || log.length === 0) {
|
|
133
|
+
el.innerHTML = '<div class="empty">No log entries yet.</div>';
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
el.innerHTML = log.slice().reverse().map(e =>
|
|
137
|
+
'<div class="log-entry">' +
|
|
138
|
+
'<span class="log-ts">' + shortTime(e.timestamp) + '</span> ' +
|
|
139
|
+
'<span class="log-level-' + (e.level || 'info') + '">[' + (e.level || 'info') + ']</span> ' +
|
|
140
|
+
escHtml(e.message || '') +
|
|
141
|
+
'</div>'
|
|
142
|
+
).join('');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function shortTime(t) {
|
|
146
|
+
if (!t) return '';
|
|
147
|
+
try { return new Date(t).toLocaleTimeString(); } catch { return t; }
|
|
148
|
+
}
|