Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
167 commits
Select commit Hold shift + click to select a range
2c469b2
[Spec 1313] Initial specification draft
mohidmakhdoomi Aug 1, 2026
71bcd0d
chore(porch): 1313 init spir
mohidmakhdoomi Aug 1, 2026
1146f13
chore(porch): 1313 specify build-complete
mohidmakhdoomi Aug 1, 2026
ee77a93
[Spec 1313] Specification with multi-agent review
mohidmakhdoomi Aug 1, 2026
ca45786
chore(porch): 1313 spec-approval gate-requested
mohidmakhdoomi Aug 1, 2026
c8f42f3
[Spec 1313] Rebuttal to iteration-1 spec consultation
mohidmakhdoomi Aug 1, 2026
34584b4
[Spec 1313] Thread: record spec-approval gate reached (specify phase …
mohidmakhdoomi Aug 1, 2026
a6dd38a
chore(porch): 1313 spec-approval gate-approved
mohidmakhdoomi Aug 1, 2026
840a4df
chore(porch): 1313 plan phase-transition
mohidmakhdoomi Aug 1, 2026
efea499
[Spec 1313] Initial implementation plan
mohidmakhdoomi Aug 1, 2026
b318367
chore(porch): 1313 plan build-complete
mohidmakhdoomi Aug 1, 2026
4559f5d
[Spec 1313] Plan with multi-agent review
mohidmakhdoomi Aug 1, 2026
ea3ecc6
chore(porch): 1313 plan re-iter (iter 2)
mohidmakhdoomi Aug 1, 2026
bd31218
chore(porch): 1313 plan build-complete
mohidmakhdoomi Aug 1, 2026
b6a0e04
[Spec 1313] Plan iter-2: implementation-seam fixes
mohidmakhdoomi Aug 1, 2026
db8a82e
chore(porch): 1313 plan-approval gate-requested
mohidmakhdoomi Aug 1, 2026
f3928ec
[Spec 1313] Thread: plan-approval gate reached (plan phase complete)
mohidmakhdoomi Aug 1, 2026
4d92549
[Spec 1313] Plan iter-2 consultation context (porch artifact)
mohidmakhdoomi Aug 1, 2026
06d0dc3
chore(porch): 1313 plan-approval gate-approved
mohidmakhdoomi Aug 1, 2026
f464bd8
chore(porch): 1313 implement phase-transition
mohidmakhdoomi Aug 1, 2026
4bd4d4e
[Spec 1313][Phase: mailbox-persistence] feat: Add mailbox persistence…
mohidmakhdoomi Aug 1, 2026
389001e
[Spec 1313] Thread: Phase 1 (mailbox store) implemented + verified green
mohidmakhdoomi Aug 1, 2026
59e1f83
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 1, 2026
b661d4c
chore(porch): 1313 advance plan phase → phase_2
mohidmakhdoomi Aug 1, 2026
9ea7ea4
[Spec 1313][Phase: render-gate] feat: Add rendered-empty gate + claud…
mohidmakhdoomi Aug 1, 2026
a781207
[Spec 1313] Thread: Phase 2 (render-gate) committed + verified green
mohidmakhdoomi Aug 1, 2026
b433487
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 1, 2026
e57182d
[Spec 1313][Phase: render-gate] fix: address iter-1 review (claude-pi…
mohidmakhdoomi Aug 1, 2026
6361045
[Spec 1313] Phase 2 iter-1 rebuttal (porch artifact)
mohidmakhdoomi Aug 1, 2026
c9947eb
[Spec 1313] Thread: Phase 2 iter-1 review addressed (claude-picker, p…
mohidmakhdoomi Aug 1, 2026
4b1a1cd
chore(porch): 1313 implement re-iter (iter 2)
mohidmakhdoomi Aug 1, 2026
333195f
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 1, 2026
c3320e9
chore(porch): 1313 advance plan phase → phase_3
mohidmakhdoomi Aug 1, 2026
dbea5fd
[Spec 1313][Phase: agy-profile] feat: Add agy classifier profile (col…
mohidmakhdoomi Aug 1, 2026
36b9eee
[Spec 1313] Thread: Phase 3 (agy profile) — measurement note + implem…
mohidmakhdoomi Aug 1, 2026
b07e40c
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 1, 2026
bef818b
chore(porch): 1313 advance plan phase → phase_4
mohidmakhdoomi Aug 1, 2026
a7e1c14
[Spec 1313] Thread: Phase 3 approved (unanimous); phase_4 scope recorded
mohidmakhdoomi Aug 1, 2026
7f3e1c5
[Spec 1313] Thread: Phase 4 design note (recovery anchor)
mohidmakhdoomi Aug 1, 2026
7dcfb0b
[Spec 1313][Phase: delivery-orchestration] feat: mailbox-first send d…
mohidmakhdoomi Aug 1, 2026
34e0808
[Spec 1313] Thread: Phase 4 committed — e2e root-caused (pre-existing…
mohidmakhdoomi Aug 1, 2026
25fbe12
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 1, 2026
c6862f8
[Spec 1313][Phase: delivery-orchestration] fix: address phase-4 revie…
mohidmakhdoomi Aug 1, 2026
65ddeab
[Spec 1313] Thread: Phase 4 review iter1 — 3 Codex fixes landed (rete…
mohidmakhdoomi Aug 1, 2026
45f1818
chore(porch): 1313 implement re-iter (iter 2)
mohidmakhdoomi Aug 1, 2026
c008441
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 1, 2026
b56423b
chore(porch): 1313 advance plan phase → phase_5
mohidmakhdoomi Aug 1, 2026
dca07f9
[Spec 1313] Thread: Phase 4 approved (unanimous iter2); starting phase_5
mohidmakhdoomi Aug 1, 2026
3a79e71
[Spec 1313][Phase: fast-delivery-triggers] feat: submit + quiescence …
mohidmakhdoomi Aug 1, 2026
5e76dcd
[Spec 1313] Thread: Phase 5 implemented (submit+quiescence triggers);…
mohidmakhdoomi Aug 1, 2026
3cf0c6a
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 1, 2026
90272ae
[Spec 1313][Phase: fast-delivery-triggers] fix: consolidate user-inpu…
mohidmakhdoomi Aug 1, 2026
abce011
[Spec 1313] Thread: Phase 5 review iter1 — Codex fix landed (consolid…
mohidmakhdoomi Aug 1, 2026
db78edd
chore(porch): 1313 implement re-iter (iter 2)
mohidmakhdoomi Aug 1, 2026
c47adf3
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 1, 2026
1f94770
chore(porch): 1313 advance plan phase → phase_6
mohidmakhdoomi Aug 1, 2026
12dfa45
[Spec 1313][Phase: cron-rerouting] feat: route cron delivery through …
mohidmakhdoomi Aug 1, 2026
15feff1
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 1, 2026
5ad85c1
chore(porch): 1313 advance plan phase → phase_7
mohidmakhdoomi Aug 1, 2026
540f1e9
[Spec 1313][Phase: inbox-broadcasts-escalation] feat: afx inbox CLI, …
mohidmakhdoomi Aug 1, 2026
bdc48b7
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 1, 2026
25e6cdf
chore(porch): 1313 implement re-iter (iter 2)
mohidmakhdoomi Aug 1, 2026
3b99a34
[Spec 1313][Phase: inbox-broadcasts-escalation] fix: address iter-1 C…
mohidmakhdoomi Aug 1, 2026
9dcbd82
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 1, 2026
58ce276
chore(porch): 1313 implement re-iter (iter 3)
mohidmakhdoomi Aug 1, 2026
8794245
[Spec 1313][Phase: inbox-broadcasts-escalation] fix: scope `afx inbox…
mohidmakhdoomi Aug 1, 2026
1d7bb26
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 1, 2026
6e676bc
[Spec 1313][Phase: inbox-broadcasts-escalation] fix: guard `POST /api…
mohidmakhdoomi Aug 1, 2026
70c8ddd
chore(porch): 1313 implement force-advance (safety ceiling reached at…
mohidmakhdoomi Aug 1, 2026
1d3337b
chore(porch): 1313 advance plan phase → phase_8
mohidmakhdoomi Aug 1, 2026
0bbea9d
[Spec 1313][Phase: dashboard-vscode-indicators] feat: held-count indi…
mohidmakhdoomi Aug 1, 2026
2b32a32
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 1, 2026
3656039
[Spec 1313][Phase: dashboard-vscode-indicators] test: Playwright dash…
mohidmakhdoomi Aug 1, 2026
a4ab675
chore(porch): 1313 implement re-iter (iter 2)
mohidmakhdoomi Aug 1, 2026
a8c3aac
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 1, 2026
52de4a1
chore(porch): 1313 advance plan phase → phase_9
mohidmakhdoomi Aug 1, 2026
fe53dea
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 1, 2026
c197951
chore(porch): 1313 implement re-iter (iter 2)
mohidmakhdoomi Aug 1, 2026
da1c083
[Spec 1313][Phase: documentation-skeleton-mirror] docs: afx send outc…
mohidmakhdoomi Aug 1, 2026
c21bd5c
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 1, 2026
eb94fa3
chore(porch): 1313 all plan phases complete → review
mohidmakhdoomi Aug 1, 2026
2407e90
[Spec 1313][Phase: review] docs: review doc + arch/lessons routing
mohidmakhdoomi Aug 1, 2026
35a64b5
chore(porch): 1313 review build-complete
mohidmakhdoomi Aug 1, 2026
62a5c3b
[Spec 1313][Phase: review] fix: close two mailbox delivery races (ite…
mohidmakhdoomi Aug 1, 2026
efbe65a
chore(porch): 1313 pr gate-requested
mohidmakhdoomi Aug 1, 2026
559e05e
[Spec 1313][Phase: review] feat: add `afx inbox show <id>` (per-id bo…
mohidmakhdoomi Aug 1, 2026
5f58a84
chore(porch): 1313 pr gate-approved
mohidmakhdoomi Aug 1, 2026
75b9c59
chore(porch): 1313 verify phase-transition
mohidmakhdoomi Aug 1, 2026
0d1d378
chore(porch): 1313 verify-approval gate-requested
mohidmakhdoomi Aug 1, 2026
53275cb
[Spec 1313][Phase: review] test: CI-aware bound for render-gate seed-…
mohidmakhdoomi Aug 1, 2026
3424b16
[Spec 1313][Phase: review] fix: resolve afx send to architects (rende…
mohidmakhdoomi Aug 2, 2026
a4a7a80
[Spec 1313][Phase: review] fix: honor TOWER_ARCHITECT_CMD in reconcil…
mohidmakhdoomi Aug 2, 2026
9c978a9
[Spec 1313][Phase: review] docs: record round-2/3 re-CMAP + migration…
mohidmakhdoomi Aug 2, 2026
e3d3454
[Spec 1313][Phase: review] fix: render-gate whole-ring hardening (cap…
mohidmakhdoomi Aug 2, 2026
32db9f1
[Spec 1313][Phase: verify] docs: record live e2e ALL-PASS in builder …
mohidmakhdoomi Aug 2, 2026
dd3644f
chore(porch): 1313 rollback verify → implement
mohidmakhdoomi Aug 2, 2026
4fd27e5
[Spec 1313][Phase: implement] fix: reconcile merge-integration test f…
mohidmakhdoomi Aug 3, 2026
761adcd
[Spec 1313][Phase: implement] feat: remove render-gate over-ceiling h…
mohidmakhdoomi Aug 3, 2026
3e0741c
[Spec 1313][Phase: implement] fix: address 3-way CMAP round 1 on over…
mohidmakhdoomi Aug 3, 2026
bcc1b7a
[Spec 1313][Phase: implement] fix: address 3-way CMAP round 2 (verifi…
mohidmakhdoomi Aug 3, 2026
5e411a1
[Spec 1313][Phase: implement] fix: address 3-way CMAP round 3 (verify…
mohidmakhdoomi Aug 3, 2026
b2eb6d2
[Spec 1313][Phase: implement] fix: address 3-way CMAP round 4 (verify…
mohidmakhdoomi Aug 3, 2026
a3c4ea1
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 3, 2026
96f9d1a
chore(porch): 1313 advance plan phase → phase_2
mohidmakhdoomi Aug 3, 2026
0f47994
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 3, 2026
df42430
chore(porch): 1313 implement re-iter (iter 2)
mohidmakhdoomi Aug 3, 2026
b5cb5d1
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 3, 2026
9aace88
chore(porch): 1313 advance plan phase → phase_3
mohidmakhdoomi Aug 3, 2026
6b600eb
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 3, 2026
804ab6d
chore(porch): 1313 advance plan phase → phase_4
mohidmakhdoomi Aug 3, 2026
b82685b
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 3, 2026
5c5e4f4
chore(porch): 1313 implement re-iter (iter 2)
mohidmakhdoomi Aug 3, 2026
3646d03
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 3, 2026
db3f962
chore(porch): 1313 advance plan phase → phase_5
mohidmakhdoomi Aug 3, 2026
7ec3e4e
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 3, 2026
6e905a7
chore(porch): 1313 implement re-iter (iter 2)
mohidmakhdoomi Aug 3, 2026
504e894
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 3, 2026
fc2834a
chore(porch): 1313 advance plan phase → phase_6
mohidmakhdoomi Aug 3, 2026
2037b36
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 3, 2026
f23b67c
chore(porch): 1313 advance plan phase → phase_7
mohidmakhdoomi Aug 3, 2026
b758253
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 3, 2026
9851af2
chore(porch): 1313 implement re-iter (iter 2)
mohidmakhdoomi Aug 3, 2026
23c59a8
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 3, 2026
82ff91f
chore(porch): 1313 implement re-iter (iter 3)
mohidmakhdoomi Aug 3, 2026
f0af5d6
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 3, 2026
b56776d
chore(porch): 1313 implement force-advance (safety ceiling reached at…
mohidmakhdoomi Aug 3, 2026
d8fda8d
chore(porch): 1313 advance plan phase → phase_8
mohidmakhdoomi Aug 3, 2026
2edf4a6
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 3, 2026
9e91eec
chore(porch): 1313 implement re-iter (iter 2)
mohidmakhdoomi Aug 3, 2026
f35e647
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 3, 2026
013920b
chore(porch): 1313 advance plan phase → phase_9
mohidmakhdoomi Aug 3, 2026
0774347
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 3, 2026
f0b1809
chore(porch): 1313 implement re-iter (iter 2)
mohidmakhdoomi Aug 3, 2026
707dc87
[Spec 1313][Phase: implement] docs: thread — resume for phase_9 finis…
mohidmakhdoomi Aug 3, 2026
d40f1fa
chore(porch): 1313 implement build-complete
mohidmakhdoomi Aug 3, 2026
50d2e2d
chore(porch): 1313 all plan phases complete → review
mohidmakhdoomi Aug 3, 2026
38b0bac
[Spec 1313][Phase: review] docs: rewrite review from scratch + arch/l…
mohidmakhdoomi Aug 3, 2026
181f36e
chore(porch): 1313 review build-complete
mohidmakhdoomi Aug 3, 2026
8f1ca1a
chore(porch): 1313 review re-iter (iter 2)
mohidmakhdoomi Aug 3, 2026
4ffb438
chore(porch): 1313 review build-complete
mohidmakhdoomi Aug 3, 2026
f31dce4
[Spec 1313][Phase: review] docs: address review round-2 COMMENT (Stat…
mohidmakhdoomi Aug 3, 2026
19e0c76
chore(porch): 1313 pr gate-requested
mohidmakhdoomi Aug 3, 2026
9e12a70
[Spec 1313][Phase: review] docs: thread — STOPPED at pr gate (review …
mohidmakhdoomi Aug 3, 2026
f34fa9b
[Spec 1313][Phase: review] fix: hold on dropped PTY write (silent-los…
mohidmakhdoomi Aug 3, 2026
28f37f5
[Spec 1313][Phase: review] docs: thread — round-3 fix landed, re-veri…
mohidmakhdoomi Aug 3, 2026
ea96490
[Spec 1313][Phase: review] revert: restore spec-1280 T16 guard (1280 …
mohidmakhdoomi Aug 3, 2026
ff88906
[Spec 1313][Phase: review] docs: thread — spec-1280 T16 restored (128…
mohidmakhdoomi Aug 3, 2026
dece1a5
[Spec 1313][Phase: review] revert: restore CLAUDE.md/AGENTS.md to ori…
mohidmakhdoomi Aug 6, 2026
413b244
[Spec 1313][Phase: review] docs: qualify "T16 passes" as rebased-stat…
mohidmakhdoomi Aug 6, 2026
622b5e0
[Spec 1313][Phase: review] docs: thread — rebase task + 1307 --delay …
mohidmakhdoomi Aug 6, 2026
b2a08a8
[Spec 1313][Phase: review] reconcile: re-home Spec 1307 --delay onto …
mohidmakhdoomi Aug 6, 2026
517a264
[Spec 1313][Phase: review] docs: restore hot-tier cap — demote Spec 9…
mohidmakhdoomi Aug 6, 2026
46232e1
[Spec 1313][Phase: review] docs: thread — 1307 --delay contract verif…
mohidmakhdoomi Aug 6, 2026
705d2fc
[Spec 1313][Phase: review] fix: render gate — exempt claude's suggest…
mohidmakhdoomi Aug 6, 2026
39152cd
[Spec 1313][Phase: review] docs: record ghost-cursor fix — review rou…
mohidmakhdoomi Aug 6, 2026
82db1c4
[Spec 1313][Phase: review] docs: thread — ghost-cursor false-busy PR-…
mohidmakhdoomi Aug 6, 2026
345ac8d
[Spec 1313][Phase: review] fix: ghost exemption requires a non-empty …
mohidmakhdoomi Aug 6, 2026
68d74c5
[Spec 1313][Phase: review] docs: record ghost-exemption tightening (C…
mohidmakhdoomi Aug 6, 2026
8bedc5e
[Spec 1313][Phase: review] fix: reconcile render gate with #1205 capp…
mohidmakhdoomi Aug 6, 2026
5f14473
[Spec 1313][Phase: review] docs: round-2 persistent-mirror — arch §7 …
mohidmakhdoomi Aug 6, 2026
4f95fa1
[Spec 1313][Phase: review] docs: thread — round-2 pushed + re-parked …
mohidmakhdoomi Aug 6, 2026
e8070fb
chore(porch): 1313 pr gate-approved
mohidmakhdoomi Aug 6, 2026
9dd637f
chore(porch): 1313 verify phase-transition
mohidmakhdoomi Aug 7, 2026
a8d8477
[Spec 1313][Phase: review] fix: durable --delay, delayed-interrupt re…
mohidmakhdoomi Aug 7, 2026
cb7d10d
[Spec 1313][Phase: review] docs: maintainer-round doc re-truing + rev…
mohidmakhdoomi Aug 7, 2026
9f7eeb4
[Spec 1313][Phase: review] fix: hermetic stripAnsi + owner-notice onc…
mohidmakhdoomi Aug 7, 2026
7d69977
[Spec 1313][Phase: review] docs: architect re-review round — required…
mohidmakhdoomi Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,11 @@
"default": true,
"description": "Show a VSCode notification toast when a builder reaches a human-approval gate (plan-approval, code-review, etc.)"
},
"codev.mailboxEscalationToasts.enabled": {
"type": "boolean",
"default": true,
"description": "Show a VSCode notification toast when a held afx-send message crosses the escalation age (default 60s). The persistent held-count status-bar indicator is unaffected by this toggle. Read or dismiss held messages with the `afx inbox` CLI."
},
"codev.overviewRefreshSeconds": {
"type": "number",
"default": 60,
Expand Down
131 changes: 131 additions & 0 deletions apps/vscode/src/__tests__/mailbox-escalation-toast.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* Spec 1313 Phase 8: unit tests for the `mailbox-escalation` toast handler.
* `vscode` is mocked (this is a `src/__tests__` vitest unit, not the Electron
* `src/test` harness); we drive the SSE callback the handler subscribes to and
* assert on `window.showWarningMessage`.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';

const h = vi.hoisted(() => ({
showWarningMessage: vi.fn(),
getBool: vi.fn((_key: string, dflt: boolean) => dflt),
}));

vi.mock('vscode', () => ({
window: { showWarningMessage: h.showWarningMessage },
workspace: {
getConfiguration: () => ({ get: (key: string, dflt: boolean) => h.getBool(key, dflt) }),
},
}));

const { activateMailboxEscalationToasts } = await import('../notifications/mailbox-escalation-toast.js');

type SSEHandler = (e: { type: string; data: string }) => void;

function makeCtx() {
return { subscriptions: [] as { dispose(): void }[] };
}

function makeConnectionManager(workspacePath: string | null) {
let handler: SSEHandler | null = null;
return {
getWorkspacePath: () => workspacePath,
onSSEEvent: (fn: SSEHandler) => {
handler = fn;
return { dispose() {} };
},
/** Simulate Tower pushing an SSE `data:` payload. */
fire: (data: string) => handler?.({ type: 'message', data }),
};
}

function escalationEvent(overrides: Record<string, unknown> = {}): string {
const payload = {
workspacePath: '/ws',
toAgent: 'spir-1',
mailboxId: 'mb1',
ageMs: 65_000,
reason: 'busy',
...overrides,
};
return JSON.stringify({ type: 'mailbox-escalation', body: JSON.stringify(payload) });
}

function activate(cm: ReturnType<typeof makeConnectionManager>) {
const ctx = makeCtx();
// Structural fakes stand in for vscode.ExtensionContext / ConnectionManager.
activateMailboxEscalationToasts(ctx as any, cm as any);
return ctx;
}

beforeEach(() => {
h.showWarningMessage.mockClear();
h.getBool.mockReset();
h.getBool.mockImplementation((_key: string, dflt: boolean) => dflt);
});

describe('activateMailboxEscalationToasts', () => {
it('raises a warning toast for a matching escalation, with metadata (no body)', () => {
const cm = makeConnectionManager('/ws');
activate(cm);
cm.fire(escalationEvent({ toAgent: 'architect:main', ageMs: 63_000, reason: 'busy' }));

expect(h.showWarningMessage).toHaveBeenCalledTimes(1);
const msg = h.showWarningMessage.mock.calls[0][0] as string;
expect(msg).toContain('architect:main');
expect(msg).toContain('63s');
expect(msg).toContain('afx inbox');
});

it('dedupes by mailboxId — a redelivered event does not re-toast', () => {
const cm = makeConnectionManager('/ws');
activate(cm);
cm.fire(escalationEvent({ mailboxId: 'dup' }));
cm.fire(escalationEvent({ mailboxId: 'dup' }));
expect(h.showWarningMessage).toHaveBeenCalledTimes(1);
});

it('toasts again for a different mailboxId', () => {
const cm = makeConnectionManager('/ws');
activate(cm);
cm.fire(escalationEvent({ mailboxId: 'a' }));
cm.fire(escalationEvent({ mailboxId: 'b' }));
expect(h.showWarningMessage).toHaveBeenCalledTimes(2);
});

it('ignores escalations for a different workspace on a shared Tower', () => {
const cm = makeConnectionManager('/ws');
activate(cm);
cm.fire(escalationEvent({ workspacePath: '/other' }));
expect(h.showWarningMessage).not.toHaveBeenCalled();
});

it('ignores non-escalation SSE envelope types', () => {
const cm = makeConnectionManager('/ws');
activate(cm);
cm.fire(JSON.stringify({ type: 'overview-changed', body: '{}' }));
expect(h.showWarningMessage).not.toHaveBeenCalled();
});

it('ignores malformed (non-JSON) SSE data without throwing', () => {
const cm = makeConnectionManager('/ws');
activate(cm);
expect(() => cm.fire('not-json')).not.toThrow();
expect(h.showWarningMessage).not.toHaveBeenCalled();
});

it('does not toast when disabled via codev.mailboxEscalationToasts.enabled', () => {
h.getBool.mockImplementation(() => false);
const cm = makeConnectionManager('/ws');
activate(cm);
cm.fire(escalationEvent());
expect(h.showWarningMessage).not.toHaveBeenCalled();
});

it('ignores a payload missing its mailboxId', () => {
const cm = makeConnectionManager('/ws');
activate(cm);
cm.fire(escalationEvent({ mailboxId: '' }));
expect(h.showWarningMessage).not.toHaveBeenCalled();
});
});
179 changes: 179 additions & 0 deletions apps/vscode/src/__tests__/mailbox-indicators.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* Spec 1313 Phase 8: pure unit tests for the VSCode held-mail indicator
* helpers. No `vscode` mock — these are deliberately vscode-free so the
* count / tooltip / attention / toast-text math is testable in isolation.
*/
import { describe, it, expect } from 'vitest';
import {
heldStatusSegment,
heldTooltipClause,
heldBadgeCount,
composeStatusBarText,
composeActivityBadge,
escalationToastText,
escalationMatchesWorkspace,
} from '../mailbox-indicators.js';

function makePayload(overrides: Partial<{
workspacePath: string;
toAgent: string;
mailboxId: string;
ageMs: number;
reason: string | null;
}> = {}) {
return {
workspacePath: '/ws',
toAgent: 'spir-1',
mailboxId: 'mb1',
ageMs: 65_000,
reason: 'busy' as string | null,
...overrides,
};
}

describe('heldStatusSegment', () => {
it('is empty when nothing is held', () => {
expect(heldStatusSegment(0, false)).toBe('');
expect(heldStatusSegment(0, true)).toBe('');
});

it('is empty for a negative or absent count (defensive)', () => {
expect(heldStatusSegment(-1, false)).toBe('');
// Simulates an older Tower that omits the field (undefined at runtime).
expect(heldStatusSegment(undefined as unknown as number, false)).toBe('');
});

it('renders a mail-icon segment when held and not escalated', () => {
expect(heldStatusSegment(2, false)).toBe(' · $(mail) 2 held');
});

it('swaps to the warning icon when escalated (the attention state)', () => {
expect(heldStatusSegment(2, true)).toBe(' · $(warning) 2 held');
});
});

describe('heldTooltipClause', () => {
it('is empty when nothing is held', () => {
expect(heldTooltipClause(0)).toBe('');
expect(heldTooltipClause(-3)).toBe('');
});

it('is singular for one and plural for many', () => {
expect(heldTooltipClause(1)).toBe('1 held message');
expect(heldTooltipClause(4)).toBe('4 held messages');
});
});

describe('heldBadgeCount', () => {
it('clamps negatives and absent values to 0', () => {
expect(heldBadgeCount(-1)).toBe(0);
expect(heldBadgeCount(0)).toBe(0);
expect(heldBadgeCount(undefined as unknown as number)).toBe(0);
});

it('passes a positive count through unchanged', () => {
expect(heldBadgeCount(5)).toBe(5);
});
});

describe('composeStatusBarText', () => {
it('renders the base builder count with no extras when nothing needs attention', () => {
expect(composeStatusBarText(2, 0, 0, 0, false)).toBe('$(server) Codev: 2 builders');
});

it('appends blocked, waiting, and held segments in order', () => {
expect(composeStatusBarText(3, 1, 2, 4, false)).toBe(
'$(server) Codev: 3 builders · $(bell) 1 blocked · $(comment-discussion) 2 waiting · $(mail) 4 held',
);
});

it('uses the warning icon for the held segment when escalated', () => {
expect(composeStatusBarText(1, 0, 0, 2, true)).toBe('$(server) Codev: 1 builders · $(warning) 2 held');
});

it('omits the held segment entirely when nothing is held', () => {
expect(composeStatusBarText(5, 1, 0, 0, true)).toBe('$(server) Codev: 5 builders · $(bell) 1 blocked');
});
});

describe('composeActivityBadge', () => {
it('is undefined when nothing needs the user', () => {
expect(composeActivityBadge(0, 0, 0)).toBeUndefined();
// A negative/absent held count is clamped, so it cannot fabricate a badge.
expect(composeActivityBadge(0, 0, -2)).toBeUndefined();
});

it('folds held-only into the badge with a held tooltip', () => {
expect(composeActivityBadge(0, 0, 3)).toEqual({ value: 3, tooltip: '3 held messages' });
});

it('preserves the singular/plural blocked-only phrasing', () => {
expect(composeActivityBadge(1, 0, 0)).toEqual({ value: 1, tooltip: '1 builder blocked at a human-approval gate' });
expect(composeActivityBadge(2, 0, 0)).toEqual({ value: 2, tooltip: '2 builders blocked at human-approval gates' });
});

it('preserves the idle-only phrasing', () => {
expect(composeActivityBadge(0, 1, 0)).toEqual({ value: 1, tooltip: '1 builder waiting on input' });
});

it('combines blocked + idle with the compact phrasing', () => {
expect(composeActivityBadge(2, 3, 0)).toEqual({ value: 5, tooltip: '2 blocked, 3 waiting on input' });
});

it('folds held into blocked + idle and joins the clauses', () => {
expect(composeActivityBadge(1, 1, 2)).toEqual({
value: 4,
tooltip: '1 blocked, 1 waiting on input · 2 held messages',
});
expect(composeActivityBadge(2, 0, 1)).toEqual({
value: 3,
tooltip: '2 builders blocked at human-approval gates · 1 held message',
});
});
});

describe('escalationToastText', () => {
it('names the recipient, the held duration in seconds, and the why-held reason', () => {
const text = escalationToastText(makePayload({ toAgent: 'architect:main', ageMs: 62_000, reason: 'busy' }));
expect(text).toContain('architect:main');
expect(text).toContain('62s');
expect(text).toContain('(busy)');
expect(text).toContain('afx inbox');
});

it('omits the reason parens when the reason is null', () => {
const text = escalationToastText(makePayload({ reason: null }));
expect(text).not.toContain('(');
});

it('rounds sub-second/odd ages and never goes negative', () => {
expect(escalationToastText(makePayload({ ageMs: 60_500 }))).toContain('61s');
expect(escalationToastText(makePayload({ ageMs: -10 }))).toContain('0s');
});

it('carries no message body (redaction — payload has none to leak)', () => {
// The payload type has no body field; assert the text is metadata only by
// confirming it is fully determined by the metadata we passed.
const text = escalationToastText(makePayload({ toAgent: 'b', ageMs: 60_000, reason: 'no-profile' }));
expect(text).toBe('Codev: a message to b has been held 60s (no-profile) — past the escalation age. Review with: afx inbox');
});
});

describe('escalationMatchesWorkspace', () => {
it('matches an identical path', () => {
expect(escalationMatchesWorkspace('/ws/a', '/ws/a')).toBe(true);
});

it('normalizes trailing slashes and . / .. segments', () => {
expect(escalationMatchesWorkspace('/ws/a/', '/ws/a')).toBe(true);
expect(escalationMatchesWorkspace('/ws/a/../a', '/ws/a')).toBe(true);
});

it('rejects a different workspace', () => {
expect(escalationMatchesWorkspace('/ws/a', '/ws/b')).toBe(false);
});

it('matches everything when no active workspace is known yet (startup)', () => {
expect(escalationMatchesWorkspace('/ws/a', null)).toBe(true);
});
});
41 changes: 26 additions & 15 deletions apps/vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import { connectTunnel, disconnectTunnel } from './commands/tunnel.js';
import { listCronTasks } from './commands/cron.js';
import { addReviewComment } from './commands/review.js';
import { activateGateToasts } from './notifications/gate-toast.js';
import { activateMailboxEscalationToasts } from './notifications/mailbox-escalation-toast.js';
import { composeStatusBarText, composeActivityBadge } from './mailbox-indicators.js';
import { activateReviewDecorations } from './review-decorations.js';
import { activateReviewComments } from './comments/plan-review.js';
import { MarkdownPreviewProvider } from './markdown-preview/preview-provider.js';
Expand Down Expand Up @@ -360,10 +362,19 @@ export async function activate(context: vscode.ExtensionContext) {
const now = Date.now();
const blockedCount = data.builders.filter(b => b.blocked).length;
const idleCount = data.builders.filter(b => isIdleWaiting(b, now)).length;
let text = `$(server) Codev: ${builderCount} builders`;
if (blockedCount > 0) { text += ` · $(bell) ${blockedCount} blocked`; }
if (idleCount > 0) { text += ` · $(comment-discussion) ${idleCount} waiting`; }
statusBarItem.text = text;
// Spec 1313 Phase 8: workspace-wide held-mail count (all recipients, incl.
// architects — the authoritative `data.heldCount`, not a per-builder sum),
// with a warning-flavored attention state once a held row has escalated.
// The text/fold logic is pure + unit-tested in `composeStatusBarText`.
const heldCount = data.heldCount;
const escalated = data.mailboxEscalated === true;
statusBarItem.text = composeStatusBarText(builderCount, blockedCount, idleCount, heldCount, escalated);
// Amber background is the persistent, log-free attention state for the count;
// it clears when the escalated row resolves (an overview refetch on the
// held-state-change broadcast flips `mailboxEscalated` back to false).
statusBarItem.backgroundColor = (heldCount > 0 && escalated)
? new vscode.ThemeColor('statusBarItem.warningBackground')
: undefined;
};

// List views show their item count in the title: "Agents (3)".
Expand Down Expand Up @@ -412,17 +423,12 @@ export async function activate(context: vscode.ExtensionContext) {
const now = Date.now();
const blockedCount = data.builders.filter(b => b.blocked).length;
const idleCount = data.builders.filter(b => isIdleWaiting(b, now)).length;
const total = blockedCount + idleCount;
if (total === 0) {
buildersView.badge = undefined;
return;
}
const tooltip = (blockedCount > 0 && idleCount > 0)
? `${blockedCount} blocked, ${idleCount} waiting on input`
: blockedCount > 0
? (blockedCount === 1 ? '1 builder blocked at a human-approval gate' : `${blockedCount} builders blocked at human-approval gates`)
: (idleCount === 1 ? '1 builder waiting on input' : `${idleCount} builders waiting on input`);
buildersView.badge = { value: total, tooltip };
// Spec 1313 Phase 8: fold the workspace held-mail count into the badge so the
// activity-bar icon reflects it even when the sidebar is collapsed. Held is a
// count only (not a per-builder "needs me" gate); the tooltip disambiguates it
// from the blocked/idle signals. The fold + tooltip composition (and the
// undefined-when-empty clear) is pure + unit-tested in `composeActivityBadge`.
buildersView.badge = composeActivityBadge(blockedCount, idleCount, data.heldCount);
};

// Close builder/dev terminal tabs when their builder disappears from the
Expand Down Expand Up @@ -1360,6 +1366,11 @@ export async function activate(context: vscode.ExtensionContext) {
// user to watch the Builders tree. Respects `codev.gateToasts.enabled`.
activateGateToasts(context, overviewCache);

// Spec 1313 Phase 8: toast when a held message crosses the escalation age
// (the `mailbox-escalation` SSE event). Visibility only — read/dismiss via
// `afx inbox`. Respects `codev.mailboxEscalationToasts.enabled`.
activateMailboxEscalationToasts(context, connectionManager);

// Auto-open builder terminals on Tower spawn events
const builderSpawnHandler = new BuilderSpawnHandler(connectionManager, terminalManager, outputChannel);
context.subscriptions.push(
Expand Down
Loading
Loading