@yemi33/minions 0.1.2304 → 0.1.2306
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/js/refresh.js +17 -13
- package/dashboard/js/render-other.js +251 -3
- package/dashboard/js/render-plans.js +1 -1
- package/dashboard/js/render-prs.js +4 -1
- package/dashboard/js/render-utils.js +13 -0
- package/dashboard/js/render-work-items.js +13 -14
- package/dashboard/js/settings.js +3 -3
- package/dashboard/layout.html +13 -0
- package/dashboard/slim/js/modals-tiles.js +21 -130
- package/dashboard/slim/styles.css +9 -14
- package/dashboard/styles.css +79 -0
- package/dashboard.js +41 -9
- package/docs/completion-reports.md +25 -0
- package/docs/copilot-cli-schema.md +31 -1
- package/docs/design-state-storage.md +1 -1
- package/docs/live-checkout-mode.md +5 -3
- package/engine/ado.js +37 -0
- package/engine/consolidation.js +59 -2
- package/engine/lifecycle.js +149 -0
- package/engine/live-checkout.js +290 -79
- package/engine/playbook.js +2 -2
- package/engine/queries.js +11 -7
- package/engine/shared.js +6 -5
- package/engine/supervisor.js +131 -19
- package/engine.js +2 -2
- package/package.json +1 -1
package/dashboard/js/refresh.js
CHANGED
|
@@ -134,7 +134,8 @@ const RENDER_VERSIONS = {
|
|
|
134
134
|
prdProgress: 2,
|
|
135
135
|
prdPrs: 1,
|
|
136
136
|
inbox: 2,
|
|
137
|
-
|
|
137
|
+
// Bumped 4→5 for the clickable checkout-mode pill + picker (W-mr1b67zi0006b788).
|
|
138
|
+
projects: 5,
|
|
138
139
|
notes: 1,
|
|
139
140
|
prd: 3,
|
|
140
141
|
prs: 3,
|
|
@@ -147,7 +148,7 @@ const RENDER_VERSIONS = {
|
|
|
147
148
|
dispatch: 2,
|
|
148
149
|
engineLog: 2,
|
|
149
150
|
metrics: 1,
|
|
150
|
-
workItems:
|
|
151
|
+
workItems: 7,
|
|
151
152
|
skills: 1,
|
|
152
153
|
commands: 1,
|
|
153
154
|
mcpServers: 1,
|
|
@@ -1066,8 +1067,17 @@ let _refreshInFlight = false;
|
|
|
1066
1067
|
// it's not obvious the dashboard itself is dead (vs. wedged, vs. the
|
|
1067
1068
|
// engine being down).
|
|
1068
1069
|
//
|
|
1069
|
-
// Trip conditions:
|
|
1070
|
-
//
|
|
1070
|
+
// Trip conditions (2026-06-30): SUSTAINED failure only — the banner must never
|
|
1071
|
+
// flash while the dashboard is merely slow. 3 consecutive failed polls
|
|
1072
|
+
// (timeout/abort or 5xx). At the 4s cadence a wedged dashboard aborts each poll
|
|
1073
|
+
// at safeFetch's 15s timeout, so 3 in a row is ~45s of continuous
|
|
1074
|
+
// unresponsiveness — well beyond the legitimate 15-25s cold getStatus() rebuild
|
|
1075
|
+
// documented in restart-health.js. A truly-dead dashboard still trips promptly
|
|
1076
|
+
// (~12s) because connection-refused rejects fast rather than waiting out the
|
|
1077
|
+
// 15s timeout. The old "> N seconds since last success" OR side was REMOVED:
|
|
1078
|
+
// aborts during a healthy cold rebuild pushed elapsed-time past the budget and
|
|
1079
|
+
// surfaced the banner while the dashboard was alive-but-busy. Consecutive
|
|
1080
|
+
// failures are the only reliable "actually unreachable" signal.
|
|
1071
1081
|
// On trip we show a sticky red banner with two recovery actions:
|
|
1072
1082
|
// 1. "Restart Minions" → ccRestartMinions (works when dashboard is
|
|
1073
1083
|
// alive-but-stale; falls through to copy-to-clipboard when the POST
|
|
@@ -1080,14 +1090,7 @@ let _lastStatusOkAt = Date.now();
|
|
|
1080
1090
|
let _consecutiveStatusFails = 0;
|
|
1081
1091
|
let _unreachableSince = 0; // 0 = currently reachable
|
|
1082
1092
|
let _unreachableAgeTimer = null;
|
|
1083
|
-
// 3 + 20s (was 2 + 12s) — the prior thresholds tripped a banner on a single
|
|
1084
|
-
// safeFetch abort (timeout 15s in state.js) because the OR'd age side was
|
|
1085
|
-
// already satisfied. With safeFetch=15s, the age budget needs >15s to avoid
|
|
1086
|
-
// the trip-on-first-slow-response footgun. 3 consecutive fails ≈ 12s of real
|
|
1087
|
-
// outage at the 4s poll cadence, which still surfaces the banner promptly
|
|
1088
|
-
// during a real dashboard crash but tolerates one slow rebuild + retry.
|
|
1089
1093
|
const _UNREACHABLE_FAIL_THRESHOLD = 3;
|
|
1090
|
-
const _UNREACHABLE_AGE_MS = 20000;
|
|
1091
1094
|
// Once the banner is up we throttle polls (exponential, capped at 30s) to
|
|
1092
1095
|
// avoid hammering a struggling dashboard / network. Reset on recovery.
|
|
1093
1096
|
let _nextPollAllowedAt = 0;
|
|
@@ -1345,8 +1348,9 @@ async function refresh(opts) {
|
|
|
1345
1348
|
_diagEntry.error_message = String((e && e.message) || e);
|
|
1346
1349
|
}
|
|
1347
1350
|
_consecutiveStatusFails++;
|
|
1348
|
-
|
|
1349
|
-
|
|
1351
|
+
// Sustained failure only — see the detector header for why elapsed time
|
|
1352
|
+
// alone must not trip (it flashed the banner on healthy-but-slow rebuilds).
|
|
1353
|
+
if (_consecutiveStatusFails >= _UNREACHABLE_FAIL_THRESHOLD) {
|
|
1350
1354
|
_markDashboardUnreachable(e);
|
|
1351
1355
|
}
|
|
1352
1356
|
// Backoff: once we've tripped the banner, throttle subsequent polls
|
|
@@ -96,18 +96,266 @@ function _renderProjectBranch(p) {
|
|
|
96
96
|
// is set, coding WIs author in isolated worktrees and only the named validation
|
|
97
97
|
// type runs in-place on the live checkout. This is a materially different
|
|
98
98
|
// dispatch profile from full-live, so it gets its own pill.
|
|
99
|
+
// W-mr1b67zi0006b788: the pill is clickable — opens an inline picker (see
|
|
100
|
+
// _openCheckoutModeMenu below) to switch between Worktrees / Live checkout /
|
|
101
|
+
// Hybrid dispatch modes without leaving the Projects view. Shared
|
|
102
|
+
// data-checkout-pill / role="button" / tabindex / aria-haspopup attributes
|
|
103
|
+
// are attached to all three pill variants; the delegated listener wired at
|
|
104
|
+
// the bottom of this file reads data-checkout-pill to know which project to
|
|
105
|
+
// open the menu for.
|
|
99
106
|
function _renderWorktreeModePill(p) {
|
|
100
107
|
if (!p) return '';
|
|
108
|
+
const name = escHtml(p.name);
|
|
109
|
+
const common = ' data-checkout-pill="' + name + '" role="button" tabindex="0" aria-haspopup="true" aria-label="Change checkout mode"';
|
|
101
110
|
if (p.checkoutMode === 'live') {
|
|
102
111
|
if (p.liveValidationType) {
|
|
103
112
|
const vt = escapeHtml(p.liveValidationType);
|
|
104
|
-
return ' <span class="project-mode-pill project-mode-hybrid" title="Hybrid live-validation — coding work items author in isolated worktrees; only the "' + vt + '" validation type runs in-place on the live checkout (capped to one mutating dispatch, refused on a dirty tree)">⚡ Hybrid · ' + vt + '</span>';
|
|
113
|
+
return ' <span class="project-mode-pill project-mode-hybrid project-mode-pill-clickable"' + common + ' title="Hybrid live-validation — coding work items author in isolated worktrees; only the "' + vt + '" validation type runs in-place on the live checkout (capped to one mutating dispatch, refused on a dirty tree). Click to change.">⚡ Hybrid · ' + vt + '</span>';
|
|
105
114
|
}
|
|
106
|
-
return ' <span class="project-mode-pill project-mode-live" title="Live-checkout dispatch mode — agents run in-place inside the project working tree (no isolated worktree); capped to one mutating dispatch and refused on a dirty tree">⚡ Live checkout</span>';
|
|
115
|
+
return ' <span class="project-mode-pill project-mode-live project-mode-pill-clickable"' + common + ' title="Live-checkout dispatch mode — agents run in-place inside the project working tree (no isolated worktree); capped to one mutating dispatch and refused on a dirty tree. Click to change.">⚡ Live checkout</span>';
|
|
107
116
|
}
|
|
108
|
-
return ' <span class="project-mode-pill project-mode-isolated" title="Worktree dispatch mode (default) — each agent runs in its own git worktree">Worktrees</span>';
|
|
117
|
+
return ' <span class="project-mode-pill project-mode-isolated project-mode-pill-clickable"' + common + ' title="Worktree dispatch mode (default) — each agent runs in its own git worktree. Click to change.">Worktrees</span>';
|
|
109
118
|
}
|
|
110
119
|
|
|
120
|
+
// Work-item types eligible for hybrid live-validation (W-mr1b67zi0006b788).
|
|
121
|
+
// Mirrors the set the server documents for project.liveValidation.type;
|
|
122
|
+
// intentionally excludes the illustrative "build-and-test" example from the
|
|
123
|
+
// server comments since that isn't a real WORK_TYPE value.
|
|
124
|
+
const CHECKOUT_MODE_HYBRID_TYPES = ['implement', 'fix', 'docs', 'decompose', 'explore', 'ask', 'review', 'test', 'verify', 'setup'];
|
|
125
|
+
const CHECKOUT_MODE_HYBRID_DEFAULT_TYPE = 'test';
|
|
126
|
+
|
|
127
|
+
function _findProjectInLastStatus(name) {
|
|
128
|
+
const projects = window._lastStatus && window._lastStatus.projects;
|
|
129
|
+
if (!Array.isArray(projects)) return null;
|
|
130
|
+
return projects.find(function(p) { return p.name === name; }) || null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function _closeCheckoutModeMenu() {
|
|
134
|
+
const existing = document.getElementById('checkout-mode-menu');
|
|
135
|
+
if (existing) existing.remove();
|
|
136
|
+
document.removeEventListener('mousedown', _checkoutModeMenuOutsideClick, true);
|
|
137
|
+
document.removeEventListener('keydown', _checkoutModeMenuKeydown, true);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function _checkoutModeMenuOutsideClick(e) {
|
|
141
|
+
const menu = document.getElementById('checkout-mode-menu');
|
|
142
|
+
if (!menu) return;
|
|
143
|
+
if (menu.contains(e.target)) return;
|
|
144
|
+
if (e.target.closest && e.target.closest('[data-checkout-pill]')) return;
|
|
145
|
+
_closeCheckoutModeMenu();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function _checkoutModeMenuKeydown(e) {
|
|
149
|
+
if (e.key === 'Escape') _closeCheckoutModeMenu();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Builds and positions the checkout-mode picker popover beneath the clicked
|
|
153
|
+
// pill. No innerHTML is used here (createElement/textContent throughout) to
|
|
154
|
+
// stay clear of the innerHTML lint gate (npm run lint).
|
|
155
|
+
function _openCheckoutModeMenu(projectName, anchorEl) {
|
|
156
|
+
_closeCheckoutModeMenu();
|
|
157
|
+
const project = _findProjectInLastStatus(projectName);
|
|
158
|
+
if (!project) return;
|
|
159
|
+
|
|
160
|
+
const menu = document.createElement('div');
|
|
161
|
+
menu.id = 'checkout-mode-menu';
|
|
162
|
+
menu.className = 'checkout-mode-menu';
|
|
163
|
+
menu.setAttribute('role', 'menu');
|
|
164
|
+
|
|
165
|
+
const title = document.createElement('div');
|
|
166
|
+
title.className = 'checkout-mode-menu-title';
|
|
167
|
+
title.textContent = 'Checkout mode — ' + projectName;
|
|
168
|
+
menu.appendChild(title);
|
|
169
|
+
|
|
170
|
+
function addItem(labelText, mode, descText) {
|
|
171
|
+
const item = document.createElement('div');
|
|
172
|
+
item.className = 'checkout-mode-menu-item';
|
|
173
|
+
item.setAttribute('role', 'menuitemradio');
|
|
174
|
+
item.tabIndex = 0;
|
|
175
|
+
const label = document.createElement('div');
|
|
176
|
+
label.className = 'checkout-mode-menu-item-label';
|
|
177
|
+
label.textContent = labelText;
|
|
178
|
+
item.appendChild(label);
|
|
179
|
+
if (descText) {
|
|
180
|
+
const desc = document.createElement('div');
|
|
181
|
+
desc.className = 'checkout-mode-menu-item-desc';
|
|
182
|
+
desc.textContent = descText;
|
|
183
|
+
item.appendChild(desc);
|
|
184
|
+
}
|
|
185
|
+
const isActive = (mode === 'worktree' && project.checkoutMode !== 'live')
|
|
186
|
+
|| (mode === 'live' && project.checkoutMode === 'live' && !project.liveValidationType)
|
|
187
|
+
|| (mode === 'hybrid' && project.checkoutMode === 'live' && !!project.liveValidationType);
|
|
188
|
+
if (isActive) {
|
|
189
|
+
item.setAttribute('aria-checked', 'true');
|
|
190
|
+
item.classList.add('checkout-mode-menu-item-active');
|
|
191
|
+
} else {
|
|
192
|
+
item.setAttribute('aria-checked', 'false');
|
|
193
|
+
}
|
|
194
|
+
item.addEventListener('click', function() {
|
|
195
|
+
if (mode === 'hybrid') {
|
|
196
|
+
_renderCheckoutModeHybridStep(menu, projectName);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
_closeCheckoutModeMenu();
|
|
200
|
+
_applyCheckoutModeChange(projectName, mode === 'live' ? 'live' : 'worktree', null);
|
|
201
|
+
});
|
|
202
|
+
item.addEventListener('keydown', function(e) {
|
|
203
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); item.click(); }
|
|
204
|
+
});
|
|
205
|
+
menu.appendChild(item);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
addItem('Worktrees (default)', 'worktree', 'Each dispatch runs in its own isolated git worktree. Fully parallel, never touches your working directory.');
|
|
209
|
+
addItem('Live checkout', 'live', 'Agents run in-place in the project folder. One mutating dispatch at a time; refuses to run on a dirty tree.');
|
|
210
|
+
addItem('Hybrid dispatch…', 'hybrid', 'Coding work authors in isolated worktrees; only a chosen validation step (e.g. build-and-test) runs on the real checkout.');
|
|
211
|
+
|
|
212
|
+
document.body.appendChild(menu);
|
|
213
|
+
const rect = anchorEl.getBoundingClientRect();
|
|
214
|
+
menu.style.position = 'absolute';
|
|
215
|
+
menu.style.top = (window.scrollY + rect.bottom + 4) + 'px';
|
|
216
|
+
menu.style.left = (window.scrollX + rect.left) + 'px';
|
|
217
|
+
|
|
218
|
+
document.addEventListener('mousedown', _checkoutModeMenuOutsideClick, true);
|
|
219
|
+
document.addEventListener('keydown', _checkoutModeMenuKeydown, true);
|
|
220
|
+
const first = menu.querySelector('.checkout-mode-menu-item');
|
|
221
|
+
if (first) first.focus();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Second step of the picker for hybrid mode: pick which work-item type runs
|
|
225
|
+
// in-place on the live checkout while everything else authors in isolated
|
|
226
|
+
// worktrees. Renders a <select> + Back/Apply into the existing menu element.
|
|
227
|
+
function _renderCheckoutModeHybridStep(menu, projectName) {
|
|
228
|
+
const project = _findProjectInLastStatus(projectName);
|
|
229
|
+
while (menu.firstChild) menu.removeChild(menu.firstChild);
|
|
230
|
+
|
|
231
|
+
const title = document.createElement('div');
|
|
232
|
+
title.className = 'checkout-mode-menu-title';
|
|
233
|
+
title.textContent = 'Hybrid dispatch — ' + projectName;
|
|
234
|
+
menu.appendChild(title);
|
|
235
|
+
|
|
236
|
+
const label = document.createElement('div');
|
|
237
|
+
label.className = 'checkout-mode-menu-item';
|
|
238
|
+
label.textContent = 'Work-item type to validate live:';
|
|
239
|
+
menu.appendChild(label);
|
|
240
|
+
|
|
241
|
+
const select = document.createElement('select');
|
|
242
|
+
select.className = 'checkout-mode-menu-select';
|
|
243
|
+
CHECKOUT_MODE_HYBRID_TYPES.forEach(function(t) {
|
|
244
|
+
const opt = document.createElement('option');
|
|
245
|
+
opt.value = t;
|
|
246
|
+
opt.textContent = t;
|
|
247
|
+
select.appendChild(opt);
|
|
248
|
+
});
|
|
249
|
+
select.value = (project && project.liveValidationType && CHECKOUT_MODE_HYBRID_TYPES.includes(project.liveValidationType))
|
|
250
|
+
? project.liveValidationType
|
|
251
|
+
: CHECKOUT_MODE_HYBRID_DEFAULT_TYPE;
|
|
252
|
+
menu.appendChild(select);
|
|
253
|
+
|
|
254
|
+
const actions = document.createElement('div');
|
|
255
|
+
actions.className = 'checkout-mode-menu-actions';
|
|
256
|
+
|
|
257
|
+
const backBtn = document.createElement('button');
|
|
258
|
+
backBtn.type = 'button';
|
|
259
|
+
backBtn.className = 'checkout-mode-menu-btn';
|
|
260
|
+
backBtn.textContent = 'Back';
|
|
261
|
+
backBtn.addEventListener('click', function() {
|
|
262
|
+
const anchor = document.querySelector('[data-checkout-pill="' + CSS.escape(projectName) + '"]');
|
|
263
|
+
_closeCheckoutModeMenu();
|
|
264
|
+
if (anchor) _openCheckoutModeMenu(projectName, anchor);
|
|
265
|
+
});
|
|
266
|
+
actions.appendChild(backBtn);
|
|
267
|
+
|
|
268
|
+
const applyBtn = document.createElement('button');
|
|
269
|
+
applyBtn.type = 'button';
|
|
270
|
+
applyBtn.className = 'checkout-mode-menu-btn checkout-mode-menu-btn-primary';
|
|
271
|
+
applyBtn.textContent = 'Apply';
|
|
272
|
+
applyBtn.addEventListener('click', function() {
|
|
273
|
+
const type = select.value;
|
|
274
|
+
_closeCheckoutModeMenu();
|
|
275
|
+
_applyCheckoutModeChange(projectName, 'live', type);
|
|
276
|
+
});
|
|
277
|
+
actions.appendChild(applyBtn);
|
|
278
|
+
|
|
279
|
+
menu.appendChild(actions);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Applies a checkout-mode change: confirms via confirmDialog(), optimistically
|
|
283
|
+
// flips the cached project entry and re-renders BEFORE awaiting the network
|
|
284
|
+
// call (mirrors projectChipRemove / removePinnedNote), then POSTs to
|
|
285
|
+
// /api/settings. On failure, reverts the optimistic flip, re-renders, and
|
|
286
|
+
// shows an error toast.
|
|
287
|
+
async function _applyCheckoutModeChange(projectName, newMode, liveValidationType) {
|
|
288
|
+
const project = _findProjectInLastStatus(projectName);
|
|
289
|
+
if (!project) return;
|
|
290
|
+
const label = newMode === 'worktree'
|
|
291
|
+
? 'Worktrees'
|
|
292
|
+
: (liveValidationType ? ('Hybrid (' + liveValidationType + ')') : 'Live checkout');
|
|
293
|
+
const ok = await confirmDialog({
|
|
294
|
+
title: 'Change checkout mode?',
|
|
295
|
+
message: 'Switch "' + projectName + '" to ' + label + '? This changes how every future dispatch on this project runs.',
|
|
296
|
+
confirmLabel: 'Change mode',
|
|
297
|
+
cancelLabel: 'Cancel',
|
|
298
|
+
danger: newMode !== 'worktree',
|
|
299
|
+
});
|
|
300
|
+
if (!ok) return;
|
|
301
|
+
|
|
302
|
+
const prevMode = project.checkoutMode;
|
|
303
|
+
const prevType = project.liveValidationType;
|
|
304
|
+
const nextType = (newMode === 'live' && liveValidationType) ? liveValidationType : null;
|
|
305
|
+
// Optimistic flip BEFORE awaiting the API call (per dashboard convention —
|
|
306
|
+
// see projectChipRemove / removePinnedNote); reverted in the catch below.
|
|
307
|
+
project.checkoutMode = newMode;
|
|
308
|
+
project.liveValidationType = nextType;
|
|
309
|
+
if (window._lastStatus && Array.isArray(window._lastStatus.projects)) renderProjects(window._lastStatus.projects);
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const res = await safeFetch('/api/settings', {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
headers: { 'Content-Type': 'application/json' },
|
|
315
|
+
body: JSON.stringify({
|
|
316
|
+
projects: [{
|
|
317
|
+
name: projectName,
|
|
318
|
+
checkoutMode: newMode,
|
|
319
|
+
liveValidation: nextType ? { type: nextType } : null,
|
|
320
|
+
}],
|
|
321
|
+
}),
|
|
322
|
+
});
|
|
323
|
+
const result = await res.json().catch(function() { return {}; });
|
|
324
|
+
if (!res.ok) throw new Error((result && result.error) || ('HTTP ' + res.status));
|
|
325
|
+
showToast('cmd-toast', 'Checkout mode updated for "' + projectName + '".', true);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
project.checkoutMode = prevMode;
|
|
328
|
+
project.liveValidationType = prevType;
|
|
329
|
+
if (window._lastStatus && Array.isArray(window._lastStatus.projects)) renderProjects(window._lastStatus.projects);
|
|
330
|
+
showToast('cmd-toast', 'Failed to update checkout mode: ' + (err && err.message || err), false);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Delegated click/keydown on the persistent #projects-list container (its
|
|
335
|
+
// children are replaced wholesale on every renderProjects() call, but the
|
|
336
|
+
// container itself survives re-renders — see the Long-Running Commands /
|
|
337
|
+
// event-delegation convention referenced in the playbook).
|
|
338
|
+
(function _wireCheckoutModePillDelegation() {
|
|
339
|
+
const list = document.getElementById('projects-list');
|
|
340
|
+
if (!list) return;
|
|
341
|
+
list.addEventListener('click', function(e) {
|
|
342
|
+
const pill = e.target.closest('[data-checkout-pill]');
|
|
343
|
+
if (!pill) return;
|
|
344
|
+
e.stopPropagation();
|
|
345
|
+
const name = pill.getAttribute('data-checkout-pill');
|
|
346
|
+
_openCheckoutModeMenu(name, pill);
|
|
347
|
+
});
|
|
348
|
+
list.addEventListener('keydown', function(e) {
|
|
349
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
350
|
+
const pill = e.target.closest('[data-checkout-pill]');
|
|
351
|
+
if (!pill) return;
|
|
352
|
+
e.preventDefault();
|
|
353
|
+
e.stopPropagation();
|
|
354
|
+
const name = pill.getAttribute('data-checkout-pill');
|
|
355
|
+
_openCheckoutModeMenu(name, pill);
|
|
356
|
+
});
|
|
357
|
+
})();
|
|
358
|
+
|
|
111
359
|
function _projectCachePath(project) {
|
|
112
360
|
return String((project && (project.localPath || project.path)) || '').replace(/\\/g, '/');
|
|
113
361
|
}
|
|
@@ -371,7 +371,7 @@ function renderPlans(plans) {
|
|
|
371
371
|
'onclick="event.stopPropagation();planDelete(\'' + escapeHtml(p.file) + '\')">Delete</button>' : '';
|
|
372
372
|
|
|
373
373
|
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>' : '';
|
|
374
|
-
const statusColors = { 'completed': 'var(--green)', 'dispatched': 'var(--blue)', 'converting': 'var(--yellow)', 'paused': 'var(--muted)', 'awaiting-approval': 'var(--yellow)', 'approved': 'var(--
|
|
374
|
+
const statusColors = { 'completed': 'var(--green)', 'dispatched': 'var(--blue)', 'converting': 'var(--yellow)', 'paused': 'var(--muted)', 'awaiting-approval': 'var(--yellow)', 'approved': 'var(--purple,#a855f7)', 'rejected': 'var(--red)', 'has-failures': 'var(--red)', 'revision-requested': 'var(--purple,#a855f7)', 'active': 'var(--muted)', 'draft': 'var(--muted)' };
|
|
375
375
|
const cardClass = effectiveStatus === 'dispatched' || effectiveStatus === 'converting' ? 'working' : effectiveStatus === 'awaiting-approval' || effectiveStatus === 'paused' ? 'awaiting' : effectiveStatus;
|
|
376
376
|
// P-e8d49105 — cross-repo plans surface every touched project as its
|
|
377
377
|
// own badge. Single-project plans (and old PRDs without _projects)
|
|
@@ -646,7 +646,10 @@ async function togglePrObserve(btn) {
|
|
|
646
646
|
try {
|
|
647
647
|
const res = await fetch('/api/pull-requests/observe', {
|
|
648
648
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
649
|
-
|
|
649
|
+
// Canonical contextOnly is the primary payload (P-2b6e4d81); the
|
|
650
|
+
// legacy `observe` body param remains a server-side back-compat
|
|
651
|
+
// fallback for any other caller, so the client no longer sends it.
|
|
652
|
+
body: JSON.stringify({ host, slug, number, contextOnly: !next })
|
|
650
653
|
});
|
|
651
654
|
if (!res.ok) {
|
|
652
655
|
const d = await res.json().catch(() => ({}));
|
|
@@ -114,6 +114,19 @@ function ccSegmentsApplyChunk(segments, text, segmentId) {
|
|
|
114
114
|
var last = segments.length ? segments[segments.length - 1] : null;
|
|
115
115
|
if (hasId) {
|
|
116
116
|
if (last && last.kind === 'text' && last.id === segmentId) { last.text = text; return segments; }
|
|
117
|
+
// W-mr1bgfyr — a reconnect replay (or any other id-less push, see the
|
|
118
|
+
// heuristic branch below) leaves the trailing text segment with `id:
|
|
119
|
+
// null` even though the resumed live stream now carries a real
|
|
120
|
+
// segmentId for that SAME logical block. Without this, the id
|
|
121
|
+
// mismatch (null !== segmentId) opened a brand-new duplicate text
|
|
122
|
+
// segment on every following chunk, rendering as a second bubble/block
|
|
123
|
+
// stacked under the first with a little more text each time instead of
|
|
124
|
+
// updating in place. Adopt the id and merge into the existing segment.
|
|
125
|
+
if (last && last.kind === 'text' && last.id === null) {
|
|
126
|
+
last.text = _ccSegMergeText(last.text, text);
|
|
127
|
+
last.id = segmentId;
|
|
128
|
+
return segments;
|
|
129
|
+
}
|
|
117
130
|
segments.push({ kind: 'text', id: segmentId, text: text });
|
|
118
131
|
return segments;
|
|
119
132
|
}
|
|
@@ -42,6 +42,7 @@ const _WI_ENRICHMENT_FIELDS = [
|
|
|
42
42
|
// * already_dispatched — engine reconciles on next tick
|
|
43
43
|
// * branch_locked — wait for the holding dispatch
|
|
44
44
|
// * dependency_unmet — auto-clears when upstream dep finishes (engine discovery loop + DEPENDENCY_MET watch)
|
|
45
|
+
// * live_checkout_busy — one live-mode dispatch at a time; clears when running dispatch finishes
|
|
45
46
|
//
|
|
46
47
|
// Returns { kind, short, full } when the item needs attention, else null.
|
|
47
48
|
// Pure helper (no DOM / escapeHtml deps) so it can be unit-tested in Node.
|
|
@@ -219,7 +220,7 @@ function wiRow(item) {
|
|
|
219
220
|
+ escapeHtml(((item._managedSpawnPartial.healthy || []).length) + '/' + (((item._managedSpawnPartial.healthy || []).length) + item._managedSpawnPartial.failed.length))
|
|
220
221
|
+ ' healthy</span>'
|
|
221
222
|
: '') +
|
|
222
|
-
(item._skipReason && item.status === 'pending' ? ' <span style="font-size:var(--text-xs);color:var(--yellow);margin-left:4px" title="Dispatch blocked: ' + escapeHtml(item._skipReason) + (item._blockedBy ? ' (by ' + escapeHtml(item._blockedBy) + ')' : '') + '">' + escapeHtml(item._skipReason.replace(/_/g, ' ')) + (item._blockedBy ? ' <span style="color:var(--muted)">(' + escapeHtml(item._blockedBy) + ')</span>' : '') + '</span>' : '') +
|
|
223
|
+
(item._skipReason && item.status === 'pending' && item._skipReason !== 'live_checkout_busy' ? ' <span style="font-size:var(--text-xs);color:var(--yellow);margin-left:4px" title="Dispatch blocked: ' + escapeHtml(item._skipReason) + (item._blockedBy ? ' (by ' + escapeHtml(item._blockedBy) + ')' : '') + '">' + escapeHtml(item._skipReason.replace(/_/g, ' ')) + (item._blockedBy ? ' <span style="color:var(--muted)">(' + escapeHtml(item._blockedBy) + ')</span>' : '') + '</span>' : '') +
|
|
223
224
|
(item.status === 'failed' ? ' ' + wiRetryBtn(item) : '') +
|
|
224
225
|
'</td>' +
|
|
225
226
|
'<td>' +
|
|
@@ -875,10 +876,17 @@ function _wiRenderDetail(item) {
|
|
|
875
876
|
// Artifacts — branch (non-navigation) + navigation chips routed through
|
|
876
877
|
// renderArtifactLink so chip click → openArtifact() → modal-stack push +
|
|
877
878
|
// URL hash + browser-Back integration come for free (P-e265cd31).
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
//
|
|
879
|
+
// NOTE (issue #603): item._source is the WI's *project name* (stamped by
|
|
880
|
+
// fetchWorkItemsFromDisk above / used by every action button to route
|
|
881
|
+
// retry/cancel/delete/etc.), NOT a plan/PRD file id. A prior "Source" chip
|
|
882
|
+
// here treated item._source as if it held a genuine source-plan/PRD
|
|
883
|
+
// filename, which rendered a bogus chip labeled with the project name on
|
|
884
|
+
// nearly every work item and 404'd when clicked (there is no plan/PRD
|
|
885
|
+
// named e.g. "src"). The genuine source-plan/PRD case is already fully
|
|
886
|
+
// covered by arts.plan / arts.prd / arts.sourcePlan below, which are
|
|
887
|
+
// populated server-side from the WI's real sourcePlan/_planFileName/
|
|
888
|
+
// planFile fields (engine/queries.js) — no separate item._source-based
|
|
889
|
+
// chip is needed.
|
|
882
890
|
var arts = item._artifacts || {};
|
|
883
891
|
var artPills = '';
|
|
884
892
|
var branchStyle = 'display:inline-flex;align-items:center;gap:3px;padding:2px 8px;border-radius:10px;font-size:var(--text-sm);background:var(--surface2);border:1px solid var(--border);color:var(--text);cursor:default';
|
|
@@ -886,15 +894,6 @@ function _wiRenderDetail(item) {
|
|
|
886
894
|
if (arts.plan) artPills += renderArtifactLink({ type: 'plan', id: arts.plan, label: 'Plan' }) + ' ';
|
|
887
895
|
if (arts.prd) artPills += renderArtifactLink({ type: 'prd', id: arts.prd, label: 'PRD' }) + ' ';
|
|
888
896
|
if (arts.sourcePlan) artPills += renderArtifactLink({ type: 'plan', id: arts.sourcePlan, label: 'Source Plan' }) + ' ';
|
|
889
|
-
// Source chip (item._source) — render when it's a file path that isn't
|
|
890
|
-
// already represented in arts.plan / arts.prd / arts.sourcePlan.
|
|
891
|
-
if (item._source && typeof item._source === 'string' && item._source !== 'central') {
|
|
892
|
-
var sourceAlreadyShown = arts.plan === item._source || arts.prd === item._source || arts.sourcePlan === item._source;
|
|
893
|
-
if (!sourceAlreadyShown) {
|
|
894
|
-
var sourceType = item._source.endsWith('.json') ? 'prd' : 'plan';
|
|
895
|
-
artPills += renderArtifactLink({ type: sourceType, id: item._source, label: 'Source', title: item._source }) + ' ';
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
897
|
if (arts.notes && arts.notes.length > 0) {
|
|
899
898
|
arts.notes.forEach(function(n) {
|
|
900
899
|
var noteFile = (n && typeof n === 'object') ? (n.file || n) : String(n || '');
|
package/dashboard/js/settings.js
CHANGED
|
@@ -282,8 +282,8 @@ async function openSettings() {
|
|
|
282
282
|
'<div data-search="' + escHtml(wtModeSearch) + '" style="margin-bottom:6px">' +
|
|
283
283
|
'<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Checkout mode</label>' +
|
|
284
284
|
'<select id="set-checkoutMode-' + escHtml(p.name) + '" data-checkout-mode-select="' + escHtml(p.name) + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)">' +
|
|
285
|
-
'<option value="worktree"' + (currentWtMode === 'worktree' ? ' selected' : '') + '>Worktree (default)</option>' +
|
|
286
|
-
'<option value="live"' + (currentWtMode === 'live' ? ' selected' : '') + '>Live checkout</option>' +
|
|
285
|
+
'<option value="worktree" title="Each dispatch runs in its own isolated git worktree. Fully parallel, never touches your working directory."' + (currentWtMode === 'worktree' ? ' selected' : '') + '>Worktree (default)</option>' +
|
|
286
|
+
'<option value="live" title="Agents run in-place in the project folder. One mutating dispatch at a time; refuses to run on a dirty tree."' + (currentWtMode === 'live' ? ' selected' : '') + '>Live checkout</option>' +
|
|
287
287
|
'</select>' +
|
|
288
288
|
'<div data-checkout-mode-chip="' + escHtml(p.name) + '" style="' + (currentWtMode === 'live' ? '' : 'display:none;') + 'margin-top:6px;padding:6px 8px;background:rgba(234,179,8,0.12);border:1px solid var(--yellow);border-radius:4px;color:var(--yellow);font-size:var(--text-xs);line-height:1.4">' +
|
|
289
289
|
'⚠ Live mode: dispatches run directly in this repo\'s checkout. Only one mutating dispatch runs at a time. Dirty working trees block dispatch — commit or stash before running.' +
|
|
@@ -384,7 +384,7 @@ async function openSettings() {
|
|
|
384
384
|
settingsToggle('Auto-reap orphan worktree holders', 'set-autoReapOrphanWorktreeHolders', !!e.autoReapOrphanWorktreeHolders,
|
|
385
385
|
'When ON, the periodic orphan-worktree sweep automatically kills unambiguous orphan `spawn-agent.js` processes holding a stuck worktree\'s cwd (Windows file-lock pattern, e.g. W-mq1k8z6o003acd89 / PID 16828). Safe-reap criteria: cmdline must match spawn-agent.js, reference the worktree basename, AND process age must exceed `engine.agentTimeout * 2`. Default OFF because killing a foreign process is destructive. The holder SCAN runs either way and is appended to the escalation inbox note.') +
|
|
386
386
|
settingsToggle('Live-checkout auto-reset (fleet-wide)', 'set-liveCheckoutAutoReset', !!e.liveCheckoutAutoReset,
|
|
387
|
-
'When ON, a dirty or broken live-checkout (in-place) tree is force-recovered before dispatch via `git fetch origin` + `git reset --hard origin/<branch>` instead of failing the dispatch with non-retryable LIVE_CHECKOUT_DIRTY. This is the FLEET-WIDE fallback; a per-project `liveCheckoutAutoReset` in config.json overrides it. Default
|
|
387
|
+
'When ON, a dirty or broken live-checkout (in-place) tree is force-recovered before dispatch via `git fetch origin` + `git reset --hard origin/<branch>` instead of failing the dispatch with non-retryable LIVE_CHECKOUT_DIRTY. This is the FLEET-WIDE fallback; a per-project `liveCheckoutAutoReset` in config.json overrides it. Default ON — discarding disposable dirt keeps live-checkout branches aligned with origin instead of accumulating auto-save WIP commits; the reset is DESTRUCTIVE (it discards the operator\'s uncommitted changes in the live checkout, and a `live-checkout-autoreset-<workItem>` inbox note records exactly what was discarded for reflog recovery), so set it OFF for live checkouts whose local drift you want preserved. Only affects projects running in live/in-place checkout mode.') +
|
|
388
388
|
'</div>' +
|
|
389
389
|
'<div class="settings-grid-2">' +
|
|
390
390
|
settingsField('CC Session Idle Timeout', 'set-ccWorkerIdleTimeoutMs', Math.round((e.ccWorkerIdleTimeoutMs || 1800000) / 60000), 'minutes', 'How long a persistent `copilot --acp` CC worker stays warm with no activity before the idle reaper kills it. After a reap the next message cold-spawns a FRESH session with no memory of prior turns and CC shows a "context cleared after inactivity" notice. Shorter = less idle memory/process footprint; longer = more context durability across gaps between messages. Only affects the Copilot worker-pool path (CC Worker Pool ON). Clamped 1–480 minutes.') +
|
package/dashboard/layout.html
CHANGED
|
@@ -17,6 +17,19 @@
|
|
|
17
17
|
document.documentElement.setAttribute('data-font-size', (v && valid[v]) ? v : 'small');
|
|
18
18
|
} catch (e) { /* private mode / disabled storage — fall through to default */ }
|
|
19
19
|
})();
|
|
20
|
+
// W-mqrdggys000f94a4 — chrome-off "embed" mode. When the SPA is loaded with
|
|
21
|
+
// ?embed=1 (e.g. inside the slim cockpit's Queued-work iframe pointed at
|
|
22
|
+
// /work?embed=1), tag <html> so the embed CSS hides the global chrome
|
|
23
|
+
// (header, sidebar, projects bar, banners) and the iframed page renders as
|
|
24
|
+
// just its panel. Applied in <head> so the first paint already omits the
|
|
25
|
+
// chrome (no flash of the full layout).
|
|
26
|
+
(function() {
|
|
27
|
+
try {
|
|
28
|
+
if (/[?&]embed=1(?:&|$)/.test(window.location.search)) {
|
|
29
|
+
document.documentElement.classList.add('embed');
|
|
30
|
+
}
|
|
31
|
+
} catch (e) { /* defensive — never block boot on a URL parse quirk */ }
|
|
32
|
+
})();
|
|
20
33
|
</script>
|
|
21
34
|
<style>/* __CSS__ */</style>
|
|
22
35
|
</head>
|