@yemi33/minions 0.1.2176 → 0.1.2178

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.
@@ -143,6 +143,7 @@ const RENDER_VERSIONS = {
143
143
  version: 1,
144
144
  adoThrottle: 1,
145
145
  ghThrottle: 1,
146
+ pausedBanner: 1,
146
147
  dispatch: 2,
147
148
  engineLog: 2,
148
149
  metrics: 1,
@@ -713,6 +714,13 @@ function _processStatusUpdate(data, opts) {
713
714
  _safeRender('adoThrottle', function() { renderAdoThrottleAlert(data.adoThrottle); });
714
715
  _changed('ghThrottle', data.ghThrottle);
715
716
  _safeRender('ghThrottle', function() { renderGhThrottleAlert(data.ghThrottle); });
717
+ // P-g7a2b4c5 — sticky kill-switch banner. Cache key is the pair of paused
718
+ // flags so the banner only re-renders when either flag actually flips (the
719
+ // full data.engine slice changes every tick because heartbeat/lastTickAt
720
+ // advance).
721
+ const _pausedCacheKey = (data.engine && (data.engine.pollingPaused ? 'p' : '-') + (data.engine.autoFixPaused ? 'a' : '-')) || '--';
722
+ _changed('pausedBanner', _pausedCacheKey);
723
+ _safeRender('pausedBanner', function() { renderPausedBanner(data.engine); });
716
724
  // Dispatch now comes from /api/dispatch — a dedicated fresh-JSON
717
725
  // endpoint that re-runs getDispatchQueue() server-side on every
718
726
  // request (issue #2949). Completion-report sidecars are now loaded
@@ -245,6 +245,98 @@ function renderGhThrottleAlert(ghThrottle) {
245
245
  el.style.display = 'flex';
246
246
  }
247
247
 
248
+ // P-g7a2b4c5 — Sticky cross-page kill-switch banner. Renders whenever either
249
+ // engine.pollingPaused or engine.autoFixPaused is true, with per-row Resume
250
+ // buttons that POST to the convenience endpoints from P-f3c9d0e7. Built with
251
+ // the DOM API (no innerHTML) so the eslint-plugin-no-unsanitized gate passes
252
+ // cleanly — banner content is system-controlled but follows the project
253
+ // convention of avoiding innerHTML in new code anyway.
254
+ function renderPausedBanner(engine) {
255
+ const el = document.getElementById('paused-banner');
256
+ if (!el) return;
257
+ const pollingPaused = !!(engine && engine.pollingPaused);
258
+ const autoFixPaused = !!(engine && engine.autoFixPaused);
259
+ if (!pollingPaused && !autoFixPaused) {
260
+ el.hidden = true;
261
+ while (el.firstChild) el.removeChild(el.firstChild);
262
+ return;
263
+ }
264
+
265
+ // Wipe + rebuild — list-of-pauses model, one row per active pause.
266
+ while (el.firstChild) el.removeChild(el.firstChild);
267
+ el.hidden = false;
268
+
269
+ const title = document.createElement('div');
270
+ title.className = 'paused-banner-title';
271
+ title.textContent = '⚠️ Some engine functions are paused:';
272
+ el.appendChild(title);
273
+
274
+ const list = document.createElement('ul');
275
+ list.className = 'paused-banner-list';
276
+ el.appendChild(list);
277
+
278
+ const rows = [];
279
+ if (pollingPaused) {
280
+ rows.push({
281
+ key: 'polling',
282
+ label: 'Polling',
283
+ detail: 'no new PR state arriving',
284
+ endpoint: '/api/engine/polling/resume',
285
+ });
286
+ }
287
+ if (autoFixPaused) {
288
+ rows.push({
289
+ key: 'autofix',
290
+ label: 'PR auto-fix',
291
+ detail: 'no new PR-triggered dispatches',
292
+ endpoint: '/api/engine/auto-fix/resume',
293
+ });
294
+ }
295
+
296
+ rows.forEach(function (row) {
297
+ const li = document.createElement('li');
298
+ li.className = 'paused-banner-row';
299
+ li.setAttribute('data-paused-row', row.key);
300
+
301
+ const labelSpan = document.createElement('span');
302
+ labelSpan.className = 'paused-banner-row-label';
303
+ labelSpan.textContent = '• ' + row.label;
304
+ li.appendChild(labelSpan);
305
+
306
+ const detailSpan = document.createElement('span');
307
+ detailSpan.className = 'paused-banner-row-detail';
308
+ detailSpan.textContent = '— ' + row.detail;
309
+ li.appendChild(detailSpan);
310
+
311
+ const btn = document.createElement('button');
312
+ btn.type = 'button';
313
+ btn.className = 'paused-banner-row-resume';
314
+ btn.textContent = 'Resume';
315
+ btn.setAttribute('data-paused-resume', row.key);
316
+ btn.addEventListener('click', function () {
317
+ if (btn.disabled) return;
318
+ btn.disabled = true;
319
+ btn.classList.add('clicked');
320
+ fetch(row.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' } })
321
+ .then(function (resp) {
322
+ if (!resp.ok) throw new Error('HTTP ' + resp.status);
323
+ if (typeof showToast === 'function') showToast('cmd-toast', row.label + ' resumed', true);
324
+ if (typeof refreshNow === 'function') refreshNow();
325
+ })
326
+ .catch(function (err) {
327
+ btn.disabled = false;
328
+ btn.classList.remove('clicked');
329
+ if (typeof showToast === 'function') {
330
+ showToast('cmd-toast', 'Resume failed: ' + (err && err.message ? err.message : 'unknown'), false);
331
+ }
332
+ });
333
+ });
334
+ li.appendChild(btn);
335
+
336
+ list.appendChild(li);
337
+ });
338
+ }
339
+
248
340
  function renderDispatch(dispatch, opts) {
249
341
  opts = opts || {};
250
342
  if (!dispatch) return;
@@ -8,7 +8,10 @@ function _plansNext() { _plansPage++; refresh(); }
8
8
 
9
9
  function openCreatePlanModal() {
10
10
  const projOpts = (typeof cmdProjects !== 'undefined' ? cmdProjects : []).map(p =>
11
- '<option value="' + escapeHtml(p) + '">' + escapeHtml(p) + '</option>'
11
+ '<label style="display:flex;align-items:center;gap:6px;padding:4px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;font-size:var(--text-md);color:var(--text)">' +
12
+ '<input type="checkbox" class="plan-new-project-cb" value="' + escapeHtml(p) + '" onchange="_updatePlanProjectHint()">' +
13
+ escapeHtml(p) +
14
+ '</label>'
12
15
  ).join('');
13
16
  const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
14
17
 
@@ -17,7 +20,11 @@ function openCreatePlanModal() {
17
20
  document.getElementById('modal-body').innerHTML =
18
21
  '<div style="display:flex;flex-direction:column;gap:10px">' +
19
22
  '<label style="color:var(--text);font-size:var(--text-md)">Title <input id="plan-new-title" style="' + inputStyle + '" placeholder="e.g. Add user authentication with JWT"></label>' +
20
- '<label style="color:var(--text);font-size:var(--text-md)">Project <select id="plan-new-project" style="' + inputStyle + '"><option value="">Multiple / cross-repo (agent routes per item)</option>' + projOpts + '</select><span style="display:block;font-size:var(--text-sm);color:var(--muted);margin-top:2px">Pick a project to scope every item to one repo. Leave on cross-repo for plans that span multiple projects.</span></label>' +
23
+ '<div style="color:var(--text);font-size:var(--text-md)">Projects' +
24
+ '<div id="plan-new-project-list" style="display:flex;flex-wrap:wrap;gap:6px;margin-top:4px">' + projOpts + '</div>' +
25
+ '<span style="display:block;font-size:var(--text-sm);color:var(--muted);margin-top:2px">Tick one project to scope every item to that repo. Leave all unticked for plans where the agent should route each item per the PRD.</span>' +
26
+ '<p id="plan-new-project-hint" style="display:none;font-size:var(--text-sm);color:var(--muted);margin:4px 0 0 0;padding:6px 8px;background:var(--bg);border:1px dashed var(--border);border-radius:var(--radius-sm)">Cross-repo plan — each PRD item will route to the project you assign in the PRD.</p>' +
27
+ '</div>' +
21
28
  '<label style="color:var(--text);font-size:var(--text-md)">Plan Content <textarea id="plan-new-content" rows="12" style="' + inputStyle + ';resize:vertical;font-family:monospace;font-size:var(--text-md)" placeholder="Write your plan in markdown...\n\nDescribe what needs to be built, the approach, requirements, and any constraints.\n\nThe squad will convert this into a PRD with structured work items."></textarea></label>' +
22
29
  '<div style="font-size:var(--text-base);color:var(--muted)">After creating, click Execute on the plan card to have an agent convert it into a PRD with work items.</div>' +
23
30
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">' +
@@ -29,13 +36,34 @@ function openCreatePlanModal() {
29
36
  setTimeout(() => document.getElementById('plan-new-title')?.focus(), 100);
30
37
  }
31
38
 
39
+ // Toggle the cross-repo hint under the project picker. Visible only when
40
+ // ≥2 boxes are checked — a single check is still a regular single-project
41
+ // plan, so the hint would be misleading.
42
+ function _updatePlanProjectHint() {
43
+ const boxes = document.querySelectorAll('.plan-new-project-cb');
44
+ let checkedCount = 0;
45
+ for (const cb of boxes) { if (cb.checked) checkedCount++; }
46
+ const hint = document.getElementById('plan-new-project-hint');
47
+ if (hint) hint.style.display = checkedCount >= 2 ? 'block' : 'none';
48
+ }
49
+
32
50
  async function _submitCreatePlan(e) {
33
51
  var btn = (e || window.event)?.target; if (btn) { btn.disabled = true; btn.textContent = 'Creating...'; }
34
52
  const title = document.getElementById('plan-new-title')?.value?.trim();
35
53
  const content = document.getElementById('plan-new-content')?.value?.trim();
36
54
  if (!title) { if (btn) { btn.disabled = false; btn.textContent = 'Create Plan'; } alert('Title is required'); return; }
37
55
  if (!content) { if (btn) { btn.disabled = false; btn.textContent = 'Create Plan'; } alert('Plan content is required'); return; }
38
- const project = document.getElementById('plan-new-project')?.value || '';
56
+ // Collect every checked .plan-new-project-cb into a name array. Server
57
+ // accepts string OR array (P-2e9b54d1) but we preserve today's contract
58
+ // for the 0/1-selected cases: 0 → '' (omit-equivalent), 1 → string,
59
+ // ≥2 → array. Order follows DOM order of the checkboxes.
60
+ const boxes = document.querySelectorAll('.plan-new-project-cb');
61
+ const selected = [];
62
+ for (const cb of boxes) { if (cb.checked) selected.push(cb.value); }
63
+ let project;
64
+ if (selected.length === 0) project = '';
65
+ else if (selected.length === 1) project = selected[0];
66
+ else if (selected.length > 1) project = selected;
39
67
 
40
68
  try {
41
69
  const res = await fetch('/api/plans/create', {
@@ -313,8 +341,8 @@ function renderPlans(plans) {
313
341
  const showPause = effectiveStatus === 'dispatched' && prdFile && !isArchived;
314
342
  // Resume pill not needed — paused state is handled by the actions block above
315
343
  const showResume = false;
316
- const verifyWi = allWi.find(w => w.itemType === 'verify' && w.sourcePlan === prdFile);
317
- const hasVerifyWi = !!verifyWi;
344
+ const verifyWis = allWi.filter(w => w.itemType === 'verify' && w.sourcePlan === prdFile);
345
+ const hasVerifyWi = verifyWis.length > 0;
318
346
  const showVerify = effectiveStatus === 'completed' && prdFile && !isArchived && !hasVerifyWi;
319
347
  const pauseBtn = showPause ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--yellow)" ' +
320
348
  'onclick="event.stopPropagation();planPause(\'' + escapeHtml(prdFile) + '\',this)">Pause</button>' : '';
@@ -338,17 +366,48 @@ function renderPlans(plans) {
338
366
  const versionBadge = p.version ? ' <span style="font-size:var(--text-xs);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>' : '';
339
367
  const statusColors = { 'completed': 'var(--green)', 'dispatched': 'var(--blue)', 'converting': 'var(--yellow)', 'paused': 'var(--muted)', 'awaiting-approval': 'var(--yellow)', 'approved': 'var(--green)', 'rejected': 'var(--red)', 'has-failures': 'var(--red)', 'revision-requested': 'var(--purple,#a855f7)', 'active': 'var(--muted)' };
340
368
  const cardClass = effectiveStatus === 'dispatched' || effectiveStatus === 'converting' ? 'working' : effectiveStatus === 'awaiting-approval' || effectiveStatus === 'paused' ? 'awaiting' : effectiveStatus;
369
+ // P-e8d49105 — cross-repo plans surface every touched project as its
370
+ // own badge. Single-project plans (and old PRDs without _projects)
371
+ // fall back to the legacy `p.project` plain-text span so existing
372
+ // visuals stay identical.
373
+ const projectsRollup = Array.isArray(p._projects) ? p._projects : [];
374
+ const projectMeta = projectsRollup.length >= 2
375
+ ? projectsRollup.map(function(pn) { return '<span class="prd-project-badge">' + escapeHtml(pn) + '</span>'; }).join(' ')
376
+ : (p.project ? '<span>' + escapeHtml(p.project) + '</span>' : (projectsRollup.length === 1 ? '<span>' + escapeHtml(projectsRollup[0]) + '</span>' : ''));
377
+ // P-66b1faec — when a plan touches >= 2 projects, surface a tiny pill
378
+ // per project in the meta line: `<name> <complete>/<total>` plus ✓ when
379
+ // all items in that project are done, ⏳ otherwise. Single-project plans
380
+ // (or PRD records that omit the rollup, e.g. MD drafts) render nothing
381
+ // extra so the existing meta line is visually unchanged.
382
+ const perProjectProgress = (p && p._perProjectProgress && typeof p._perProjectProgress === 'object')
383
+ ? p._perProjectProgress : {};
384
+ const perProjectKeys = Object.keys(perProjectProgress);
385
+ const perProjectPills = perProjectKeys.length >= 2
386
+ ? perProjectKeys.map(function(pn) {
387
+ const entry = perProjectProgress[pn] || { complete: 0, total: 0 };
388
+ const complete = Number(entry.complete) || 0;
389
+ const total = Number(entry.total) || 0;
390
+ const isComplete = total > 0 && complete === total;
391
+ const tail = isComplete ? ' ✓' : ' ⏳';
392
+ const color = isComplete ? 'var(--green)' : 'var(--muted)';
393
+ return '<span title="' + escapeHtml(pn) + ': ' + complete + '/' + total +
394
+ (isComplete ? ' complete' : ' in progress') + '" ' +
395
+ 'style="font-size:var(--text-xs);font-weight:600;padding:1px 6px;border-radius:3px;background:rgba(110,118,129,0.15);color:' + color + '">' +
396
+ escapeHtml(pn) + ' ' + complete + '/' + total + tail + '</span>';
397
+ }).join(' ')
398
+ : '';
341
399
  return '<div class="plan-card ' + cardClass + '" data-file="plans/' + escapeHtml(p.file) + '" style="cursor:pointer' + (isArchived ? ';opacity:0.7' : '') + '" onclick="if(shouldIgnoreSelectionClick(event))return;planView(\'' + escapeHtml(p.file) + '\')">' +
342
400
  '<div class="plan-card-header">' +
343
401
  '<div><div class="plan-card-title">' + escapeHtml(p.summary || p.file) + versionBadge + '</div>' +
344
402
  '<div class="plan-card-meta">' +
345
403
  '<span style="font-weight:600;color:' + (statusColors[effectiveStatus] || 'var(--muted)') + '">' + label + '</span>' +
346
- (p.project ? '<span>' + escapeHtml(p.project) + '</span>' : '') +
404
+ projectMeta +
347
405
  '<span>' + p.itemCount + ' items</span>' +
406
+ perProjectPills +
348
407
  (p.updatedAt ? '<span title="Last updated: ' + p.updatedAt + '">Updated ' + timeAgo(p.updatedAt) + '</span>' : '') +
349
408
  (p.completedAt ? '<span>' + p.completedAt.slice(0, 10) + '</span>' : '') +
350
409
  (p.generatedBy ? '<span>by ' + escapeHtml(p.generatedBy) + '</span>' : '') +
351
- executeBtn + pauseBtn + resumeBtn + verifyBtn + (hasVerifyWi ? _renderVerifyBadge(verifyWi) : '') + archiveReadyBadge + archiveBtn + deleteBtn +
410
+ executeBtn + pauseBtn + resumeBtn + verifyBtn + (verifyWis.length >= 2 ? verifyWis.map(v => _renderVerifyBadge(v, { projectLabel: v.project || '' })).join(' ') : (hasVerifyWi ? _renderVerifyBadge(verifyWis[0]) : '')) + archiveReadyBadge + archiveBtn + deleteBtn +
352
411
  '</div>' +
353
412
  '</div>' +
354
413
  '</div>' +
@@ -620,12 +679,18 @@ function _renderPlanModal(normalizedFile, raw, lastMod) {
620
679
  if (effectiveStatus === 'dispatched') {
621
680
  modalActions += '<span style="' + bs + ';color:var(--blue)">In Progress</span> ';
622
681
  }
623
- // Verify / Verified badge
624
- const modalVerifyWi = (window._lastWorkItems || []).find(w => w.itemType === 'verify' && w.sourcePlan === (prdFile || normalizedFile));
625
- if (effectiveStatus === 'completed' && prdFile && !isArchived && !modalVerifyWi) {
682
+ // Verify / Verified badge — cross-repo plans fan out one verify WI per
683
+ // project (engine/lifecycle.js); collect them all and render one badge
684
+ // per project so operators can see every repo's verify status.
685
+ const modalVerifyWis = (window._lastWorkItems || []).filter(w => w.itemType === 'verify' && w.sourcePlan === (prdFile || normalizedFile));
686
+ if (effectiveStatus === 'completed' && prdFile && !isArchived && modalVerifyWis.length === 0) {
626
687
  modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--green)" onclick="triggerVerify(\'' + escapeHtml(prdFile) + '\',this)">Verify</button> ';
627
688
  }
628
- if (modalVerifyWi) modalActions += _renderVerifyBadge(modalVerifyWi);
689
+ if (modalVerifyWis.length >= 2) {
690
+ modalActions += modalVerifyWis.map(v => _renderVerifyBadge(v, { projectLabel: v.project || '' })).join(' ');
691
+ } else if (modalVerifyWis.length === 1) {
692
+ modalActions += _renderVerifyBadge(modalVerifyWis[0]);
693
+ }
629
694
  // Archive + Delete (always, unless archived)
630
695
  if (!isArchived) {
631
696
  modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--muted)" onclick="planArchive(\'' + escapeHtml(prdFile || normalizedFile) + '\')">Archive</button> ';
@@ -918,10 +983,14 @@ async function planRegeneratePRD(source) {
918
983
  } catch (e) { alert('Error: ' + e.message); }
919
984
  }
920
985
 
921
- function _renderVerifyBadge(verifyWi) {
986
+ function _renderVerifyBadge(verifyWi, opts) {
922
987
  const statusColors = { pending: 'var(--muted)', dispatched: 'var(--blue)', done: 'var(--green)', failed: 'var(--red)' };
923
988
  const color = statusColors[verifyWi.status] || 'var(--muted)';
924
- const label = verifyWi.status === 'dispatched' ? 'Verifying...' : verifyWi.status === 'done' ? '\u2714 Verified' : verifyWi.status === 'failed' ? 'Verify failed' : 'Verify pending';
989
+ const baseLabel = verifyWi.status === 'dispatched' ? 'Verifying...' : verifyWi.status === 'done' ? '\u2714 Verified' : verifyWi.status === 'failed' ? 'Verify failed' : 'Verify pending';
990
+ // Cross-repo plans fan out one verify WI per project (lifecycle.js); the
991
+ // call-site passes opts.projectLabel so each badge identifies its repo.
992
+ const projectLabel = (opts && opts.projectLabel) ? opts.projectLabel : '';
993
+ const label = projectLabel ? baseLabel + ' (' + escapeHtml(projectLabel) + ')' : baseLabel;
925
994
  // E2E PR — check by prdItems, branch, or title. Issue #2949 — pullRequests
926
995
  // moved off /api/status to /api/pull-requests (window._lastPullRequests).
927
996
  const allPrs = window._lastPullRequests || [];
@@ -158,7 +158,11 @@ async function openSettings() {
158
158
 
159
159
  const paneAutoFix =
160
160
  '<h3>Auto-fix &amp; Review Loop</h3>' +
161
- '<div class="settings-pane-sub">Dispatch gates that decide when an agent is auto-spawned in response to PR build failures, merge conflicts, review verdicts, and human comments. All require the matching provider polling toggle (Polling tab).</div>' +
161
+ '<div class="settings-pane-sub">PR-triggered dispatch gates only. Each toggle below decides when an agent is auto-spawned in response to a PR signal (build failure, merge conflict, review verdict, human comment). All require the matching provider polling (Polling tab). The <em>Pause All Auto-fix</em> kill-switch below inerts every per-cause gate at once. Non-dispatch knobs (auto-merge, auto-vote, plan/decompose defaults) moved to the new <strong>PR Lifecycle</strong> and <strong>Workflow Defaults</strong> panes.</div>' +
162
+ '<div class="settings-stack" style="margin-bottom:16px">' +
163
+ settingsToggle('🛑 Pause ALL PR auto-fix dispatches', 'set-autoFixPaused', !!e.autoFixPaused, 'Halts every PR-triggered dispatch (review, re-review, build-failure fix, review-feedback fix, human-comment fix, merge-conflict fix) on the next tick. In-flight agents keep running. Reversible without restart. Per-cause flags below are inert while this is set.') +
164
+ '</div>' +
165
+ '<div class="settings-pane-sub" style="margin-bottom:8px">Per-cause dispatch gates — each one names the exact discoverFromPrs site it gates. When <em>Pause All Auto-fix</em> is ON these are inert; when it is OFF, each gate decides independently whether its site fires.</div>' +
162
166
  '<div class="settings-stack" style="margin-bottom:16px">' +
163
167
  settingsToggle('Auto-fix Builds', 'set-autoFixBuilds', e.autoFixBuilds !== false, 'Shared dispatch gate: auto-fix agent when a PR build fails; also requires that PR provider polling is enabled') +
164
168
  settingsToggle('Auto-fix Conflicts', 'set-autoFixConflicts', e.autoFixConflicts !== false, 'Shared dispatch gate: auto-fix agent when a PR merge conflict is detected; also requires that PR provider polling is enabled') +
@@ -166,16 +170,42 @@ async function openSettings() {
166
170
  settingsToggle('Auto-re-review PRs', 'set-autoReReviewPrs', e.autoReReviewPrs !== false, 'Shared dispatch gate: review agent after a fix push is awaiting re-review; also requires that PR provider polling is enabled') +
167
171
  settingsToggle('Auto-fix Review Feedback', 'set-autoFixReviewFeedback', e.autoFixReviewFeedback !== false, 'Shared dispatch gate: fix agent for minions changes-requested verdicts; also requires that PR provider polling is enabled') +
168
172
  settingsToggle('Auto-fix Human Comments', 'set-autoFixHumanComments', e.autoFixHumanComments !== false, 'Shared dispatch gate: fix agent for actionable human PR comments; also requires that PR provider polling is enabled') +
169
- settingsToggle('Auto-apply review vote to PR', 'set-autoApplyReviewVote', !!e.autoApplyReviewVote, 'When ON, Minions review verdicts (APPROVE / REQUEST_CHANGES) automatically flip the platform vote on ADO/GitHub. When OFF (default), verdicts are informational only and the human casts the final vote.') +
170
- settingsToggle('Eval Loop', 'set-evalLoop', e.evalLoop !== false, 'Auto-review implementations and iterate fix cycles until pass') +
171
- settingsToggle('Auto-decompose', 'set-autoDecompose', e.autoDecompose !== false, 'Large implement items are auto-split into sub-tasks') +
172
- settingsToggle('Auto-complete PRs', 'set-autoCompletePrs', !!e.autoCompletePrs, 'Auto-merge PRs when builds pass and review is approved (opt-in)') +
173
- settingsToggle('Auto-approve Plans', 'set-autoApprovePlans', !!e.autoApprovePlans, 'PRDs are approved automatically without human review') +
174
- settingsToggle('Auto-archive Plans', 'set-autoArchive', !!e.autoArchive, 'Automatically archive plans after verify completes (off = manual archive via dashboard)') +
175
- settingsToggle('Auto-consolidate Memory', 'set-autoConsolidateMemory', !!e.autoConsolidateMemory, 'Periodically spawn the KB sweep (dedup + compress + normalize knowledge/) from the engine tick on a 4h cadence. Inbox→notes consolidation already runs every tick (gated by the Consolidation Threshold above); this toggle controls only the KB sweep that was previously dashboard-button-only.') +
173
+ settingsToggle('Eval Loop', 'set-evalLoop', e.evalLoop !== false, 'Gates the review→fix iteration loop only (first review, re-review, review-feedback fix). Does NOT gate build-failure, merge-conflict, or human-comment fixes. Use the emergency stop above to halt everything.') +
174
+ settingsToggle('Pre-dispatch Eval (cheap LLM gate)', 'set-enablePreDispatchEval', e.enablePreDispatchEval !== false, 'P-d2a9f6e5: cheap LLM gate that screens work items for clear/actionable/testable criteria BEFORE queueing the agent. Catches noop dispatches authored from impossible/ambiguous WIs. Fail-open on any validator error.') +
175
+ settingsToggle('Pre-dispatch Eval: skip PRD-sourced items', 'set-preDispatchEvalSkipPrdSourced', e.preDispatchEvalSkipPrdSourced !== false, 'W-mq9acoo800177bcb: short-circuit the validator for items materialized from an approved/active PRD — plan-to-prd already LLM-vets them. Reduces queue-time on an N-item approved PRD from ~N×25s to nearly instant. OFF = always re-validate even PRD-sourced items.') +
176
176
  '</div>' +
177
177
  '<div class="settings-grid-2">' +
178
178
  settingsField('Eval Max Cost', 'set-evalMaxCost', e.evalMaxCost === null || e.evalMaxCost === undefined ? '' : e.evalMaxCost, '$', 'USD ceiling per work item across all eval iterations (blank = no limit)') +
179
+ settingsField('Pre-dispatch Eval Concurrency', 'set-preDispatchEvalConcurrency', e.preDispatchEvalConcurrency || 6, '', 'W-mq9acoo800177bcb: max parallel pre-dispatch validator calls per discovery tick. Default 6. Clamped to [1, 20]. Raises throughput when multiple PRDs queue at once; lower if the LLM provider throttles. 1 = sequential (pre-fix behavior).') +
180
+ '</div>' +
181
+ '<div style="margin-top:12px;padding:6px 8px;border:1px solid var(--border);border-radius:4px;background:rgba(130,160,210,0.06);font-size:var(--text-sm);color:var(--muted)">' +
182
+ 'Moved to <strong>PR Lifecycle</strong>: Auto-complete PRs, Auto-apply review vote. ' +
183
+ 'Moved to <strong>Workflow Defaults</strong>: Auto-archive Plans, Auto-approve Plans, Auto-decompose, Auto-consolidate Memory.' +
184
+ '</div>';
185
+
186
+ // P-g7a2b4c5 — new "PR Lifecycle" pane. Owns the two PR-lifecycle knobs
187
+ // that historically lived under Auto-fix but do NOT gate any dispatch:
188
+ // autoApplyReviewVote (lifecycle vote-posting), autoCompletePrs (auto-merge).
189
+ const paneLifecycle =
190
+ '<h3>PR Lifecycle</h3>' +
191
+ '<div class="settings-pane-sub">Post-merge / post-review lifecycle knobs. These are not dispatch gates — they control what Minions does with a PR once review or build state lands.</div>' +
192
+ '<div class="settings-stack">' +
193
+ settingsToggle('Auto-apply review vote to PR', 'set-autoApplyReviewVote', !!e.autoApplyReviewVote, 'When ON, Minions review verdicts (APPROVE / REQUEST_CHANGES) automatically flip the platform vote on ADO/GitHub. When OFF (default), verdicts are informational only and the human casts the final vote.') +
194
+ settingsToggle('Auto-complete PRs', 'set-autoCompletePrs', !!e.autoCompletePrs, 'Auto-merge PRs when builds pass and review is approved (opt-in). Independent of the per-cause auto-fix gates above.') +
195
+ '</div>';
196
+
197
+ // P-g7a2b4c5 — new "Workflow Defaults" pane. Owns the four workflow-level
198
+ // automation defaults that historically lived under Auto-fix: plan-flow
199
+ // (approve / decompose / archive) and memory consolidation. None of these
200
+ // gate a PR-triggered dispatch.
201
+ const paneWorkflow =
202
+ '<h3>Workflow Defaults</h3>' +
203
+ '<div class="settings-pane-sub">Workflow-level automation defaults. Independent of PR dispatch and PR lifecycle — these control plan flow and memory upkeep.</div>' +
204
+ '<div class="settings-stack">' +
205
+ settingsToggle('Auto-approve Plans', 'set-autoApprovePlans', !!e.autoApprovePlans, 'PRDs are approved automatically without human review.') +
206
+ settingsToggle('Auto-decompose', 'set-autoDecompose', e.autoDecompose !== false, 'Large implement items are auto-split into sub-tasks.') +
207
+ settingsToggle('Auto-archive Plans', 'set-autoArchive', !!e.autoArchive, 'Automatically archive plans after verify completes (off = manual archive via dashboard).') +
208
+ settingsToggle('Auto-consolidate Memory', 'set-autoConsolidateMemory', !!e.autoConsolidateMemory, 'Periodically spawn the KB sweep (dedup + compress + normalize knowledge/) from the engine tick on a 4h cadence. Inbox→notes consolidation already runs every tick (gated by the Consolidation Threshold under Advanced); this toggle controls only the KB sweep that was previously dashboard-button-only.') +
179
209
  '</div>';
180
210
 
181
211
  // W-mpmwxkrw000872ec — Appearance pane. Hosts dashboard-wide visual
@@ -268,11 +298,35 @@ async function openSettings() {
268
298
 
269
299
  const panePolling =
270
300
  '<h3>Polling</h3>' +
271
- '<div class="settings-pane-sub">Cadence for fetching PR build status, votes, and comments from the platforms. Disabling a provider here turns the matching Auto-fix gates into no-ops.</div>' +
301
+ '<div class="settings-pane-sub">Cadence for fetching PR build status, votes, and comments from the platforms. Disabling a provider here turns the matching Auto-fix gates into no-ops. The <em>Pause All Polling</em> kill-switch below overrides both provider toggles and inerts every PR auto-dispatch gate (Auto-fix Builds / Conflicts / Review / etc.) until cleared.</div>' +
302
+ '<div class="settings-stack" style="margin-bottom:16px">' +
303
+ settingsToggle('🛑 Pause ALL polling', 'set-pollingPaused', !!e.pollingPaused, 'Halts every PR poll, reconciliation, rebase processing, and work-discovery scan on the next tick. Reversible without restart. Use when hitting API rate limits.') +
304
+ '</div>' +
305
+ '<div class="settings-pane-sub" style="margin-bottom:8px">Legacy bundle toggles — granular controls below override these when set.</div>' +
272
306
  '<div class="settings-stack" style="margin-bottom:12px">' +
273
- settingsToggle('ADO Polling', 'set-adoPollEnabled', e.adoPollEnabled !== false, 'Keeps ADO PR build results, votes, and comments fresh each tick; ADO PR dispatch gates are inert when this is off') +
274
- settingsToggle('GitHub Polling', 'set-ghPollEnabled', e.ghPollEnabled !== false, 'Keeps GitHub PR build results, votes, and comments fresh each tick; GitHub PR dispatch gates are inert when this is off') +
307
+ settingsToggle('ADO Polling', 'set-adoPollEnabled', e.adoPollEnabled !== false, 'Legacy bundle macro when OFF, silences all three ADO axes (status, comments, reconcile) and ADO PR dispatch gates are inert when this is off. Per-axis flags below take priority when explicitly set. Keep ON unless you want a one-knob ADO kill.') +
308
+ settingsToggle('GitHub Polling', 'set-ghPollEnabled', e.ghPollEnabled !== false, 'Legacy bundle macro when OFF, silences all three GitHub axes (status, comments, reconcile) and GitHub PR dispatch gates are inert when this is off. Per-axis flags below take priority when explicitly set. Keep ON unless you want a one-knob GitHub kill.') +
275
309
  '</div>' +
310
+ '<details class="settings-collapsible" style="margin-bottom:12px"><summary>Granular per-poller controls (P-c4d8e1a3)</summary>' +
311
+ '<div class="settings-pane-sub" style="margin-top:8px">Override individual ADO/GitHub poll axes + the pending-rebase processor. Explicit values here win; if unset, the legacy ADO/GitHub Polling macros above apply; otherwise the default is ON. <strong>Status</strong> + <strong>Comments</strong> still honor the <em>Pause All Polling</em> kill-switch above; <strong>Reconcile</strong> and <strong>Process Pending Rebases</strong> are recovery sweeps and ignore it (by design).</div>' +
312
+ '<div class="settings-stack" style="margin-top:8px">' +
313
+ settingsToggle('ADO PR Status Poll', 'set-adoPrStatusPollEnabled', e.adoPrStatusPollEnabled !== false, 'Granular: ADO PR build/merge/review status poll (section 2.6). Default ON. When OFF, ADO PR status will not refresh, but reconcile + comments still run (unless their own granular flag is OFF).') +
314
+ settingsToggle('ADO PR Comments Poll', 'set-adoPrCommentsPollEnabled', e.adoPrCommentsPollEnabled !== false, 'Granular: ADO PR human-comments poll (section 2.7). Default ON. When OFF, ADO PR comments will not surface — auto-fix-human-comments still composes against this flag for ADO.') +
315
+ settingsToggle('ADO PR Reconcile', 'set-adoPrReconcileEnabled', e.adoPrReconcileEnabled !== false, 'Granular: ADO PR reconciliation recovery sweep (section 2.7 tail). Default ON. Setting OFF stops engine from healing missed PR state transitions for ADO — only do this if reconcile is misbehaving.') +
316
+ settingsToggle('GitHub PR Status Poll', 'set-ghPrStatusPollEnabled', e.ghPrStatusPollEnabled !== false, 'Granular: GitHub PR build/merge/review status poll. Default ON. When OFF, GitHub PR status will not refresh, but reconcile + comments still run (unless their own granular flag is OFF).') +
317
+ settingsToggle('GitHub PR Comments Poll', 'set-ghPrCommentsPollEnabled', e.ghPrCommentsPollEnabled !== false, 'Granular: GitHub PR human-comments poll. Default ON. When OFF, GitHub PR comments will not surface — auto-fix-human-comments still composes against this flag for GitHub.') +
318
+ settingsToggle('GitHub PR Reconcile', 'set-ghPrReconcileEnabled', e.ghPrReconcileEnabled !== false, 'Granular: GitHub PR reconciliation recovery sweep. Default ON. Setting OFF stops engine from healing missed PR state transitions for GitHub — only do this if reconcile is misbehaving.') +
319
+ settingsToggle('Process Pending Rebases', 'set-processPendingRebasesEnabled', e.processPendingRebasesEnabled !== false, 'Granular: pending-rebase processor that runs after each status poll cycle. Default ON. Setting OFF freezes the rebase queue — useful if rebase-on-tick is causing platform churn or you want manual control. Has no legacy macro counterpart.') +
320
+ '</div></details>' +
321
+ '<details class="settings-collapsible" style="margin-bottom:12px"><summary>Granular work-discovery controls (P-d6f0a2b5)</summary>' +
322
+ '<div class="settings-pane-sub" style="margin-top:8px">Silence individual discovery phases inside the per-tick <code>engine.discoverWork()</code> sweep. Each flag complements (not replaces) the per-project <code>project.workSources.*.enabled</code> toggles — a <strong>false</strong> at either the global or per-project level skips the matching discovery call. There is no legacy macro to fall back to; defaults are ON. Use these for incident-response (e.g. <em>turn off PR discovery while triaging a runaway auto-fix loop</em>) or long migrations that must not auto-create work items.</div>' +
323
+ '<div class="settings-stack" style="margin-top:8px">' +
324
+ settingsToggle('PR Discovery', 'set-prDiscoveryEnabled', e.prDiscoveryEnabled !== false, 'Granular: discoverFromPrs per-project — gates the PR-driven fix / review / build-test work queue. Default ON. OFF stops the engine from queuing new fix/review/test work from PRs at all (per-project workSources.pullRequests.enabled still composes independently).') +
325
+ settingsToggle('Work Items Discovery', 'set-workItemsDiscoveryEnabled', e.workItemsDiscoveryEnabled !== false, 'Granular: discoverFromWorkItems per-project — gates the project-local work-items.json scan (includes items auto-filed from plans, design docs, build failures). Default ON. OFF freezes per-project work-item pickup; per-project workSources.workItems.enabled still composes independently.') +
326
+ settingsToggle('Central Work Discovery', 'set-centralWorkDiscoveryEnabled', e.centralWorkDiscoveryEnabled !== false, 'Granular: discoverCentralWorkItems — gates the top-level project-agnostic work-items.json scan. Default ON. OFF keeps centralWork iterable as [] so the downstream dispatch path stays safe.') +
327
+ settingsToggle('Scheduled Work Discovery', 'set-scheduledWorkDiscoveryEnabled', e.scheduledWorkDiscoveryEnabled !== false, 'Granular: discoverScheduledWork — gates the cron-style scheduled tasks + scheduled meetings block. Default ON. OFF stops cron-style scheduled tasks from firing.') +
328
+ settingsToggle('Plan Materialization', 'set-planMaterializationEnabled', e.planMaterializationEnabled !== false, 'Granular: reconcilePrdStatuses + materializePlansAsWorkItems pair — gates the PRD reconcile backward-scan and plan-to-work-item materialization. Default ON. OFF suppresses the pair atomically so a long migration does not race new PRD items into the queue.') +
329
+ '</div></details>' +
276
330
  '<div class="settings-grid-2">' +
277
331
  settingsField('PR Status Poll Frequency', 'set-prPollStatusEvery', e.prPollStatusEvery ?? 12, 'ticks', 'Poll PR build/review/merge status every N ticks for both ADO and GitHub (~12 min at default tick rate)') +
278
332
  settingsField('PR Comments Poll Frequency', 'set-prPollCommentsEvery', e.prPollCommentsEvery ?? 12, 'ticks', 'Poll PR human comments every N ticks for both ADO and GitHub (~12 min at default tick rate)') +
@@ -344,6 +398,12 @@ async function openSettings() {
344
398
  '</select>' +
345
399
  '</div>' +
346
400
  settingsField('Copilot fallback model', 'set-copilotFallbackModel', e.copilotFallbackModel || '', 'e.g. gpt-5.4', 'Copilot has no --fallback-model flag. On a MODEL_UNAVAILABLE (overloaded/503) retry, the engine OVERRIDES --model with this value (Copilot only).') +
401
+ '</div>' +
402
+ '<div class="settings-stack" style="margin-top:12px">' +
403
+ settingsField('Copilot: disable agent MCP servers', 'set-copilotAgentDisabledMcpServers',
404
+ Array.isArray(e.copilotAgentDisabledMcpServers) ? e.copilotAgentDisabledMcpServers.join(', ') : (e.copilotAgentDisabledMcpServers || ''),
405
+ 'e.g. playwright, maestro, loop',
406
+ 'Comma-separated MCP server names (from ~/.copilot/mcp-config.json) the engine disables for autonomous Copilot agent dispatches via --disable-mcp-server. Copilot loads your user MCP config on every spawn regardless of --add-dir/hermeticHarness; list the noisy local/UI servers (e.g. playwright, maestro) to keep them out of agents. Empty = inherit all.') +
347
407
  '</div>';
348
408
 
349
409
  const paneClaude =
@@ -446,9 +506,15 @@ async function openSettings() {
446
506
 
447
507
  // Section registry — order is intentional (Runtime + Auto-fix surface first
448
508
  // per Caleb's feedback). Each entry maps a rail-button id → pane HTML.
509
+ // P-g7a2b4c5 — added "PR Lifecycle" and "Workflow Defaults" panes for the
510
+ // non-dispatch toggles that historically lived under Auto-fix; placed
511
+ // adjacent to Auto-fix so operators following the legacy mental model find
512
+ // the moved flags quickly.
449
513
  const sections = [
450
514
  { id: 'runtime', label: 'Runtime & Models', featured: true, html: paneRuntime },
451
515
  { id: 'autofix', label: 'Auto-fix & Review Loop', featured: true, html: paneAutoFix },
516
+ { id: 'lifecycle', label: 'PR Lifecycle', html: paneLifecycle },
517
+ { id: 'workflow', label: 'Workflow Defaults', html: paneWorkflow },
452
518
  { id: 'appearance', label: 'Appearance', html: paneAppearance },
453
519
  { id: 'projects', label: 'Projects', html: paneProjects },
454
520
  { id: 'polling', label: 'Polling', html: panePolling },
@@ -904,6 +970,7 @@ async function saveSettings() {
904
970
  autoApplyReviewVote: document.getElementById('set-autoApplyReviewVote').checked,
905
971
  autoFixBuilds: document.getElementById('set-autoFixBuilds').checked,
906
972
  autoFixConflicts: document.getElementById('set-autoFixConflicts').checked,
973
+ autoFixPaused: document.getElementById('set-autoFixPaused').checked,
907
974
  autoReviewPrs: document.getElementById('set-autoReviewPrs').checked,
908
975
  autoReReviewPrs: document.getElementById('set-autoReReviewPrs').checked,
909
976
  autoFixReviewFeedback: document.getElementById('set-autoFixReviewFeedback').checked,
@@ -914,9 +981,31 @@ async function saveSettings() {
914
981
  orphanHolderScanTimeoutMs: document.getElementById('set-orphanHolderScanTimeoutMs')?.value,
915
982
  adoPollEnabled: document.getElementById('set-adoPollEnabled').checked,
916
983
  ghPollEnabled: document.getElementById('set-ghPollEnabled').checked,
984
+ pollingPaused: document.getElementById('set-pollingPaused').checked,
985
+ // P-c4d8e1a3 — granular per-poller flags (default true). All seven submit
986
+ // booleans; the server stores them on config.engine and resolvePollFlag
987
+ // resolves precedence at engine runtime.
988
+ adoPrStatusPollEnabled: document.getElementById('set-adoPrStatusPollEnabled').checked,
989
+ adoPrCommentsPollEnabled: document.getElementById('set-adoPrCommentsPollEnabled').checked,
990
+ adoPrReconcileEnabled: document.getElementById('set-adoPrReconcileEnabled').checked,
991
+ ghPrStatusPollEnabled: document.getElementById('set-ghPrStatusPollEnabled').checked,
992
+ ghPrCommentsPollEnabled: document.getElementById('set-ghPrCommentsPollEnabled').checked,
993
+ ghPrReconcileEnabled: document.getElementById('set-ghPrReconcileEnabled').checked,
994
+ processPendingRebasesEnabled: document.getElementById('set-processPendingRebasesEnabled').checked,
995
+ // P-d6f0a2b5 — granular work-discovery flags (default true). Each one
996
+ // gates a single phase inside engine.discoverWork via the inline
997
+ // `config.engine?.<flag> !== false` pattern. No legacy macro counterpart.
998
+ prDiscoveryEnabled: document.getElementById('set-prDiscoveryEnabled').checked,
999
+ workItemsDiscoveryEnabled: document.getElementById('set-workItemsDiscoveryEnabled').checked,
1000
+ centralWorkDiscoveryEnabled: document.getElementById('set-centralWorkDiscoveryEnabled').checked,
1001
+ scheduledWorkDiscoveryEnabled: document.getElementById('set-scheduledWorkDiscoveryEnabled').checked,
1002
+ planMaterializationEnabled: document.getElementById('set-planMaterializationEnabled').checked,
917
1003
  prPollStatusEvery: document.getElementById('set-prPollStatusEvery').value,
918
1004
  prPollCommentsEvery: document.getElementById('set-prPollCommentsEvery').value,
919
1005
  evalMaxCost: document.getElementById('set-evalMaxCost').value || null,
1006
+ enablePreDispatchEval: document.getElementById('set-enablePreDispatchEval').checked,
1007
+ preDispatchEvalSkipPrdSourced: document.getElementById('set-preDispatchEvalSkipPrdSourced').checked,
1008
+ preDispatchEvalConcurrency: document.getElementById('set-preDispatchEvalConcurrency').value,
920
1009
  agentBusyReassignMs: document.getElementById('set-agentBusyReassignMs').value,
921
1010
  maxRetriesPerAgent: document.getElementById('set-maxRetriesPerAgent').value,
922
1011
  ignoredCommentAuthors: document.getElementById('set-ignoredCommentAuthors').value,
@@ -3,10 +3,13 @@
3
3
  // Signal the engine to tick immediately (pick up new work without waiting 60s)
4
4
  function wakeEngine() { fetch('/api/engine/wakeup', { method: 'POST' }).catch(() => {}); }
5
5
 
6
- // "Try new Slim UX" — flips the slim-ux feature flag ON then reloads so the
7
- // root route serves the slim cockpit (the takeover is checked at request time,
8
- // so no engine restart is needed). The first-visit welcome popup is gated on
9
- // the browser side in dashboard/slim/js/settings.js, not here.
6
+ // "Try new Slim UX" — flips the slim-ux feature flag ON then navigates to the
7
+ // root route so it serves the slim cockpit (the takeover is checked at request
8
+ // time, so no engine restart is needed). We navigate to '/' rather than
9
+ // location.reload() because the slim takeover is gated on pathname === '/';
10
+ // reloading from a non-home classic page (/work, /prs, …) preserves the path
11
+ // and silently re-serves the classic dashboard. The first-visit welcome popup
12
+ // is gated on the browser side in dashboard/slim/js/settings.js, not here.
10
13
  async function trySlimUx(btn) {
11
14
  if (btn) { btn.disabled = true; btn.textContent = 'Switching…'; }
12
15
  try {
@@ -16,7 +19,7 @@ async function trySlimUx(btn) {
16
19
  body: JSON.stringify({ id: 'slim-ux', enabled: true }),
17
20
  });
18
21
  if (!res.ok) throw new Error('HTTP ' + res.status);
19
- location.reload();
22
+ location.href = '/';
20
23
  } catch (e) {
21
24
  if (btn) { btn.disabled = false; btn.textContent = '✨ Try new Slim UX'; }
22
25
  alert('Could not switch to Slim UX: ' + (e && e.message ? e.message : 'unknown error'));
@@ -35,6 +35,12 @@
35
35
  </div>
36
36
  </header>
37
37
  <div class="engine-alert" id="engine-alert"></div>
38
+ <!-- P-g7a2b4c5 — sticky cross-page banner for the two operator kill-switches
39
+ (engine.pollingPaused, engine.autoFixPaused). Rendered by
40
+ renderPausedBanner() in dashboard/js/render-dispatch.js whenever either
41
+ flag is true; hidden otherwise. Lives outside .page-layout so it persists
42
+ across page nav. -->
43
+ <div class="paused-banner" id="paused-banner" hidden></div>
38
44
 
39
45
  <!-- Command Center Drawer -->
40
46
  <div id="cc-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:340" onclick="toggleCommandCenter()"></div>
@@ -21,7 +21,10 @@
21
21
  <div class="panel panel-actions">
22
22
  <div class="panel-header">
23
23
  Actions
24
- <span class="panel-sub">Command Center &amp; quick triggers</span>
24
+ <span class="panel-head-meta">
25
+ <span class="cc-model-badge" id="cc-model-badge" hidden></span>
26
+ <span class="panel-sub">Command Center &amp; quick triggers</span>
27
+ </span>
25
28
  </div>
26
29
  <div class="actions-chat">
27
30
  <div class="chat-tabs" id="chat-tabs"></div>
@@ -39,7 +42,7 @@
39
42
  rows="2"
40
43
  placeholder="Ask anything or give a command... (Enter to send, Shift+Enter for newline)"
41
44
  ></textarea>
42
- <button id="chat-stop" class="chat-stop" type="button" title="Stop the in-flight response">Stop</button>
45
+ <button id="chat-stop" class="chat-stop" type="button" title="Stop the in-flight response" aria-label="Stop agent"><svg class="chat-stop-icon" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="2"></circle><rect x="8" y="8" width="8" height="8" rx="1" fill="currentColor"></rect></svg></button>
43
46
  <button id="chat-send" class="chat-send" type="button">Send</button>
44
47
  </div>
45
48
  </div>
@@ -77,23 +80,23 @@
77
80
  count now lives in the Team cards above, so that tile is removed. -->
78
81
  <div class="cockpit-section">
79
82
  <div class="cockpit-grid" id="cockpit-grid">
80
- <div class="cockpit-tile" data-tile="engine">
83
+ <div class="cockpit-tile cockpit-tile--full" data-tile="engine">
81
84
  <div class="cockpit-label"><span class="cockpit-dot"></span> Engine</div>
82
85
  <div class="cockpit-value dim">—</div>
83
86
  <div class="cockpit-detail">checking&hellip;</div>
84
87
  </div>
85
- <div class="cockpit-tile" data-tile="dispatches">
86
- <div class="cockpit-label"><span class="cockpit-dot"></span> Active dispatches</div>
87
- <div class="cockpit-value dim">0</div>
88
- <div class="cockpit-detail">none in flight</div>
89
- </div>
90
88
  <div class="cockpit-tile" data-tile="queued">
91
89
  <div class="cockpit-label"><span class="cockpit-dot"></span> Queued work</div>
92
90
  <div class="cockpit-value dim">0</div>
93
91
  <div class="cockpit-detail">queue empty</div>
94
92
  </div>
93
+ <div class="cockpit-tile" data-tile="dispatches">
94
+ <div class="cockpit-label"><span class="cockpit-dot"></span> Active dispatches</div>
95
+ <div class="cockpit-value dim">0</div>
96
+ <div class="cockpit-detail">no agents working</div>
97
+ </div>
95
98
  <div class="cockpit-tile" data-tile="prs">
96
- <button id="slim-tile-linkpr-chip" class="linkpr-chip on-tile" type="button" title="Link a pull request">+ Link PR</button>
99
+ <button id="slim-tile-linkpr-chip" class="linkpr-chip on-tile" type="button" title="Link a pull request" aria-label="Link a pull request">+</button>
97
100
  <div class="cockpit-label"><span class="cockpit-dot"></span> Active PRs</div>
98
101
  <div class="cockpit-value dim">0</div>
99
102
  <div class="cockpit-detail">no open PRs</div>
@@ -102,7 +102,11 @@
102
102
  var res = await fetch('/api/command-center/stream', {
103
103
  method: 'POST',
104
104
  headers: { 'Content-Type': 'application/json' },
105
- body: JSON.stringify({ message: text, tabId: tabId, sessionId: sessionId, currentProject: currentProject || undefined }),
105
+ // currentProject is the slim project picker's explicit selection (null
106
+ // when the indicator reads "Select project"). noProjectSelected lets the
107
+ // server inject a "no project — ask the user" CC preamble instead of
108
+ // silently defaulting; only the slim composer sends it (W-mqayzsj3).
109
+ body: JSON.stringify({ message: text, tabId: tabId, sessionId: sessionId, currentProject: currentProject || undefined, noProjectSelected: currentProject ? undefined : true }),
106
110
  signal: signal,
107
111
  });
108
112
  if (!res.ok) {