@yemi33/minions 0.1.2245 → 0.1.2247
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/slim/body.html +28 -0
- package/dashboard/slim/js/chat.js +146 -16
- package/dashboard/slim/js/command-send.js +100 -77
- package/dashboard/slim/js/modals-tiles.js +23 -0
- package/dashboard/slim/js/plans.js +550 -0
- package/dashboard/slim/js/status.js +7 -0
- package/dashboard/slim/styles.css +4 -3
- package/dashboard-build.js +1 -1
- package/docs/slim-ux/architecture-suggestions.md +10 -0
- package/docs/slim-ux/concepts.md +20 -0
- package/engine/lifecycle.js +52 -0
- package/engine/shared.js +16 -0
- package/engine.js +4 -4
- package/package.json +1 -1
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
|
|
2
|
+
// ── Plans + PRD control panel ──────────────────────────────────────
|
|
3
|
+
// Two-tab cockpit box over the plan lifecycle: Plans (the .md drafts and
|
|
4
|
+
// their current lifecycle state) and PRD (the materialized PRDs with their
|
|
5
|
+
// work items, verify task, and linked PRs). Reuses the classic dashboard's
|
|
6
|
+
// existing endpoints — NO new server routes:
|
|
7
|
+
// Plans — /api/plans (list), /api/plans/:file (read a draft), and the
|
|
8
|
+
// lifecycle POSTs /api/plans/{approve,execute,reject,pause,
|
|
9
|
+
// regenerate,archive,delete,unarchive}.
|
|
10
|
+
// PRD — /api/prd (progress.items × PRs × verify), with the .json entries
|
|
11
|
+
// from /api/plans for the per-PRD lifecycle status, and
|
|
12
|
+
// /api/work-items for the verify task row.
|
|
13
|
+
//
|
|
14
|
+
// This file is the renderer only: it defines openPlansModal / renderPlansTab /
|
|
15
|
+
// renderPlansTile / loadPlansCounts and the lifecycle actions, and exposes
|
|
16
|
+
// them in the shared slim IIFE scope. The tile→modal binding, modal-close
|
|
17
|
+
// wiring, tab-click wiring, and the status.js tile count call are wired by the
|
|
18
|
+
// companion wire-up (modals-tiles.js / status.js) — this file does not bind
|
|
19
|
+
// the tile or the modal itself.
|
|
20
|
+
|
|
21
|
+
var _plansActiveTab = 'plans';
|
|
22
|
+
var _plansData = null; // cached /api/plans payload (array)
|
|
23
|
+
var _prdData = null; // cached /api/prd payload ({ progress, status })
|
|
24
|
+
var _plansVerifyWis = null; // cached verify work items (itemType === 'verify')
|
|
25
|
+
var _plansCount = 0; // last-known active plan + PRD count (tile)
|
|
26
|
+
|
|
27
|
+
// Plan statuses that count as "active" (in-flight / needs attention) for the
|
|
28
|
+
// tile indicator. A draft that has been converted to a PRD reports status
|
|
29
|
+
// 'converted' and is excluded here — its PRD carries the live status instead —
|
|
30
|
+
// so a plan + its PRD are never double-counted.
|
|
31
|
+
var PLAN_TERMINAL_STATUSES = { completed: 1, converted: 1, rejected: 1 };
|
|
32
|
+
function _isActivePlanStatus(s) { return !!s && !PLAN_TERMINAL_STATUSES[s]; }
|
|
33
|
+
|
|
34
|
+
var PLAN_STATUS_LABELS = {
|
|
35
|
+
completed: 'Completed', dispatched: 'In Progress', converting: 'Converting',
|
|
36
|
+
converted: 'Has PRD', paused: 'Paused', 'awaiting-approval': 'Awaiting Approval',
|
|
37
|
+
approved: 'Approved', rejected: 'Rejected', 'revision-requested': 'Revision Requested',
|
|
38
|
+
'has-failures': 'Has Failures', active: 'Active', draft: 'Draft',
|
|
39
|
+
};
|
|
40
|
+
// Map a plan/PRD status onto a reusable .tile-chip color class.
|
|
41
|
+
function _planStatusChipClass(s) {
|
|
42
|
+
if (s === 'completed' || s === 'approved') return 'green';
|
|
43
|
+
if (s === 'dispatched' || s === 'converting' || s === 'converted') return 'blue';
|
|
44
|
+
if (s === 'awaiting-approval' || s === 'paused' || s === 'revision-requested') return 'amber';
|
|
45
|
+
if (s === 'rejected' || s === 'has-failures') return 'red';
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
// Map a work-item status (done / in-progress / missing / paused / …) onto a
|
|
49
|
+
// .tile-chip color class.
|
|
50
|
+
function _wiStatusChipClass(s) {
|
|
51
|
+
if (s === 'done' || s === 'decomposed') return 'green';
|
|
52
|
+
if (s === 'in-progress') return 'blue';
|
|
53
|
+
if (s === 'paused' || s === 'blocked') return 'amber';
|
|
54
|
+
if (s === 'failed') return 'red';
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
function _chipClassAttr(cls) { return 'tile-chip' + (cls ? ' ' + cls : ''); }
|
|
58
|
+
|
|
59
|
+
// ── Cockpit tile ───────────────────────────────────────────────────
|
|
60
|
+
// Repaints the Plans tile. Called from status.js's render path and from the
|
|
61
|
+
// lazy count loader. The count is the number of active (non-terminal,
|
|
62
|
+
// non-archived) plan + PRD entries.
|
|
63
|
+
function renderPlansTile(count) {
|
|
64
|
+
if (typeof count === 'number') _plansCount = count;
|
|
65
|
+
var n = (typeof _plansCount === 'number') ? _plansCount : 0;
|
|
66
|
+
var detail = n === 0 ? 'no active plans' : (n + ' active · drafts & PRDs');
|
|
67
|
+
updateTile('plans', n, detail, n ? 'blue' : null);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _countActivePlans(plans) {
|
|
71
|
+
if (!Array.isArray(plans)) return 0;
|
|
72
|
+
var c = 0;
|
|
73
|
+
for (var i = 0; i < plans.length; i++) {
|
|
74
|
+
var p = plans[i];
|
|
75
|
+
if (p && !p.archived && _isActivePlanStatus(p.status)) c++;
|
|
76
|
+
}
|
|
77
|
+
return c;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Best-effort lazy fetch of the plan list to compute the tile count, mirroring
|
|
81
|
+
// knowledge.js#loadKnowledgeCounts. A transient failure leaves the last-known
|
|
82
|
+
// count untouched.
|
|
83
|
+
async function loadPlansCounts() {
|
|
84
|
+
try {
|
|
85
|
+
var res = await fetch('/api/plans', { headers: { 'Accept': 'application/json' } });
|
|
86
|
+
if (res.ok) {
|
|
87
|
+
_plansData = await res.json();
|
|
88
|
+
renderPlansTile(_countActivePlans(_plansData));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
} catch (e) { /* keep last-known */ }
|
|
92
|
+
renderPlansTile();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Modal open/close + tab switching ───────────────────────────────
|
|
96
|
+
function openPlansModal() {
|
|
97
|
+
var modal = document.getElementById('slim-plans-modal');
|
|
98
|
+
if (!modal) return;
|
|
99
|
+
renderPlansTab();
|
|
100
|
+
modal.classList.add('open');
|
|
101
|
+
}
|
|
102
|
+
function closePlansModal() {
|
|
103
|
+
var modal = document.getElementById('slim-plans-modal');
|
|
104
|
+
if (modal) modal.classList.remove('open');
|
|
105
|
+
}
|
|
106
|
+
function setPlansTab(tab) {
|
|
107
|
+
_plansActiveTab = tab;
|
|
108
|
+
var tabs = document.querySelectorAll('#slim-plans-tabs .kn-tab');
|
|
109
|
+
var activeTabId = null;
|
|
110
|
+
for (var i = 0; i < tabs.length; i++) {
|
|
111
|
+
var isActive = tabs[i].getAttribute('data-plans-tab') === tab;
|
|
112
|
+
tabs[i].classList.toggle('active', isActive);
|
|
113
|
+
tabs[i].setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
114
|
+
if (isActive) activeTabId = tabs[i].id;
|
|
115
|
+
}
|
|
116
|
+
var body = document.getElementById('slim-plans-body');
|
|
117
|
+
if (body && activeTabId) body.setAttribute('aria-labelledby', activeTabId);
|
|
118
|
+
renderPlansTab();
|
|
119
|
+
}
|
|
120
|
+
function renderPlansTab() {
|
|
121
|
+
var body = document.getElementById('slim-plans-body');
|
|
122
|
+
if (!body) return;
|
|
123
|
+
body.textContent = '';
|
|
124
|
+
if (_plansActiveTab === 'prd') renderPlansPrdTab(body);
|
|
125
|
+
else renderPlansPlansTab(body);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function _plansMsg(text, color) {
|
|
129
|
+
var msg = document.getElementById('slim-plans-msg');
|
|
130
|
+
if (!msg) return;
|
|
131
|
+
msg.style.color = color || 'var(--muted)';
|
|
132
|
+
msg.textContent = text || '';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Tab: Plans (lifecycle view, one card per source draft) ─────────
|
|
136
|
+
function renderPlansPlansTab(body) {
|
|
137
|
+
var intro = document.createElement('p');
|
|
138
|
+
intro.textContent = 'Plan drafts and their lifecycle. Approve to use the materialized PRD as-is, Execute to (re)generate a PRD, or Archive when done.';
|
|
139
|
+
body.appendChild(intro);
|
|
140
|
+
|
|
141
|
+
var msg = document.createElement('div');
|
|
142
|
+
msg.id = 'slim-plans-msg';
|
|
143
|
+
msg.className = 'kn-msg';
|
|
144
|
+
body.appendChild(msg);
|
|
145
|
+
|
|
146
|
+
var list = document.createElement('div');
|
|
147
|
+
list.id = 'slim-plans-list';
|
|
148
|
+
body.appendChild(list);
|
|
149
|
+
|
|
150
|
+
if (_plansData) renderPlansList(list);
|
|
151
|
+
else {
|
|
152
|
+
var loading = document.createElement('div');
|
|
153
|
+
loading.className = 'tile-empty';
|
|
154
|
+
loading.textContent = 'Loading plans…';
|
|
155
|
+
list.appendChild(loading);
|
|
156
|
+
}
|
|
157
|
+
// Always refresh on open so newly-created/converted plans appear.
|
|
158
|
+
fetch('/api/plans', { headers: { 'Accept': 'application/json' } })
|
|
159
|
+
.then(function(r) { return r.ok ? r.json() : null; })
|
|
160
|
+
.then(function(data) {
|
|
161
|
+
if (!data) return;
|
|
162
|
+
_plansData = data;
|
|
163
|
+
renderPlansTile(_countActivePlans(_plansData));
|
|
164
|
+
if (_plansActiveTab === 'plans') {
|
|
165
|
+
var lw = document.getElementById('slim-plans-list');
|
|
166
|
+
if (lw) renderPlansList(lw);
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
.catch(function() { /* keep cached */ });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Build the logical lifecycle cards: one per non-archived .md draft, plus any
|
|
173
|
+
// non-archived PRD whose source draft isn't itself listed (directly-created
|
|
174
|
+
// PRDs). Each card resolves its effective status + action target from the
|
|
175
|
+
// linked PRD when one exists.
|
|
176
|
+
function _buildPlanCards(plans) {
|
|
177
|
+
var drafts = [];
|
|
178
|
+
var prds = [];
|
|
179
|
+
for (var i = 0; i < plans.length; i++) {
|
|
180
|
+
var p = plans[i];
|
|
181
|
+
if (!p || p.archived) continue;
|
|
182
|
+
if (p.format === 'prd') prds.push(p);
|
|
183
|
+
else drafts.push(p);
|
|
184
|
+
}
|
|
185
|
+
var prdBySource = {};
|
|
186
|
+
for (var j = 0; j < prds.length; j++) {
|
|
187
|
+
if (prds[j].sourcePlan) prdBySource[prds[j].sourcePlan] = prds[j];
|
|
188
|
+
}
|
|
189
|
+
var cards = [];
|
|
190
|
+
var claimedPrd = {};
|
|
191
|
+
for (var k = 0; k < drafts.length; k++) {
|
|
192
|
+
var d = drafts[k];
|
|
193
|
+
var linked = prdBySource[d.file] || null;
|
|
194
|
+
if (linked) claimedPrd[linked.file] = true;
|
|
195
|
+
cards.push({
|
|
196
|
+
draftFile: d.file,
|
|
197
|
+
prdFile: linked ? linked.file : '',
|
|
198
|
+
target: linked ? linked.file : d.file,
|
|
199
|
+
summary: d.summary || d.file,
|
|
200
|
+
project: d.project || '',
|
|
201
|
+
status: linked ? (linked.status || 'active') : (d.status || 'draft'),
|
|
202
|
+
generatedBy: d.generatedBy || (linked && linked.generatedBy) || '',
|
|
203
|
+
generatedAt: d.generatedAt || (linked && linked.generatedAt) || '',
|
|
204
|
+
itemCount: linked ? linked.itemCount : d.itemCount,
|
|
205
|
+
planStale: !!(linked && linked.planStale),
|
|
206
|
+
archiveReady: !!(linked && linked.archiveReady),
|
|
207
|
+
requiresApproval: linked ? !!linked.requiresApproval : false,
|
|
208
|
+
readFile: d.file,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// Directly-created PRDs with no listed source draft.
|
|
212
|
+
for (var m = 0; m < prds.length; m++) {
|
|
213
|
+
var pr = prds[m];
|
|
214
|
+
if (claimedPrd[pr.file]) continue;
|
|
215
|
+
cards.push({
|
|
216
|
+
draftFile: pr.file,
|
|
217
|
+
prdFile: pr.file,
|
|
218
|
+
target: pr.file,
|
|
219
|
+
summary: pr.summary || pr.file,
|
|
220
|
+
project: pr.project || '',
|
|
221
|
+
status: pr.status || 'active',
|
|
222
|
+
generatedBy: pr.generatedBy || '',
|
|
223
|
+
generatedAt: pr.generatedAt || '',
|
|
224
|
+
itemCount: pr.itemCount,
|
|
225
|
+
planStale: !!pr.planStale,
|
|
226
|
+
archiveReady: !!pr.archiveReady,
|
|
227
|
+
requiresApproval: !!pr.requiresApproval,
|
|
228
|
+
readFile: pr.file,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
cards.sort(function(a, b) { return (b.generatedAt || '').localeCompare(a.generatedAt || ''); });
|
|
232
|
+
return cards;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function renderPlansList(listWrap) {
|
|
236
|
+
listWrap.textContent = '';
|
|
237
|
+
var cards = _buildPlanCards(_plansData || []);
|
|
238
|
+
if (!cards.length) {
|
|
239
|
+
var empty = document.createElement('div');
|
|
240
|
+
empty.className = 'tile-empty';
|
|
241
|
+
empty.textContent = 'No active plans. Drafts you create land here for approval and execution.';
|
|
242
|
+
listWrap.appendChild(empty);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
cards.forEach(function(card) { listWrap.appendChild(buildPlanRow(card)); });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildPlanRow(card) {
|
|
249
|
+
var row = document.createElement('div');
|
|
250
|
+
row.className = 'kb-row';
|
|
251
|
+
|
|
252
|
+
var top = document.createElement('div');
|
|
253
|
+
top.className = 'kb-row-top';
|
|
254
|
+
|
|
255
|
+
var chip = document.createElement('span');
|
|
256
|
+
chip.className = _chipClassAttr(_planStatusChipClass(card.status));
|
|
257
|
+
chip.textContent = PLAN_STATUS_LABELS[card.status] || card.status;
|
|
258
|
+
top.appendChild(chip);
|
|
259
|
+
|
|
260
|
+
var title = document.createElement('span');
|
|
261
|
+
title.className = 'kb-row-title';
|
|
262
|
+
title.textContent = card.summary;
|
|
263
|
+
title.title = card.summary;
|
|
264
|
+
top.appendChild(title);
|
|
265
|
+
row.appendChild(top);
|
|
266
|
+
|
|
267
|
+
var meta = document.createElement('div');
|
|
268
|
+
meta.className = 'kb-row-meta';
|
|
269
|
+
var metaBits = [];
|
|
270
|
+
if (card.project) metaBits.push(card.project);
|
|
271
|
+
if (typeof card.itemCount === 'number') metaBits.push(card.itemCount + ' item' + (card.itemCount === 1 ? '' : 's'));
|
|
272
|
+
if (card.generatedBy) metaBits.push(card.generatedBy);
|
|
273
|
+
if (card.generatedAt) metaBits.push(card.generatedAt);
|
|
274
|
+
if (card.planStale) metaBits.push('STALE');
|
|
275
|
+
meta.textContent = metaBits.join(' · ');
|
|
276
|
+
row.appendChild(meta);
|
|
277
|
+
|
|
278
|
+
// Action buttons. Clicking the row (away from a button) opens the draft.
|
|
279
|
+
var actions = document.createElement('div');
|
|
280
|
+
actions.className = 'kn-toolbar kn-toolbar-end';
|
|
281
|
+
actions.addEventListener('click', function(ev) { ev.stopPropagation(); });
|
|
282
|
+
|
|
283
|
+
var status = card.status;
|
|
284
|
+
var awaitingApproval = status === 'awaiting-approval' || (card.requiresApproval && status !== 'completed' && status !== 'rejected' && status !== 'dispatched');
|
|
285
|
+
if (awaitingApproval) {
|
|
286
|
+
actions.appendChild(_planActionBtn('Approve', 'btn-primary', function() { plansApprove(card.target); }));
|
|
287
|
+
actions.appendChild(_planActionBtn('Reject', 'btn-secondary', function() { plansReject(card.target); }));
|
|
288
|
+
} else if (status === 'paused') {
|
|
289
|
+
actions.appendChild(_planActionBtn('Resume', 'btn-primary', function() { plansApprove(card.target); }));
|
|
290
|
+
} else if (status === 'dispatched') {
|
|
291
|
+
actions.appendChild(_planActionBtn('Pause', 'btn-secondary', function() { plansPause(card.target); }));
|
|
292
|
+
}
|
|
293
|
+
// Execute: a draft with no PRD yet (re)generates one.
|
|
294
|
+
if (!card.prdFile && (status === 'draft' || status === 'active')) {
|
|
295
|
+
actions.appendChild(_planActionBtn('Execute', 'btn-primary', function() { plansExecute(card.draftFile, card.project); }));
|
|
296
|
+
}
|
|
297
|
+
actions.appendChild(_planActionBtn(card.archiveReady ? '✓ Archive' : 'Archive', 'btn-secondary', function() { plansArchive(card.target); }));
|
|
298
|
+
actions.appendChild(_planActionBtn('Delete', 'btn-secondary', function() { plansDelete(card.draftFile); }));
|
|
299
|
+
row.appendChild(actions);
|
|
300
|
+
|
|
301
|
+
row.addEventListener('click', function() { openPlanDraft(card.readFile); });
|
|
302
|
+
return row;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function _planActionBtn(label, cls, onClick) {
|
|
306
|
+
var b = document.createElement('button');
|
|
307
|
+
b.className = cls;
|
|
308
|
+
b.type = 'button';
|
|
309
|
+
b.textContent = label;
|
|
310
|
+
b.addEventListener('click', function(ev) { ev.stopPropagation(); onClick(); });
|
|
311
|
+
return b;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Read a plan draft (.md markdown or .json PRD) into a <pre>.
|
|
315
|
+
async function openPlanDraft(file) {
|
|
316
|
+
var body = document.getElementById('slim-plans-body');
|
|
317
|
+
if (!body) return;
|
|
318
|
+
body.textContent = '';
|
|
319
|
+
|
|
320
|
+
var backRow = document.createElement('div');
|
|
321
|
+
backRow.className = 'kn-toolbar';
|
|
322
|
+
var backBtn = document.createElement('button');
|
|
323
|
+
backBtn.className = 'btn-secondary';
|
|
324
|
+
backBtn.type = 'button';
|
|
325
|
+
backBtn.textContent = '← Back to plans';
|
|
326
|
+
backBtn.addEventListener('click', function() { renderPlansTab(); });
|
|
327
|
+
backRow.appendChild(backBtn);
|
|
328
|
+
body.appendChild(backRow);
|
|
329
|
+
|
|
330
|
+
var head = document.createElement('div');
|
|
331
|
+
head.className = 'kn-section-head';
|
|
332
|
+
head.textContent = file;
|
|
333
|
+
body.appendChild(head);
|
|
334
|
+
|
|
335
|
+
var pre = document.createElement('pre');
|
|
336
|
+
pre.className = 'kb-entry-content';
|
|
337
|
+
pre.textContent = 'Loading…';
|
|
338
|
+
body.appendChild(pre);
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
var res = await fetch('/api/plans/' + encodeURIComponent(file));
|
|
342
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
343
|
+
var text = await res.text();
|
|
344
|
+
pre.textContent = text;
|
|
345
|
+
} catch (e) {
|
|
346
|
+
pre.textContent = 'Failed to load plan: ' + (e && e.message ? e.message : e);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── Lifecycle actions (POST to existing endpoints, then refresh) ───
|
|
351
|
+
async function _plansPost(endpoint, payload, pending) {
|
|
352
|
+
_plansMsg(pending || 'Working…', 'var(--muted)');
|
|
353
|
+
try {
|
|
354
|
+
var res = await fetch(endpoint, {
|
|
355
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
356
|
+
body: JSON.stringify(payload),
|
|
357
|
+
});
|
|
358
|
+
var d = await res.json().catch(function() { return {}; });
|
|
359
|
+
if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
|
|
360
|
+
_plansMsg('Done.', 'var(--green)');
|
|
361
|
+
await refreshPlans();
|
|
362
|
+
return true;
|
|
363
|
+
} catch (e) {
|
|
364
|
+
_plansMsg('Error: ' + (e && e.message ? e.message : 'failed'), 'var(--red)');
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function plansApprove(file) { return _plansPost('/api/plans/approve', { file: file }, 'Approving…'); }
|
|
369
|
+
function plansExecute(file, project) { return _plansPost('/api/plans/execute', { file: file, project: project || '' }, 'Executing…'); }
|
|
370
|
+
function plansReject(file) { return _plansPost('/api/plans/reject', { file: file }, 'Rejecting…'); }
|
|
371
|
+
function plansPause(file) { return _plansPost('/api/plans/pause', { file: file }, 'Pausing…'); }
|
|
372
|
+
function plansRegenerate(source) { return _plansPost('/api/plans/regenerate', { source: source }, 'Regenerating…'); }
|
|
373
|
+
function plansUnarchive(file) { return _plansPost('/api/plans/unarchive', { file: file }, 'Restoring…'); }
|
|
374
|
+
function plansArchive(file) { return _plansPost('/api/plans/archive', { file: file }, 'Archiving…'); }
|
|
375
|
+
async function plansDelete(file) {
|
|
376
|
+
if (typeof window !== 'undefined' && window.confirm && !window.confirm('Delete this plan and clean up its work items?')) return false;
|
|
377
|
+
return _plansPost('/api/plans/delete', { file: file }, 'Deleting…');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Re-fetch the plan list (cache-busting the local copy), update the tile, and
|
|
381
|
+
// re-render whichever tab is active.
|
|
382
|
+
async function refreshPlans() {
|
|
383
|
+
try {
|
|
384
|
+
var res = await fetch('/api/plans', { headers: { 'Accept': 'application/json' } });
|
|
385
|
+
if (res.ok) {
|
|
386
|
+
_plansData = await res.json();
|
|
387
|
+
renderPlansTile(_countActivePlans(_plansData));
|
|
388
|
+
}
|
|
389
|
+
} catch (e) { /* keep cached */ }
|
|
390
|
+
if (_plansActiveTab === 'plans') {
|
|
391
|
+
var lw = document.getElementById('slim-plans-list');
|
|
392
|
+
if (lw) renderPlansList(lw);
|
|
393
|
+
} else {
|
|
394
|
+
_prdData = null;
|
|
395
|
+
renderPlansTab();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ── Tab: PRD (materialized PRDs × work items × verify × PRs) ────────
|
|
400
|
+
function renderPlansPrdTab(body) {
|
|
401
|
+
var intro = document.createElement('p');
|
|
402
|
+
intro.textContent = 'Materialized PRDs — each plan item, its status, linked PRs, and the verify task.';
|
|
403
|
+
body.appendChild(intro);
|
|
404
|
+
|
|
405
|
+
var list = document.createElement('div');
|
|
406
|
+
list.id = 'slim-prd-list';
|
|
407
|
+
body.appendChild(list);
|
|
408
|
+
|
|
409
|
+
var loading = document.createElement('div');
|
|
410
|
+
loading.className = 'tile-empty';
|
|
411
|
+
loading.textContent = 'Loading PRDs…';
|
|
412
|
+
list.appendChild(loading);
|
|
413
|
+
|
|
414
|
+
Promise.all([
|
|
415
|
+
fetch('/api/prd', { headers: { 'Accept': 'application/json' } }).then(function(r) { return r.ok ? r.json() : null; }).catch(function() { return null; }),
|
|
416
|
+
fetch('/api/work-items', { headers: { 'Accept': 'application/json' } }).then(function(r) { return r.ok ? r.json() : null; }).catch(function() { return null; }),
|
|
417
|
+
]).then(function(results) {
|
|
418
|
+
_prdData = results[0];
|
|
419
|
+
_plansVerifyWis = Array.isArray(results[1])
|
|
420
|
+
? results[1].filter(function(w) { return w && w.itemType === 'verify'; })
|
|
421
|
+
: [];
|
|
422
|
+
if (_plansActiveTab !== 'prd') return;
|
|
423
|
+
var lw = document.getElementById('slim-prd-list');
|
|
424
|
+
if (lw) renderPrdList(lw);
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function renderPrdList(listWrap) {
|
|
429
|
+
listWrap.textContent = '';
|
|
430
|
+
var progress = _prdData && _prdData.progress;
|
|
431
|
+
var items = progress && Array.isArray(progress.items) ? progress.items : [];
|
|
432
|
+
if (!items.length) {
|
|
433
|
+
var empty = document.createElement('div');
|
|
434
|
+
empty.className = 'tile-empty';
|
|
435
|
+
empty.textContent = 'No materialized PRDs yet. Execute a plan from the Plans tab to generate one.';
|
|
436
|
+
listWrap.appendChild(empty);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
// Group items by their source PRD file (preserve first-seen order).
|
|
440
|
+
var groups = [];
|
|
441
|
+
var bySource = {};
|
|
442
|
+
items.forEach(function(it) {
|
|
443
|
+
var src = it.source || '(unknown)';
|
|
444
|
+
if (!bySource[src]) {
|
|
445
|
+
bySource[src] = { source: src, summary: it.planSummary || src, status: it.planStatus || 'active', items: [] };
|
|
446
|
+
groups.push(bySource[src]);
|
|
447
|
+
}
|
|
448
|
+
bySource[src].items.push(it);
|
|
449
|
+
});
|
|
450
|
+
groups.forEach(function(g) { listWrap.appendChild(buildPrdGroup(g)); });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function buildPrdGroup(g) {
|
|
454
|
+
var wrap = document.createElement('div');
|
|
455
|
+
wrap.className = 'kb-row';
|
|
456
|
+
|
|
457
|
+
var head = document.createElement('div');
|
|
458
|
+
head.className = 'kb-row-top';
|
|
459
|
+
var chip = document.createElement('span');
|
|
460
|
+
chip.className = _chipClassAttr(_planStatusChipClass(g.status));
|
|
461
|
+
chip.textContent = PLAN_STATUS_LABELS[g.status] || g.status;
|
|
462
|
+
head.appendChild(chip);
|
|
463
|
+
var title = document.createElement('span');
|
|
464
|
+
title.className = 'kb-row-title';
|
|
465
|
+
title.textContent = g.summary;
|
|
466
|
+
title.title = g.source;
|
|
467
|
+
head.appendChild(title);
|
|
468
|
+
wrap.appendChild(head);
|
|
469
|
+
|
|
470
|
+
var done = g.items.filter(function(i) { return i.status === 'done' || i.status === 'decomposed'; }).length;
|
|
471
|
+
var meta = document.createElement('div');
|
|
472
|
+
meta.className = 'kb-row-meta';
|
|
473
|
+
meta.textContent = g.source + ' · ' + done + '/' + g.items.length + ' done';
|
|
474
|
+
wrap.appendChild(meta);
|
|
475
|
+
|
|
476
|
+
// Per-item rows.
|
|
477
|
+
g.items.forEach(function(it) { wrap.appendChild(buildPrdItemRow(it)); });
|
|
478
|
+
|
|
479
|
+
// Verify task row, if a verify work item exists for this PRD.
|
|
480
|
+
var verify = (_plansVerifyWis || []).filter(function(w) { return w.sourcePlan === g.source; });
|
|
481
|
+
if (verify.length) {
|
|
482
|
+
verify.forEach(function(w) {
|
|
483
|
+
var vrow = document.createElement('div');
|
|
484
|
+
vrow.className = 'kb-row-meta';
|
|
485
|
+
var vchip = document.createElement('span');
|
|
486
|
+
vchip.className = _chipClassAttr(_wiStatusChipClass(w.status));
|
|
487
|
+
vchip.textContent = 'Verify: ' + (w.status || 'pending');
|
|
488
|
+
vrow.appendChild(vchip);
|
|
489
|
+
wrap.appendChild(vrow);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
return wrap;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function buildPrdItemRow(it) {
|
|
496
|
+
var row = document.createElement('div');
|
|
497
|
+
row.className = 'kb-row-meta';
|
|
498
|
+
row.style.display = 'flex';
|
|
499
|
+
row.style.alignItems = 'center';
|
|
500
|
+
row.style.gap = '6px';
|
|
501
|
+
row.style.flexWrap = 'wrap';
|
|
502
|
+
|
|
503
|
+
var chip = document.createElement('span');
|
|
504
|
+
chip.className = _chipClassAttr(_wiStatusChipClass(it.status));
|
|
505
|
+
chip.textContent = it.status || 'missing';
|
|
506
|
+
row.appendChild(chip);
|
|
507
|
+
|
|
508
|
+
var name = document.createElement('span');
|
|
509
|
+
name.textContent = it.name || it.id || '(item)';
|
|
510
|
+
name.style.color = 'var(--text)';
|
|
511
|
+
row.appendChild(name);
|
|
512
|
+
|
|
513
|
+
(it.projects || []).forEach(function(p) {
|
|
514
|
+
var pb = document.createElement('span');
|
|
515
|
+
pb.className = 'tile-chip';
|
|
516
|
+
pb.textContent = p;
|
|
517
|
+
row.appendChild(pb);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
(it.prs || []).forEach(function(pr) { row.appendChild(_buildPlansPrLink(pr)); });
|
|
521
|
+
return row;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function _buildPlansPrLink(pr) {
|
|
525
|
+
var icon = pr.status === 'merged' ? '✓' : pr.status === 'abandoned' ? '✗' : '○';
|
|
526
|
+
var span = document.createElement('span');
|
|
527
|
+
span.style.display = 'inline-flex';
|
|
528
|
+
span.style.alignItems = 'center';
|
|
529
|
+
span.style.gap = '3px';
|
|
530
|
+
span.style.marginLeft = '4px';
|
|
531
|
+
var dot = document.createElement('span');
|
|
532
|
+
dot.textContent = icon;
|
|
533
|
+
dot.title = pr.status || 'active';
|
|
534
|
+
dot.style.color = pr.status === 'merged' ? 'var(--green)' : pr.status === 'abandoned' ? 'var(--red)' : 'var(--blue)';
|
|
535
|
+
span.appendChild(dot);
|
|
536
|
+
if (pr.url) {
|
|
537
|
+
var a = document.createElement('a');
|
|
538
|
+
a.href = pr.url;
|
|
539
|
+
a.target = '_blank';
|
|
540
|
+
a.rel = 'noopener';
|
|
541
|
+
a.textContent = pr.id || 'PR';
|
|
542
|
+
a.title = (pr.title || '') + ' (' + (pr.status || 'active') + ')';
|
|
543
|
+
span.appendChild(a);
|
|
544
|
+
} else {
|
|
545
|
+
var code = document.createElement('code');
|
|
546
|
+
code.textContent = pr.id || 'PR';
|
|
547
|
+
span.appendChild(code);
|
|
548
|
+
}
|
|
549
|
+
return span;
|
|
550
|
+
}
|
|
@@ -155,6 +155,13 @@
|
|
|
155
155
|
var pinned = Array.isArray(data.pinned) ? data.pinned : [];
|
|
156
156
|
if (typeof renderKnowledgeTile === 'function') renderKnowledgeTile(pinned.length);
|
|
157
157
|
|
|
158
|
+
// ── Plans tile (drafts + materialized PRDs) ───────────────────
|
|
159
|
+
// The plan/PRD count is not part of the /api/status snapshot, so this
|
|
160
|
+
// repaints the tile from the renderer's last-known cached count (seeded by
|
|
161
|
+
// the lazy loadPlansCounts fetch in modals-tiles.js#bindPlansUi and
|
|
162
|
+
// refreshed on each modal open / lifecycle action).
|
|
163
|
+
if (typeof renderPlansTile === 'function') renderPlansTile();
|
|
164
|
+
|
|
158
165
|
// ── Team member cards ──────────────────────────────────────
|
|
159
166
|
renderMembers(Array.isArray(data.agents) ? data.agents : []);
|
|
160
167
|
|
|
@@ -744,7 +744,7 @@
|
|
|
744
744
|
|
|
745
745
|
/* Knowledge control panel (slim-knowledge-modal): Pinned Context / Notes /
|
|
746
746
|
KB tabs in one box. Reuses .tile-* and .pinned-row primitives. */
|
|
747
|
-
.slim-knowledge-modal-inner { width: 720px; max-width: calc(100vw - 32px); }
|
|
747
|
+
.slim-knowledge-modal-inner, .slim-plans-modal-inner { width: 720px; max-width: calc(100vw - 32px); }
|
|
748
748
|
.kn-tabs { display: flex; gap: 6px; margin-left: 16px; }
|
|
749
749
|
.kn-tab {
|
|
750
750
|
border: 1px solid var(--border);
|
|
@@ -990,9 +990,10 @@
|
|
|
990
990
|
.cockpit-grid > [data-tile="queued"],
|
|
991
991
|
.cockpit-grid > [data-tile="dispatches"],
|
|
992
992
|
.cockpit-grid > [data-tile="prs"] { grid-column: span 2; }
|
|
993
|
-
/* Row 3: Watches | Knowledge —
|
|
993
|
+
/* Row 3: Watches | Knowledge | Plans — three equal columns (2 of 6 each). */
|
|
994
994
|
.cockpit-grid > [data-tile="watches"],
|
|
995
|
-
.cockpit-grid > [data-tile="knowledge"]
|
|
995
|
+
.cockpit-grid > [data-tile="knowledge"],
|
|
996
|
+
.cockpit-grid > [data-tile="plans"] { grid-column: span 2; }
|
|
996
997
|
.cockpit-tile {
|
|
997
998
|
background: var(--surface2);
|
|
998
999
|
border: 1px solid var(--border);
|
package/dashboard-build.js
CHANGED
|
@@ -71,7 +71,7 @@ function buildDashboardHtml() {
|
|
|
71
71
|
// original single-file source) inside the wrapper that layout.html provides.
|
|
72
72
|
const SLIM_JS_ORDER = [
|
|
73
73
|
'helpers', 'settings', 'link-pr', 'chat', 'projects',
|
|
74
|
-
'command-send', 'status', 'members', 'modals-tiles', 'history', 'pinned', 'knowledge',
|
|
74
|
+
'command-send', 'status', 'members', 'modals-tiles', 'history', 'pinned', 'knowledge', 'plans',
|
|
75
75
|
];
|
|
76
76
|
|
|
77
77
|
// Cache for the assembled slim source fragments (layout/css/body/js). Keyed on
|
|
@@ -151,6 +151,16 @@ Risky because plan resume logic depends on reading the PRD JSON twin.
|
|
|
151
151
|
be re-verified. **Recommendation: defer until at least one round of slim UX shows the unified
|
|
152
152
|
"Plans" button is the right primitive.**
|
|
153
153
|
|
|
154
|
+
**Update (shipped, lighter form).** The slim UX now ships the *unified surface* half of this
|
|
155
|
+
proposal without the risky storage/document merge: a single **Plans** cockpit tile opens a tabbed
|
|
156
|
+
**Plans / PRD** modal (`dashboard/slim/body.html` `#slim-plans-modal`, rendered by
|
|
157
|
+
`dashboard/slim/js/plans.js`). Plan and PRD remain two stores / two `/api/*` surfaces under the
|
|
158
|
+
hood — the modal just presents them behind one entry point with two tabs, reusing the existing
|
|
159
|
+
`/api/plans*`, `/api/prd`, and `/api/work-items` endpoints (no new routes). This realizes the "slim
|
|
160
|
+
shows one Plans primitive" mental-model win while leaving the document-merge and materializer
|
|
161
|
+
changes deferred as above. Documented in [`concepts.md`](./concepts.md) §3 Plan / §4 PRD under
|
|
162
|
+
"Slim dashboard."
|
|
163
|
+
|
|
154
164
|
---
|
|
155
165
|
|
|
156
166
|
## 6. Notes + KB + Pinned: which survives?
|
package/docs/slim-ux/concepts.md
CHANGED
|
@@ -129,6 +129,19 @@ flow is: human (or agent) writes plan → human approves → `plan-to-prd` agent
|
|
|
129
129
|
**Existing dashboard.** `dashboard/pages/plans.html` — plan browser, approve / reject / archive
|
|
130
130
|
buttons.
|
|
131
131
|
|
|
132
|
+
**Slim dashboard.** A **Plans** cockpit tile (`data-tile="plans"` in `dashboard/slim/body.html`)
|
|
133
|
+
opens `#slim-plans-modal` — a two-tab control panel (**Plans** / **PRD**) rendered lazily by
|
|
134
|
+
`dashboard/slim/js/plans.js` into `#slim-plans-body`. The **Plans** tab lists the `.md` drafts with
|
|
135
|
+
their status and wires the lifecycle actions (approve / execute / archive at minimum, plus reject /
|
|
136
|
+
pause / regenerate / delete / unarchive) to the **existing** `GET /api/plans`, `GET /api/plans/:file`,
|
|
137
|
+
and the `POST /api/plans/*` endpoints — **no new server routes** are added. The tile's count is
|
|
138
|
+
fetched lazily via `GET /api/plans` (`loadPlansCounts`), *not* from `/api/status` (the status
|
|
139
|
+
snapshot carries no plans/PRD count). The whole surface lives under `dashboard/slim/` and is gated
|
|
140
|
+
solely by the **`slim-ux`** feature flag — there is **no new `engine.*` flag or Settings-parity
|
|
141
|
+
toggle**; Slim itself is the gate. The renderer exposes `openPlansModal` / `renderPlansTab` /
|
|
142
|
+
`renderPlansTile` / `loadPlansCounts` in the shared Slim IIFE scope; tile→modal/tab binding is wired
|
|
143
|
+
by `modals-tiles.js` + `status.js`.
|
|
144
|
+
|
|
132
145
|
**Endpoints (`dashboard.js` lines 6952–6963).**
|
|
133
146
|
- `GET /api/plans` (6952) — list .md drafts + .json PRDs
|
|
134
147
|
- `POST /api/plans/trigger-verify` (6953)
|
|
@@ -170,6 +183,13 @@ item has acceptance criteria, complexity, dependencies, and a status (`missing |
|
|
|
170
183
|
PRD items and their states. Recent feature: PRD graph view at parity with list view (commit
|
|
171
184
|
`b4767d2d`).
|
|
172
185
|
|
|
186
|
+
**Slim dashboard.** The same `#slim-plans-modal` (opened from the Plans cockpit tile) carries a
|
|
187
|
+
**PRD** tab rendered by `dashboard/slim/js/plans.js`, mirroring the classic PRD view: it lists
|
|
188
|
+
materialized PRDs with their work items, the verify task, and linked PRs. Reads use the **existing**
|
|
189
|
+
`GET /api/prd`, `GET /api/plans` (the `.json` entries, for per-PRD lifecycle status), and
|
|
190
|
+
`GET /api/work-items` (the verify row) — no new routes. See **§3 Plan → Slim dashboard** for the
|
|
191
|
+
shared tile/modal, lazy-count source, and `slim-ux`-only gating (no new `engine.*` flag).
|
|
192
|
+
|
|
173
193
|
**Endpoints (`dashboard.js`).**
|
|
174
194
|
- `POST /api/prd-items` (6968) — create PRD item
|
|
175
195
|
- `POST /api/prd-items/update` (6969)
|