@yemi33/minions 0.1.2306 → 0.1.2307

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.
@@ -1,27 +1,19 @@
1
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.
2
+ // ── Plans cockpit tile (count only) ────────────────────────────────
3
+ // The Plans tile opens the LITERAL classic /plans screen in an iframe
4
+ // (W-mr28ko750010199f / "reuse, don't fork" — see modals-tiles.js
5
+ // renderPlansBody), so there is NO slim-specific Plans/PRD flyout, renderer, or
6
+ // lifecycle-action layer here anymore. The classic /plans screen is the single
7
+ // source of truth for Plans/PRD rendering + every lifecycle action
8
+ // (approve/execute/reject/pause/regenerate/unarchive/archive/delete).
13
9
  //
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.
10
+ // What survives in slim is only the cockpit tile's at-a-glance count: the
11
+ // plan/PRD count is not part of the /api/status poll, so it is fetched lazily
12
+ // from the existing /api/plans list (NO new server route), mirroring
13
+ // knowledge.js#loadKnowledgeCounts. status.js repaints the tile from the
14
+ // last-known cached count on each status render; modals-tiles.js seeds it once
15
+ // shortly after first paint.
20
16
 
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
17
  var _plansCount = 0; // last-known active plan + PRD count (tile)
26
18
 
27
19
  // Plan statuses that count as "active" (in-flight / needs attention) for the
@@ -31,32 +23,16 @@
31
23
  var PLAN_TERMINAL_STATUSES = { completed: 1, converted: 1, rejected: 1 };
32
24
  function _isActivePlanStatus(s) { return !!s && !PLAN_TERMINAL_STATUSES[s]; }
33
25
 
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 '';
26
+ function _countActivePlans(plans) {
27
+ if (!Array.isArray(plans)) return 0;
28
+ var c = 0;
29
+ for (var i = 0; i < plans.length; i++) {
30
+ var p = plans[i];
31
+ if (p && !p.archived && _isActivePlanStatus(p.status)) c++;
32
+ }
33
+ return c;
56
34
  }
57
- function _chipClassAttr(cls) { return 'tile-chip' + (cls ? ' ' + cls : ''); }
58
35
 
59
- // ── Cockpit tile ───────────────────────────────────────────────────
60
36
  // Repaints the Plans tile. Called from status.js's render path and from the
61
37
  // lazy count loader. The count is the number of active (non-terminal,
62
38
  // non-archived) plan + PRD entries.
@@ -67,485 +43,17 @@
67
43
  updateTile('plans', n, detail, n ? 'blue' : null);
68
44
  }
69
45
 
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
46
  // Best-effort lazy fetch of the plan list to compute the tile count, mirroring
81
47
  // knowledge.js#loadKnowledgeCounts. A transient failure leaves the last-known
82
- // count untouched.
48
+ // count untouched. Uses the existing /api/plans list endpoint — no new route.
83
49
  async function loadPlansCounts() {
84
50
  try {
85
51
  var res = await fetch('/api/plans', { headers: { 'Accept': 'application/json' } });
86
52
  if (res.ok) {
87
- _plansData = await res.json();
88
- renderPlansTile(_countActivePlans(_plansData));
53
+ var plans = await res.json();
54
+ renderPlansTile(_countActivePlans(plans));
89
55
  return;
90
56
  }
91
57
  } catch (e) { /* keep last-known */ }
92
58
  renderPlansTile();
93
59
  }
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
- var sp = prds[j].sourcePlan;
188
- if (sp) prdBySource[sp.replace(/^plans\//, '')] = prds[j];
189
- }
190
- var cards = [];
191
- var claimedPrd = {};
192
- for (var k = 0; k < drafts.length; k++) {
193
- var d = drafts[k];
194
- var linked = prdBySource[d.file] || null;
195
- if (linked) claimedPrd[linked.file] = true;
196
- cards.push({
197
- draftFile: d.file,
198
- prdFile: linked ? linked.file : '',
199
- target: linked ? linked.file : d.file,
200
- summary: d.summary || d.file,
201
- project: d.project || '',
202
- status: linked ? (linked.status || 'active') : (d.status || 'draft'),
203
- generatedBy: d.generatedBy || (linked && linked.generatedBy) || '',
204
- generatedAt: d.generatedAt || (linked && linked.generatedAt) || '',
205
- itemCount: linked ? linked.itemCount : d.itemCount,
206
- planStale: !!(linked && linked.planStale),
207
- archiveReady: !!(linked && linked.archiveReady),
208
- requiresApproval: linked ? !!linked.requiresApproval : false,
209
- readFile: d.file,
210
- });
211
- }
212
- // Directly-created PRDs with no listed source draft.
213
- for (var m = 0; m < prds.length; m++) {
214
- var pr = prds[m];
215
- if (claimedPrd[pr.file]) continue;
216
- cards.push({
217
- draftFile: pr.file,
218
- prdFile: pr.file,
219
- target: pr.file,
220
- summary: pr.summary || pr.file,
221
- project: pr.project || '',
222
- status: pr.status || 'active',
223
- generatedBy: pr.generatedBy || '',
224
- generatedAt: pr.generatedAt || '',
225
- itemCount: pr.itemCount,
226
- planStale: !!pr.planStale,
227
- archiveReady: !!pr.archiveReady,
228
- requiresApproval: !!pr.requiresApproval,
229
- readFile: pr.file,
230
- });
231
- }
232
- cards.sort(function(a, b) { return (b.generatedAt || '').localeCompare(a.generatedAt || ''); });
233
- return cards;
234
- }
235
-
236
- function renderPlansList(listWrap) {
237
- listWrap.textContent = '';
238
- var cards = _buildPlanCards(_plansData || []);
239
- if (!cards.length) {
240
- var empty = document.createElement('div');
241
- empty.className = 'tile-empty';
242
- empty.textContent = 'No active plans. Drafts you create land here for approval and execution.';
243
- listWrap.appendChild(empty);
244
- return;
245
- }
246
- cards.forEach(function(card) { listWrap.appendChild(buildPlanRow(card)); });
247
- }
248
-
249
- function buildPlanRow(card) {
250
- var row = document.createElement('div');
251
- row.className = 'kb-row';
252
-
253
- var top = document.createElement('div');
254
- top.className = 'kb-row-top';
255
-
256
- var chip = document.createElement('span');
257
- chip.className = _chipClassAttr(_planStatusChipClass(card.status));
258
- chip.textContent = PLAN_STATUS_LABELS[card.status] || card.status;
259
- top.appendChild(chip);
260
-
261
- var title = document.createElement('span');
262
- title.className = 'kb-row-title';
263
- title.textContent = card.summary;
264
- title.title = card.summary;
265
- top.appendChild(title);
266
- row.appendChild(top);
267
-
268
- var meta = document.createElement('div');
269
- meta.className = 'kb-row-meta';
270
- var metaBits = [];
271
- if (card.project) metaBits.push(card.project);
272
- if (typeof card.itemCount === 'number') metaBits.push(card.itemCount + ' item' + (card.itemCount === 1 ? '' : 's'));
273
- if (card.generatedBy) metaBits.push(card.generatedBy);
274
- if (card.generatedAt) metaBits.push(card.generatedAt);
275
- if (card.planStale) metaBits.push('STALE');
276
- meta.textContent = metaBits.join(' · ');
277
- row.appendChild(meta);
278
-
279
- // Action buttons. Clicking the row (away from a button) opens the draft.
280
- var actions = document.createElement('div');
281
- actions.className = 'kn-toolbar kn-toolbar-end';
282
- actions.addEventListener('click', function(ev) { ev.stopPropagation(); });
283
-
284
- var status = card.status;
285
- var awaitingApproval = status === 'awaiting-approval' || (card.requiresApproval && status !== 'completed' && status !== 'rejected' && status !== 'dispatched');
286
- if (awaitingApproval) {
287
- actions.appendChild(_planActionBtn('Approve', 'btn-primary', function() { plansApprove(card.target); }));
288
- actions.appendChild(_planActionBtn('Reject', 'btn-secondary', function() { plansReject(card.target); }));
289
- } else if (status === 'paused') {
290
- actions.appendChild(_planActionBtn('Resume', 'btn-primary', function() { plansApprove(card.target); }));
291
- } else if (status === 'dispatched') {
292
- actions.appendChild(_planActionBtn('Pause', 'btn-secondary', function() { plansPause(card.target); }));
293
- }
294
- // Execute: a draft with no PRD yet (re)generates one.
295
- if (!card.prdFile && (status === 'draft' || status === 'active')) {
296
- actions.appendChild(_planActionBtn('Execute', 'btn-primary', function() { plansExecute(card.draftFile, card.project); }));
297
- }
298
- actions.appendChild(_planActionBtn(card.archiveReady ? '✓ Archive' : 'Archive', 'btn-secondary', function() { plansArchive(card.target); }));
299
- actions.appendChild(_planActionBtn('Delete', 'btn-secondary', function() { plansDelete(card.draftFile); }));
300
- row.appendChild(actions);
301
-
302
- row.addEventListener('click', function() { openPlanDraft(card.readFile); });
303
- return row;
304
- }
305
-
306
- function _planActionBtn(label, cls, onClick) {
307
- var b = document.createElement('button');
308
- b.className = cls;
309
- b.type = 'button';
310
- b.textContent = label;
311
- b.addEventListener('click', function(ev) { ev.stopPropagation(); onClick(); });
312
- return b;
313
- }
314
-
315
- // Read a plan draft (.md markdown or .json PRD) into a <pre>.
316
- async function openPlanDraft(file) {
317
- var body = document.getElementById('slim-plans-body');
318
- if (!body) return;
319
- body.textContent = '';
320
-
321
- var backRow = document.createElement('div');
322
- backRow.className = 'kn-toolbar';
323
- var backBtn = document.createElement('button');
324
- backBtn.className = 'btn-secondary';
325
- backBtn.type = 'button';
326
- backBtn.textContent = '← Back to plans';
327
- backBtn.addEventListener('click', function() { renderPlansTab(); });
328
- backRow.appendChild(backBtn);
329
- body.appendChild(backRow);
330
-
331
- var head = document.createElement('div');
332
- head.className = 'kn-section-head';
333
- head.textContent = file;
334
- body.appendChild(head);
335
-
336
- var pre = document.createElement('pre');
337
- pre.className = 'kb-entry-content';
338
- pre.textContent = 'Loading…';
339
- body.appendChild(pre);
340
-
341
- try {
342
- var res = await fetch('/api/plans/' + encodeURIComponent(file));
343
- if (!res.ok) throw new Error('HTTP ' + res.status);
344
- var text = await res.text();
345
- pre.textContent = text;
346
- } catch (e) {
347
- pre.textContent = 'Failed to load plan: ' + (e && e.message ? e.message : e);
348
- }
349
- }
350
-
351
- // ── Lifecycle actions (POST to existing endpoints, then refresh) ───
352
- async function _plansPost(endpoint, payload, pending) {
353
- _plansMsg(pending || 'Working…', 'var(--muted)');
354
- try {
355
- var res = await fetch(endpoint, {
356
- method: 'POST', headers: { 'Content-Type': 'application/json' },
357
- body: JSON.stringify(payload),
358
- });
359
- var d = await res.json().catch(function() { return {}; });
360
- if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
361
- _plansMsg('Done.', 'var(--green)');
362
- await refreshPlans();
363
- return true;
364
- } catch (e) {
365
- _plansMsg('Error: ' + (e && e.message ? e.message : 'failed'), 'var(--red)');
366
- return false;
367
- }
368
- }
369
- function plansApprove(file) { return _plansPost('/api/plans/approve', { file: file }, 'Approving…'); }
370
- function plansExecute(file, project) { return _plansPost('/api/plans/execute', { file: file, project: project || '' }, 'Executing…'); }
371
- function plansReject(file) { return _plansPost('/api/plans/reject', { file: file }, 'Rejecting…'); }
372
- function plansPause(file) { return _plansPost('/api/plans/pause', { file: file }, 'Pausing…'); }
373
- function plansRegenerate(source) { return _plansPost('/api/plans/regenerate', { source: source }, 'Regenerating…'); }
374
- function plansUnarchive(file) { return _plansPost('/api/plans/unarchive', { file: file }, 'Restoring…'); }
375
- function plansArchive(file) { return _plansPost('/api/plans/archive', { file: file }, 'Archiving…'); }
376
- async function plansDelete(file) {
377
- if (typeof window !== 'undefined' && window.confirm && !window.confirm('Delete this plan and clean up its work items?')) return false;
378
- return _plansPost('/api/plans/delete', { file: file }, 'Deleting…');
379
- }
380
-
381
- // Re-fetch the plan list (cache-busting the local copy), update the tile, and
382
- // re-render whichever tab is active.
383
- async function refreshPlans() {
384
- try {
385
- var res = await fetch('/api/plans', { headers: { 'Accept': 'application/json' } });
386
- if (res.ok) {
387
- _plansData = await res.json();
388
- renderPlansTile(_countActivePlans(_plansData));
389
- }
390
- } catch (e) { /* keep cached */ }
391
- if (_plansActiveTab === 'plans') {
392
- var lw = document.getElementById('slim-plans-list');
393
- if (lw) renderPlansList(lw);
394
- } else {
395
- _prdData = null;
396
- renderPlansTab();
397
- }
398
- }
399
-
400
- // ── Tab: PRD (materialized PRDs × work items × verify × PRs) ────────
401
- function renderPlansPrdTab(body) {
402
- var intro = document.createElement('p');
403
- intro.textContent = 'Materialized PRDs — each plan item, its status, linked PRs, and the verify task.';
404
- body.appendChild(intro);
405
-
406
- var list = document.createElement('div');
407
- list.id = 'slim-prd-list';
408
- body.appendChild(list);
409
-
410
- var loading = document.createElement('div');
411
- loading.className = 'tile-empty';
412
- loading.textContent = 'Loading PRDs…';
413
- list.appendChild(loading);
414
-
415
- Promise.all([
416
- fetch('/api/prd', { headers: { 'Accept': 'application/json' } }).then(function(r) { return r.ok ? r.json() : null; }).catch(function() { return null; }),
417
- fetch('/api/work-items', { headers: { 'Accept': 'application/json' } }).then(function(r) { return r.ok ? r.json() : null; }).catch(function() { return null; }),
418
- ]).then(function(results) {
419
- _prdData = results[0];
420
- _plansVerifyWis = Array.isArray(results[1])
421
- ? results[1].filter(function(w) { return w && w.itemType === 'verify'; })
422
- : [];
423
- if (_plansActiveTab !== 'prd') return;
424
- var lw = document.getElementById('slim-prd-list');
425
- if (lw) renderPrdList(lw);
426
- });
427
- }
428
-
429
- function renderPrdList(listWrap) {
430
- listWrap.textContent = '';
431
- var progress = _prdData && _prdData.progress;
432
- var items = progress && Array.isArray(progress.items) ? progress.items : [];
433
- if (!items.length) {
434
- var empty = document.createElement('div');
435
- empty.className = 'tile-empty';
436
- empty.textContent = 'No materialized PRDs yet. Execute a plan from the Plans tab to generate one.';
437
- listWrap.appendChild(empty);
438
- return;
439
- }
440
- // Group items by their source PRD file (preserve first-seen order).
441
- var groups = [];
442
- var bySource = {};
443
- items.forEach(function(it) {
444
- var src = it.source || '(unknown)';
445
- if (!bySource[src]) {
446
- bySource[src] = { source: src, summary: it.planSummary || src, status: it.planStatus || 'active', items: [] };
447
- groups.push(bySource[src]);
448
- }
449
- bySource[src].items.push(it);
450
- });
451
- groups.forEach(function(g) { listWrap.appendChild(buildPrdGroup(g)); });
452
- }
453
-
454
- function buildPrdGroup(g) {
455
- var wrap = document.createElement('div');
456
- wrap.className = 'kb-row';
457
-
458
- var head = document.createElement('div');
459
- head.className = 'kb-row-top';
460
- var chip = document.createElement('span');
461
- chip.className = _chipClassAttr(_planStatusChipClass(g.status));
462
- chip.textContent = PLAN_STATUS_LABELS[g.status] || g.status;
463
- head.appendChild(chip);
464
- var title = document.createElement('span');
465
- title.className = 'kb-row-title';
466
- title.textContent = g.summary;
467
- title.title = g.source;
468
- head.appendChild(title);
469
- wrap.appendChild(head);
470
-
471
- var done = g.items.filter(function(i) { return i.status === 'done' || i.status === 'decomposed'; }).length;
472
- var meta = document.createElement('div');
473
- meta.className = 'kb-row-meta';
474
- meta.textContent = g.source + ' · ' + done + '/' + g.items.length + ' done';
475
- wrap.appendChild(meta);
476
-
477
- // Per-item rows.
478
- g.items.forEach(function(it) { wrap.appendChild(buildPrdItemRow(it)); });
479
-
480
- // Verify task row, if a verify work item exists for this PRD.
481
- var verify = (_plansVerifyWis || []).filter(function(w) { return w.sourcePlan === g.source; });
482
- if (verify.length) {
483
- verify.forEach(function(w) {
484
- var vrow = document.createElement('div');
485
- vrow.className = 'kb-row-meta';
486
- var vchip = document.createElement('span');
487
- vchip.className = _chipClassAttr(_wiStatusChipClass(w.status));
488
- vchip.textContent = 'Verify: ' + (w.status || 'pending');
489
- vrow.appendChild(vchip);
490
- wrap.appendChild(vrow);
491
- });
492
- }
493
- return wrap;
494
- }
495
-
496
- function buildPrdItemRow(it) {
497
- var row = document.createElement('div');
498
- row.className = 'kb-row-meta';
499
- row.style.display = 'flex';
500
- row.style.alignItems = 'center';
501
- row.style.gap = '6px';
502
- row.style.flexWrap = 'wrap';
503
-
504
- var chip = document.createElement('span');
505
- chip.className = _chipClassAttr(_wiStatusChipClass(it.status));
506
- chip.textContent = it.status || 'missing';
507
- row.appendChild(chip);
508
-
509
- var name = document.createElement('span');
510
- name.textContent = it.name || it.id || '(item)';
511
- name.style.color = 'var(--text)';
512
- row.appendChild(name);
513
-
514
- (it.projects || []).forEach(function(p) {
515
- var pb = document.createElement('span');
516
- pb.className = 'tile-chip';
517
- pb.textContent = p;
518
- row.appendChild(pb);
519
- });
520
-
521
- (it.prs || []).forEach(function(pr) { row.appendChild(_buildPlansPrLink(pr)); });
522
- return row;
523
- }
524
-
525
- function _buildPlansPrLink(pr) {
526
- var icon = pr.status === 'merged' ? '✓' : pr.status === 'abandoned' ? '✗' : '○';
527
- var span = document.createElement('span');
528
- span.style.display = 'inline-flex';
529
- span.style.alignItems = 'center';
530
- span.style.gap = '3px';
531
- span.style.marginLeft = '4px';
532
- var dot = document.createElement('span');
533
- dot.textContent = icon;
534
- dot.title = pr.status || 'active';
535
- dot.style.color = pr.status === 'merged' ? 'var(--green)' : pr.status === 'abandoned' ? 'var(--red)' : 'var(--blue)';
536
- span.appendChild(dot);
537
- if (pr.url) {
538
- var a = document.createElement('a');
539
- a.href = pr.url;
540
- a.target = '_blank';
541
- a.rel = 'noopener';
542
- a.textContent = pr.id || 'PR';
543
- a.title = (pr.title || '') + ' (' + (pr.status || 'active') + ')';
544
- span.appendChild(a);
545
- } else {
546
- var code = document.createElement('code');
547
- code.textContent = pr.id || 'PR';
548
- span.appendChild(code);
549
- }
550
- return span;
551
- }
@@ -680,12 +680,15 @@
680
680
  /* Cockpit-tile detail modal: list of dispatches / PRs / watches, or a
681
681
  key/value engine readout (reuses .agent-detail-row). */
682
682
  #slim-tile-modal .modal { width: 720px; max-width: calc(100vw - 32px); }
683
- /* W-mqrdggys000f94a4 — the Queued-work tile embeds the full classic /work
684
- screen in an iframe; widen the modal + remove body padding so the
685
- embedded work-items table gets the full width/height. */
686
- #slim-tile-modal.tile-modal--work .modal { width: 1180px; }
687
- #slim-tile-modal.tile-modal--work .modal-body { padding: 0; }
688
- .slim-work-embed {
683
+ /* W-mqrdggys000f94a4 / W-mr28ko750010199f — the Queued-work + Plans tiles
684
+ embed a full classic screen (/work, /plans) in an iframe; widen the modal +
685
+ remove body padding so the embedded screen gets the full width/height. */
686
+ #slim-tile-modal.tile-modal--work .modal,
687
+ #slim-tile-modal.tile-modal--plans .modal { width: 1180px; }
688
+ #slim-tile-modal.tile-modal--work .modal-body,
689
+ #slim-tile-modal.tile-modal--plans .modal-body { padding: 0; }
690
+ .slim-work-embed,
691
+ .slim-plans-embed {
689
692
  display: block; width: 100%; height: calc(100vh - 160px); min-height: 360px;
690
693
  border: 0; background: var(--bg);
691
694
  }
@@ -766,7 +769,7 @@
766
769
 
767
770
  /* Knowledge control panel (slim-knowledge-modal): Pinned Context / Notes /
768
771
  KB tabs in one box. Reuses .tile-* and .pinned-row primitives. */
769
- .slim-knowledge-modal-inner, .slim-plans-modal-inner { width: 720px; max-width: calc(100vw - 32px); }
772
+ .slim-knowledge-modal-inner { width: 720px; max-width: calc(100vw - 32px); }
770
773
  .kn-tabs { display: flex; gap: 6px; margin-left: 16px; }
771
774
  .kn-tab {
772
775
  border: 1px solid var(--border);
package/dashboard.js CHANGED
@@ -434,6 +434,15 @@ function mergeSettingsConfigUpdate(current, candidate, body, patch = {}) {
434
434
  } else {
435
435
  delete currentProject.checkoutMode;
436
436
  }
437
+ // W-mqtvnnj1000357fa — mirror the per-project liveCheckoutAutoStash
438
+ // override the same way: present on candidate → copy; absent → clear so
439
+ // the engine fleet-wide `engine.liveCheckoutAutoStash` applies. Without
440
+ // this branch the validated update is silently dropped before reaching disk.
441
+ if (Object.prototype.hasOwnProperty.call(candidateProject, 'liveCheckoutAutoStash')) {
442
+ currentProject.liveCheckoutAutoStash = candidateProject.liveCheckoutAutoStash;
443
+ } else {
444
+ delete currentProject.liveCheckoutAutoStash;
445
+ }
437
446
  // W-mqiaw974 (issue #241): the field was renamed from the legacy
438
447
  // `worktreeMode`. Drop any stale legacy key on every settings save so a
439
448
  // migrated project never carries both fields.
@@ -10957,6 +10966,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
10957
10966
  // the per-project dropdown. resolveCheckoutMode honors the legacy
10958
10967
  // worktreeMode field; 'worktree' (default) or 'live'.
10959
10968
  checkoutMode: shared.resolveCheckoutMode(p),
10969
+ // W-mqtvnnj1000357fa — surface the RAW per-project auto-stash override
10970
+ // (true / false / undefined) so the Settings UI can pre-select the
10971
+ // tri-state dropdown. Undefined means "use fleet default"
10972
+ // (engine.liveCheckoutAutoStash); only an explicit boolean overrides.
10973
+ liveCheckoutAutoStash: (typeof p.liveCheckoutAutoStash === 'boolean') ? p.liveCheckoutAutoStash : undefined,
10960
10974
  workSources: {
10961
10975
  pullRequests: { enabled: p.workSources?.pullRequests?.enabled !== false, cooldownMinutes: p.workSources?.pullRequests?.cooldownMinutes ?? 30 },
10962
10976
  workItems: { enabled: p.workSources?.workItems?.enabled !== false, cooldownMinutes: p.workSources?.workItems?.cooldownMinutes ?? 0 }
@@ -11412,6 +11426,18 @@ What would you like to discuss or change? When you're happy, say "approve" and I
11412
11426
  else proj.liveValidation = validatedLv;
11413
11427
  }
11414
11428
  }
11429
+ // W-mqtvnnj1000357fa — per-project live-checkout auto-stash override.
11430
+ // null / '' / undefined clears the override (engine fleet-wide
11431
+ // `engine.liveCheckoutAutoStash` applies); an explicit boolean pins
11432
+ // the per-project decision. Anything else is coerced to boolean.
11433
+ if (Object.prototype.hasOwnProperty.call(update, 'liveCheckoutAutoStash')) {
11434
+ const rawStash = update.liveCheckoutAutoStash;
11435
+ if (rawStash === null || rawStash === '' || rawStash === undefined) {
11436
+ delete proj.liveCheckoutAutoStash;
11437
+ } else {
11438
+ proj.liveCheckoutAutoStash = !!rawStash;
11439
+ }
11440
+ }
11415
11441
  }
11416
11442
  }
11417
11443
 
@@ -14889,6 +14915,14 @@ if (require.main === module) {
14889
14915
  if (!alive) {
14890
14916
  console.log(`[watchdog] Engine PID ${control.pid} is dead — auto-restarting...`);
14891
14917
  restartEngine();
14918
+ // Crash-loop counter (W-mr2c46590003e3ee) — shares the same rolling
14919
+ // window in control.json as engine/supervisor.js and
14920
+ // engine/watchdog.js so a single crash isn't tallied 2-3x across
14921
+ // the three independent respawn mechanisms; only THIS automatic
14922
+ // dead-PID path counts (the user-triggered "Restart Engine" button
14923
+ // in handleEngineRestart above is deliberate, not self-healing, so
14924
+ // it's intentionally excluded).
14925
+ try { shared.recordEngineRespawn('dashboard:in-process-watchdog'); } catch { /* best-effort */ }
14892
14926
  } else {
14893
14927
  _markEngineAsDegradedIfFrozen();
14894
14928
  }