@staix/agent-hub 0.28.0 → 0.30.0
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/CHANGELOG.md +22 -0
- package/README.md +5 -3
- package/docs/agent-notes/README.md +12 -0
- package/docs/agent-notes/adapters.md +2 -1
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +4 -1
- package/docs/agent-notes/local-worker.md +3 -1
- package/docs/agent-notes/tasks.md +7 -3
- package/docs/agent-notes/tests.md +3 -0
- package/docs/design/adapters.md +56 -0
- package/docs/design/agent-hub-architecture.md +12 -0
- package/docs/design/bus.md +8 -0
- package/docs/design/task-board.md +19 -1
- package/docs/dogfood.md +13 -7
- package/docs/operations.md +177 -20
- package/docs/quickstart.md +18 -3
- package/docs/sdlc.md +2 -2
- package/docs/security.md +45 -4
- package/docs/smoke.md +13 -2
- package/docs/specs/2026-10-12-office-view-design.md +86 -3
- package/docs/verification/2026-11-03-0.29.0.md +59 -0
- package/docs/verification/2026-11-03-0.30.0.md +54 -0
- package/docs/verification/2026-11-03-agy-1.4.12-launcher-nonce.md +42 -0
- package/docs/verification/2026-11-03-agy-1.4.12.md +90 -0
- package/docs/verification/2026-11-03-agy-tui-close.md +49 -0
- package/docs/verification/2026-11-03-control-write-stall-windows.json +241 -0
- package/docs/verification/2026-11-03-control-write-stall-windows.md +20 -0
- package/docs/verification/2026-11-03-grok-1.0.62-t0.md +150 -0
- package/docs/verification/2026-11-03-live-board-latency.json +556 -0
- package/docs/verification/2026-11-03-live-board-latency.md +64 -0
- package/docs/verification/2026-11-03-pi-150-compat.md +40 -0
- package/docs/verification/2026-11-03-pi-title-api.md +26 -0
- package/docs/verification/2026-11-03-qwen-0265-acp.md +55 -0
- package/docs/verification/2026-11-03-qwen-0265-dual-output.md +40 -0
- package/docs/verification/dogfood/2026-11-03.md +3095 -0
- package/docs/verification/pi-150-model-free.ts +57 -0
- package/docs/verification/pi-150-native.mjs +21 -0
- package/docs/verification/pi-150-title-api.mjs +31 -0
- package/docs/verified.json +14 -14
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +715 -45
- package/src/adapters/compat.ts +10 -10
- package/src/adapters/definitions/builtin-metadata.ts +2 -2
- package/src/adapters/local-worker.ts +22 -6
- package/src/adapters/pi.ts +92 -10
- package/src/adapters/registry-types.ts +4 -0
- package/src/adapters/registry.ts +2 -0
- package/src/cli/autonomy.ts +66 -1
- package/src/cli/console-state.ts +6 -2
- package/src/cli/help.ts +3 -1
- package/src/cli/identity.ts +8 -1
- package/src/cli/main.ts +39 -0
- package/src/cli/output.ts +5 -0
- package/src/cli/recovery-package.ts +9 -0
- package/src/cli/worktree-tidy.ts +158 -0
- package/src/cli/worktrees-output.ts +43 -0
- package/src/hub/board.ts +31 -5
- package/src/hub/bus.ts +4 -4
- package/src/hub/conductor.ts +4 -3
- package/src/hub/config-apply.ts +2 -1
- package/src/hub/control-client.ts +16 -1
- package/src/hub/control-write-trace.ts +409 -0
- package/src/hub/daemon.ts +408 -66
- package/src/hub/dogfood-read.ts +62 -9
- package/src/hub/hub-tools.ts +12 -6
- package/src/hub/manager.ts +9 -1
- package/src/hub/peer-doing.ts +33 -3
- package/src/hub/peers.ts +7 -0
- package/src/hub/routing.ts +9 -2
- package/src/hub/runtime-capture-prune.ts +4 -1
- package/src/hub/sdlc-release-authorization.ts +13 -4
- package/src/hub/sdlc-release.ts +23 -10
- package/src/hub/sdlc.ts +15 -2
- package/src/hub/settings.ts +3 -0
- package/src/hub/task-worktree.ts +331 -0
- package/src/hub/tasks.ts +192 -25
- package/src/hub/ui.ts +2 -0
- package/src/hub/worktree-prs.ts +100 -0
- package/src/hub/worktree-shelf.ts +246 -0
- package/src/local/deny.ts +49 -0
- package/src/local/tools.ts +4 -2
- package/src/memory/brief.ts +5 -0
- package/src/memory/capture.ts +1 -1
- package/src/memory/client.ts +5 -2
- package/src/pi/extension.ts +43 -3
- package/src/ui/index.html +133 -10
- package/src/ui/office-model.ts +64 -8
- package/src/ui/office-space.ts +1 -0
- package/src/ui/plugin.ts +14 -3
package/src/ui/index.html
CHANGED
|
@@ -257,6 +257,7 @@ details.settings-group .row:last-child{border-bottom:0}
|
|
|
257
257
|
|
|
258
258
|
.office-scene{display:block;max-width:100%;height:auto;margin-inline:auto;font-family:system-ui,sans-serif;font-size:1px}.office-scene :is(rect,path,line,polygon,circle){vector-effect:non-scaling-stroke;stroke-width:1px}.office-room{fill:var(--panel-soft);stroke:var(--line)}.office-room-title{fill:var(--ink);font-size:1.083333px;font-weight:600}.office-room-count{fill:var(--ink);font-size:1px;font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums}.office-header-surface{fill:var(--panel);stroke:var(--line)}.office-count-pill{fill:var(--panel);stroke:var(--line)}.office-desk-surface{fill:var(--panel);stroke:var(--line)}.office-desk.office-dimmed .office-avatar{opacity:.55}.office-name{fill:var(--ink);font-size:1.083333px;font-weight:600}.office-state,.office-meta,.office-empty{fill:var(--muted);font-size:1px}.office-glyph,.office-hand{font-size:1px}.office-idle .office-glyph{fill:var(--green)}.office-busy .office-glyph{fill:var(--amber)}.office-offline .office-glyph,.office-unknown .office-glyph{fill:var(--muted)}.office-paused .office-glyph{fill:var(--amber)}.office-waiting .office-glyph{fill:var(--red)}.office-hand{fill:var(--red)}.office-bubble{fill:var(--panel);stroke:var(--blue)}.office-bubble-text{fill:var(--ink);font-size:1px}.office-mode{fill:var(--muted);font-size:1px;font-family:ui-monospace,monospace}.office-card{fill:var(--control);stroke:var(--control-border)}.office-card-text{fill:var(--ink);font-size:1px}.office-card-id{font-family:ui-monospace,monospace}.office-card-accent{fill:var(--line)}.office-waiting-card .office-card-accent{fill:var(--waiting)}.office-rework-card .office-card-accent{fill:var(--red)}.office-card-checking .office-card-accent{fill:var(--blue)}.office-more{fill:var(--blue);font-size:1px}.office-scene .office-spotlight .office-desk-surface,.office-scene .office-spotlight .office-card,.office-scene [role="button"]:focus-visible > .office-card{stroke:var(--focus);stroke-width:2px}.office-card-hit{fill:transparent;stroke:none;pointer-events:all}.office-card-focus:focus-visible{outline:none}.row.spotlight{background:var(--hover)}
|
|
259
259
|
|
|
260
|
+
.office-plan-shape{fill:none;stroke:var(--muted)}.office-reserved{fill:var(--peer-color);stroke:none}.office-dock-progress{fill:var(--blue);stroke:none}.office-release-dock:focus-visible .office-room{stroke:var(--focus);stroke-width:2px}
|
|
260
261
|
.office-movable{transition:transform .8s ease}.office-arc .office-arc-path{fill:none;stroke:var(--blue);stroke-width:1px}.office-arc .office-arc-head{fill:var(--blue)}.office-arc-important .office-arc-path{stroke:var(--amber)}.office-arc-important .office-arc-head{fill:var(--amber)}.office-arc-dropped .office-arc-path{stroke:var(--red);stroke-dasharray:.6 .4}.office-arc-dropped .office-arc-head{fill:var(--red)}.office-arc-label{fill:var(--muted);font-size:1px}.office-alert{fill:var(--red);font-size:1px}.office-latch{fill:var(--amber);font-size:1px}.office-link-line{fill:none;stroke-width:1px;stroke-dasharray:.8 .5}.office-link-overlap .office-link-line{stroke:var(--amber)}.office-link-conflict .office-link-line{stroke:var(--red)}.office-link-cohort .office-link-line{stroke:var(--blue)}.office-link-label{font-size:1px;fill:var(--muted)}.office-link-conflict .office-link-label{fill:var(--red)}.office-link-overlap .office-link-label{fill:var(--amber)}.office-link-cohort .office-link-label{fill:var(--blue)}.office-checking{fill:var(--blue);font-size:.916667px}
|
|
261
262
|
.office-avatar{fill:var(--muted)}.office-idle .office-avatar{fill:var(--dot-ok)}.office-busy .office-avatar{fill:var(--dot-warn)}.office-offline .office-avatar{fill:var(--dot)}.office-paused .office-avatar{fill:var(--dot-warn)}.office-waiting .office-avatar{fill:var(--amber)}.office-waiting .office-desk-surface{stroke:var(--amber)}.office-glyph-shape{fill:none;stroke-width:1px}.office-idle .office-glyph-shape{stroke:var(--dot-ok)}.office-busy .office-glyph-shape{stroke:var(--dot-warn);fill:var(--dot-warn)}.office-paused .office-glyph-shape{stroke:var(--dot-warn);fill:var(--dot-warn)}.office-offline .office-glyph-shape{stroke:var(--dot)}.office-handshape{fill:none;stroke:var(--dot-err);stroke-width:1px}.office-desk-leg{fill:var(--panel-soft);stroke:var(--line);stroke-width:1px}.office-nameplate{fill:var(--control);stroke:var(--control-border);stroke-width:1px}.office-room-wire{fill:none;stroke:var(--line);stroke-width:1px}.office-divider{fill:none;stroke:var(--line);stroke-width:1px}.office-shelf{fill:var(--panel-soft);stroke:var(--line);stroke-width:1px}.office-shelf-card{fill:var(--green-soft);stroke:var(--green);stroke-width:1px}.office-box-top{fill:var(--control);stroke:var(--control-border);stroke-width:1px}.office-box-left{fill:var(--panel-soft);stroke:var(--line);stroke-width:1px}.office-box-right{fill:var(--panel);stroke:var(--line);stroke-width:1px}.office-busy .office-glyph,.office-hand{animation:office-pulse 1.2s ease-in-out infinite}.office-appear{animation:office-appear .5s ease-out}.office-leaving{pointer-events:none;animation:office-fade .4s linear forwards}.office-fadeout{animation-name:office-fade;animation-duration:1s;animation-timing-function:linear;animation-fill-mode:forwards}.office-settle .office-movable{transition:none}.office-settle .office-appear{animation:none}
|
|
262
263
|
@keyframes office-pulse{50%{opacity:.45}}@keyframes office-appear{from{opacity:0}}@keyframes office-fade{to{opacity:0}}
|
|
@@ -498,12 +499,14 @@ main[data-stale]>:not(#notice):not(#disconnected){opacity:.6;transition:opacity
|
|
|
498
499
|
</div>
|
|
499
500
|
</div>
|
|
500
501
|
</section>
|
|
502
|
+
<section id="worktrees-section" aria-labelledby="worktrees-title" data-view="agents" hidden><div class="section-head"><h2 id="worktrees-title">Worktrees</h2><span id="worktrees-count" class="meta"></span></div><div id="worktrees"></div></section>
|
|
501
503
|
<section aria-labelledby="bench-title" data-view="agents" hidden><div class="section-head"><h2 id="bench-title">Benchmarks</h2><span id="bench-count" class="meta"></span></div><div id="bench"></div></section>
|
|
502
504
|
</div>
|
|
503
505
|
</main>
|
|
504
506
|
</div>
|
|
505
507
|
</div>
|
|
506
508
|
<dialog id="plan-dialog" aria-labelledby="plan-title"><div class="dialog-head"><h2 id="plan-title">Plan</h2><button type="button" id="plan-close" aria-label="Close plan sheet"><svg class="icon" aria-hidden="true" focusable="false"><use href="#i-close"/></svg></button></div>
|
|
509
|
+
<section class="plan-block" aria-labelledby="plan-h-intake"><h3 id="plan-h-intake">Capture a request</h3><form id="intake"><div class="field"><label for="intake-text">What should the team do?</label><textarea id="intake-text" required maxlength="7000" aria-describedby="intake-help intake-result"></textarea></div><div class="field"><label for="intake-sources">Source IDs (optional)</label><input id="intake-sources" maxlength="400" aria-describedby="intake-help intake-result"></div><p id="intake-help" class="meta">Creates draft Backlog cards, held until approval. Use registered src: IDs separated by spaces or commas. Approve or discard with ahub intake in a terminal.</p><button id="intake-submit" class="primary" data-mutation="project" type="submit">Create draft</button><p id="intake-result" class="meta" role="status" aria-live="polite" tabindex="-1"></p></form><div class="field"><h4>Registered sources (read-only)</h4><div id="sources" class="content"><p class="empty">Waiting for a project snapshot.</p></div></div></section>
|
|
507
510
|
<section class="plan-block" aria-labelledby="plan-h-plan"><h3 id="plan-h-plan">Plan a task</h3><form id="planning"><div class="field"><label for="planning-task">Task</label><select id="planning-task"><option value="">Select task</option></select></div><div class="split"><label>Priority<select id="planning-priority"><option value="">Default</option><option value="0">P0</option><option value="1">P1</option><option value="2">P2</option><option value="3">P3</option></select></label><label>Order<input id="planning-rank" type="number" min="0" step="1"></label><label>Release<select id="planning-release"><option value="">No release</option></select></label><label>Planned start<input id="planning-start" type="datetime-local" step="any"></label><label>Due<input id="planning-due" type="datetime-local" step="any"></label></div><label><input id="planning-pinned" type="checkbox">Pin against planner changes</label><label>Reason<input id="planning-reason" maxlength="1000" required></label><div class="actions"><button id="planning-save" type="submit" data-mutation="project">Save plan</button><button id="planning-undo" type="button" data-mutation="project">Undo latest plan</button><button id="planning-reload" type="button">Reload current values</button></div><p id="planning-version" class="meta"></p></form></section><section id="split-panel" class="plan-block" aria-labelledby="plan-h-split" hidden><h3 id="plan-h-split">Split the selected backlog task</h3><form id="split-backlog"><label>Part titles, one per line<textarea id="split-titles" maxlength="2408" required></textarea></label><label><input id="split-sequential" type="checkbox">Each part waits for the previous part</label><label>Reason<input id="split-reason" maxlength="1000" required></label><p class="meta">Two to eight parts. The original task retains integration and acceptance. Unused parts can be undone from its current plan.</p><button id="split-save" type="submit" data-mutation="project">Stage parts</button></form></section><section class="plan-block" aria-labelledby="plan-h-releases"><h3 id="plan-h-releases">Releases</h3><div id="releases" class="content"></div><form id="release-new"><div class="split"><label>Name<input id="release-name" maxlength="64" required></label><label>Order<input id="release-order" type="number" min="0" value="0" step="1" required></label></div><label>Goal<input id="release-goal" maxlength="1000" required></label><button type="submit" data-mutation="project">Create release</button></form><form id="release-plan"><label>Release<select id="release-edit"><option value="">Select release</option></select></label><div class="split"><label>Planned start<input id="release-start" type="datetime-local" step="any"></label><label>Cut<input id="release-cut" type="datetime-local" step="any"></label><label>Ship<input id="release-ship" type="datetime-local" step="any"></label></div><button id="release-save" type="submit" data-mutation="project">Save dates</button><button id="release-reload" type="button">Reload current release</button><p id="release-version" class="meta"></p></form></section><section id="delivery" class="plan-block" aria-labelledby="plan-h-delivery"><h3 id="plan-h-delivery">Delivery evidence (read-only)</h3><div class="field"><label for="delivery-repo">GitHub repository</label><input id="delivery-repo" maxlength="200" placeholder="owner/repository"></div><div class="field"><label for="delivery-live">Live verification issue IDs (optional)</label><input id="delivery-live" maxlength="300" placeholder="Comma-separated issue numbers"></div><div class="actions"><button id="delivery-queue" type="button">Read merge queue</button><button id="delivery-release" type="button">Read release checklist</button></div><pre id="delivery-report" class="body" aria-live="polite"></pre></section><section class="plan-block" aria-labelledby="plan-h-propose"><h3 id="plan-h-propose">Propose a task</h3><form id="propose"><div class="field"><label for="title">Title</label><input id="title" name="title" required maxlength="300" placeholder="What should the team do?"></div><div class="field"><label for="detail">Details (optional)</label><textarea id="detail" name="detail" maxlength="8000"></textarea></div><div class="split"><div class="field"><label for="class">Class</label><select id="class" name="class"><option>implement</option><option>plan</option><option>bulk_edit</option><option>test</option><option>review</option><option>summarize</option><option>triage</option></select></div><div class="field"><label for="owner">Owner</label><select id="owner" name="owner"><option value="">Automatic</option></select></div></div><label><input id="backlog-proposal" type="checkbox"> Add to Backlog without offering</label><button class="primary" data-mutation="project" type="submit">Propose task</button></form></section>
|
|
508
511
|
</dialog>
|
|
509
512
|
<dialog id="task-detail-dialog" aria-labelledby="task-detail-title"><div class="dialog-head"><h2 id="task-detail-title">Task details</h2><button id="task-detail-close" type="button" autofocus>Close details</button></div><div id="task-detail-content"></div></dialog>
|
|
@@ -675,6 +678,7 @@ function saveDraft() {
|
|
|
675
678
|
const previous = drafts.get(selectedProjectId) || {};
|
|
676
679
|
drafts.set(selectedProjectId, {message:$('message').value, recipient:draftChoicesPending ? previous.recipient || '' : $('recipient').value,
|
|
677
680
|
...($('backlog-proposal')?.checked ? {backlog:true} : {}), title:$('title').value, detail:$('detail').value, class:$('class').value, owner:draftChoicesPending ? previous.owner || '' : $('owner').value});
|
|
681
|
+
drafts.get(selectedProjectId).intake = $('intake-text').value; drafts.get(selectedProjectId).intakeSources = $('intake-sources').value;
|
|
678
682
|
}
|
|
679
683
|
function restoreDraft(choices = false) {
|
|
680
684
|
const draft = drafts.get(selectedProjectId) || {};
|
|
@@ -682,6 +686,7 @@ function restoreDraft(choices = false) {
|
|
|
682
686
|
if (choices) {
|
|
683
687
|
for (const id of ['recipient','owner']) $(id).value = Array.from($(id).options).some((o) => o.value === draft[id]) ? draft[id] : '';
|
|
684
688
|
} else { $('message').value = draft.message || ''; $('title').value = draft.title || ''; $('detail').value = draft.detail || ''; $('class').value = draft.class || 'implement'; }
|
|
689
|
+
if (!choices) { $('intake-text').value = draft.intake || ''; $('intake-sources').value = draft.intakeSources || ''; }
|
|
685
690
|
}
|
|
686
691
|
function syncMutationControls() { document.querySelectorAll('button[data-mutation="project"]').forEach((b) => { b.disabled = pendingButtons.has(b) || Boolean(b.dataset.startKey && pendingStarts.has(b.dataset.startKey)) || (managerMode && !snapshotValid); }); }
|
|
687
692
|
// The first-load blocks the page ships in #tasks and #peers (#296): a project switch puts copies of them back and update() replaces them
|
|
@@ -703,9 +708,10 @@ function resetView(id, instance) {
|
|
|
703
708
|
$('split-titles').value='';$('split-reason').value='';$('split-sequential').checked=false;
|
|
704
709
|
$('release-name').value='';$('release-goal').value='';$('release-order').value='0';
|
|
705
710
|
$('delivery-report').textContent = ''; $('delivery-repo').value = ''; $('delivery-live').value = '';
|
|
711
|
+
$('intake-result').textContent = ''; $('intake-text').setAttribute('aria-invalid', 'false'); $('intake-sources').setAttribute('aria-invalid', 'false');
|
|
706
712
|
// A project with a running instance is polled next, so the board and the peers show loading; "No project selected" is for no project.
|
|
707
713
|
const loading = Boolean(id && instance) && typeof firstLoad !== 'undefined';
|
|
708
|
-
for (const [id, text] of [['memory-list','No project selected.'],['memory-graph',''],['memory-detail',''],['peers','No project selected.'],['tasks','No project selected.'],['task-progress','No project selected.'],['permissions','No project selected.'],['budget','No project selected.'],['settings','No project selected.']]) {
|
|
714
|
+
for (const [id, text] of [['memory-list','No project selected.'],['memory-graph',''],['memory-detail',''],['peers','No project selected.'],['tasks','No project selected.'],['task-progress','No project selected.'],['permissions','No project selected.'],['budget','No project selected.'],['settings','No project selected.'],['sources','No project selected.']]) {
|
|
709
715
|
$(id).replaceChildren();
|
|
710
716
|
if (loading && firstLoad[id]?.length) $(id).replaceChildren(...firstLoad[id].map((node) => node.cloneNode(true)));
|
|
711
717
|
else if (text) empty($(id), text);
|
|
@@ -1457,6 +1463,44 @@ function renderBoard(snapshot, peers, ids) {
|
|
|
1457
1463
|
});
|
|
1458
1464
|
refreshTaskDetail();
|
|
1459
1465
|
}
|
|
1466
|
+
function renderSources(sources) {
|
|
1467
|
+
update('sources', sources ?? null, target => {
|
|
1468
|
+
if (!Array.isArray(sources)) return empty(target, 'Source list unavailable for this project snapshot.');
|
|
1469
|
+
if (!sources.length) return empty(target, 'No registered sources. Register a file or URL with ahub source add.');
|
|
1470
|
+
for (const source of sources) {
|
|
1471
|
+
const row = el('div', undefined, 'row'), head = el('div', undefined, 'row-head');
|
|
1472
|
+
head.append(el('code', source.id, 'label'), el('span', source.kind, 'meta'));
|
|
1473
|
+
const registered = Number.isSafeInteger(source.added_at) ? new Date(source.added_at).toLocaleString() : 'unknown';
|
|
1474
|
+
row.append(head, el('p', source.bytes + ' bytes · hash ' + source.hashPrefix + ' · registered ' + registered, 'meta'));
|
|
1475
|
+
target.append(row);
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
function intakeFeedback(text, error = false) {
|
|
1480
|
+
const result = $('intake-result'); result.textContent = text; result.classList.toggle('error', error);
|
|
1481
|
+
}
|
|
1482
|
+
async function submitIntake(event) {
|
|
1483
|
+
event.preventDefault();
|
|
1484
|
+
const submit = event.submitter || $('intake-submit');
|
|
1485
|
+
if (pendingButtons.has(submit) || stopped) return;
|
|
1486
|
+
const text = $('intake-text').value.trim(), sourceText = $('intake-sources').value.trim();
|
|
1487
|
+
const sources = sourceText ? sourceText.split(/[\s,]+/).filter(Boolean) : [];
|
|
1488
|
+
$('intake-text').setAttribute('aria-invalid', 'false'); $('intake-sources').setAttribute('aria-invalid', 'false');
|
|
1489
|
+
const invalid = !text || text.length > 7000 ? 'intake-text' : sources.length > 16 || new Set(sources).size !== sources.length || sources.some(id => !/^src:[0-9a-f]{16}$/.test(id)) ? 'intake-sources' : '';
|
|
1490
|
+
if (invalid) {
|
|
1491
|
+
intakeFeedback(invalid === 'intake-text' ? 'Enter a request of 1 to 7000 characters.' : 'Use at most 16 distinct registered src: IDs; run ahub source list to check them.', true);
|
|
1492
|
+
$(invalid).setAttribute('aria-invalid', 'true'); $(invalid).focus(); return;
|
|
1493
|
+
}
|
|
1494
|
+
const captured = context();
|
|
1495
|
+
intakeFeedback('Creating draft. Waiting for the hub result...');
|
|
1496
|
+
const accepted = await action({action:'intake', text, sources}, submit);
|
|
1497
|
+
const current = context();
|
|
1498
|
+
if (stopped || captured.generation !== current.generation || captured.projectId !== current.projectId || captured.instanceId !== current.instanceId) return;
|
|
1499
|
+
if (accepted) {
|
|
1500
|
+
if ($('intake-text').value.trim() === text && $('intake-sources').value.trim() === sourceText) { $('intake-text').value = ''; $('intake-sources').value = ''; }
|
|
1501
|
+
intakeFeedback('Draft saved to Backlog. Inspect it there; approve or discard with ahub intake in a terminal.');
|
|
1502
|
+
} else intakeFeedback('Intake was not confirmed. Read the notice and check Backlog before submitting again.', true);
|
|
1503
|
+
}
|
|
1460
1504
|
const attentionBaseTitle = typeof document === 'undefined' ? 'agent-hub | Local dashboard' : document.title;
|
|
1461
1505
|
let attentionSignature = '', attentionFirst;
|
|
1462
1506
|
function reachAttention() {
|
|
@@ -1745,7 +1789,7 @@ function render(snapshot) {
|
|
|
1745
1789
|
const current = (snapshot.boardLegend || []).find(item => item.peer === id)?.activeTasks, summary = el('p', undefined, 'peer-summary');
|
|
1746
1790
|
summary.append(el('span', peer.queued + ' queued' + (peer.queuedImportant ? ' (' + peer.queuedImportant + ' important)' : ''), 'meta'), el('span', Array.isArray(current) && current.length ? 'Working on ' + current.map(task => '#' + task).join(', ') : 'No active task', 'meta'));
|
|
1747
1791
|
// #878: the session title or current plan item the hub observed, already screened (a stub while private).
|
|
1748
|
-
if (typeof peer.doing?.text === 'string' && peer.doing.text) summary.append(el('span', 'Doing: ' + peer.doing.text, 'meta'));
|
|
1792
|
+
if (typeof peer.doing?.text === 'string' && peer.doing.text) summary.append(el('span', (peer.doing.source === 'label' ? 'Unnamed session: ' : 'Doing: ') + peer.doing.text, 'meta'));
|
|
1749
1793
|
row.append(head, summary);
|
|
1750
1794
|
// A status notice asks a person to act (for example, reattach a native session): it stays visible, never folded away.
|
|
1751
1795
|
if (typeof peer.statusNotice?.message === 'string' && peer.statusNotice.message) row.append(el('p', peer.statusNotice.message, 'meta notice-warn'));
|
|
@@ -1783,6 +1827,7 @@ function render(snapshot) {
|
|
|
1783
1827
|
const peerSignature = JSON.stringify([ids, assignable]);
|
|
1784
1828
|
if (signatures.get('peer-options') !== peerSignature) { signatures.set('peer-options', peerSignature); peerOptions($('owner'), assignable, 'Automatic'); peerOptions($('recipient'), ids, 'All peers'); if (draftChoicesPending) { restoreDraft(true); draftChoicesPending = false; } }
|
|
1785
1829
|
renderBoard(snapshot, peers, ids);
|
|
1830
|
+
renderSources(snapshot.sources);
|
|
1786
1831
|
renderApprovals(snapshot.permissions);
|
|
1787
1832
|
update('budget', snapshot.budget, (target) => {
|
|
1788
1833
|
if (!Object.keys(snapshot.budget).length) return empty(target, 'No usage readings yet. Usage appears when a peer reports it.');
|
|
@@ -1810,6 +1855,7 @@ function render(snapshot) {
|
|
|
1810
1855
|
}
|
|
1811
1856
|
});
|
|
1812
1857
|
renderBench(snapshot.bench);
|
|
1858
|
+
renderWorktrees(snapshot.worktrees);
|
|
1813
1859
|
renderMemory(snapshot.memory);
|
|
1814
1860
|
renderSettings(snapshot.settings, snapshot.roster);
|
|
1815
1861
|
renderUsage(snapshot.usage, snapshot.tasks, snapshot.budget);
|
|
@@ -1879,7 +1925,7 @@ function officeTile(w, h, cls) {
|
|
|
1879
1925
|
}
|
|
1880
1926
|
function officeViewBox(w, h, model) {
|
|
1881
1927
|
if (officeProjection !== 'iso') return '0 0 ' + w + ' ' + h;
|
|
1882
|
-
const corners = model ? [...model.rooms, ...model.desks].flatMap(tile => [officeProject(tile.x, tile.y), officeProject(tile.x + tile.w, tile.y), officeProject(tile.x, tile.y + tile.h), officeProject(tile.x + tile.w, tile.y + tile.h)].map(([x, y]) => [x, y + (model.
|
|
1928
|
+
const corners = model ? [...model.rooms, ...(model.docks || []), ...model.desks].flatMap(tile => [officeProject(tile.x, tile.y), officeProject(tile.x + tile.w, tile.y), officeProject(tile.x, tile.y + tile.h), officeProject(tile.x + tile.w, tile.y + tile.h)].map(([x, y]) => [x, y + (model.desks.includes(tile) ? 0 : officeRoomOffset)])) : [officeProject(0, 0), officeProject(w, 0), officeProject(0, h), officeProject(w, h)];
|
|
1883
1929
|
const xs = corners.map((p) => p[0]), ys = corners.map((p) => p[1]), m = 3;
|
|
1884
1930
|
const minX = Math.min(...xs), minY = Math.min(...ys);
|
|
1885
1931
|
return (minX - m) + ' ' + (minY - 6) + ' ' + (Math.max(...xs) - minX + 2 * m) + ' ' + (Math.max(...ys) - minY + 9);
|
|
@@ -1934,16 +1980,16 @@ function officeClip(text, max) {
|
|
|
1934
1980
|
}
|
|
1935
1981
|
let officeClipSerial = 0;
|
|
1936
1982
|
let officeMeasureContext;
|
|
1937
|
-
function officeTextWidth(value, monospace) {
|
|
1983
|
+
function officeTextWidth(value, monospace, size = OFFICE_SCALE, weight = '') {
|
|
1938
1984
|
if (officeMeasureContext === undefined) {
|
|
1939
1985
|
try { officeMeasureContext = document.createElement('canvas').getContext?.('2d') || null; } catch { officeMeasureContext = null; }
|
|
1940
1986
|
}
|
|
1941
1987
|
if (officeMeasureContext) {
|
|
1942
|
-
officeMeasureContext.font =
|
|
1988
|
+
officeMeasureContext.font = (weight ? weight + ' ' : '') + size + 'px ' + (monospace ? 'ui-monospace,monospace' : 'system-ui,sans-serif');
|
|
1943
1989
|
return officeMeasureContext.measureText(value).width;
|
|
1944
1990
|
}
|
|
1945
1991
|
// Non-browser fixtures or unavailable canvas: conservative glyph widths; clipPath is the hard boundary.
|
|
1946
|
-
return [...value].reduce((width, char) => width + (!/^[ -~]$/.test(char) ? 1 : monospace ? 0.6 : /[MW@#%]/.test(char) ? 0.9 : /[ilI., ']/.test(char) ? 0.3 : 0.55), 0) *
|
|
1992
|
+
return [...value].reduce((width, char) => width + (!/^[ -~]$/.test(char) ? 1 : monospace ? 0.6 : /[MW@#%]/.test(char) ? 0.9 : /[ilI., ']/.test(char) ? 0.3 : 0.55), 0) * size;
|
|
1947
1993
|
}
|
|
1948
1994
|
function officeCardLabel(card, width) {
|
|
1949
1995
|
const prefix = '#' + card.id, rest = String(card.label).slice(prefix.length);
|
|
@@ -1990,6 +2036,7 @@ function spotlightRow(row) { if (spotlitRow && spotlitRow !== row) spotlitRow.cl
|
|
|
1990
2036
|
function activateOfficePeer(id) { switchView('agents'); const row = peerRows.get(id); if (!row) return; spotlightRow(row); row.scrollIntoView({ block: 'nearest' }); row.focus?.(); }
|
|
1991
2037
|
function activateOfficeTask(id) { setColumnFocus(null); if (!taskRows.has(id)) officeFilter(''); const row = taskRows.get(id); if (!row) return; spotlightRow(row); row.scrollIntoView({ block: 'nearest' }); openTaskDetail(id); }
|
|
1992
2038
|
function officeFilter(state) { $('task-filter').value = state; $('task-peer-filter').value = ''; $('task-release-filter').value = ''; if (!stopped && snapshotValid && lastSnapshot) render(lastSnapshot); }
|
|
2039
|
+
function officeRelease(id) { setColumnFocus(null); $('task-filter').value = ''; $('task-peer-filter').value = ''; $('task-release-filter').value = id == null ? '' : String(id); if(id == null)$('task-lanes').value = 'releases'; if (!stopped && snapshotValid && lastSnapshot) render(lastSnapshot); switchView('board'); $('tasks').scrollIntoView({block:'nearest'}); }
|
|
1993
2040
|
// Fade timing for a transient mark: it holds, then fades over the final second so it is gone at `until`.
|
|
1994
2041
|
// Applied once at creation; later renders leave the running animation alone.
|
|
1995
2042
|
function officeFade(node, until, now) {
|
|
@@ -2006,16 +2053,25 @@ function officeFade(node, until, now) {
|
|
|
2006
2053
|
function officeCardPaint(node, card) {
|
|
2007
2054
|
node.setAttribute('class', 'office-card-node office-movable' + (card.waiting ? ' office-waiting-card' : '') + (card.rework ? ' office-rework-card' : '') + (card.checking ? ' office-card-checking' : ''));
|
|
2008
2055
|
node.setAttribute('aria-label', card.name);
|
|
2009
|
-
const innerWidth = Math.max(0.1, card.w - 1.6 - (card.checking ? 1 : 0));
|
|
2056
|
+
const innerWidth = Math.max(0.1, card.w - 1.6 - (card.checking ? 1 : 0) - (card.releaseTag ? 1.8 : 0));
|
|
2010
2057
|
const label = officeCardLabel(card, innerWidth * OFFICE_SCALE), prefix = '#' + card.id, clipId = node.dataset.officeClipId || (node.dataset.officeClipId = 'office-card-clip-' + (++officeClipSerial));
|
|
2011
2058
|
const clip = officeNode('clipPath', { id: clipId, clipPathUnits: 'userSpaceOnUse' });
|
|
2012
2059
|
clip.append(officeNode('rect', { x: 0.8, y: 0, width: innerWidth, height: card.h }));
|
|
2013
2060
|
const text = officeNode('text', { x: 0.8, y: card.h / 2 + 0.33, class: 'office-card-text', 'clip-path': 'url(#' + clipId + ')' });
|
|
2061
|
+
const marked = card.reserved || card.pinned || card.waiting && card.state === 'backlog';
|
|
2062
|
+
if(marked)text.setAttribute('y','0.95');
|
|
2014
2063
|
text.append(officeNode('tspan', { class: 'office-card-id' }, prefix), officeNode('tspan', {}, label.slice(prefix.length)));
|
|
2015
2064
|
const children = [officeNode('title', {}, card.label), clip,
|
|
2016
2065
|
officeNode('rect', { x: 0, y: 0, width: card.w, height: card.h, rx: 0.5, class: 'office-card' }),
|
|
2017
2066
|
officeNode('rect', { x: 0.15, y: 0.25, width: 0.2, height: Math.max(0.4, card.h - 0.5), rx: 0.1, class: 'office-card-accent' }), text];
|
|
2018
2067
|
if (card.checking) children.push(officeIcon('i-check', card.w - 1.5, card.h / 2 - 0.46, 0.92, 'office-checking'));
|
|
2068
|
+
const marks=[]; let mx=0.8;
|
|
2069
|
+
const mark=(shape,label)=>{const g=officeNode('g',{});shape.setAttribute('transform','translate('+mx+',1.35)');g.append(shape,officeNode('text',{x:mx+0.85,y:2.05,class:'office-meta'},label));marks.push(g);mx+=0.85+officeTextWidth(label,false)/OFFICE_SCALE+0.6;};
|
|
2070
|
+
if(card.reserved){const key=Object.hasOwn(peerPalette,card.reserved)?card.reserved:'other';mark(officeNode('circle',{cx:0.3,cy:0.3,r:0.25,class:'office-reserved peer-'+key}),officeClip(card.reserved,8));}
|
|
2071
|
+
if(card.pinned)mark(officeNode('path',{d:'M0 0H.6L.45 .4H.15ZM.3 .4V.8',class:'office-plan-shape'}),'pin');
|
|
2072
|
+
if(card.waiting && card.state==='backlog')mark(officeNode('path',{d:'M.3 0 .65 .65H0Z',class:'office-plan-shape'}),'waits'+(card.plannedDate?' '+card.plannedDate:''));
|
|
2073
|
+
if(marks.length){const group=officeNode('g',{'clip-path':'url(#'+clipId+')',class:'office-card-marks'});group.append(...marks);children.push(group);}
|
|
2074
|
+
if(card.releaseTag)children.push(officeNode('text',{x:card.w-0.6,y:marked?0.95:card.h/2+0.33,'text-anchor':'end',class:'office-meta'},'R'+card.releaseTag.ordinal));
|
|
2019
2075
|
node.replaceChildren(...children);
|
|
2020
2076
|
}
|
|
2021
2077
|
function officeDeskPaint(node, desk) {
|
|
@@ -2064,6 +2120,7 @@ function officeDeskPaint(node, desk) {
|
|
|
2064
2120
|
if (desk.tray.needsReview) tray.push(desk.tray.needsReview + ' to review');
|
|
2065
2121
|
if (desk.tray.heldBy) tray.push('held by ' + desk.tray.heldBy);
|
|
2066
2122
|
if (tray.length) children.push(officeNode('text', { x: 1, y: line, class: 'office-meta' }, officeClip(tray.join(' / '), 38)));
|
|
2123
|
+
if(desk.planner){const badge=officeNode('g',{class:'office-planner','aria-label':'planner, '+desk.unplanned+' unplanned backlog cards'});badge.append(officeNode('path',{d:'M7 5H8V6H7ZM7.5 6V6.5',class:'office-plan-shape'}),officeNode('text',{x:8.4,y:6,class:'office-meta'},String(desk.unplanned)));children.push(badge);}
|
|
2067
2124
|
node.replaceChildren(...children);
|
|
2068
2125
|
}
|
|
2069
2126
|
// More is a sibling of the desk button, so assistive technology exposes both targets.
|
|
@@ -2082,6 +2139,7 @@ function officeRoomHeader(node, room) {
|
|
|
2082
2139
|
node.replaceChildren(...(iso ? [officeNode('rect', { x: -0.5, y: -2.55, width: 12.5, height: 2, rx: 0.5, class: 'office-header-surface' })] : []), officeNode('text', { x: iso ? 0 : 1, y, class: 'office-room-title' }, room.label),
|
|
2083
2140
|
officeNode('rect', { x: right - 3, y: y - 1.35, width: 3, height: 2, rx: 0.5, class: 'office-count-pill' }),
|
|
2084
2141
|
officeNode('text', { x: right - 1.5, y, 'text-anchor': 'middle', class: 'office-room-count' }, String(room.count)));
|
|
2142
|
+
if(room.unplanned!==undefined)node.append(officeNode('text',{x:iso?0:1,y:y+1.15,class:'office-meta'},room.unplanned+' unplanned'));
|
|
2085
2143
|
}
|
|
2086
2144
|
function officeRoomPaint(node, room) {
|
|
2087
2145
|
node.setAttribute('class', 'office-roombox office-roombox-' + room.id);
|
|
@@ -2095,12 +2153,21 @@ function officeRoomPaint(node, room) {
|
|
|
2095
2153
|
const at = iso ? officeIsoLocal(room.w / 2, room.h - 1.5) : [1, room.h - 1];
|
|
2096
2154
|
const more = officeNode('g', { tabindex: '0', role: 'button', 'aria-label': 'Show ' + room.overflow + ' more ' + room.filterState.replaceAll('_', ' ') + ' tasks in the task list' });
|
|
2097
2155
|
more.append(officeNode('text', { x: at[0], y: at[1], 'text-anchor': iso ? 'middle' : 'start', class: 'office-more' }, '+' + room.overflow + ' more'));
|
|
2098
|
-
const open = () => officeFilter(room.filterState);
|
|
2156
|
+
const open = () => { setColumnFocus(null); officeFilter(room.filterState); switchView('board'); };
|
|
2099
2157
|
more.addEventListener('click', open);
|
|
2100
2158
|
more.addEventListener('keydown', (event) => officeKeys(event, open));
|
|
2101
2159
|
node.append(more);
|
|
2102
2160
|
}
|
|
2103
2161
|
}
|
|
2162
|
+
function officeDockPaint(node,dock) {
|
|
2163
|
+
officePlace(node,dock.x,dock.y,true);node.setAttribute('class','office-release-dock');node.setAttribute('aria-label','Release '+dock.ordinal+': '+dock.name+', '+dock.state+', '+dock.approved+' / '+dock.scope+' approved'+(dock.cut?', cut '+dock.cut:''));
|
|
2164
|
+
const iso=officeProjection==='iso',at=iso?officeIsoLocal(dock.w/2,7):[1,5],x=at[0],y=at[1],align=iso?'middle':'start';
|
|
2165
|
+
const full='R'+dock.ordinal+' '+dock.name,width=dock.w-2,chars=[...full],measure=value=>officeTextWidth(value,false,13,'600');let title=full;
|
|
2166
|
+
if(measure(title)>width*OFFICE_SCALE){let low=0,high=chars.length;while(low<high){const mid=Math.ceil((low+high)/2);if(measure(chars.slice(0,mid).join('')+'...')<=width*OFFICE_SCALE)low=mid;else high=mid-1;}title=chars.slice(0,low).join('').trimEnd()+'...';}
|
|
2167
|
+
const clipId=node.dataset.officeDockClipId || (node.dataset.officeDockClipId='office-dock-clip-'+(++officeClipSerial)),clip=officeNode('clipPath',{id:clipId,clipPathUnits:'userSpaceOnUse'});clip.append(officeNode('rect',{x:iso?x-width/2:x,y:y-1.15,width,height:1.5}));
|
|
2168
|
+
node.replaceChildren(officeTile(dock.w,dock.h,'office-room'),officeNode('title',{},full),clip,officeNode('text',{x,y,'text-anchor':align,class:'office-room-title office-dock-title','clip-path':'url(#'+clipId+')'},title),officeNode('text',{x,y:y+1.5,'text-anchor':align,class:'office-meta'},dock.state+' · '+dock.approved+' / '+dock.scope),officeNode('rect',{x:iso?x-5:1,y:y+2.3,width:10,height:0.3,class:'office-count-pill'}),officeNode('rect',{x:iso?x-5:1,y:y+2.3,width:10*dock.ratio,height:0.3,class:'office-dock-progress'}));
|
|
2169
|
+
if(dock.cut)node.append(officeNode('text',{x,y:y+4,'text-anchor':align,class:'office-meta'},'Cut '+dock.cut));
|
|
2170
|
+
}
|
|
2104
2171
|
let officeScene = null;
|
|
2105
2172
|
function officeEnsure(target) {
|
|
2106
2173
|
if (officeScene) return officeScene;
|
|
@@ -2163,6 +2230,11 @@ function drawOffice(target, model) {
|
|
|
2163
2230
|
const visible = roomCards.get(original.id), room = { ...original, cards: visible, overflow: original.overflow + original.cards.length - visible.length };
|
|
2164
2231
|
keep('room:' + room.id, JSON.stringify({ ...room, cards: undefined, visibleCount: room.cards.length }), scene.layers.rooms, (node) => officeRoomPaint(node, room), false);
|
|
2165
2232
|
}
|
|
2233
|
+
for(const dock of model.docks || []){
|
|
2234
|
+
const {node,fresh}=keep('dock:'+dock.id,JSON.stringify(dock),scene.layers.rooms,n=>officeDockPaint(n,dock),false);
|
|
2235
|
+
if(fresh&&!node.getAttribute('tabindex')){node.setAttribute('tabindex','0');node.setAttribute('role','button');const open=()=>officeRelease(dock.id);node.addEventListener('click',open);node.addEventListener('keydown',event=>officeKeys(event,open));}
|
|
2236
|
+
}
|
|
2237
|
+
if(model.extraReleases>0 && model.docks.length){const dock=model.docks.at(-1);const {node,fresh}=keep('dock:more',JSON.stringify([dock,model.extraReleases]),scene.layers.headers,n=>{officePlace(n,dock.x,dock.y,true);n.setAttribute('aria-label','Show '+model.extraReleases+' more releases');const at=officeProjection==='iso'?officeIsoLocal(dock.w/2,dock.h-2):[1,dock.h-2];n.replaceChildren(officeNode('text',{x:at[0],y:at[1],class:'office-more'},'+'+model.extraReleases+' releases'));},false);if(fresh&&!node.getAttribute('tabindex')){node.setAttribute('tabindex','0');node.setAttribute('role','button');const open=()=>officeRelease(null);node.addEventListener('click',open);node.addEventListener('keydown',event=>officeKeys(event,open));}}
|
|
2166
2238
|
const deskCards = new Map(model.desks.map(desk => [desk.id, officeDeskCards(desk)]));
|
|
2167
2239
|
for (const original of model.desks) {
|
|
2168
2240
|
const visible = deskCards.get(original.id), desk = { ...original, cards: visible, extraCards: original.extraCards + original.cards.length - visible.length };
|
|
@@ -2194,7 +2266,7 @@ function drawOffice(target, model) {
|
|
|
2194
2266
|
};
|
|
2195
2267
|
const paintNodes = [], focusNodes = [];
|
|
2196
2268
|
for (const card of cards) {
|
|
2197
|
-
const { node } = keep('paint:card:' + card.id, JSON.stringify([card.label, card.name, card.waiting, card.rework, card.checking, card.w, card.h]), scene.layers.cards, n => { officeCardPaint(n, card); n.setAttribute('aria-hidden', 'true'); });
|
|
2269
|
+
const { node } = keep('paint:card:' + card.id, JSON.stringify([card.label, card.name, card.waiting, card.rework, card.checking, card.reserved, card.pinned, card.plannedDate, card.releaseTag, card.w, card.h]), scene.layers.cards, n => { officeCardPaint(n, card); n.setAttribute('aria-hidden', 'true'); });
|
|
2198
2270
|
paintNodes.push(node); placeCard(node, card);
|
|
2199
2271
|
}
|
|
2200
2272
|
for (const card of focusCards) {
|
|
@@ -2211,7 +2283,7 @@ function drawOffice(target, model) {
|
|
|
2211
2283
|
focusNodes.push(node); placeCard(node, card);
|
|
2212
2284
|
}
|
|
2213
2285
|
officeOrderNodes(scene.layers.cards, paintNodes); officeOrderNodes(scene.layers.cardFocus, focusNodes);
|
|
2214
|
-
for (const room of model.rooms) keep('header:' + room.id, JSON.stringify([room.x, room.y, room.w, room.count, room.label]), scene.layers.headers, node => officeRoomHeader(node, room), false);
|
|
2286
|
+
for (const room of model.rooms) keep('header:' + room.id, JSON.stringify([room.x, room.y, room.w, room.count, room.label,room.unplanned]), scene.layers.headers, node => officeRoomHeader(node, room), false);
|
|
2215
2287
|
if (model.extraPeers > 0) {
|
|
2216
2288
|
keep('extra:peers', String(model.extraPeers) + ':' + model.width, scene.layers.marks,
|
|
2217
2289
|
(node) => { officePlace(node, model.width, 0); node.replaceChildren(officeNode('text', { x: 0, y: 1.8, 'text-anchor': 'end', class: 'office-meta' }, '+' + model.extraPeers + ' peers')); }, false);
|
|
@@ -2299,6 +2371,56 @@ let streamFilter = '';
|
|
|
2299
2371
|
function applyStreamFilter(node) { node.hidden = streamFilter !== '' && node.dataset.kind !== streamFilter; }
|
|
2300
2372
|
function refreshStreamEmpty() { const has = $('stream').querySelector('.event:not([hidden])') !== null, any = $('stream').querySelector('.event') !== null; $('stream-empty').hidden = has || !any; }
|
|
2301
2373
|
// #251: benchmark runs on this machine and arm comparisons per suite. Every bar carries its number as text.
|
|
2374
|
+
/* #1057: the worktree shelf. A panel refresh reads nothing from GitHub: the rows come from the hub's cached facts, and only the explicit 'Refresh PR states' button asks it to read PR states. Removal is never a button: the panel shows the command to run in a terminal. */
|
|
2375
|
+
function renderWorktrees(shelf) {
|
|
2376
|
+
const live = shelf && Array.isArray(shelf.live) ? shelf.live : [], settled = shelf && Array.isArray(shelf.settled) ? shelf.settled : [];
|
|
2377
|
+
const known = Boolean(shelf) && !shelf.unavailable && !shelf.pending;
|
|
2378
|
+
$('worktrees-count').textContent = known ? live.length + ' live, ' + settled.length + ' settled' : '';
|
|
2379
|
+
update('worktrees', shelf === undefined ? null : shelf, (target) => {
|
|
2380
|
+
if (shelf === undefined) return empty(target, 'Worktrees are shown on a current snapshot.');
|
|
2381
|
+
if (shelf.pending) return empty(target, 'Reading worktrees...');
|
|
2382
|
+
if (shelf.unavailable) return empty(target, 'Worktrees unavailable: git could not list this project\'s worktrees, or the hub is restarting.');
|
|
2383
|
+
const reasonText = {merged: 'PR merged', closed: 'PR closed', quiet: 'quiet for ' + (shelf.quietDays || 0) + '+ days', parked: 'parked by you'};
|
|
2384
|
+
const group = (title, rows, isSettled) => {
|
|
2385
|
+
const block = el('div', undefined, 'row'), head = el('div', undefined, 'row-head');
|
|
2386
|
+
head.append(el('span', title + ' (' + rows.length + ')', 'label'));
|
|
2387
|
+
block.append(head);
|
|
2388
|
+
if (!rows.length) block.append(el('p', isSettled ? 'Nothing settled.' : 'No live worktrees.', 'meta'));
|
|
2389
|
+
for (const item of rows) {
|
|
2390
|
+
const line = el('div', undefined, 'row-head'), name = el('span', String(item.path), 'label');
|
|
2391
|
+
line.append(name);
|
|
2392
|
+
if (item.branch) line.append(el('span', String(item.branch), 'meta'));
|
|
2393
|
+
if (item.task !== undefined) line.append(badge('#' + item.task + (item.taskState ? ' ' + item.taskState : '')));
|
|
2394
|
+
if (item.pr !== undefined) line.append(badge('PR ' + item.pr));
|
|
2395
|
+
if (isSettled && item.reason) line.append(badge(reasonText[item.reason] || String(item.reason), item.reason === 'parked' ? '' : 'ok'));
|
|
2396
|
+
if (item.outside) line.append(badge('outside project'));
|
|
2397
|
+
// `rel` and `data-ts` take epoch milliseconds (refreshRelativeTimes reads Number(dataset.ts)), never an ISO string.
|
|
2398
|
+
const stamp = el('span'); if (item.lastActiveAt) { stamp.dataset.ts = String(item.lastActiveAt); stamp.title = time(item.lastActiveAt); stamp.textContent = 'active ' + rel(item.lastActiveAt); } else stamp.textContent = 'activity unknown'; line.append(stamp);
|
|
2399
|
+
// Park moves a live row to the shelf and Pin keeps a settled row live; both are the person's call and the only writes here. 'Unset' returns the row to what the rules derive.
|
|
2400
|
+
if (!item.fixed) line.append(button(isSettled ? 'Pin' : 'Park', {action: 'worktree_override_set', path: String(item.path), value: isSettled ? 'active' : 'settled'}));
|
|
2401
|
+
else line.append(badge('never shelved'));
|
|
2402
|
+
if (item.reason === 'parked') line.append(button('Unset', {action: 'worktree_override_clear', path: String(item.path)}));
|
|
2403
|
+
block.append(line);
|
|
2404
|
+
}
|
|
2405
|
+
return block;
|
|
2406
|
+
};
|
|
2407
|
+
target.append(group('Live', live, false), group('Settled', settled, true));
|
|
2408
|
+
if (shelf.complete === false) target.append(el('p', 'Some worktrees could not be read in time; they are listed as live. The next refresh tries again.', 'meta'));
|
|
2409
|
+
const tools = el('div', undefined, 'row-head');
|
|
2410
|
+
const readNote = el('span', undefined, 'meta');
|
|
2411
|
+
if (shelf.prsReadAt) { readNote.append('PR states read '); const when = el('span'); when.dataset.ts = String(shelf.prsReadAt); when.title = time(shelf.prsReadAt); when.textContent = rel(shelf.prsReadAt); readNote.append(when); }
|
|
2412
|
+
else readNote.textContent = 'PR states not read yet: merged work shows as merged only after a guarded merge or a refresh.';
|
|
2413
|
+
tools.append(button('Refresh PR states', {action: 'worktrees_refresh_prs'}), readNote);
|
|
2414
|
+
const foot = el('div', undefined, 'row');
|
|
2415
|
+
foot.append(tools);
|
|
2416
|
+
if (settled.length) {
|
|
2417
|
+
const tidy = el('p', 'To remove settled worktrees safely, run in a terminal (it previews first and removes only after you confirm): ', 'meta');
|
|
2418
|
+
tidy.append(el('code', 'ahub worktrees tidy'));
|
|
2419
|
+
foot.append(tidy);
|
|
2420
|
+
}
|
|
2421
|
+
target.append(foot);
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2302
2424
|
function renderBench(bench) {
|
|
2303
2425
|
const pct = (v) => v === null || v === undefined ? '-' : Math.round(v * 100) + '%';
|
|
2304
2426
|
const num = (v) => v === null || v === undefined ? '-' : String(Math.round(v));
|
|
@@ -2541,6 +2663,7 @@ function renderStreamChips() {
|
|
|
2541
2663
|
group.append(chip);
|
|
2542
2664
|
}
|
|
2543
2665
|
}
|
|
2666
|
+
$('intake').addEventListener('submit', submitIntake);
|
|
2544
2667
|
$('planning-task').addEventListener('change',loadPlanning);$('planning-reload').addEventListener('click',loadPlanning);$('release-edit').addEventListener('change',loadReleasePlanning);$('release-reload').addEventListener('click',loadReleasePlanning);
|
|
2545
2668
|
$('planning').addEventListener('submit',async event=>{event.preventDefault();if(!planningEdit || !sameBoardContext(planningEdit.context) || boardViewState?.snapshot.planningCapabilities?.enabled!==true)return notice('Select a task from its current board.',true);
|
|
2546
2669
|
const fields={priority:$('planning-priority').value===''?null:Number($('planning-priority').value),rank:$('planning-rank').value===''?null:Number($('planning-rank').value),release:$('planning-release').value===''?null:Number($('planning-release').value),plannedStart:plannedDateInput('planning-start'),due:plannedDateInput('planning-due'),pinned:$('planning-pinned').checked};
|
package/src/ui/office-model.ts
CHANGED
|
@@ -33,6 +33,16 @@ export interface OfficeTaskInput {
|
|
|
33
33
|
deps?: readonly number[];
|
|
34
34
|
/** Optional (phase 3): the task's check is running; the task stays in_progress meanwhile. */
|
|
35
35
|
checking?: boolean;
|
|
36
|
+
rank?: number | null;
|
|
37
|
+
priority?: number | null;
|
|
38
|
+
reserved?: string | null;
|
|
39
|
+
release?: number | null;
|
|
40
|
+
pinned?: boolean;
|
|
41
|
+
plannedStart?: number | null;
|
|
42
|
+
}
|
|
43
|
+
export interface OfficeReleaseInput {
|
|
44
|
+
id: number; name?: string; state?: string; order?: number; cut?: number | null;
|
|
45
|
+
tasks?: readonly { id: number; state?: string; deps?: readonly number[] }[];
|
|
36
46
|
}
|
|
37
47
|
/** Optional (phase 3): a warning line between desks. Task and peer ids only, never paths or symbols. */
|
|
38
48
|
export interface OfficeLinkInput {
|
|
@@ -55,7 +65,8 @@ export interface OfficeEventInput {
|
|
|
55
65
|
};
|
|
56
66
|
}
|
|
57
67
|
export interface OfficeSnapshotInput {
|
|
58
|
-
status?: { peers?: Record<string, OfficePeerInput> };
|
|
68
|
+
status?: { peers?: Record<string, OfficePeerInput>; roleInfo?: Record<string, { flowRoles?: readonly string[] }> };
|
|
69
|
+
releases?: readonly OfficeReleaseInput[];
|
|
59
70
|
tasks?: readonly OfficeTaskInput[];
|
|
60
71
|
permissions?: readonly { peer?: string }[];
|
|
61
72
|
budget?: Record<string, { paused?: { reason?: string; resetsAt?: number | null } | null; [key: string]: unknown }>;
|
|
@@ -84,6 +95,10 @@ export interface OfficeCard {
|
|
|
84
95
|
rework: boolean;
|
|
85
96
|
/** The task's check is running; the card stays at its owner's desk. */
|
|
86
97
|
checking: boolean;
|
|
98
|
+
reserved: string;
|
|
99
|
+
pinned: boolean;
|
|
100
|
+
plannedDate: string;
|
|
101
|
+
releaseTag?: { ordinal: number; name: string };
|
|
87
102
|
/** Visible text: "#id public title". */
|
|
88
103
|
label: string;
|
|
89
104
|
/** Accessible name: "#id class: public title (state)", plus waiting/rework/review marks. */
|
|
@@ -114,6 +129,8 @@ export interface OfficeDesk {
|
|
|
114
129
|
breakResetsAt: number | null;
|
|
115
130
|
cards: OfficeCard[];
|
|
116
131
|
extraCards: number;
|
|
132
|
+
planner: boolean;
|
|
133
|
+
unplanned: number;
|
|
117
134
|
}
|
|
118
135
|
export interface OfficeRoom {
|
|
119
136
|
id: string;
|
|
@@ -123,6 +140,12 @@ export interface OfficeRoom {
|
|
|
123
140
|
count: number;
|
|
124
141
|
cards: OfficeCard[];
|
|
125
142
|
overflow: number;
|
|
143
|
+
unplanned?: number;
|
|
144
|
+
x: number; y: number; w: number; h: number;
|
|
145
|
+
}
|
|
146
|
+
export interface OfficeDock {
|
|
147
|
+
id: number; name: string; state: string; ordinal: number;
|
|
148
|
+
approved: number; scope: number; ratio: number; cut: string;
|
|
126
149
|
x: number; y: number; w: number; h: number;
|
|
127
150
|
}
|
|
128
151
|
export interface OfficeTransitions {
|
|
@@ -158,6 +181,8 @@ export interface OfficeScene {
|
|
|
158
181
|
desks: OfficeDesk[];
|
|
159
182
|
extraPeers: number;
|
|
160
183
|
rooms: OfficeRoom[];
|
|
184
|
+
docks: OfficeDock[];
|
|
185
|
+
extraReleases: number;
|
|
161
186
|
counts: OfficeCounts;
|
|
162
187
|
transitions: OfficeTransitions;
|
|
163
188
|
links: OfficeLink[];
|
|
@@ -168,7 +193,7 @@ export interface OfficeScene {
|
|
|
168
193
|
* every `cards` list. Zero geometry: the dashboard never draws it, acon Space maps it. Same allowlisted fields as `cards`.
|
|
169
194
|
*/
|
|
170
195
|
attributedCards: OfficeCard[];
|
|
171
|
-
/** Task id -> placement ("room:todo" | "room:work" | "room:review" | "shelf:done" | "desk:<peer>" | "counted:unknown"). */
|
|
196
|
+
/** Task id -> placement ("room:backlog" | "room:todo" | "room:work" | "room:review" | "shelf:done" | "desk:<peer>" | "counted:unknown"). */
|
|
172
197
|
placed: Record<string, string>;
|
|
173
198
|
arcs: OfficeArc[];
|
|
174
199
|
alerts: OfficeAlert[];
|
|
@@ -204,6 +229,17 @@ export function officeModel(snapshot: OfficeSnapshotInput, previous: OfficeScene
|
|
|
204
229
|
const tasks = Array.isArray(snapshot?.tasks) ? snapshot.tasks.filter((task) => task && typeof task.id === "number") : [];
|
|
205
230
|
const permissions = Array.isArray(snapshot?.permissions) ? snapshot.permissions : [];
|
|
206
231
|
const budget = snapshot?.budget && typeof snapshot.budget === "object" ? snapshot.budget : {};
|
|
232
|
+
const date = (value: unknown) => {
|
|
233
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 8_640_000_000_000_000) return "";
|
|
234
|
+
const local = new Date(value);
|
|
235
|
+
return `${local.getFullYear()}-${String(local.getMonth() + 1).padStart(2, "0")}-${String(local.getDate()).padStart(2, "0")}`;
|
|
236
|
+
};
|
|
237
|
+
const rank = (value: unknown) => typeof value === "number" && Number.isFinite(value) ? value : Infinity;
|
|
238
|
+
const backlog = tasks.filter(task => task.state === "backlog").sort((a, b) => (rank(a.rank) - rank(b.rank)) || (rank(a.priority) - rank(b.priority)) || a.id - b.id);
|
|
239
|
+
const unplanned = backlog.filter(task => rank(task.rank) === Infinity && !text(task.reserved)).length;
|
|
240
|
+
const releases: OfficeReleaseInput[] = (Array.isArray(snapshot?.releases) ? snapshot.releases : []).filter(row => row && Number.isSafeInteger(row.id) && row.id > 0 && ["planning", "active", "frozen"].includes(text(row.state)))
|
|
241
|
+
.sort((a, b) => (rank(a.order) - rank(b.order)) || a.id - b.id);
|
|
242
|
+
const docked = releases.slice(0, 3);
|
|
207
243
|
|
|
208
244
|
// State counts mirror taskProgress (src/ui/task-progress.ts) exactly, including the waiting
|
|
209
245
|
// rule on deps; test/office-model.test.ts pins the equality so a drift fails there.
|
|
@@ -272,17 +308,24 @@ export function officeModel(snapshot: OfficeSnapshotInput, previous: OfficeScene
|
|
|
272
308
|
const card = (task: OfficeTaskInput, x: number, y: number, w: number, h: number): OfficeCard => {
|
|
273
309
|
const state = text(task.state) || "unknown";
|
|
274
310
|
const taskClass = text(task.class) || "task";
|
|
275
|
-
const
|
|
311
|
+
const plannedDate = state === "backlog" && date(task.plannedStart) && task.plannedStart! > now ? date(task.plannedStart) : "";
|
|
312
|
+
const waiting = waitingById.get(task.id) === true || state === "backlog" && ((task.deps ?? []).some(id => states.get(id) !== "approved") || !!plannedDate);
|
|
276
313
|
const rework = state === "changes_requested";
|
|
277
314
|
const checking = task.checking === true;
|
|
278
315
|
const owner = text(task.owner);
|
|
279
316
|
const reviewer = text(task.reviewer);
|
|
317
|
+
const reserved = state === "backlog" ? text(task.reserved) : "", pinned = state === "backlog" && task.pinned === true;
|
|
318
|
+
const releaseIndex = docked.findIndex(row => row.id === task.release);
|
|
319
|
+
const releaseTag = releaseIndex < 0 ? undefined : { ordinal: releaseIndex + 1, name: text(docked[releaseIndex]!.name) || "[withheld]" };
|
|
280
320
|
let name = `#${task.id} ${taskClass}: ${text(task.title)} (${state.replaceAll("_", " ")})`;
|
|
281
|
-
if (waiting) name += ", waiting on dependencies";
|
|
321
|
+
if (waiting) name += state === "backlog" ? ", waits" + (plannedDate ? ` until ${plannedDate}` : "") : ", waiting on dependencies";
|
|
322
|
+
if (reserved) name += `, reserved for ${reserved}`;
|
|
323
|
+
if (pinned) name += ", pinned";
|
|
324
|
+
if (releaseTag) name += `, release ${releaseTag.ordinal}: ${releaseTag.name}`;
|
|
282
325
|
if (rework) name += ", rework";
|
|
283
326
|
if (checking) name += ", check running";
|
|
284
327
|
if (state === "in_review") name += `, ${owner || "unassigned"} reviewed by ${reviewer || "unassigned"}`;
|
|
285
|
-
return { id: task.id, taskClass, state, owner, reviewer, waiting, rework, checking, label: `#${task.id} ${text(task.title)}`, name, x, y, w, h };
|
|
328
|
+
return { id: task.id, taskClass, state, owner, reviewer, waiting, rework, checking, reserved, pinned, plannedDate, ...(releaseTag ? { releaseTag } : {}), label: `#${task.id} ${text(task.title)}`, name, x, y, w, h };
|
|
286
329
|
};
|
|
287
330
|
|
|
288
331
|
const desks: OfficeDesk[] = shownIds.map((id) => {
|
|
@@ -320,6 +363,8 @@ export function officeModel(snapshot: OfficeSnapshotInput, previous: OfficeScene
|
|
|
320
363
|
if (finishing) parts.push("finishing a turn");
|
|
321
364
|
if (condition !== "waiting" && count > 0) parts.push(`${count} approval${count === 1 ? "" : "s"} waiting`);
|
|
322
365
|
if (!attached) parts.push("disconnected, work retained");
|
|
366
|
+
const planner = Array.isArray(snapshot.status?.roleInfo?.[id]?.flowRoles) && snapshot.status!.roleInfo![id]!.flowRoles!.includes("planner");
|
|
367
|
+
if (planner) parts.push(`planner, ${unplanned} unplanned backlog cards`);
|
|
323
368
|
// An over-full desk shows two chips and a "+N more" line in the third slot.
|
|
324
369
|
const shown = work.length > DESK_CARDS ? work.slice(0, DESK_CARDS - 1) : work;
|
|
325
370
|
return {
|
|
@@ -331,12 +376,14 @@ export function officeModel(snapshot: OfficeSnapshotInput, previous: OfficeScene
|
|
|
331
376
|
finishing,
|
|
332
377
|
cards: shown.map((task, index) => card(task, x + 0.8, y + 8.5 + index * 2.5, DESK_W - 1.6, 2)),
|
|
333
378
|
extraCards: work.length - shown.length,
|
|
379
|
+
planner, unplanned: planner ? unplanned : 0,
|
|
334
380
|
};
|
|
335
381
|
});
|
|
336
382
|
|
|
337
383
|
const desksBottom = Math.max(0, ...desks.map(desk => desk.y + desk.h));
|
|
338
384
|
const roomsY = desksBottom ? desksBottom + 2 : 0;
|
|
339
385
|
const roomDefs = [
|
|
386
|
+
{ id: "backlog", label: "Backlog", filterState: "backlog", list: backlog, count: counts.backlog ?? 0 },
|
|
340
387
|
{ id: "todo", label: "To do", filterState: "proposed", list: todo, count: counts.proposed + counts.waiting },
|
|
341
388
|
{ id: "work", label: "Work floor", filterState: "work", list: workUnplaced, count: counts.in_progress + counts.changes_requested },
|
|
342
389
|
{ id: "review", label: "Review", filterState: "in_review", list: review, count: counts.in_review },
|
|
@@ -347,12 +394,20 @@ export function officeModel(snapshot: OfficeSnapshotInput, previous: OfficeScene
|
|
|
347
394
|
const y = roomsY + Math.floor(index / cols) * ROOM_DY;
|
|
348
395
|
return {
|
|
349
396
|
id: def.id, label: def.label, filterState: def.filterState, count: def.count,
|
|
397
|
+
...(def.id === "backlog" && !desks.some(desk => desk.planner) ? { unplanned } : {}),
|
|
350
398
|
// One column at 12 px per unit: 28 px cards, 6 px gaps, eight cards at most.
|
|
351
399
|
cards: def.list.slice(0, ROOM_CARDS).map((task, row) => card(task, x + 1, y + 3.4 + row * (ROOM_CARD_H + ROOM_CARD_GAP), ROOM_W - 2, ROOM_CARD_H)),
|
|
352
400
|
overflow: Math.max(0, def.list.length - ROOM_CARDS),
|
|
353
401
|
x, y, w: ROOM_W, h: ROOM_H,
|
|
354
402
|
};
|
|
355
403
|
});
|
|
404
|
+
const docks: OfficeDock[] = docked.map((row, index) => {
|
|
405
|
+
const scope: NonNullable<OfficeReleaseInput["tasks"]> = Array.isArray(row.tasks) ? row.tasks.filter((task: NonNullable<OfficeReleaseInput["tasks"]>[number]) => task && Number.isSafeInteger(task.id) && task.id > 0) : [];
|
|
406
|
+
const approved = scope.filter(task => task.state === "approved").length, position = roomDefs.length + index;
|
|
407
|
+
return { id: row.id, name: text(row.name) || "[withheld]", state: text(row.state), ordinal: index + 1, approved, scope: scope.length,
|
|
408
|
+
ratio: scope.length ? approved / scope.length : 0, cut: date(row.cut), x: (position % cols) * ROOM_DX,
|
|
409
|
+
y: roomsY + Math.floor(position / cols) * ROOM_DY, w: ROOM_W, h: ROOM_H };
|
|
410
|
+
});
|
|
356
411
|
|
|
357
412
|
const placed: Record<string, string> = {};
|
|
358
413
|
for (const task of tasks) { const state = text(task.state);
|
|
@@ -362,7 +417,7 @@ export function officeModel(snapshot: OfficeSnapshotInput, previous: OfficeScene
|
|
|
362
417
|
: state === "approved" ? "shelf:done"
|
|
363
418
|
: state === "cancelled" ? "counted:cancelled"
|
|
364
419
|
: state === "in_progress" || state === "changes_requested" ? (owner && shownSet.has(owner) ? `desk:${owner}` : "room:work")
|
|
365
|
-
: state === "backlog" ? "
|
|
420
|
+
: state === "backlog" ? "room:backlog" : "counted:unknown";
|
|
366
421
|
}
|
|
367
422
|
// A bubble's attributed task may sit past the rendering cut; keep its public card so a consumer that draws more cards than the page can.
|
|
368
423
|
const drawn = new Set<number>([...desks.flatMap((desk) => desk.cards), ...rooms.flatMap((room) => room.cards)].map((entry) => entry.id));
|
|
@@ -454,9 +509,10 @@ export function officeModel(snapshot: OfficeSnapshotInput, previous: OfficeScene
|
|
|
454
509
|
}
|
|
455
510
|
return {
|
|
456
511
|
cols,
|
|
457
|
-
width: Math.max(desksWidth, ...rooms.map(room => room.x + room.w), 1),
|
|
458
|
-
height: Math.max(desksBottom, ...rooms.map(room => room.y + room.h), 1),
|
|
512
|
+
width: Math.max(desksWidth, ...[...rooms, ...docks].map(room => room.x + room.w), 1),
|
|
513
|
+
height: Math.max(desksBottom, ...[...rooms, ...docks].map(room => room.y + room.h), 1),
|
|
459
514
|
desks, extraPeers, rooms, counts, transitions, slots, placed, attributedCards,
|
|
515
|
+
docks, extraReleases: Math.max(0, releases.length - docks.length),
|
|
460
516
|
links,
|
|
461
517
|
arcs: keptArcs, alerts: keptAlerts, latched, settle, at, eventTime, lastSeq,
|
|
462
518
|
};
|
package/src/ui/office-space.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface SpaceScene {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const ZONES: Record<string, { kind: ZoneKind; title: string }> = {
|
|
24
|
+
backlog: { kind: "queue", title: "Backlog" },
|
|
24
25
|
todo: { kind: "queue", title: "To do" }, work: { kind: "work", title: "Work floor" },
|
|
25
26
|
review: { kind: "review", title: "Review" }, done: { kind: "done", title: "Done" },
|
|
26
27
|
};
|
package/src/ui/plugin.ts
CHANGED
|
@@ -46,10 +46,10 @@ export interface ConsolePlugin {
|
|
|
46
46
|
|
|
47
47
|
const TITLES: Record<(typeof DASHBOARD_VIEWS)[number], string> = { board: "Board", office: "Office", stream: "Stream", agents: "Agents", message: "Message", memory: "Memory", settings: "Settings" };
|
|
48
48
|
/** The dashboard's own actions plus the inbox verbs; the manager whitelist is still applied to each forwarded call. */
|
|
49
|
-
const FORWARDED = ["start", "stop", "send", "pause", "resume", "permit", "propose", "assign", "backlog_pull", "backlog_return", "planning", "planning_undo", "planner_pull", "backlog_split", "release_new", "release_change", "setting", "setting_undo", "setting_preview", "setting_reload", "config_edit_preview", "config_edit_apply", "start_peer", "sdlc_queue", "sdlc_release"] as const;
|
|
49
|
+
const FORWARDED = ["start", "stop", "send", "pause", "resume", "permit", "propose", "intake", "assign", "backlog_pull", "backlog_return", "planning", "planning_undo", "planner_pull", "backlog_split", "release_new", "release_change", "setting", "setting_undo", "setting_preview", "setting_reload", "config_edit_preview", "config_edit_apply", "start_peer", "sdlc_queue", "sdlc_release", "worktree_override_set", "worktree_override_clear", "worktrees_refresh_prs"] as const;
|
|
50
50
|
const INBOX_VERBS = ["approve", "deny", "open"] as const;
|
|
51
51
|
/** Outward or destructive: answered needsConfirm first (agent-console AGENTS.md); previews are read-only and stay direct. */
|
|
52
|
-
const CONFIRMED = new Set(["stop", "permit", "setting", "setting_undo", "setting_reload", "config_edit_apply", "start_peer", "backlog_split", "send", "approve", "deny"]);
|
|
52
|
+
const CONFIRMED = new Set(["stop", "permit", "setting", "setting_undo", "setting_reload", "config_edit_apply", "start_peer", "backlog_split", "send", "approve", "deny", "worktree_override_set", "worktrees_refresh_prs"]);
|
|
53
53
|
const fingerprintOf = (input: Record<string, unknown>) => {
|
|
54
54
|
const { confirmed: _c, fingerprint: _f, reason: _r, ...rest } = input;
|
|
55
55
|
return createHash("sha256").update(JSON.stringify(Object.keys(rest).sort().map((key) => [key, rest[key]]))).digest("hex");
|
|
@@ -176,6 +176,17 @@ export function createProvider(options: { home: string; registry?: Registry; lif
|
|
|
176
176
|
verbs: [...(allow ? [{ id: "approve", label: "Allow once", primary: true }] : []), ...(deny ? [{ id: "deny", label: "Deny", destructive: true }] : [])],
|
|
177
177
|
link: { panel: "agents", params: { project: project.id } } });
|
|
178
178
|
}
|
|
179
|
+
// #1057 D4: one alert when the shelf holds more settled worktrees than `worktrees.alert_over` (0 = never). Counts only: no path, branch or title, and no action
|
|
180
|
+
// beyond opening the panel, because removal is the person's `ahub worktrees tidy` and never a button.
|
|
181
|
+
const shelf = snap.worktrees as { settled?: unknown[]; alertOver?: number } | undefined;
|
|
182
|
+
const settledCount = Array.isArray(shelf?.settled) ? shelf!.settled.length : 0;
|
|
183
|
+
if (typeof shelf?.alertOver === "number" && shelf.alertOver > 0 && settledCount > shelf.alertOver) {
|
|
184
|
+
const id = `worktrees:${project.id}`;
|
|
185
|
+
const at = seen.get(id) ?? new Date().toISOString();
|
|
186
|
+
seen.set(id, at);
|
|
187
|
+
items.push({ id, group: "alert", kind: "worktrees", title: `${settledCount} settled worktrees`, detail: `More than ${shelf.alertOver}. Review them in Agents, then run ahub worktrees tidy.`, project: name, at,
|
|
188
|
+
chip: { text: "worktrees", tone: "warn" }, verbs: [{ id: "open", label: "Open Agents", primary: true }], link: { panel: "agents", params: { project: project.id } } });
|
|
189
|
+
}
|
|
179
190
|
for (const t of (snap.tasks as { id: number; title: string; state: string; reviewer: string | null; owner: string | null }[] | undefined) ?? []) {
|
|
180
191
|
if (t.state !== "in_review" || t.reviewer !== "user") continue;
|
|
181
192
|
const id = `review:${project.id}:${t.id}`;
|
|
@@ -231,7 +242,7 @@ export function createProvider(options: { home: string; registry?: Registry; lif
|
|
|
231
242
|
/** approve/deny resolve to the hub's own `permit` with the request's option; nothing else is reachable from an item. */
|
|
232
243
|
async function inboxVerb(input: Record<string, unknown>): Promise<ActionResult> {
|
|
233
244
|
const m = typeof input.id === "string" ? /^perm:([^:]+):(.+)$/.exec(input.id) : null;
|
|
234
|
-
if (input.action === "open") return { ok: true, message: "Open the Board panel to review." };
|
|
245
|
+
if (input.action === "open") return { ok: true, message: typeof input.id === "string" && input.id.startsWith("worktrees:") ? "Open the Agents panel to review the shelf." : "Open the Board panel to review." };
|
|
235
246
|
if (!m) return { ok: false, error: "unknown inbox item" };
|
|
236
247
|
const project = (await running()).find((p) => p.id === m[1]);
|
|
237
248
|
if (!project) return { ok: false, error: "project is not running" };
|