@yemi33/minions 0.1.2453 → 0.1.2455
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/bin/install-internal-minions.js +136 -6
- package/bin/minions.js +27 -13
- package/dashboard/js/agent-identity.js +121 -0
- package/dashboard/js/modal.js +4 -0
- package/dashboard/js/refresh.js +38 -3
- package/dashboard/js/render-agents.js +14 -2
- package/dashboard/js/render-dispatch.js +2 -2
- package/dashboard/js/render-other.js +147 -13
- package/dashboard/js/render-prd.js +6 -5
- package/dashboard/js/render-prs.js +44 -25
- package/dashboard/js/render-work-items.js +442 -22
- package/dashboard/js/settings.js +8 -0
- package/dashboard/js/utils.js +40 -0
- package/dashboard/pages/tools.html +1 -0
- package/dashboard/pages/work.html +13 -0
- package/dashboard/shared/pr-author.js +75 -0
- package/dashboard/shared/pr-filters.js +28 -5
- package/dashboard/styles.css +85 -0
- package/dashboard-build.js +3 -3
- package/dashboard.js +93 -7
- package/docs/README.md +1 -0
- package/docs/copilot-cli-schema.md +1 -0
- package/docs/engine-restart.md +4 -2
- package/docs/internal-install.md +44 -5
- package/docs/named-agents.md +48 -0
- package/docs/pr-author-identity.md +63 -10
- package/docs/runtime-adapters.md +39 -0
- package/docs/temporary-agents.md +172 -0
- package/engine/ado/comment.js +261 -4
- package/engine/agents/llm.js +26 -0
- package/engine/agents/playbook.js +2 -1
- package/engine/api/settings-validation.js +25 -0
- package/engine/core/operator-identity.js +23 -1
- package/engine/core/queries.js +53 -2
- package/engine/core/shared.js +286 -9
- package/engine/db/migrations/032-review-enrolled-pr-context-only.js +95 -0
- package/engine/operations/cli.js +102 -1
- package/engine/orchestration/lifecycle.js +7 -0
- package/engine/orchestration/routing.js +4 -1
- package/engine/providers/gh-comment.js +159 -0
- package/engine/recovery/stop-stack.js +16 -2
- package/engine/runtimes/claude.js +3 -0
- package/engine/runtimes/codex.js +4 -0
- package/engine/runtimes/copilot.js +23 -2
- package/engine.js +2 -2
- package/package.json +1 -1
- package/playbooks/fix.md +23 -2
- package/playbooks/shared-rules.md +24 -2
|
@@ -6,6 +6,66 @@ let wiPage = 0;
|
|
|
6
6
|
let wiFilterOptionsSignature = '';
|
|
7
7
|
const WI_PER_PAGE = 20;
|
|
8
8
|
|
|
9
|
+
// ── Bulk-operations mode (W-mscjvm33007r79ef) ─────────────────────────────
|
|
10
|
+
// OFF by default: the normal Work Items UI shows no selection affordances and
|
|
11
|
+
// cannot multi-select. An explicit "Bulk select" control flips wiBulkMode on,
|
|
12
|
+
// which renders per-row checkboxes + a bulk action bar. wiSelectedIds tracks
|
|
13
|
+
// the selection by work-item id so it stays coherent across the 4s refresh
|
|
14
|
+
// poll, filtering, paging, and item state changes — bulk actions resolve ids
|
|
15
|
+
// to LIVE items at apply time and skip any that vanished (never act on stale
|
|
16
|
+
// or nonexistent items).
|
|
17
|
+
let wiBulkMode = false;
|
|
18
|
+
const wiSelectedIds = new Set();
|
|
19
|
+
|
|
20
|
+
// Bulk actions reuse the EXISTING single-item POST endpoints and their exact
|
|
21
|
+
// availability rules, so bulk never broadens or invents an action. `statuses:
|
|
22
|
+
// null` means "any status" (Delete). Each spec drives eligibility, the
|
|
23
|
+
// endpoint, the confirm gate, and whether the row is optimistically removed.
|
|
24
|
+
//
|
|
25
|
+
// `destructive` IS the bulk confirmation policy (it is the only gate on the
|
|
26
|
+
// single confirm() in wiBulkAction), so it must mirror whether the single-item
|
|
27
|
+
// counterpart confirms: cancelWorkItem/deleteWorkItem confirm, so bulk Cancel
|
|
28
|
+
// and Delete confirm too. A bulk action multiplies the blast radius, so it can
|
|
29
|
+
// never be LESS guarded than acting on one item. Cancel is destructive because
|
|
30
|
+
// the server kills the running agent (handleWorkItemsCancel) and there is no
|
|
31
|
+
// undo — retry only accepts `failed`, so a cancelled `dispatched` item cannot
|
|
32
|
+
// be restored. Parity is enforced by test/unit/dashboard-work-items-bulk-actions.test.js.
|
|
33
|
+
const WI_BULK_ACTIONS = {
|
|
34
|
+
cancel: { label: 'Cancel', endpoint: '/api/work-items/cancel', statuses: ['pending', 'dispatched', 'queued', 'failed'], destructive: true, optimisticRemove: true, wake: false },
|
|
35
|
+
archive: { label: 'Archive', endpoint: '/api/work-items/archive', statuses: ['done', 'failed'], destructive: false, optimisticRemove: true, wake: false },
|
|
36
|
+
retry: { label: 'Retry', endpoint: '/api/work-items/retry', statuses: ['failed'], destructive: false, optimisticRemove: false, wake: true },
|
|
37
|
+
delete: { label: 'Delete', endpoint: '/api/work-items/delete', statuses: null, destructive: true, optimisticRemove: true, wake: false },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Pure eligibility predicate — mirrors the per-item button visibility in
|
|
41
|
+
// wiRow() so a bulk action is only offered for the same statuses. Exported for
|
|
42
|
+
// unit testing (no DOM deps).
|
|
43
|
+
function wiActionEligible(item, action) {
|
|
44
|
+
const spec = WI_BULK_ACTIONS[action];
|
|
45
|
+
if (!spec || !item) return false;
|
|
46
|
+
if (spec.statuses === null) return true;
|
|
47
|
+
return spec.statuses.indexOf(item.status || 'pending') !== -1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Pure planner — resolves a selection (ids) against the LIVE item list and
|
|
51
|
+
// splits it into { eligible, ineligible, missing } for a given action. This is
|
|
52
|
+
// the anti-stale core: an id no longer present in `items` lands in `missing`
|
|
53
|
+
// and is never acted on. Exported for unit testing.
|
|
54
|
+
function wiBulkPlan(items, selectedIds, action) {
|
|
55
|
+
const idSet = selectedIds instanceof Set ? selectedIds : new Set(selectedIds || []);
|
|
56
|
+
const byId = new Map((items || []).map(function(i) { return [i.id, i]; }));
|
|
57
|
+
const eligible = [];
|
|
58
|
+
const ineligible = [];
|
|
59
|
+
const missing = [];
|
|
60
|
+
idSet.forEach(function(id) {
|
|
61
|
+
const it = byId.get(id);
|
|
62
|
+
if (!it) { missing.push(id); return; }
|
|
63
|
+
if (wiActionEligible(it, action)) eligible.push(it);
|
|
64
|
+
else ineligible.push(it);
|
|
65
|
+
});
|
|
66
|
+
return { eligible: eligible, ineligible: ineligible, missing: missing };
|
|
67
|
+
}
|
|
68
|
+
|
|
9
69
|
// Filter + sort controls modeled on the PR table (dashboard/js/render-prs.js).
|
|
10
70
|
// Each dimension maps to a <select> in dashboard/pages/work.html; options are
|
|
11
71
|
// populated dynamically from the current data set with a signature guard so the
|
|
@@ -184,7 +244,7 @@ function wiRetryBtn(item) {
|
|
|
184
244
|
return '<button class="pr-pager-btn" style="font-size:var(--text-sm);padding:1px 6px;color:var(--yellow);border-color:var(--yellow);margin-left:4px" onclick="event.stopPropagation();retryWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')">Retry</button>';
|
|
185
245
|
}
|
|
186
246
|
|
|
187
|
-
function wiRow(item) {
|
|
247
|
+
function wiRow(item, bulk) {
|
|
188
248
|
const statusBadge = (s) => {
|
|
189
249
|
const cls = s === 'failed' ? 'rejected' : s === 'dispatched' ? 'building' : s === 'pending' || s === 'queued' ? 'active' : s === 'done' ? 'approved' : s === 'decomposed' ? 'approved' : 'draft';
|
|
190
250
|
return '<span class="pr-badge ' + cls + '">' + escapeHtml(s) + '</span>';
|
|
@@ -210,9 +270,22 @@ function wiRow(item) {
|
|
|
210
270
|
var prRef = prFollowup.parent_pr_id || prFollowup.parent_pr_url;
|
|
211
271
|
var prNumMatch = String(prRef).match(/(\d+)(?!.*\d)/);
|
|
212
272
|
var prLabel = prNumMatch ? ('PR #' + prNumMatch[1]) : 'parent PR';
|
|
213
|
-
|
|
273
|
+
var followupBy = prFollowup.parent_comment_author
|
|
274
|
+
? ((window.MinionsPrAuthor && window.MinionsPrAuthor.name)
|
|
275
|
+
? window.MinionsPrAuthor.name(prFollowup.parent_comment_author)
|
|
276
|
+
: prFollowup.parent_comment_author)
|
|
277
|
+
: '';
|
|
278
|
+
followupChip = ' <a class="pr-badge draft" style="font-size:var(--text-sm);text-decoration:none" target="_blank" rel="noopener" href="' + escapeHtml(prFollowup.parent_pr_url) + '" title="Follow-up dispatched from ' + escapeHtml(prRef) + (followupBy ? ' by ' + escapeHtml(followupBy) : '') + '" onclick="event.stopPropagation()">↩ from ' + escapeHtml(prLabel) + '</a>';
|
|
214
279
|
}
|
|
215
|
-
|
|
280
|
+
var _wiSelected = bulk && wiSelectedIds.has(item.id);
|
|
281
|
+
var bulkCell = bulk
|
|
282
|
+
? '<td class="wi-bulk-cell"><input type="checkbox" class="wi-bulk-cb" data-wi-id="' + escapeHtml(item.id) + '" data-wi-source="' + escapeHtml(item._source || '') + '" aria-label="Select ' + escapeHtml((item.title || item.id || '').slice(0, 80)) + '"' + (_wiSelected ? ' checked' : '') + ' onclick="event.stopPropagation();wiToggleSelect(this)"></td>'
|
|
283
|
+
: '';
|
|
284
|
+
// The click handler receives the SAME `bulk` flag the row was rendered with
|
|
285
|
+
// (see wiRowClick) — a row drawn without a checkbox must never toggle a
|
|
286
|
+
// selection the operator cannot see.
|
|
287
|
+
return '<tr data-wi-id="' + escapeHtml(item.id) + '"' + (_wiSelected ? ' class="wi-row-selected"' : '') + ' style="cursor:pointer" onclick="wiRowClick(event,\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\',' + (bulk ? 'true' : 'false') + ')">' +
|
|
288
|
+
bulkCell +
|
|
216
289
|
'<td style="min-width:280px;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escapeHtml((item.title || '').slice(0, 200)) + '">' + escapeHtml(item.title || '') + followupChip + '</td>' +
|
|
217
290
|
'<td><span style="font-size:var(--text-sm);color:var(--muted)">' + escapeHtml(item._source || '') + '</span>' +
|
|
218
291
|
(item.scope === 'fan-out' ? ' <span class="pr-badge ' + (item.status === 'done' || item.status === 'failed' ? 'draft' : 'building') + '" style="font-size:var(--text-sm)">fan-out</span>' : '') + '</td>' +
|
|
@@ -245,8 +318,8 @@ function wiRow(item) {
|
|
|
245
318
|
'</td>' +
|
|
246
319
|
'<td>' +
|
|
247
320
|
(item.completedAgents && item.completedAgents.length > 0
|
|
248
|
-
? '<span class="pr-agent">' +
|
|
249
|
-
: '<span class="pr-agent">' +
|
|
321
|
+
? '<span class="pr-agent">' + item.completedAgents.map(function(a) { return MinionsAgentIdentity.nameWithBadgeHtml({ id: a }); }).join(', ') + '</span>'
|
|
322
|
+
: '<span class="pr-agent">' + ((item.dispatched_to || item.agent) ? MinionsAgentIdentity.nameWithBadgeHtml({ id: item.dispatched_to || item.agent }) : '—') + '</span>') +
|
|
250
323
|
'</td>' +
|
|
251
324
|
'<td>' + prLink + (failSnippet ? failSnippet : '') + '</td>' +
|
|
252
325
|
'<td><span class="pr-date">' + escapeHtml((item.created || '').slice(0, 16).replace('T', ' ')) + '</span></td>' +
|
|
@@ -267,15 +340,18 @@ function wiRow(item) {
|
|
|
267
340
|
(acCount ? '<span title="' + acCount + ' acceptance criteria">☑' + acCount + '</span>' : '');
|
|
268
341
|
})() +
|
|
269
342
|
'</td>' +
|
|
270
|
-
'<td style="white-space:nowrap;text-align:right">' +
|
|
271
|
-
// W-
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
(
|
|
343
|
+
'<td class="wi-actions-cell" style="white-space:nowrap;text-align:right">' +
|
|
344
|
+
// W-mscjvm33007r79ef — every per-item action EXCEPT Delete now lives in an
|
|
345
|
+
// overflow menu (wiOpenOverflowMenu), built on demand from the live item so
|
|
346
|
+
// it always reflects current availability/disabled states. The ⋯ button is
|
|
347
|
+
// omitted when the item has no eligible menu action (see
|
|
348
|
+
// _wiOverflowActionsFor), so an actionless item shows only Delete.
|
|
349
|
+
// The "feedback given" indicator is informational (not an action) and is
|
|
350
|
+
// preserved inline so operators still see prior feedback at a glance.
|
|
351
|
+
(item._humanFeedback ? '<span style="font-size:var(--text-sm);margin-right:4px" title="Feedback given">' + (item._humanFeedback.rating === 'up' ? '👍' : '👎') + '</span>' : '') +
|
|
352
|
+
(_wiOverflowActionsFor(item).length
|
|
353
|
+
? '<button class="wi-overflow-btn pr-pager-btn" style="font-size:var(--text-sm);padding:1px 8px;margin-right:4px" data-wi-id="' + escapeHtml(item.id) + '" data-wi-source="' + escapeHtml(item._source || '') + '" aria-haspopup="menu" aria-expanded="false" aria-label="More actions for ' + escapeHtml((item.title || item.id || '').slice(0, 80)) + '" title="More actions" onclick="event.stopPropagation();wiOpenOverflowMenu(this)">⋯</button>'
|
|
354
|
+
: '') +
|
|
279
355
|
'<button class="btn-destructive" onclick="event.stopPropagation();deleteWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Delete work item and kill agent">✕</button>' +
|
|
280
356
|
'</td>' +
|
|
281
357
|
'</tr>';
|
|
@@ -427,6 +503,9 @@ function renderWorkItems(items, opts) {
|
|
|
427
503
|
const view = computed.view;
|
|
428
504
|
filteredWorkItems = view;
|
|
429
505
|
const el = document.getElementById('work-items-content');
|
|
506
|
+
// Every return path below replaces el.innerHTML, so drop any overflow menu
|
|
507
|
+
// anchored inside it first (see _wiCloseOverflowMenuIn).
|
|
508
|
+
_wiCloseOverflowMenuIn(el);
|
|
430
509
|
const countEl = document.getElementById('wi-count');
|
|
431
510
|
const summaryEl = document.getElementById('wi-filter-summary');
|
|
432
511
|
if (countEl) countEl.textContent = view.length;
|
|
@@ -437,12 +516,15 @@ function renderWorkItems(items, opts) {
|
|
|
437
516
|
}
|
|
438
517
|
if (!allWorkItems.length) {
|
|
439
518
|
wiPage = 0;
|
|
519
|
+
if (wiSelectedIds.size) wiSelectedIds.clear();
|
|
440
520
|
el.innerHTML = '<p class="empty">No work items. Add tasks via Command Center above.</p>';
|
|
521
|
+
_wiSyncBulkUI();
|
|
441
522
|
return;
|
|
442
523
|
}
|
|
443
524
|
if (!view.length) {
|
|
444
525
|
wiPage = 0;
|
|
445
526
|
el.innerHTML = '<p class="empty">No work items match the selected filters.</p>';
|
|
527
|
+
_wiSyncBulkUI();
|
|
446
528
|
return;
|
|
447
529
|
}
|
|
448
530
|
|
|
@@ -452,8 +534,21 @@ function renderWorkItems(items, opts) {
|
|
|
452
534
|
const start = wiPage * WI_PER_PAGE;
|
|
453
535
|
const pageItems = view.slice(start, start + WI_PER_PAGE);
|
|
454
536
|
|
|
455
|
-
|
|
456
|
-
|
|
537
|
+
// Keep the bulk selection coherent as items appear/disappear across refreshes,
|
|
538
|
+
// filtering, and paging: drop any selected id that no longer exists in the
|
|
539
|
+
// full list. Selection is by id (not row index), so it survives re-sorts and
|
|
540
|
+
// page changes; bulk actions still re-resolve to live items at apply time.
|
|
541
|
+
if (wiSelectedIds.size) {
|
|
542
|
+
const liveIds = new Set(allWorkItems.map(function(w) { return w.id; }));
|
|
543
|
+
wiSelectedIds.forEach(function(id) { if (!liveIds.has(id)) wiSelectedIds.delete(id); });
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const bulkHeadCell = wiBulkMode ? '<th class="wi-bulk-cell"></th>' : '';
|
|
547
|
+
let html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr>' + bulkHeadCell + '<th>Title</th><th>Project</th><th>Type</th><th>Priority</th><th>Status</th><th>Agent</th><th>PR</th><th>Created</th><th>Updated</th><th></th><th></th></tr></thead><tbody>';
|
|
548
|
+
// Wrap so wiRow gets (item, wiBulkMode) — NOT (item, index, array) from a bare
|
|
549
|
+
// .map(wiRow), which would pass a truthy index as `bulk` for every row after
|
|
550
|
+
// the first and render stray checkboxes.
|
|
551
|
+
html += pageItems.map(function(it) { return wiRow(it, wiBulkMode); }).join('');
|
|
457
552
|
html += '</tbody></table></div>';
|
|
458
553
|
|
|
459
554
|
if (view.length > WI_PER_PAGE) {
|
|
@@ -473,6 +568,7 @@ function renderWorkItems(items, opts) {
|
|
|
473
568
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() by wiRow() (fields: work item id/title/source/status/agent/PR links/dates/follow-up metadata)
|
|
474
569
|
el.innerHTML = html;
|
|
475
570
|
restoreDashboardScrollState(el, scrollState);
|
|
571
|
+
_wiSyncBulkUI();
|
|
476
572
|
}
|
|
477
573
|
|
|
478
574
|
async function editWorkItem(id, source) {
|
|
@@ -681,7 +777,7 @@ async function toggleWorkItemArchive() {
|
|
|
681
777
|
'<td style="min-width:240px;max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escapeHtml(i.title || '') + '</td>' +
|
|
682
778
|
'<td><span class="dispatch-type ' + (i.type || '') + '">' + escapeHtml(i.type || '') + '</span></td>' +
|
|
683
779
|
'<td style="color:' + (i.status === 'done' ? 'var(--green)' : 'var(--red)') + '">' + escapeHtml(i.status || '') + '</td>' +
|
|
684
|
-
'<td>' +
|
|
780
|
+
'<td>' + (i.dispatched_to ? MinionsAgentIdentity.nameWithBadgeHtml({ id: i.dispatched_to }) : '—') + '</td>' +
|
|
685
781
|
'<td class="pr-date">' + shortTime(i.archivedAt) + '</td>' +
|
|
686
782
|
'</tr>';
|
|
687
783
|
}).join('') + '</tbody></table></div>';
|
|
@@ -972,7 +1068,7 @@ function _wiRenderDetail(item) {
|
|
|
972
1068
|
_descHtml = escapeHtml(item.title || '—');
|
|
973
1069
|
}
|
|
974
1070
|
html += field('Description', '<div id="wi-detail-desc" style="font-size:var(--text-md);max-height:320px;overflow-y:auto;padding:8px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm)">' + _descHtml + '</div>');
|
|
975
|
-
html += field('Agent',
|
|
1071
|
+
html += field('Agent', (item.dispatched_to || item.agent) ? MinionsAgentIdentity.nameWithBadgeHtml({ id: item.dispatched_to || item.agent }) : escapeHtml('Auto'));
|
|
976
1072
|
// W-ms0ytfdd000d5a26 — the model this work item actually ran on. Resolved
|
|
977
1073
|
// server-side (engine/core/queries.js#resolveWorkItemModelInfo) so persisted
|
|
978
1074
|
// dispatch evidence — what the runtime reported, else what Minions requested
|
|
@@ -998,7 +1094,7 @@ function _wiRenderDetail(item) {
|
|
|
998
1094
|
html += field('Working directory', '<code style="font-size:var(--text-sm);background:var(--surface2);padding:2px 6px;border-radius:var(--radius-sm)">' + escapeHtml(item.meta.workdir) + '</code>');
|
|
999
1095
|
}
|
|
1000
1096
|
if (item.created) html += field('Created', escapeHtml(formatLocalDateTime(item.created)));
|
|
1001
|
-
if (item.dispatched_at) html += field('Dispatched', escapeHtml(formatLocalDateTime(item.dispatched_at)) + ' to ' +
|
|
1097
|
+
if (item.dispatched_at) html += field('Dispatched', escapeHtml(formatLocalDateTime(item.dispatched_at)) + ' to ' + (item.dispatched_to ? MinionsAgentIdentity.nameWithBadgeHtml({ id: item.dispatched_to }) : escapeHtml('?')));
|
|
1002
1098
|
if (item.completedAt) html += field('Completed', escapeHtml(formatLocalDateTime(item.completedAt)));
|
|
1003
1099
|
if (item.failReason) html += field('Failure Reason', '<span style="color:var(--red)">' + escapeHtml(item.failReason) + '</span>');
|
|
1004
1100
|
// W-mrtdet41000h935a — surface the cancellation reason for CANCELLED items.
|
|
@@ -1289,9 +1385,13 @@ function openWorkItemDetail(id, source) {
|
|
|
1289
1385
|
}
|
|
1290
1386
|
|
|
1291
1387
|
function openAllWorkItems() {
|
|
1388
|
+
// Re-opening the modal replaces its rows; defence-in-depth so the menu can
|
|
1389
|
+
// never outlive its anchor even when the outside-click dismissal is bypassed
|
|
1390
|
+
// (e.g. a programmatic open).
|
|
1391
|
+
_wiCloseOverflowMenuIn(document.getElementById('modal-body'));
|
|
1292
1392
|
document.getElementById('modal-title').textContent = 'All Work Items (' + filteredWorkItems.length + ')';
|
|
1293
1393
|
const html = '<div class="pr-table-wrap"><table class="pr-table"><thead><tr><th>Title</th><th>Project</th><th>Type</th><th>Priority</th><th>Status</th><th>Agent</th><th>PR</th><th>Created</th><th>Updated</th><th></th><th></th></tr></thead><tbody>' +
|
|
1294
|
-
filteredWorkItems.map(wiRow).join('') + '</tbody></table></div>';
|
|
1394
|
+
filteredWorkItems.map(function(it) { return wiRow(it, false); }).join('') + '</tbody></table></div>';
|
|
1295
1395
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() by wiRow() (fields: work item id/title/source/status/agent/PR links/dates/follow-up metadata)
|
|
1296
1396
|
document.getElementById('modal-body').innerHTML = html;
|
|
1297
1397
|
document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
@@ -1417,10 +1517,330 @@ function _wiInjectSourceChipFromFrontmatter(content, containerId) {
|
|
|
1417
1517
|
bodyEl.insertAdjacentHTML('afterbegin', sourceBlock);
|
|
1418
1518
|
}
|
|
1419
1519
|
|
|
1520
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
1521
|
+
// Overflow (⋯) menu + bulk-operations mode — W-mscjvm33007r79ef
|
|
1522
|
+
// ════════════════════════════════════════════════════════════════════════
|
|
1523
|
+
|
|
1524
|
+
// Actions offered in a row's overflow menu, derived from the LIVE item so the
|
|
1525
|
+
// menu always matches current status. Each entry invokes the existing per-item
|
|
1526
|
+
// action fn, preserving its confirmation/optimistic/toast/error behavior. Used
|
|
1527
|
+
// both to build the menu and (via .length) to decide whether to render the ⋯
|
|
1528
|
+
// button at all. Also drives the source-inspection guarantee that Delete is NOT
|
|
1529
|
+
// in here (Delete stays a directly-visible button).
|
|
1530
|
+
function _wiOverflowActionsFor(item) {
|
|
1531
|
+
if (!item) return [];
|
|
1532
|
+
var src = item._source || '';
|
|
1533
|
+
var st = item.status;
|
|
1534
|
+
var agentId = item.dispatched_to || item.agent;
|
|
1535
|
+
var out = [];
|
|
1536
|
+
if (st === 'dispatched' && agentId) {
|
|
1537
|
+
out.push({ label: 'View live output', fn: function() { openArtifact('agent', agentId); } });
|
|
1538
|
+
}
|
|
1539
|
+
if (st === 'pending' || st === 'failed') {
|
|
1540
|
+
out.push({ label: 'Edit', fn: function() { editWorkItem(item.id, src); } });
|
|
1541
|
+
}
|
|
1542
|
+
if (st === 'done' || st === 'failed') {
|
|
1543
|
+
out.push({ label: 'Archive', fn: function() { archiveWorkItem(item.id, src); } });
|
|
1544
|
+
}
|
|
1545
|
+
if ((st === 'done' || st === 'failed') && !item._humanFeedback) {
|
|
1546
|
+
out.push({ label: 'Give feedback', fn: function() { feedbackWorkItem(item.id, src); } });
|
|
1547
|
+
}
|
|
1548
|
+
if (st === 'pending' || st === 'dispatched' || st === 'queued' || st === 'failed') {
|
|
1549
|
+
out.push({ label: 'Cancel', fn: function() { cancelWorkItem(item.id, src); } });
|
|
1550
|
+
}
|
|
1551
|
+
return out;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
let _wiOverflowEl = null;
|
|
1555
|
+
let _wiOverflowAnchor = null;
|
|
1556
|
+
|
|
1557
|
+
// Accessible popover built with pure DOM construction (createElement /
|
|
1558
|
+
// textContent — never innerHTML), mirroring _openCheckoutModeMenu in
|
|
1559
|
+
// render-other.js so it sidesteps the XSS lint gate. role=menu + menuitem,
|
|
1560
|
+
// arrow-key navigation, Escape/outside-click dismissal, focus management.
|
|
1561
|
+
function wiOpenOverflowMenu(btn) {
|
|
1562
|
+
if (_wiOverflowAnchor === btn) { wiCloseOverflowMenu(true); return; }
|
|
1563
|
+
wiCloseOverflowMenu(false);
|
|
1564
|
+
var id = btn.getAttribute('data-wi-id');
|
|
1565
|
+
var src = btn.getAttribute('data-wi-source') || '';
|
|
1566
|
+
var item = allWorkItems.find(function(w) { return w.id === id && (!src || (w._source || '') === src); });
|
|
1567
|
+
var actions = _wiOverflowActionsFor(item);
|
|
1568
|
+
if (!actions.length) return;
|
|
1569
|
+
var menu = document.createElement('div');
|
|
1570
|
+
menu.className = 'wi-overflow-menu';
|
|
1571
|
+
menu.setAttribute('role', 'menu');
|
|
1572
|
+
menu.setAttribute('aria-label', 'Work item actions');
|
|
1573
|
+
actions.forEach(function(a) {
|
|
1574
|
+
var mi = document.createElement('button');
|
|
1575
|
+
mi.type = 'button';
|
|
1576
|
+
mi.className = 'wi-overflow-item';
|
|
1577
|
+
mi.setAttribute('role', 'menuitem');
|
|
1578
|
+
mi.tabIndex = -1;
|
|
1579
|
+
mi.textContent = a.label;
|
|
1580
|
+
mi.addEventListener('click', function(ev) {
|
|
1581
|
+
ev.stopPropagation();
|
|
1582
|
+
wiCloseOverflowMenu(true);
|
|
1583
|
+
a.fn();
|
|
1584
|
+
});
|
|
1585
|
+
menu.appendChild(mi);
|
|
1586
|
+
});
|
|
1587
|
+
document.body.appendChild(menu);
|
|
1588
|
+
// Position after append so offsetWidth is known; right-align under the button.
|
|
1589
|
+
var r = btn.getBoundingClientRect();
|
|
1590
|
+
var top = r.bottom + window.scrollY + 2;
|
|
1591
|
+
var left = r.right + window.scrollX - menu.offsetWidth;
|
|
1592
|
+
if (left < 4) left = 4;
|
|
1593
|
+
menu.style.top = top + 'px';
|
|
1594
|
+
menu.style.left = left + 'px';
|
|
1595
|
+
btn.setAttribute('aria-expanded', 'true');
|
|
1596
|
+
_wiOverflowEl = menu;
|
|
1597
|
+
_wiOverflowAnchor = btn;
|
|
1598
|
+
// Defer listener attach so the opening click doesn't immediately close it.
|
|
1599
|
+
setTimeout(function() {
|
|
1600
|
+
document.addEventListener('mousedown', _wiOverflowOutside, true);
|
|
1601
|
+
document.addEventListener('keydown', _wiOverflowKeydown, true);
|
|
1602
|
+
}, 0);
|
|
1603
|
+
var first = menu.querySelector('.wi-overflow-item');
|
|
1604
|
+
if (first) first.focus();
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
function wiCloseOverflowMenu(returnFocus) {
|
|
1608
|
+
var anchor = _wiOverflowAnchor;
|
|
1609
|
+
if (_wiOverflowEl && _wiOverflowEl.parentNode) _wiOverflowEl.parentNode.removeChild(_wiOverflowEl);
|
|
1610
|
+
if (anchor) anchor.setAttribute('aria-expanded', 'false');
|
|
1611
|
+
_wiOverflowEl = null;
|
|
1612
|
+
_wiOverflowAnchor = null;
|
|
1613
|
+
document.removeEventListener('mousedown', _wiOverflowOutside, true);
|
|
1614
|
+
document.removeEventListener('keydown', _wiOverflowKeydown, true);
|
|
1615
|
+
if (returnFocus && anchor) { try { anchor.focus(); } catch (e) { /* anchor gone after re-render */ } }
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function _wiOverflowOutside(e) {
|
|
1619
|
+
if (_wiOverflowEl && !_wiOverflowEl.contains(e.target) && e.target !== _wiOverflowAnchor) {
|
|
1620
|
+
wiCloseOverflowMenu(false);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// The menu is appended to document.body but is ANCHORED to a ⋯ button living
|
|
1625
|
+
// inside a container that gets re-rendered wholesale — the work-items table on
|
|
1626
|
+
// every non-304 /api/work-items poll, the all-items modal on every open.
|
|
1627
|
+
// Replacing that container's innerHTML destroys the anchor and strands the
|
|
1628
|
+
// menu, which keeps the action closures built from the item as it looked when
|
|
1629
|
+
// the menu opened: a row that has since flipped dispatched → done would still
|
|
1630
|
+
// offer Cancel, breaking the "menu always reflects current availability"
|
|
1631
|
+
// invariant. Close it BEFORE the swap, scoped to the container actually being
|
|
1632
|
+
// replaced so a re-render of one surface cannot dismiss a menu opened from the
|
|
1633
|
+
// other. Pass returnFocus=false — a background poll must not yank focus to an
|
|
1634
|
+
// anchor that is about to vanish.
|
|
1635
|
+
function _wiCloseOverflowMenuIn(container) {
|
|
1636
|
+
if (container && _wiOverflowAnchor && container.contains(_wiOverflowAnchor)) {
|
|
1637
|
+
wiCloseOverflowMenu(false);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// The modal is torn down by paths this module never sees — browser Back /
|
|
1642
|
+
// Alt+Left (popstate), sidebar navigation, a frame pop — and none of them fire
|
|
1643
|
+
// the mousedown/keydown dismissal above. Opt the menu into the shared modal
|
|
1644
|
+
// teardown so a menu opened from a row inside the "See all" modal can't outlive
|
|
1645
|
+
// it. Registering the SCOPED closer keeps a menu anchored outside the modal
|
|
1646
|
+
// unaffected. typeof-guarded: this module is also require()d by unit tests.
|
|
1647
|
+
if (typeof registerTransientPopupCloser === 'function') registerTransientPopupCloser(_wiCloseOverflowMenuIn);
|
|
1648
|
+
|
|
1649
|
+
function _wiOverflowKeydown(e) {
|
|
1650
|
+
if (!_wiOverflowEl) return;
|
|
1651
|
+
var items = Array.prototype.slice.call(_wiOverflowEl.querySelectorAll('.wi-overflow-item'));
|
|
1652
|
+
if (!items.length) return;
|
|
1653
|
+
var idx = items.indexOf(document.activeElement);
|
|
1654
|
+
if (e.key === 'Escape') {
|
|
1655
|
+
e.preventDefault();
|
|
1656
|
+
wiCloseOverflowMenu(true);
|
|
1657
|
+
} else if (e.key === 'ArrowDown') {
|
|
1658
|
+
e.preventDefault();
|
|
1659
|
+
items[(idx + 1) % items.length].focus();
|
|
1660
|
+
} else if (e.key === 'ArrowUp') {
|
|
1661
|
+
e.preventDefault();
|
|
1662
|
+
items[(idx - 1 + items.length) % items.length].focus();
|
|
1663
|
+
} else if (e.key === 'Home') {
|
|
1664
|
+
e.preventDefault();
|
|
1665
|
+
items[0].focus();
|
|
1666
|
+
} else if (e.key === 'End') {
|
|
1667
|
+
e.preventDefault();
|
|
1668
|
+
items[items.length - 1].focus();
|
|
1669
|
+
} else if (e.key === 'Tab') {
|
|
1670
|
+
wiCloseOverflowMenu(false);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// ── Row click + selection ─────────────────────────────────────────────────
|
|
1675
|
+
// A row behaves the way it was RENDERED: `bulk` is the per-render flag wiRow()
|
|
1676
|
+
// baked into the handler, NOT the module-global wiBulkMode. They diverge —
|
|
1677
|
+
// openAllWorkItems() renders wiRow(it, false) while bulk mode can be on — and
|
|
1678
|
+
// reading the global there let a click on a checkbox-less modal row silently
|
|
1679
|
+
// toggle wiSelectedIds: invisible state feeding the destructive bulk Delete,
|
|
1680
|
+
// with the detail view never opening. Fail closed: only an explicit bulk row
|
|
1681
|
+
// toggles selection; anything else opens the detail (preserving the
|
|
1682
|
+
// pre-existing text-selection guard).
|
|
1683
|
+
function wiRowClick(event, id, source, bulk) {
|
|
1684
|
+
if (shouldIgnoreSelectionClick(event)) return;
|
|
1685
|
+
if (bulk === true) { _wiToggleId(id); return; }
|
|
1686
|
+
openWorkItemDetail(id, source);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function _wiToggleId(id) {
|
|
1690
|
+
if (wiSelectedIds.has(id)) wiSelectedIds.delete(id);
|
|
1691
|
+
else wiSelectedIds.add(id);
|
|
1692
|
+
_wiReflectRowSelection(id);
|
|
1693
|
+
_wiSyncBulkUI();
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function wiToggleSelect(cb) {
|
|
1697
|
+
var id = cb.getAttribute('data-wi-id');
|
|
1698
|
+
if (!id) return;
|
|
1699
|
+
if (cb.checked) wiSelectedIds.add(id);
|
|
1700
|
+
else wiSelectedIds.delete(id);
|
|
1701
|
+
_wiReflectRowSelection(id);
|
|
1702
|
+
_wiSyncBulkUI();
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
function _wiReflectRowSelection(id) {
|
|
1706
|
+
var sel = wiSelectedIds.has(id);
|
|
1707
|
+
var content = document.getElementById('work-items-content');
|
|
1708
|
+
if (!content) return;
|
|
1709
|
+
var esc = (typeof CSS !== 'undefined' && CSS.escape) ? CSS.escape(id) : String(id).replace(/"/g, '\\"');
|
|
1710
|
+
content.querySelectorAll('tr[data-wi-id="' + esc + '"]').forEach(function(row) {
|
|
1711
|
+
row.classList.toggle('wi-row-selected', sel);
|
|
1712
|
+
var cb = row.querySelector('.wi-bulk-cb');
|
|
1713
|
+
if (cb) cb.checked = sel;
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// ── Bulk mode enter/exit ──────────────────────────────────────────────────
|
|
1718
|
+
function wiToggleBulkMode() {
|
|
1719
|
+
wiBulkMode = !wiBulkMode;
|
|
1720
|
+
if (!wiBulkMode) wiSelectedIds.clear();
|
|
1721
|
+
wiCloseOverflowMenu(false);
|
|
1722
|
+
_wiSyncBulkToggleBtn();
|
|
1723
|
+
renderWorkItems(allWorkItems, { preserveScroll: true });
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function wiExitBulkMode() {
|
|
1727
|
+
if (!wiBulkMode) return;
|
|
1728
|
+
wiBulkMode = false;
|
|
1729
|
+
wiSelectedIds.clear();
|
|
1730
|
+
wiCloseOverflowMenu(false);
|
|
1731
|
+
_wiSyncBulkToggleBtn();
|
|
1732
|
+
renderWorkItems(allWorkItems, { preserveScroll: true });
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
function wiClearSelection() {
|
|
1736
|
+
if (!wiSelectedIds.size) { _wiSyncBulkUI(); return; }
|
|
1737
|
+
var ids = Array.from(wiSelectedIds);
|
|
1738
|
+
wiSelectedIds.clear();
|
|
1739
|
+
ids.forEach(_wiReflectRowSelection);
|
|
1740
|
+
_wiSyncBulkUI();
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// Select-all targets the CURRENT filtered set (every page of the active
|
|
1744
|
+
// filter), keyed by id so it stays coherent across paging and re-sorts.
|
|
1745
|
+
function wiToggleSelectAll(cb) {
|
|
1746
|
+
if (cb.checked) filteredWorkItems.forEach(function(w) { wiSelectedIds.add(w.id); });
|
|
1747
|
+
else wiSelectedIds.clear();
|
|
1748
|
+
filteredWorkItems.forEach(function(w) { _wiReflectRowSelection(w.id); });
|
|
1749
|
+
_wiSyncBulkUI();
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
function _wiSyncBulkToggleBtn() {
|
|
1753
|
+
var btn = document.getElementById('wi-bulk-toggle');
|
|
1754
|
+
if (!btn) return;
|
|
1755
|
+
btn.setAttribute('aria-pressed', wiBulkMode ? 'true' : 'false');
|
|
1756
|
+
btn.classList.toggle('active', wiBulkMode);
|
|
1757
|
+
btn.textContent = wiBulkMode ? 'Done' : 'Bulk select';
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// Re-sync the bulk bar to current state after every render: bar visibility,
|
|
1761
|
+
// selected count, select-all checkbox/indeterminate against the filtered set,
|
|
1762
|
+
// and per-action button enablement + eligible counts.
|
|
1763
|
+
function _wiSyncBulkUI() {
|
|
1764
|
+
_wiSyncBulkToggleBtn();
|
|
1765
|
+
var bar = document.getElementById('wi-bulk-bar');
|
|
1766
|
+
if (bar) bar.hidden = !wiBulkMode;
|
|
1767
|
+
if (!wiBulkMode) return;
|
|
1768
|
+
var count = wiSelectedIds.size;
|
|
1769
|
+
var countEl = document.getElementById('wi-bulk-count');
|
|
1770
|
+
if (countEl) countEl.textContent = count + ' selected';
|
|
1771
|
+
var selAll = document.getElementById('wi-bulk-selectall-cb');
|
|
1772
|
+
if (selAll) {
|
|
1773
|
+
var total = filteredWorkItems.length;
|
|
1774
|
+
var selectedInView = filteredWorkItems.reduce(function(n, w) { return n + (wiSelectedIds.has(w.id) ? 1 : 0); }, 0);
|
|
1775
|
+
selAll.checked = total > 0 && selectedInView === total;
|
|
1776
|
+
selAll.indeterminate = selectedInView > 0 && selectedInView < total;
|
|
1777
|
+
}
|
|
1778
|
+
Object.keys(WI_BULK_ACTIONS).forEach(function(action) {
|
|
1779
|
+
var b = document.querySelector('#wi-bulk-bar [data-bulk-action="' + action + '"]');
|
|
1780
|
+
if (!b) return;
|
|
1781
|
+
var plan = wiBulkPlan(allWorkItems, wiSelectedIds, action);
|
|
1782
|
+
var n = plan.eligible.length;
|
|
1783
|
+
b.disabled = count === 0 || n === 0;
|
|
1784
|
+
var base = WI_BULK_ACTIONS[action].label;
|
|
1785
|
+
b.textContent = n > 0 ? base + ' (' + n + ')' : base;
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// Apply a bulk action. Resolves the selection to LIVE items, applies only to
|
|
1790
|
+
// eligible ones, confirms destructive actions, reports partial failures
|
|
1791
|
+
// accurately (never claims success if any failed), and never acts on
|
|
1792
|
+
// stale/missing ids.
|
|
1793
|
+
async function wiBulkAction(action) {
|
|
1794
|
+
var spec = WI_BULK_ACTIONS[action];
|
|
1795
|
+
if (!spec) return;
|
|
1796
|
+
var plan = wiBulkPlan(allWorkItems, wiSelectedIds, action);
|
|
1797
|
+
var targets = plan.eligible;
|
|
1798
|
+
if (!targets.length) {
|
|
1799
|
+
showToast('cmd-toast', 'No selected items are eligible for ' + spec.label + '.', false);
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
if (spec.destructive) {
|
|
1803
|
+
if (!confirm(spec.label + ' ' + targets.length + ' work item' + (targets.length === 1 ? '' : 's') + '? This will kill any running agents and cannot be undone.')) return;
|
|
1804
|
+
}
|
|
1805
|
+
var results = await Promise.allSettled(targets.map(function(it) {
|
|
1806
|
+
return fetch(spec.endpoint, {
|
|
1807
|
+
method: 'POST',
|
|
1808
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1809
|
+
body: JSON.stringify({ id: it.id, source: it._source || undefined })
|
|
1810
|
+
}).then(function(r) {
|
|
1811
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
1812
|
+
return r.json().catch(function() { return {}; });
|
|
1813
|
+
});
|
|
1814
|
+
}));
|
|
1815
|
+
var succeeded = [];
|
|
1816
|
+
var failed = [];
|
|
1817
|
+
results.forEach(function(res, i) {
|
|
1818
|
+
if (res.status === 'fulfilled') succeeded.push(targets[i]);
|
|
1819
|
+
else failed.push(targets[i]);
|
|
1820
|
+
});
|
|
1821
|
+
if (spec.optimisticRemove) {
|
|
1822
|
+
succeeded.forEach(function(it) { markDeleted('wi:' + it.id); _removeWiRow(it.id); });
|
|
1823
|
+
}
|
|
1824
|
+
succeeded.forEach(function(it) { wiSelectedIds.delete(it.id); });
|
|
1825
|
+
if (spec.wake && succeeded.length) wakeEngine();
|
|
1826
|
+
var skipped = plan.ineligible.length + plan.missing.length;
|
|
1827
|
+
if (failed.length) {
|
|
1828
|
+
// Roll back optimistic hides for the failed ids and refresh to the truth.
|
|
1829
|
+
failed.forEach(function(it) { clearDeleted('wi:' + it.id); });
|
|
1830
|
+
showToast('cmd-toast', spec.label + ': ' + succeeded.length + ' succeeded, ' + failed.length + ' failed' + (skipped ? ', ' + skipped + ' skipped' : '') + '.', false);
|
|
1831
|
+
refresh();
|
|
1832
|
+
} else {
|
|
1833
|
+
showToast('cmd-toast', spec.label + ': ' + succeeded.length + ' item' + (succeeded.length === 1 ? '' : 's') + (skipped ? ' (' + skipped + ' skipped)' : '') + '.', true);
|
|
1834
|
+
if (!spec.optimisticRemove) refresh();
|
|
1835
|
+
}
|
|
1836
|
+
_wiSyncBulkUI();
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
|
|
1420
1840
|
if (typeof window !== 'undefined') {
|
|
1421
|
-
window.MinionsWork = { wiRow, renderWorkItems, editWorkItem, submitWorkItemEdit, deleteWorkItem, archiveWorkItem, toggleWorkItemArchive, retryWorkItem, wiPrev, wiNext, feedbackWorkItem, openCreateWorkItemModal, openWorkItemDetail, openAllWorkItems, viewAgentOutput, openInboxNote, needsAttentionInfo, applyWiFilters, resetWiFilters, toggleWiSortDir };
|
|
1841
|
+
window.MinionsWork = { wiRow, renderWorkItems, editWorkItem, submitWorkItemEdit, deleteWorkItem, archiveWorkItem, toggleWorkItemArchive, retryWorkItem, wiPrev, wiNext, feedbackWorkItem, openCreateWorkItemModal, openWorkItemDetail, openAllWorkItems, viewAgentOutput, openInboxNote, needsAttentionInfo, applyWiFilters, resetWiFilters, toggleWiSortDir, wiOpenOverflowMenu, wiCloseOverflowMenu, wiRowClick, wiToggleSelect, wiToggleBulkMode, wiExitBulkMode, wiClearSelection, wiToggleSelectAll, wiBulkAction };
|
|
1422
1842
|
}
|
|
1423
1843
|
// exported for testing — pure helpers safe to require under Node (no DOM deps).
|
|
1424
1844
|
if (typeof module !== 'undefined' && module.exports) {
|
|
1425
|
-
module.exports = { needsAttentionInfo, formatPendingReason };
|
|
1845
|
+
module.exports = { needsAttentionInfo, formatPendingReason, wiActionEligible, wiBulkPlan, WI_BULK_ACTIONS };
|
|
1426
1846
|
}
|
package/dashboard/js/settings.js
CHANGED
|
@@ -780,6 +780,10 @@ async function openSettings() {
|
|
|
780
780
|
settingsField('Operator login (used in branch names)', 'set-operatorLogin', e.operatorLogin || '', '', 'Override the human operator login used in user/<loginname>/<wi-id>-<slug> branches. Empty = auto-resolve via gh / git email / OS username (currently resolves to: ' + (e._resolvedOperatorLogin || 'unknown') + ')') +
|
|
781
781
|
settingsField('Ignored Comment Authors', 'set-ignoredCommentAuthors', (e.ignoredCommentAuthors || []).join(', '), '', 'Comma-separated usernames — comments auto-closed, never trigger fixes') +
|
|
782
782
|
'</div>' +
|
|
783
|
+
'<div class="settings-grid-2">' +
|
|
784
|
+
settingsField('ADO identity id', 'set-operatorAdoIdentityId', (e.operatorAdoIdentity && e.operatorAdoIdentity.id) || '', '', 'Your Azure DevOps identity GUID. Used ONLY to determine whether an auto-fix-enabled ADO PR is your own vs another author\'s (foreign-author warning). Empty = ADO ownership shows as "unknown". Never an email.') +
|
|
785
|
+
settingsField('ADO identity descriptor', 'set-operatorAdoIdentityDescriptor', (e.operatorAdoIdentity && e.operatorAdoIdentity.descriptor) || '', '', 'Your Azure DevOps identity descriptor (e.g. aad.<base64>). Alternative stable identity for the foreign-author ownership check. Never an email.') +
|
|
786
|
+
'</div>' +
|
|
783
787
|
'<h4>Experimental Behavior</h4>' +
|
|
784
788
|
'<div class="settings-stack">' +
|
|
785
789
|
settingsToggle('Allow Temp Agents', 'set-allowTempAgents', !!e.allowTempAgents, 'Spawn ephemeral agents when all permanent agents are busy') +
|
|
@@ -1724,6 +1728,10 @@ async function saveSettings() {
|
|
|
1724
1728
|
spawnPhaseGraceMs: secToMs('set-spawnPhaseGraceMs'),
|
|
1725
1729
|
spawnPhaseMaxCpuSeconds: document.getElementById('set-spawnPhaseMaxCpuSeconds').value,
|
|
1726
1730
|
operatorLogin: (document.getElementById('set-operatorLogin')?.value ?? '').trim(),
|
|
1731
|
+
operatorAdoIdentity: {
|
|
1732
|
+
id: (document.getElementById('set-operatorAdoIdentityId')?.value ?? '').trim(),
|
|
1733
|
+
descriptor: (document.getElementById('set-operatorAdoIdentityDescriptor')?.value ?? '').trim(),
|
|
1734
|
+
},
|
|
1727
1735
|
autoApprovePlans: document.getElementById('set-autoApprovePlans').checked,
|
|
1728
1736
|
evalLoop: document.getElementById('set-evalLoop').checked,
|
|
1729
1737
|
autoDecompose: document.getElementById('set-autoDecompose').checked,
|