@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.
- package/CHANGELOG.md +59 -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 +309 -0
- package/dashboard/js/modal.js +131 -0
- package/dashboard/js/refresh.js +59 -0
- package/dashboard/js/render-agents.js +44 -0
- package/dashboard/js/render-dispatch.js +171 -0
- package/dashboard/js/render-inbox.js +163 -0
- package/dashboard/js/render-kb.js +125 -0
- package/dashboard/js/render-other.js +181 -0
- package/dashboard/js/render-plans.js +536 -0
- package/dashboard/js/render-prd.js +688 -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 +155 -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 +52 -0
- package/dashboard.js +44 -1
- 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 & 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
|
+
}
|