@yemi33/minions 0.1.12 → 0.1.14

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 (37) hide show
  1. package/CHANGELOG.md +59 -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 +309 -0
  9. package/dashboard/js/modal.js +131 -0
  10. package/dashboard/js/refresh.js +59 -0
  11. package/dashboard/js/render-agents.js +44 -0
  12. package/dashboard/js/render-dispatch.js +171 -0
  13. package/dashboard/js/render-inbox.js +163 -0
  14. package/dashboard/js/render-kb.js +125 -0
  15. package/dashboard/js/render-other.js +181 -0
  16. package/dashboard/js/render-plans.js +536 -0
  17. package/dashboard/js/render-prd.js +688 -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 +155 -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 +52 -0
  36. package/dashboard.js +44 -1
  37. package/package.json +1 -1
@@ -0,0 +1,536 @@
1
+ // render-plans.js — Plan rendering functions extracted from dashboard.html
2
+
3
+ async function refreshPlans() {
4
+ try {
5
+ const plans = await fetch('/api/plans').then(r => r.json());
6
+ renderPlans(plans);
7
+ } catch {}
8
+ }
9
+
10
+ function renderPlans(plans) {
11
+ const el = document.getElementById('plans-list');
12
+ const countEl = document.getElementById('plans-count');
13
+ countEl.textContent = plans.length;
14
+
15
+ if (plans.length === 0) {
16
+ el.innerHTML = '<p class="empty">No plans yet. Use /plan in the command center to create one.</p>';
17
+ return;
18
+ }
19
+
20
+ const statusLabels = { 'awaiting-approval': 'Awaiting Approval', 'paused': 'Paused', 'approved': 'Approved', 'rejected': 'Rejected', 'revision-requested': 'Revision Requested', 'completed': 'Completed', 'active': 'Active' };
21
+ const statusClass = (s) => s === 'awaiting-approval' || s === 'paused' ? 'awaiting' : s || '';
22
+ const normalizeSourcePlanKey = (name) => {
23
+ if (!name) return '';
24
+ if (name.endsWith('.md') || name.endsWith('.json')) return name;
25
+ return name + '.md';
26
+ };
27
+
28
+ // Check which plans have active dispatches or pending plan-to-prd work items
29
+ const activeDisp = (window._lastDispatch?.active || []);
30
+ const pendingDisp = (window._lastDispatch?.pending || []);
31
+ const workingPlanFiles = new Set();
32
+ for (const d of [...activeDisp, ...pendingDisp]) {
33
+ const src = d.meta?.item?.sourcePlan || '';
34
+ if (src) workingPlanFiles.add(src);
35
+ // Match plan-to-prd tasks by planFile in item meta
36
+ const planFile = d.meta?.item?.planFile || d.meta?.planFile || '';
37
+ if (planFile) workingPlanFiles.add(planFile);
38
+ // Also match by task description
39
+ if (d.type === 'plan-to-prd' && d.task) {
40
+ for (const p of plans) { if (d.task.includes(p.file?.replace('.md', '').replace('.json', '') || '___')) workingPlanFiles.add(p.file); }
41
+ }
42
+ }
43
+ // Check work items for pending/dispatched plan-to-prd or implement tasks
44
+ const allWi = window._lastWorkItems || [];
45
+ for (const w of allWi) {
46
+ if (w.type === 'plan-to-prd' && (w.status === 'pending' || w.status === 'dispatched') && w.planFile) {
47
+ workingPlanFiles.add(w.planFile);
48
+ }
49
+ // Also track which PRD .json files have active work
50
+ if (w.sourcePlan && (w.status === 'dispatched' || w.status === 'pending')) {
51
+ workingPlanFiles.add(w.sourcePlan);
52
+ }
53
+ }
54
+
55
+ // Link .md plans to their PRD .json — if a PRD is being worked on, the source plan is too
56
+ // Convention: plan-w025-2026-03-15.md → officeagent-2026-03-15.json (same date, different prefix)
57
+ const workingJsons = new Set([...workingPlanFiles].filter(f => f.endsWith('.json')));
58
+ if (workingJsons.size > 0) {
59
+ for (const p of plans) {
60
+ if (p.format === 'draft' && p.file.endsWith('.md')) {
61
+ // A .md plan is "working" if any PRD .json has active dispatches
62
+ // (since the .md is the source that generated those PRDs)
63
+ if (workingJsons.size > 0) workingPlanFiles.add(p.file);
64
+ }
65
+ }
66
+ }
67
+
68
+ // Track which .md plans have a paused PRD, and map .md → PRD .json
69
+ const pausedPlanFiles = new Set();
70
+ const awaitingApprovalPlanFiles = new Set();
71
+ const planToPrdFile = {}; // .md filename → .json PRD filename
72
+ for (const p of plans) {
73
+ if (p.format === 'prd' && !p.archived && p.sourcePlan) {
74
+ const sourceKeys = [p.sourcePlan, normalizeSourcePlanKey(p.sourcePlan)];
75
+ for (const sourceKey of sourceKeys) {
76
+ if (!sourceKey) continue;
77
+ planToPrdFile[sourceKey] = p.file;
78
+ if (p.status === 'paused') pausedPlanFiles.add(sourceKey);
79
+ if (p.status === 'awaiting-approval') awaitingApprovalPlanFiles.add(sourceKey);
80
+ }
81
+ }
82
+ }
83
+
84
+ const activePlans = plans.filter(p => !p.archived && p.format !== 'prd');
85
+ const archivedPlans = plans.filter(p => p.archived && p.format !== 'prd');
86
+ countEl.textContent = activePlans.length + (archivedPlans.length ? ' + ' + archivedPlans.length + ' archived' : '');
87
+
88
+ function renderPlanCard(p) {
89
+ const status = p.status || 'active';
90
+ const isWorking = workingPlanFiles.has(p.file);
91
+ const isPrdPaused = pausedPlanFiles.has(p.file);
92
+ const isPrdAwaitingApproval = awaitingApprovalPlanFiles.has(p.file);
93
+ const isPrdBlocked = isPrdPaused || isPrdAwaitingApproval;
94
+ const isArchived = p.archived;
95
+ const label = isArchived
96
+ ? 'Completed'
97
+ : isPrdAwaitingApproval
98
+ ? 'Awaiting Approval'
99
+ : isPrdPaused
100
+ ? 'Paused'
101
+ : isWorking
102
+ ? 'In Progress'
103
+ : (statusLabels[status] || status);
104
+ const needsAction = (status === 'awaiting-approval' || status === 'paused' || isPrdAwaitingApproval || isPrdPaused) && !isArchived;
105
+ const isRevision = status === 'revision-requested' && !isArchived;
106
+ const isCompleted = status === 'completed';
107
+ const isDraft = (p.format === 'draft' || status === 'draft') && !isCompleted;
108
+ const isAwaitingApproval = status === 'awaiting-approval';
109
+ const isPaused = status === 'paused';
110
+ const isApproved = status === 'approved' || status === 'active';
111
+ // For .md drafts: show Execute only if no PRD exists yet (not already executed)
112
+ const prdFile = planToPrdFile[p.file] || (p.file.endsWith('.json') ? p.file : '');
113
+
114
+ let actions = '';
115
+ const resumeVisible = ((isPrdBlocked || isAwaitingApproval || isPaused) && prdFile && !isArchived);
116
+ if (needsAction && !resumeVisible) {
117
+ actions = '<div class="plan-card-actions" onclick="event.stopPropagation()">' +
118
+ '<button class="plan-btn approve" onclick="planApprove(\'' + escHtml(p.file) + '\')">Approve</button>' +
119
+ '<button class="plan-btn" style="color:var(--blue);border-color:var(--blue)" onclick="planDiscuss(\'' + escHtml(p.file) + '\')">Discuss &amp; Revise</button>' +
120
+ '<button class="plan-btn reject" onclick="planReject(\'' + escHtml(p.file) + '\')">Reject</button>' +
121
+ '</div>' +
122
+ '<div id="revise-input-' + escHtml(p.file).replace(/\./g, '-') + '" style="display:none">' +
123
+ '<textarea class="plan-feedback-input" placeholder="What should be changed? Be specific..." id="revise-feedback-' + escHtml(p.file).replace(/\./g, '-') + '"></textarea>' +
124
+ '<div class="plan-card-actions" style="margin-top:4px">' +
125
+ '<button class="plan-btn revise" onclick="planSubmitRevise(\'' + escHtml(p.file) + '\')">Submit Revision Request</button>' +
126
+ '<button class="plan-btn" onclick="planHideRevise(\'' + escHtml(p.file) + '\')">Cancel</button>' +
127
+ '</div>' +
128
+ '</div>';
129
+ } else if (isRevision) {
130
+ actions = '<div class="plan-card-meta" style="margin-top:6px;color:var(--purple,#a855f7)">Revision in progress: ' + escHtml((p.revisionFeedback || '').slice(0, 100)) + '</div>';
131
+ }
132
+
133
+ const executeBtn = isDraft && !isWorking && !isPrdBlocked && !isArchived && !prdFile ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green);font-weight:600" ' +
134
+ 'onclick="event.stopPropagation();planExecute(\'' + escHtml(p.file) + '\',\'' + escHtml(p.project) + '\',this)">Execute</button>' : '';
135
+ // Pause/Resume: target the PRD .json file if it exists, otherwise the plan itself
136
+ const effectivelyPaused = isPrdBlocked || isAwaitingApproval || isPaused;
137
+ const showPause = !effectivelyPaused && prdFile && !isArchived && !isCompleted;
138
+ const showResume = effectivelyPaused && prdFile && !isArchived;
139
+ const pauseBtn = showPause ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--yellow)" ' +
140
+ 'onclick="event.stopPropagation();planPause(\'' + escHtml(prdFile) + '\')">Pause</button>' : '';
141
+ const resumeBtn = showResume
142
+ ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green)" ' +
143
+ 'onclick="event.stopPropagation();planApprove(\'' + escHtml(prdFile) + '\')">' + (isPrdAwaitingApproval || isAwaitingApproval ? 'Approve' : 'Resume') + '</button>'
144
+ : '';
145
+ const deleteBtn = !isArchived ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red)" ' +
146
+ 'onclick="event.stopPropagation();planDelete(\'' + escHtml(p.file) + '\')">Delete</button>' : '';
147
+
148
+ const versionBadge = p.version ? ' <span style="font-size:9px;font-weight:700;padding:1px 5px;border-radius:3px;background:rgba(56,139,253,0.15);color:var(--blue);vertical-align:middle">v' + p.version + '</span>' : '';
149
+ return '<div class="plan-card ' + statusClass(status) + (isWorking && !isPrdBlocked ? ' working' : '') + (isPrdBlocked ? ' awaiting' : '') + '" data-file="plans/' + escHtml(p.file) + '" style="cursor:pointer' + (isArchived ? ';opacity:0.7' : '') + '" onclick="planView(\'' + escHtml(p.file) + '\')">' +
150
+ '<div class="plan-card-header">' +
151
+ '<div><div class="plan-card-title">' + escHtml(p.summary || p.file) + versionBadge + '</div>' +
152
+ '<div class="plan-card-meta">' +
153
+ '<span style="font-weight:600;color:' + (isArchived || isCompleted ? 'var(--green)' : isPrdAwaitingApproval ? 'var(--yellow)' : isPrdPaused ? 'var(--muted)' : isWorking ? 'var(--blue)' : needsAction ? 'var(--yellow,#d29922)' : status === 'approved' ? 'var(--green)' : 'var(--muted)') + '">' + label + '</span>' +
154
+ '<span>' + escHtml(p.project) + '</span>' +
155
+ '<span>' + p.itemCount + ' items</span>' +
156
+ (p.updatedAt ? '<span title="Last updated: ' + p.updatedAt + '">Updated ' + timeAgo(p.updatedAt) + '</span>' : '') +
157
+ (p.completedAt ? '<span>' + p.completedAt.slice(0, 10) + '</span>' : '') +
158
+ (p.generatedBy ? '<span>by ' + escHtml(p.generatedBy) + '</span>' : '') +
159
+ executeBtn + pauseBtn + resumeBtn + deleteBtn +
160
+ '</div>' +
161
+ '</div>' +
162
+ '</div>' +
163
+ actions +
164
+ '</div>';
165
+ }
166
+
167
+ let html = activePlans.map(renderPlanCard).join('');
168
+
169
+ if (archivedPlans.length > 0) {
170
+ window._archivedPlans = archivedPlans;
171
+ window._archivedPlanRenderer = renderPlanCard;
172
+ html += '<div style="margin-top:8px;text-align:right;position:relative" data-file="plan-archives">' +
173
+ '<button class="pr-pager-btn" style="font-size:10px;padding:3px 10px;color:var(--muted)" onclick="openArchivedPlansModal()">' +
174
+ 'View Archives (' + archivedPlans.length + ')' +
175
+ '</button>' +
176
+ '</div>';
177
+ }
178
+
179
+ el.innerHTML = html;
180
+ restoreNotifBadges();
181
+ }
182
+
183
+ function openArchivedPlansModal() {
184
+ const plans = window._archivedPlans || [];
185
+ const render = window._archivedPlanRenderer;
186
+ if (!plans.length || !render) return;
187
+
188
+ const html = plans.map(p => {
189
+ const itemCount = p.itemCount || 0;
190
+ const completed = p.completedAt ? p.completedAt.slice(0, 10) : '';
191
+ return '<div class="plan-card" data-file="plans/' + escHtml(p.file) + '" style="cursor:pointer;opacity:0.8" onclick="planView(\'' + escHtml(p.file) + '\')">' +
192
+ '<div class="plan-card-header">' +
193
+ '<div><div class="plan-card-title" style="font-size:13px">' + escHtml(p.summary || p.file) + '</div>' +
194
+ '<div class="plan-card-meta">' +
195
+ '<span style="color:var(--green);font-weight:600">Completed</span>' +
196
+ (p.project ? '<span>' + escHtml(p.project) + '</span>' : '') +
197
+ '<span>' + itemCount + ' items</span>' +
198
+ (completed ? '<span>' + completed + '</span>' : '') +
199
+ (p.generatedBy ? '<span>by ' + escHtml(p.generatedBy) + '</span>' : '') +
200
+ '</div>' +
201
+ '</div>' +
202
+ '</div>' +
203
+ '</div>';
204
+ }).join('');
205
+
206
+ document.getElementById('modal-title').textContent = 'Archived Plans';
207
+ document.getElementById('modal-body').innerHTML = html;
208
+ document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
209
+ document.getElementById('modal-body').style.whiteSpace = 'normal';
210
+ document.getElementById('modal').classList.add('open');
211
+ }
212
+
213
+ // Disable all PRD action buttons to prevent double-clicks
214
+ function qaDisablePrdButtons() {
215
+ const container = document.getElementById('qa-generate-prd-btn');
216
+ if (container) container.querySelectorAll('button').forEach(b => { b.disabled = true; b.style.opacity = '0.5'; });
217
+ }
218
+
219
+ // Show plan version action buttons (Run alongside / Replace / Just save)
220
+ function showPlanVersionActions(thread, newFile, originalFile) {
221
+ const esc = newFile.replace(/'/g, "\\'");
222
+ // Look up existing PRD for the original plan's project
223
+ const allPlans = window._lastStatus?.plans || [];
224
+ const origPlan = allPlans.find(p => p.file === originalFile);
225
+ const project = origPlan?.project || '';
226
+ const existingPrd = allPlans.find(p => p.file.endsWith('.json') && p.project === project && p.status !== 'completed');
227
+
228
+ const btn = document.createElement('div');
229
+ btn.id = 'qa-generate-prd-btn';
230
+ btn.style.cssText = 'margin:8px 0;padding:8px 12px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:6px;display:flex;flex-wrap:wrap;align-items:center;gap:8px';
231
+
232
+ if (existingPrd) {
233
+ btn.innerHTML = '<span style="color:var(--green);font-weight:600;font-size:12px;width:100%">New plan version created — existing PRD running</span>' +
234
+ '<button onclick="qaNewPrd(\'' + esc + '\')" style="background:var(--green);color:#fff;border:none;border-radius:4px;padding:4px 12px;font-size:11px;font-weight:600;cursor:pointer" title="Execute this plan as a separate PRD alongside the current one">Run alongside</button>' +
235
+ '<button onclick="qaReplacePrd(\'' + esc + '\')" style="background:var(--orange);color:#fff;border:none;border-radius:4px;padding:4px 12px;font-size:11px;font-weight:600;cursor:pointer" title="Pause existing PRD, clean pending items, execute this plan instead">Replace old PRD</button>' +
236
+ '<button onclick="qaJustSave(this)" style="background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:4px 12px;font-size:11px;cursor:pointer" title="Keep the new version saved without dispatching any work">Just save</button>' +
237
+ '<span style="color:var(--muted);font-size:10px;width:100%">Run alongside keeps current work going. Replace pauses it and starts fresh.</span>';
238
+ } else {
239
+ btn.innerHTML = '<span style="color:var(--green);font-weight:600;font-size:12px;width:100%">New plan version created</span>' +
240
+ '<button onclick="qaNewPrd(\'' + esc + '\')" style="background:var(--green);color:#fff;border:none;border-radius:4px;padding:4px 12px;font-size:11px;font-weight:600;cursor:pointer">Execute plan</button>' +
241
+ '<button onclick="qaJustSave(this)" style="background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:4px 12px;font-size:11px;cursor:pointer">Just save</button>' +
242
+ '<span style="color:var(--muted);font-size:10px">Execute dispatches an agent to create PRD items from this plan</span>';
243
+ }
244
+ // Remove any previous action buttons
245
+ const old = thread.querySelector('#qa-generate-prd-btn');
246
+ if (old) old.remove();
247
+ thread.appendChild(btn);
248
+ }
249
+
250
+ function qaJustSave(el) {
251
+ const container = el.closest('#qa-generate-prd-btn');
252
+ if (container) container.innerHTML = '<span style="color:var(--muted);font-size:11px">Saved. No work dispatched.</span>';
253
+ }
254
+
255
+ async function planExecute(file, project, btn) {
256
+ if (btn) { btn.textContent = 'Executing...'; btn.disabled = true; btn.style.color = 'var(--blue)'; }
257
+ try {
258
+ const res = await fetch('/api/plans/execute', {
259
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
260
+ body: JSON.stringify({ file, project })
261
+ });
262
+ const data = await res.json();
263
+ if (res.ok) {
264
+ // Inject into local work items so the next render immediately hides Execute
265
+ if (!data.alreadyQueued) {
266
+ (window._lastWorkItems = window._lastWorkItems || []).push({
267
+ id: data.id, type: 'plan-to-prd', status: 'pending', planFile: file
268
+ });
269
+ }
270
+ closeModal();
271
+ showToast('cmd-toast', data.alreadyQueued ? 'Already queued (' + data.id + ')' : 'Queued ' + data.id + ' — agent will convert plan to PRD', true);
272
+ refreshPlans();
273
+ } else {
274
+ if (btn) { btn.textContent = 'Execute'; btn.disabled = false; btn.style.color = 'var(--green)'; }
275
+ alert('Failed: ' + (data.error || 'unknown'));
276
+ }
277
+ } catch (e) {
278
+ if (btn) { btn.textContent = 'Execute'; btn.disabled = false; btn.style.color = 'var(--green)'; }
279
+ alert('Error: ' + e.message);
280
+ }
281
+ }
282
+
283
+ async function planSubmitRevise(file) {
284
+ const id = 'revise-feedback-' + file.replace(/\./g, '-');
285
+ const feedback = document.getElementById(id).value.trim();
286
+ if (!feedback) { showToast('cmd-toast', 'Please enter feedback', false); return; }
287
+ try {
288
+ const res = await fetch('/api/plans/revise', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file, feedback }) });
289
+ const data = await res.json();
290
+ showToast('cmd-toast', 'Revision requested — agent will update the plan (' + data.workItemId + ')', true);
291
+ planHideRevise(file);
292
+ refreshPlans();
293
+ } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
294
+ }
295
+
296
+ function planShowRevise(file) {
297
+ const id = 'revise-input-' + file.replace(/\./g, '-');
298
+ document.getElementById(id).style.display = 'block';
299
+ }
300
+
301
+ function planHideRevise(file) {
302
+ const id = 'revise-input-' + file.replace(/\./g, '-');
303
+ document.getElementById(id).style.display = 'none';
304
+ }
305
+
306
+ async function planView(file) {
307
+ try {
308
+ const normalizedFile = normalizePlanFile(file);
309
+ const planRes = await fetch('/api/plans/' + encodeURIComponent(normalizedFile));
310
+ const lastMod = planRes.headers.get('Last-Modified');
311
+ const resolvedPath = planRes.headers.get('X-Resolved-Path');
312
+ const raw = await planRes.text();
313
+ let title = normalizedFile;
314
+ let text = '';
315
+
316
+ if (normalizedFile.endsWith('.json')) {
317
+ // PRD JSON — format nicely
318
+ const plan = JSON.parse(raw);
319
+ title = plan.plan_summary || normalizedFile;
320
+ const items = (plan.missing_features || []).map((f, i) =>
321
+ (i + 1) + '. [' + f.id + '] ' + f.name + ' (' + (f.estimated_complexity || '?') + ', ' + (f.priority || '?') + ')' +
322
+ (f.depends_on?.length ? ' → depends on: ' + f.depends_on.join(', ') : '') +
323
+ '\n ' + (f.description || '').slice(0, 200) +
324
+ (f.acceptance_criteria?.length ? '\n Criteria: ' + f.acceptance_criteria.join('; ') : '')
325
+ ).join('\n\n');
326
+ text = 'Project: ' + (plan.project || '?') +
327
+ '\nStrategy: ' + (plan.branch_strategy || 'parallel') +
328
+ '\nBranch: ' + (plan.feature_branch || 'per-item') +
329
+ '\nStatus: ' + (plan.status || 'active') +
330
+ '\nGenerated by: ' + (plan.generated_by || '?') + ' on ' + (plan.generated_at || '?') +
331
+ '\n\n--- Items (' + (plan.missing_features || []).length + ') ---\n\n' + items +
332
+ (plan.open_questions?.length ? '\n\n--- Open Questions ---\n\n' + plan.open_questions.map(q => '• ' + q).join('\n') : '');
333
+ } else {
334
+ // Markdown plan — show as-is
335
+ text = raw;
336
+ const titleMatch = raw.match(/^#\s+(?:Plan:\s*)?(.+)/m);
337
+ if (titleMatch) title = titleMatch[1];
338
+ }
339
+
340
+ // Version badge for the modal title
341
+ const vMatch = normalizedFile.match(/-v(\d+)/);
342
+ const versionLabel = vMatch ? ' (v' + vMatch[1] + ')' : '';
343
+
344
+ // Determine plan type and status for action buttons
345
+ const isMdPlan = normalizedFile.endsWith('.md');
346
+ let planStatus = '';
347
+ try { if (normalizedFile.endsWith('.json')) planStatus = JSON.parse(raw).status || ''; } catch {}
348
+ const isActive = planStatus === 'approved' || planStatus === 'active';
349
+ const isPaused = planStatus === 'awaiting-approval' || planStatus === 'paused';
350
+ // Check if work is in progress for this plan
351
+ const wi = window._lastWorkItems || [];
352
+ const hasActiveWork = wi.some(w =>
353
+ (w.status === 'pending' || w.status === 'dispatched') &&
354
+ (w.planFile === normalizedFile || w.sourcePlan === normalizedFile ||
355
+ // For .md plans: any work item with a sourcePlan .json means the plan is being executed
356
+ (isMdPlan && w.sourcePlan && w.sourcePlan.endsWith('.json')))
357
+ );
358
+ const prdCompleted = wi.some(w => w.type === 'plan-to-prd' && w.status === 'done' && w.planFile === normalizedFile);
359
+ // Check if a PRD already exists for this plan (via plans list sourcePlan linkage)
360
+ const hasPrd = (window._lastStatus?.plans || []).some(p => p.sourcePlan === normalizedFile && p.format === 'prd');
361
+ const modalShowResume = isPaused;
362
+ const modalExecuteBtn = isMdPlan && !modalShowResume && !hasActiveWork && !prdCompleted && !hasPrd ? '<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--green);font-weight:600" ' +
363
+ 'onclick="planExecute(\'' + escHtml(normalizedFile) + '\',\'\',this)">Execute</button>' : '';
364
+ const modalCompletedLabel = prdCompleted && !hasActiveWork ? '<span style="font-size:10px;color:var(--green);font-weight:600">Completed</span>' : '';
365
+ const modalInProgressLabel = hasActiveWork ? '<span style="font-size:10px;color:var(--blue)">In Progress</span>' : '';
366
+ const isModalCompleted = planStatus === 'completed';
367
+ const modalPauseBtn = isActive && !isMdPlan && !isModalCompleted ? '<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--yellow)" ' +
368
+ 'onclick="planPause(\'' + escHtml(normalizedFile) + '\');closeModal()">Pause</button>' : '';
369
+ const modalResumeBtn = isPaused ? '<button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--green)" ' +
370
+ 'onclick="planApprove(\'' + escHtml(normalizedFile) + '\');closeModal()">Resume</button>' : '';
371
+
372
+ const lastModLabel = lastMod ? '<div style="font-size:10px;color:var(--muted);font-weight:400;margin-top:2px">Last updated: ' + new Date(lastMod).toLocaleString() + '</div>' : '';
373
+ const actionBtns = '<div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px">' +
374
+ (modalCompletedLabel || '') + (modalInProgressLabel || '') + (modalExecuteBtn || '') + (modalPauseBtn || '') + (modalResumeBtn || '') +
375
+ ' <button class="pr-pager-btn" style="font-size:10px;padding:2px 10px;color:var(--red)" ' +
376
+ 'onclick="planDelete(\'' + escHtml(normalizedFile) + '\')">Delete</button>' +
377
+ '</div>';
378
+ document.getElementById('modal-title').innerHTML = escHtml(title) + (versionLabel ? ' <span style="font-size:11px;font-weight:700;padding:1px 6px;border-radius:3px;background:rgba(56,139,253,0.15);color:var(--blue)">' + escHtml(versionLabel) + '</span>' : '') + lastModLabel + actionBtns;
379
+ document.getElementById('modal-body').textContent = text;
380
+ document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
381
+ document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
382
+ _modalDocContext = { title, content: text, selection: '' };
383
+ _modalFilePath = resolvedPath || ((normalizedFile.endsWith('.json') ? 'prd/' : 'plans/') + normalizedFile); showModalQa();
384
+ // Clear notification badge when opening this document
385
+ const card = findCardForFile(_modalFilePath);
386
+ if (card) clearNotifBadge(card);
387
+ // steer btn removed — unified send
388
+ document.getElementById('modal').classList.add('open');
389
+ } catch (e) { console.error(e); }
390
+ }
391
+
392
+ async function planApprove(file) {
393
+ try {
394
+ await fetch('/api/plans/approve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file }) });
395
+ showToast('cmd-toast', 'Plan approved — work will begin on next engine tick', true);
396
+ refreshPlans();
397
+ } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
398
+ }
399
+
400
+ async function planDelete(file) {
401
+ if (!confirm('Delete plan "' + file + '"? This cannot be undone.')) return;
402
+ try {
403
+ const res = await fetch('/api/plans/delete', {
404
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
405
+ body: JSON.stringify({ file })
406
+ });
407
+ if (res.ok) {
408
+ closeModal();
409
+ showToast('cmd-toast', 'Plan deleted', true);
410
+ refreshPlans();
411
+ refresh();
412
+ } else {
413
+ const d = await res.json();
414
+ alert('Failed: ' + (d.error || 'unknown'));
415
+ }
416
+ } catch (e) { alert('Error: ' + e.message); }
417
+ }
418
+
419
+ async function planPause(file) {
420
+ try {
421
+ await fetch('/api/plans/pause', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file }) });
422
+ showToast('cmd-toast', 'Plan paused — no new items will be dispatched', true);
423
+ refreshPlans();
424
+ refresh();
425
+ } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
426
+ }
427
+
428
+ async function planReject(file) {
429
+ if (!confirm('Reject this plan? It will not be executed.')) return;
430
+ const reason = prompt('Reason for rejection (optional):') || '';
431
+ try {
432
+ await fetch('/api/plans/reject', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file, reason }) });
433
+ showToast('cmd-toast', 'Plan rejected', true);
434
+ refreshPlans();
435
+ } catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); }
436
+ }
437
+
438
+ async function planDiscuss(file) {
439
+ try {
440
+ const res = await fetch('/api/plans/discuss', {
441
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
442
+ body: JSON.stringify({ file })
443
+ });
444
+ const data = await res.json();
445
+ if (!res.ok) throw new Error(data.error);
446
+
447
+ // Show the launch command in a modal
448
+ const content = `To discuss and revise this plan interactively, run this command in a terminal:\n\n` +
449
+ `━━━ Bash / Git Bash ━━━\n${data.command}\n\n` +
450
+ `━━━ PowerShell ━━━\n${data.psCommand}\n\n` +
451
+ `━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
452
+ `This launches an interactive Claude session with the plan pre-loaded.\n` +
453
+ `Chat naturally to review and refine. When you're satisfied, say "approve" and the session will write the approved plan back to disk.\n\n` +
454
+ `The engine will pick it up on the next tick and start dispatching work.`;
455
+
456
+ document.getElementById('modal-title').textContent = 'Discuss Plan: ' + file;
457
+ document.getElementById('modal-body').textContent = content;
458
+ document.getElementById('modal').classList.add('open');
459
+ } catch (e) {
460
+ showToast('cmd-toast', 'Error: ' + e.message, false);
461
+ }
462
+ }
463
+
464
+ async function planOpenInDocChat(file) {
465
+ try {
466
+ const normalizedFile = normalizePlanFile(file);
467
+ const planRes = await fetch('/api/plans/' + encodeURIComponent(normalizedFile));
468
+ const resolvedPath = planRes.headers.get('X-Resolved-Path');
469
+ const raw = await planRes.text();
470
+ let title = normalizedFile;
471
+ let text = raw;
472
+ if (normalizedFile.endsWith('.json')) {
473
+ try { title = JSON.parse(raw).plan_summary || file; } catch {}
474
+ }
475
+ document.getElementById('modal-title').textContent = 'Edit: ' + title;
476
+ document.getElementById('modal-body').textContent = text;
477
+ document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
478
+ document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
479
+ _modalDocContext = { title: title, content: text, selection: '' };
480
+ _modalFilePath = resolvedPath || ((normalizedFile.endsWith('.json') ? 'prd/' : 'plans/') + normalizedFile); showModalQa();
481
+ const card = findCardForFile(_modalFilePath);
482
+ if (card) clearNotifBadge(card);
483
+ document.getElementById('modal').classList.add('open');
484
+ } catch (e) { alert('Error opening plan: ' + e.message); }
485
+ }
486
+
487
+ async function planRegeneratePRD(source) {
488
+ if (!confirm('Reset pending/failed items to pick up plan changes?\n\nIn-progress and completed items won\'t be affected.')) return;
489
+ try {
490
+ const res = await fetch('/api/plans/regenerate', {
491
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
492
+ body: JSON.stringify({ source })
493
+ });
494
+ const d = await res.json();
495
+ if (res.ok && d.ok) {
496
+ refresh();
497
+ showToast('cmd-toast', 'Regenerated: ' + d.reset + ' reset, ' + d.kept + ' kept, ' + d.new + ' new', true);
498
+ } else {
499
+ alert('Failed: ' + (d.error || 'unknown'));
500
+ }
501
+ } catch (e) { alert('Error: ' + e.message); }
502
+ }
503
+
504
+ async function openVerifyGuide(file) {
505
+ try {
506
+ const normalizedFile = normalizePlanFile(file);
507
+ const content = await fetch('/api/plans/' + encodeURIComponent(normalizedFile)).then(r => r.text());
508
+ document.getElementById('modal-title').innerHTML = 'Manual Testing Guide' +
509
+ ' <button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;margin-left:8px;vertical-align:middle" onclick="openArchivedPrdModal()">Back</button>';
510
+ document.getElementById('modal-body').textContent = content;
511
+ document.getElementById('modal-body').style.fontFamily = 'Consolas, monospace';
512
+ document.getElementById('modal-body').style.whiteSpace = 'pre-wrap';
513
+ _modalDocContext = { title: 'Manual Testing Guide', content, selection: '' };
514
+ _modalFilePath = 'prd/' + normalizedFile; showModalQa();
515
+ const card = findCardForFile(_modalFilePath);
516
+ if (card) clearNotifBadge(card);
517
+ document.getElementById('modal').classList.add('open');
518
+ } catch (e) { alert('Failed to load guide: ' + e.message); }
519
+ }
520
+
521
+ async function triggerVerify(file) {
522
+ try {
523
+ const res = await fetch('/api/plans/trigger-verify', {
524
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
525
+ body: JSON.stringify({ file })
526
+ });
527
+ const d = await res.json();
528
+ if (res.ok && d.ok) {
529
+ closeModal();
530
+ refresh();
531
+ showToast('cmd-toast', d.verifyId ? 'Verify task ' + d.verifyId + ' created' : (d.message || 'Done'), true);
532
+ } else {
533
+ alert('Failed: ' + (d.error || 'unknown'));
534
+ }
535
+ } catch (e) { alert('Error: ' + e.message); }
536
+ }