@yemi33/minions 0.1.11 → 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.
Files changed (44) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dashboard/js/command-center.js +377 -0
  3. package/dashboard/js/command-history.js +70 -0
  4. package/dashboard/js/command-input.js +268 -0
  5. package/dashboard/js/command-parser.js +129 -0
  6. package/dashboard/js/detail-panel.js +98 -0
  7. package/dashboard/js/live-stream.js +69 -0
  8. package/dashboard/js/modal-qa.js +268 -0
  9. package/dashboard/js/modal.js +131 -0
  10. package/dashboard/js/refresh.js +59 -0
  11. package/dashboard/js/render-agents.js +17 -0
  12. package/dashboard/js/render-dispatch.js +148 -0
  13. package/dashboard/js/render-inbox.js +126 -0
  14. package/dashboard/js/render-kb.js +107 -0
  15. package/dashboard/js/render-other.js +181 -0
  16. package/dashboard/js/render-plans.js +304 -0
  17. package/dashboard/js/render-prd.js +469 -0
  18. package/dashboard/js/render-prs.js +94 -0
  19. package/dashboard/js/render-schedules.js +158 -0
  20. package/dashboard/js/render-skills.js +89 -0
  21. package/dashboard/js/render-work-items.js +219 -0
  22. package/dashboard/js/settings.js +135 -0
  23. package/dashboard/js/state.js +84 -0
  24. package/dashboard/js/utils.js +39 -0
  25. package/dashboard/layout.html +123 -0
  26. package/dashboard/pages/engine.html +12 -0
  27. package/dashboard/pages/home.html +31 -0
  28. package/dashboard/pages/inbox.html +17 -0
  29. package/dashboard/pages/plans.html +4 -0
  30. package/dashboard/pages/prd.html +5 -0
  31. package/dashboard/pages/prs.html +4 -0
  32. package/dashboard/pages/schedule.html +10 -0
  33. package/dashboard/pages/work.html +5 -0
  34. package/dashboard/styles.css +598 -0
  35. package/dashboard-build.js +51 -0
  36. package/dashboard.html +179 -107
  37. package/dashboard.js +51 -1
  38. package/engine/ado.js +14 -0
  39. package/engine/cli.js +11 -0
  40. package/engine/github.js +14 -0
  41. package/engine/lifecycle.js +25 -29
  42. package/engine.js +106 -19
  43. package/package.json +1 -1
  44. package/routing.md +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,65 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.13 (2026-03-26)
4
+
5
+ ### Dashboard
6
+ - dashboard-build.js
7
+ - dashboard.js
8
+ - dashboard/js/command-center.js
9
+ - dashboard/js/command-history.js
10
+ - dashboard/js/command-input.js
11
+ - dashboard/js/command-parser.js
12
+ - dashboard/js/detail-panel.js
13
+ - dashboard/js/live-stream.js
14
+ - dashboard/js/modal-qa.js
15
+ - dashboard/js/modal.js
16
+ - dashboard/js/refresh.js
17
+ - dashboard/js/render-agents.js
18
+ - dashboard/js/render-dispatch.js
19
+ - dashboard/js/render-inbox.js
20
+ - dashboard/js/render-kb.js
21
+ - dashboard/js/render-other.js
22
+ - dashboard/js/render-plans.js
23
+ - dashboard/js/render-prd.js
24
+ - dashboard/js/render-prs.js
25
+ - dashboard/js/render-schedules.js
26
+ - dashboard/js/render-skills.js
27
+ - dashboard/js/render-work-items.js
28
+ - dashboard/js/settings.js
29
+ - dashboard/js/state.js
30
+ - dashboard/js/utils.js
31
+ - dashboard/layout.html
32
+ - dashboard/pages/engine.html
33
+ - dashboard/pages/home.html
34
+ - dashboard/pages/inbox.html
35
+ - dashboard/pages/plans.html
36
+ - dashboard/pages/prd.html
37
+ - dashboard/pages/prs.html
38
+ - dashboard/pages/schedule.html
39
+ - dashboard/pages/work.html
40
+ - dashboard/styles.css
41
+
42
+ ### Other
43
+ - test/unit.test.js
44
+
45
+ ## 0.1.12 (2026-03-26)
46
+
47
+ ### Engine
48
+ - engine.js
49
+ - engine/ado.js
50
+ - engine/cli.js
51
+ - engine/github.js
52
+ - engine/lifecycle.js
53
+
54
+ ### Dashboard
55
+ - dashboard.html
56
+ - dashboard.js
57
+
58
+ ### Other
59
+ - TODO.md
60
+ - routing.md
61
+ - test/unit.test.js
62
+
3
63
  ## 0.1.11 (2026-03-26)
4
64
 
5
65
  ### Engine
@@ -0,0 +1,377 @@
1
+ // command-center.js — Command center panel functions extracted from dashboard.html
2
+
3
+ let _ccSessionId = localStorage.getItem('cc-session-id') || null;
4
+ let _ccMessages = JSON.parse(localStorage.getItem('cc-messages') || '[]');
5
+ let _ccOpen = false;
6
+ let _ccSending = false;
7
+ let _ccQueue = [];
8
+
9
+ function toggleCommandCenter() {
10
+ _ccOpen = !_ccOpen;
11
+ const drawer = document.getElementById('cc-drawer');
12
+ drawer.style.display = _ccOpen ? 'flex' : 'none';
13
+ if (_ccOpen) {
14
+ clearNotifBadge(document.getElementById('cc-toggle-btn'));
15
+ document.getElementById('cc-input').focus();
16
+ ccRestoreMessages();
17
+ } else if (_ccSending) {
18
+ // Closing drawer while CC is processing — show animated badge
19
+ showNotifBadge(document.getElementById('cc-toggle-btn'), 'processing');
20
+ }
21
+ }
22
+
23
+ function ccNewSession() {
24
+ fetch('/api/command-center/new-session', { method: 'POST' }).catch(() => {});
25
+ _ccSessionId = null;
26
+ _ccMessages = [];
27
+ localStorage.removeItem('cc-session-id');
28
+ localStorage.removeItem('cc-messages');
29
+ document.getElementById('cc-messages').innerHTML = '';
30
+ ccUpdateSessionIndicator();
31
+ }
32
+
33
+ function ccRestoreMessages() {
34
+ const el = document.getElementById('cc-messages');
35
+ if (el.children.length > 0 || _ccMessages.length === 0) return; // Already rendered or nothing to restore
36
+ for (const msg of _ccMessages) {
37
+ ccAddMessage(msg.role, msg.html, true);
38
+ }
39
+ }
40
+
41
+ function ccSaveState() {
42
+ try {
43
+ if (_ccSessionId) localStorage.setItem('cc-session-id', _ccSessionId);
44
+ // Keep last 30 messages for display
45
+ const toSave = _ccMessages.slice(-30);
46
+ localStorage.setItem('cc-messages', JSON.stringify(toSave));
47
+ } catch {} // localStorage might be full
48
+ }
49
+
50
+ function ccUpdateSessionIndicator() {
51
+ const el = document.getElementById('cc-session-info');
52
+ if (!el) return;
53
+ if (_ccSessionId) {
54
+ const turns = _ccMessages.filter(m => m.role === 'user').length;
55
+ el.textContent = `Session: ${turns} turn${turns !== 1 ? 's' : ''}`;
56
+ el.style.color = 'var(--green)';
57
+ } else {
58
+ el.textContent = 'New session';
59
+ el.style.color = 'var(--muted)';
60
+ }
61
+ }
62
+
63
+ function ccAddMessage(role, html, skipSave) {
64
+ const el = document.getElementById('cc-messages');
65
+ const isUser = role === 'user';
66
+ const div = document.createElement('div');
67
+ const isAssistant = !isUser;
68
+ div.className = isAssistant ? 'cc-msg-assistant' : '';
69
+ div.style.cssText = 'padding:8px 12px;border-radius:8px;font-size:12px;line-height:1.6;max-width:95%;' +
70
+ (isUser ? 'background:var(--blue);color:#fff;align-self:flex-end' : 'background:var(--surface2);color:var(--text);align-self:flex-start;border:1px solid var(--border);position:relative');
71
+ div.innerHTML = (isAssistant && !html.includes('color:var(--red)') && !html.includes('cc-queued-pill') ? llmCopyBtn() : '') + html;
72
+ el.appendChild(div);
73
+ el.scrollTop = el.scrollHeight;
74
+ if (!skipSave) {
75
+ _ccMessages.push({ role, html });
76
+ ccSaveState();
77
+ }
78
+ }
79
+
80
+ async function ccSend() {
81
+ const input = document.getElementById('cc-input');
82
+ const message = input.value.trim();
83
+ if (!message) return;
84
+ input.value = '';
85
+
86
+ // If already processing, queue the message and show it — it'll send when current finishes
87
+ if (_ccSending) {
88
+ _ccQueue.push(message);
89
+ ccAddMessage('user', escHtml(message));
90
+ const preview = message.split(/\s+/).slice(0, 6).join(' ') + (message.split(/\s+/).length > 6 ? '...' : '');
91
+ ccAddMessage('assistant', '<span class="cc-queued-pill" style="color:var(--muted);font-size:10px">Queued: "' + escHtml(preview) + '" — will send after current request</span>');
92
+ return;
93
+ }
94
+ await _ccDoSend(message);
95
+
96
+ // Drain queue
97
+ while (_ccQueue.length > 0) {
98
+ const next = _ccQueue.shift();
99
+ // Remove the "Queued" placeholder for this message
100
+ const msgs = document.getElementById('cc-messages');
101
+ const queuedPills = msgs.querySelectorAll('.cc-queued-pill');
102
+ for (const pill of queuedPills) {
103
+ if (pill.closest('div')) { pill.closest('div').remove(); break; }
104
+ }
105
+ await _ccDoSend(next, true); // skipUserMsg=true since already shown when queued
106
+ }
107
+ }
108
+
109
+ async function _ccDoSend(message, skipUserMsg) {
110
+ _ccSending = true;
111
+
112
+ if (!skipUserMsg) ccAddMessage('user', escHtml(message));
113
+
114
+ // Show thinking indicator with timer + queue count
115
+ const queueCount = _ccQueue.length;
116
+ const queueBadge = queueCount > 0 ? ' <span style="font-size:9px;background:var(--surface);padding:1px 5px;border-radius:8px;border:1px solid var(--border)">+' + queueCount + ' queued</span>' : '';
117
+ const thinking = document.createElement('div');
118
+ thinking.id = 'cc-thinking';
119
+ thinking.style.cssText = 'padding:8px 12px;border-radius:8px;font-size:11px;color:var(--muted);align-self:flex-start;display:flex;align-items:center;gap:8px';
120
+ thinking.innerHTML = '<span class="dot-pulse" style="display:inline-flex;gap:3px"><span style="width:4px;height:4px;background:var(--blue);border-radius:50%;animation:dotPulse 1.2s infinite"></span><span style="width:4px;height:4px;background:var(--blue);border-radius:50%;animation:dotPulse 1.2s infinite;animation-delay:0.2s"></span><span style="width:4px;height:4px;background:var(--blue);border-radius:50%;animation:dotPulse 1.2s infinite;animation-delay:0.4s"></span></span> <span id="cc-thinking-text">Thinking...</span> <span id="cc-thinking-time" style="font-size:10px;color:var(--border)"></span>' + queueBadge;
121
+ document.getElementById('cc-messages').appendChild(thinking);
122
+ const ccMsgs = document.getElementById('cc-messages');
123
+ ccMsgs.scrollTop = ccMsgs.scrollHeight;
124
+
125
+ const ccStartTime = Date.now();
126
+ const phases = [
127
+ [0, 'Thinking...'],
128
+ [3000, 'Reading minions context...'],
129
+ [8000, 'Analyzing...'],
130
+ [15000, 'Using tools to dig deeper...'],
131
+ [30000, 'Still working (multi-turn)...'],
132
+ [60000, 'Deep research in progress...'],
133
+ [180000, 'Still going (this is unusually long)...'],
134
+ [300000, 'Timing out soon...'],
135
+ ];
136
+ const ccTimer = setInterval(() => {
137
+ const elapsed = Date.now() - ccStartTime;
138
+ const secs = Math.floor(elapsed / 1000);
139
+ const timeEl = document.getElementById('cc-thinking-time');
140
+ const textEl = document.getElementById('cc-thinking-text');
141
+ if (timeEl) timeEl.textContent = secs + 's';
142
+ if (textEl) {
143
+ for (let i = phases.length - 1; i >= 0; i--) {
144
+ if (elapsed >= phases[i][0]) { textEl.textContent = phases[i][1]; break; }
145
+ }
146
+ }
147
+ }, 500);
148
+
149
+ try {
150
+ const ccAbort = AbortSignal.timeout(960000);
151
+ const res = await fetch('/api/command-center', {
152
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
153
+ body: JSON.stringify({ message, sessionId: _ccSessionId }),
154
+ signal: ccAbort
155
+ });
156
+ const data = await res.json();
157
+
158
+ clearInterval(ccTimer);
159
+ thinking.remove();
160
+
161
+ if (data.error) {
162
+ ccAddMessage('assistant', '<span style="color:var(--red)">' + escHtml(data.error) + '</span>');
163
+ return;
164
+ }
165
+
166
+ // Track session — if server started a new session, clear stale frontend messages
167
+ if (data.sessionId) {
168
+ if (data.newSession && _ccSessionId && _ccSessionId !== data.sessionId) {
169
+ // Session was silently reset (expired/turn limit) — clear old messages except the current one
170
+ const el = document.getElementById('cc-messages');
171
+ // Keep only the user message we just added (last child)
172
+ const lastMsg = el.lastElementChild;
173
+ el.innerHTML = '';
174
+ if (lastMsg) el.appendChild(lastMsg);
175
+ _ccMessages = _ccMessages.slice(-1); // Keep only the message we just sent
176
+ }
177
+ _ccSessionId = data.sessionId;
178
+ ccSaveState();
179
+ ccUpdateSessionIndicator();
180
+ }
181
+
182
+ // Render markdown-ish response
183
+ const ccElapsed = Math.round((Date.now() - ccStartTime) / 1000);
184
+ const rendered = (data.text || '').replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
185
+ .replace(/`([^`]+)`/g, '<code style="background:var(--surface);padding:1px 4px;border-radius:3px;font-size:11px">$1</code>')
186
+ .replace(/\n/g, '<br>');
187
+ ccAddMessage('assistant', rendered + '<div style="font-size:9px;color:var(--muted);margin-top:4px;text-align:right">' + ccElapsed + 's</div>');
188
+
189
+ // Execute actions
190
+ if (data.actions && data.actions.length > 0) {
191
+ for (const action of data.actions) {
192
+ await ccExecuteAction(action);
193
+ }
194
+ }
195
+ } catch (e) {
196
+ clearInterval(ccTimer);
197
+ thinking.remove();
198
+ ccAddMessage('assistant', '<span style="color:var(--red)">Error: ' + escHtml(e.message) + '</span>');
199
+ } finally {
200
+ _ccSending = false;
201
+ // Show notification badge on CC button if drawer is closed
202
+ if (!_ccOpen) showNotifBadge(document.getElementById('cc-toggle-btn'));
203
+ }
204
+ }
205
+
206
+ async function ccExecuteAction(action) {
207
+ const msgs = document.getElementById('cc-messages');
208
+ const status = document.createElement('div');
209
+ status.style.cssText = 'padding:4px 10px;border-radius:4px;font-size:10px;align-self:flex-start;border:1px dashed var(--border);color:var(--muted)';
210
+
211
+ try {
212
+ switch (action.type) {
213
+ case 'dispatch': {
214
+ const res = await fetch('/api/work-items', {
215
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
216
+ body: JSON.stringify({
217
+ title: action.title, type: action.workType || 'implement',
218
+ priority: action.priority || 'medium', description: action.description || '',
219
+ project: action.project || '', agents: action.agents || [],
220
+ })
221
+ });
222
+ const d = await res.json();
223
+ status.innerHTML = '&#10003; Dispatched: <strong>' + escHtml(d.id || action.title) + '</strong>';
224
+ status.style.color = 'var(--green)';
225
+ break;
226
+ }
227
+ case 'note': {
228
+ const today = new Date().toISOString().slice(0, 10);
229
+ await fetch('/api/notes', {
230
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
231
+ body: JSON.stringify({ title: action.title, what: action.content || action.description, author: 'command-center' })
232
+ });
233
+ status.innerHTML = '&#10003; Note saved: <strong>' + escHtml(action.title) + '</strong>';
234
+ status.style.color = 'var(--green)';
235
+ break;
236
+ }
237
+ case 'plan': {
238
+ await fetch('/api/plan', {
239
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
240
+ body: JSON.stringify({ title: action.title, description: action.description, project: action.project, branchStrategy: action.branchStrategy || 'parallel' })
241
+ });
242
+ status.innerHTML = '&#10003; Plan queued: <strong>' + escHtml(action.title) + '</strong>';
243
+ status.style.color = 'var(--green)';
244
+ break;
245
+ }
246
+ case 'cancel': {
247
+ await fetch('/api/agents/cancel', {
248
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
249
+ body: JSON.stringify({ agentId: action.agent, reason: action.reason || 'Cancelled via command center' })
250
+ });
251
+ status.innerHTML = '&#10003; Cancelled agent: <strong>' + escHtml(action.agent) + '</strong>';
252
+ status.style.color = 'var(--orange)';
253
+ break;
254
+ }
255
+ case 'retry': {
256
+ for (const id of (action.ids || [])) {
257
+ await fetch('/api/work-items/retry', {
258
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
259
+ body: JSON.stringify({ id, source: '' })
260
+ });
261
+ }
262
+ status.innerHTML = '&#10003; Retried: <strong>' + escHtml((action.ids || []).join(', ')) + '</strong>';
263
+ status.style.color = 'var(--green)';
264
+ break;
265
+ }
266
+ case 'pause-plan': {
267
+ await fetch('/api/plans/pause', {
268
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
269
+ body: JSON.stringify({ file: action.file })
270
+ });
271
+ status.innerHTML = '&#10003; Paused plan: <strong>' + escHtml(action.file) + '</strong>';
272
+ status.style.color = 'var(--orange)';
273
+ break;
274
+ }
275
+ case 'approve-plan': {
276
+ await fetch('/api/plans/approve', {
277
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
278
+ body: JSON.stringify({ file: action.file })
279
+ });
280
+ status.innerHTML = '&#10003; Approved plan: <strong>' + escHtml(action.file) + '</strong>';
281
+ status.style.color = 'var(--green)';
282
+ break;
283
+ }
284
+ case 'edit-prd-item': {
285
+ await fetch('/api/prd-items/update', {
286
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
287
+ body: JSON.stringify({ source: action.source, itemId: action.itemId, name: action.name, description: action.description, priority: action.priority, estimated_complexity: action.complexity })
288
+ });
289
+ status.innerHTML = '&#10003; Updated PRD item: <strong>' + escHtml(action.itemId) + '</strong>';
290
+ status.style.color = 'var(--green)';
291
+ break;
292
+ }
293
+ case 'remove-prd-item': {
294
+ await fetch('/api/prd-items/remove', {
295
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
296
+ body: JSON.stringify({ source: action.source, itemId: action.itemId })
297
+ });
298
+ status.innerHTML = '&#10003; Removed PRD item: <strong>' + escHtml(action.itemId) + '</strong>';
299
+ status.style.color = 'var(--orange)';
300
+ break;
301
+ }
302
+ case 'delete-work-item': {
303
+ await fetch('/api/work-items/delete', {
304
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
305
+ body: JSON.stringify({ id: action.id, source: action.source || '' })
306
+ });
307
+ status.innerHTML = '&#10003; Deleted work item: <strong>' + escHtml(action.id) + '</strong>';
308
+ status.style.color = 'var(--orange)';
309
+ break;
310
+ }
311
+ case 'plan-edit': {
312
+ // Read the plan, send instruction to doc-chat, show version actions
313
+ const normalizedFile = normalizePlanFile(action.file);
314
+ const planContent = await fetch('/api/plans/' + encodeURIComponent(normalizedFile)).then(r => r.text());
315
+ const res = await fetch('/api/doc-chat', {
316
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
317
+ body: JSON.stringify({
318
+ message: action.instruction,
319
+ document: planContent,
320
+ title: normalizedFile,
321
+ filePath: 'plans/' + normalizedFile,
322
+ }),
323
+ });
324
+ const data = await res.json();
325
+ if (data.ok && data.edited) {
326
+ status.innerHTML = '&#10003; Plan edited: <strong>' + escHtml(action.file) + '</strong>';
327
+ status.style.color = 'var(--green)';
328
+ } else {
329
+ status.innerHTML = data.answer ? escHtml(data.answer) : '&#10007; Could not edit plan';
330
+ status.style.color = data.answer ? 'var(--muted)' : 'var(--red)';
331
+ }
332
+ break;
333
+ }
334
+ case 'execute-plan': {
335
+ await fetch('/api/plans/execute', {
336
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
337
+ body: JSON.stringify({ file: action.file, project: action.project || '' })
338
+ });
339
+ status.innerHTML = '&#10003; Plan execution queued: <strong>' + escHtml(action.file) + '</strong>';
340
+ status.style.color = 'var(--green)';
341
+ refreshPlans();
342
+ break;
343
+ }
344
+ case 'file-edit': {
345
+ // doc-chat reads current content from disk via filePath — pass placeholder for required field
346
+ const res = await fetch('/api/doc-chat', {
347
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
348
+ body: JSON.stringify({
349
+ message: action.instruction,
350
+ document: '(loaded from disk)',
351
+ title: action.file.split('/').pop(),
352
+ filePath: action.file,
353
+ }),
354
+ });
355
+ const data = await res.json();
356
+ if (data.ok && data.edited) {
357
+ status.innerHTML = '&#10003; Edited: <strong>' + escHtml(action.file) + '</strong>';
358
+ status.style.color = 'var(--green)';
359
+ } else {
360
+ status.innerHTML = data.answer ? escHtml(data.answer) : '&#10007; Could not edit file';
361
+ status.style.color = data.answer ? 'var(--muted)' : 'var(--red)';
362
+ }
363
+ break;
364
+ }
365
+ default:
366
+ status.innerHTML = '? Unknown action: ' + escHtml(action.type);
367
+ status.style.color = 'var(--muted)';
368
+ }
369
+ } catch (e) {
370
+ status.innerHTML = '&#10007; Action failed: ' + escHtml(e.message);
371
+ status.style.color = 'var(--red)';
372
+ }
373
+
374
+ msgs.appendChild(status);
375
+ msgs.scrollTop = msgs.scrollHeight;
376
+ refresh();
377
+ }
@@ -0,0 +1,70 @@
1
+ // command-history.js — Command history functions extracted from dashboard.html
2
+
3
+ const CMD_HISTORY_KEY = 'minions-cmd-history';
4
+ const CMD_HISTORY_MAX = 50;
5
+ let _cmdHistoryIdx = -1; // -1 = not browsing history
6
+ let _cmdHistoryDraft = ''; // saves current draft when browsing
7
+
8
+ function cmdGetHistory() {
9
+ try { return JSON.parse(localStorage.getItem(CMD_HISTORY_KEY) || '[]'); } catch { return []; }
10
+ }
11
+
12
+ function cmdSaveHistory(raw, intent) {
13
+ const history = cmdGetHistory();
14
+ history.unshift({ text: raw, intent, timestamp: new Date().toISOString() });
15
+ if (history.length > CMD_HISTORY_MAX) history.length = CMD_HISTORY_MAX;
16
+ localStorage.setItem(CMD_HISTORY_KEY, JSON.stringify(history));
17
+ }
18
+
19
+ function cmdShowHistory() {
20
+ const history = cmdGetHistory();
21
+ const title = document.getElementById('modal-title');
22
+ const body = document.getElementById('modal-body');
23
+ title.textContent = 'Past Commands (' + history.length + ')';
24
+
25
+ if (history.length === 0) {
26
+ body.innerHTML = '<div class="cmd-history-empty">No commands yet. Submit something from the command center.</div>';
27
+ } else {
28
+ const intentColors = { 'work-item': 'var(--blue)', 'note': 'var(--green)', 'plan': 'var(--purple,#a855f7)' };
29
+ const intentLabels = { 'work-item': 'Work Item', 'note': 'Note', 'plan': 'Plan' };
30
+ body.innerHTML = '<ul class="cmd-history-list">' + history.map((item, i) => {
31
+ const date = new Date(item.timestamp);
32
+ const ago = timeSinceStr(date);
33
+ const intentLabel = intentLabels[item.intent] || item.intent || 'work-item';
34
+ const intentColor = intentColors[item.intent] || 'var(--blue)';
35
+ return '<li class="cmd-history-item">' +
36
+ '<div class="cmd-history-item-body">' +
37
+ '<div class="cmd-history-item-text">' + escHtml(item.text) + '</div>' +
38
+ '<div class="cmd-history-item-meta">' +
39
+ '<span class="chip" style="color:' + intentColor + '">' + intentLabel + '</span>' +
40
+ '<span>' + ago + '</span>' +
41
+ '<span>' + date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) + '</span>' +
42
+ '</div>' +
43
+ '</div>' +
44
+ '<button class="cmd-history-resubmit" onclick="cmdResubmit(' + i + ')">Resubmit</button>' +
45
+ '</li>';
46
+ }).join('') + '</ul>';
47
+ }
48
+
49
+ document.getElementById('modal').classList.add('open');
50
+ }
51
+
52
+ function cmdResubmit(idx) {
53
+ const history = cmdGetHistory();
54
+ const item = history[idx];
55
+ if (!item) return;
56
+ document.getElementById('modal').classList.remove('open');
57
+ const input = document.getElementById('cmd-input');
58
+ input.value = item.text;
59
+ cmdAutoResize();
60
+ cmdRenderMeta();
61
+ input.focus();
62
+ }
63
+
64
+ function timeSinceStr(date) {
65
+ const s = Math.floor((Date.now() - date.getTime()) / 1000);
66
+ if (s < 60) return s + 's ago';
67
+ if (s < 3600) return Math.floor(s / 60) + 'm ago';
68
+ if (s < 86400) return Math.floor(s / 3600) + 'h ago';
69
+ return Math.floor(s / 86400) + 'd ago';
70
+ }