Problem
The Builders tree treats every gate-blocked builder as equally urgent — they all sort to the top with a bell icon and a wait-time suffix. In practice, "blocked" splits into two very different cases:
- Truly blocked — the builder hit a gate (e.g.
plan-approval) and nobody has touched it since. This is the one that wants attention.
- Blocked while the architect is actively in dialog with it — the gate is open but the engineer is mid-conversation, sending messages, iterating. Surfacing this row at the top with the same urgency as a truly-stuck builder is noise; the engineer already knows it's there because they're typing to it right now.
The current top-bucket placement makes the second case feel like an alarm that won't quiet.
Current state
- Sort order in
packages/vscode/src/views/builders.ts:23–28 (orderForDisplay):
blocked (any b.blocked truthy), sorted by blockedSince ascending.
idleWaiting (isIdleWaiting(b, now)).
active (everything else).
OverviewBuilder.lastDataAt (@cluesmith/codev-types, exposed via overview.ts:73) already tracks when Tower last received output from the builder's shell — updated whenever the architect's input echoes back or the agent emits anything.
isIdleWaiting(b) (packages/core/src/builder-helpers.ts:30) returns true for non-blocked, non-complete builders silent past IDLE_WAITING_THRESHOLD_MS = 5 minutes (line 19). The same threshold is the natural cut-off for "actively communicating" applied to blocked builders.
Proposed behavior
1. New predicate
Add isActivelyCommBlocked(b, now) in packages/core/src/builder-helpers.ts:
export function isActivelyCommBlocked(b: OverviewBuilder, now: number = Date.now()): boolean {
if (!b.blocked) return false;
if (!b.lastDataAt) return false;
return now - new Date(b.lastDataAt).getTime() <= IDLE_WAITING_THRESHOLD_MS;
}
Co-located with isIdleWaiting so the threshold policy stays in one place.
2. New four-bucket sort order
Top-down:
- Truly blocked —
b.blocked && !isActivelyCommBlocked(b). Sorted longest-blockedSince first.
- Idle waiting —
isIdleWaiting(b) (unchanged).
- Active — everything that doesn't match the others (unchanged).
- Actively-comm blocked —
isActivelyCommBlocked(b). Sorted most-recent-lastDataAt first (so the one you're typing into right now is at the bottom-most slot — close to where the input cursor sits in the terminal pane).
3. Visual treatment
- Truly blocked: existing bell icon + wait-time suffix; unchanged.
- Actively-comm blocked: distinct icon (e.g.
$(comment-discussion) instead of $(bell)) and the row label still surfaces the gate (blocked on <gate>) so the user remembers which gate is in play — but the visual weight is de-emphasised (no wait-time scream).
- Idle-waiting and active: unchanged.
4. Threshold reuse
Use the existing IDLE_WAITING_THRESHOLD_MS = 5 minutes — no new tunable. A blocked builder you haven't typed to in >5 minutes promotes itself back to "truly blocked" (top bucket), which matches user intent: dialog has stalled, the gate is now really blocking.
Acceptance criteria
Out of scope
- A separate threshold for "actively communicating" different from
IDLE_WAITING_THRESHOLD_MS (intentionally — one threshold, one policy).
- Manual "I'm working on this" flag.
- Auto-collapse / auto-pin of the actively-comm group.
Problem
The Builders tree treats every gate-blocked builder as equally urgent — they all sort to the top with a bell icon and a wait-time suffix. In practice, "blocked" splits into two very different cases:
plan-approval) and nobody has touched it since. This is the one that wants attention.The current top-bucket placement makes the second case feel like an alarm that won't quiet.
Current state
packages/vscode/src/views/builders.ts:23–28(orderForDisplay):blocked(anyb.blockedtruthy), sorted byblockedSinceascending.idleWaiting(isIdleWaiting(b, now)).active(everything else).OverviewBuilder.lastDataAt(@cluesmith/codev-types, exposed viaoverview.ts:73) already tracks when Tower last received output from the builder's shell — updated whenever the architect's input echoes back or the agent emits anything.isIdleWaiting(b)(packages/core/src/builder-helpers.ts:30) returns true for non-blocked, non-complete builders silent pastIDLE_WAITING_THRESHOLD_MS = 5 minutes(line 19). The same threshold is the natural cut-off for "actively communicating" applied to blocked builders.Proposed behavior
1. New predicate
Add
isActivelyCommBlocked(b, now)inpackages/core/src/builder-helpers.ts:Co-located with
isIdleWaitingso the threshold policy stays in one place.2. New four-bucket sort order
Top-down:
b.blocked && !isActivelyCommBlocked(b). Sorted longest-blockedSincefirst.isIdleWaiting(b)(unchanged).isActivelyCommBlocked(b). Sorted most-recent-lastDataAtfirst (so the one you're typing into right now is at the bottom-most slot — close to where the input cursor sits in the terminal pane).3. Visual treatment
$(comment-discussion)instead of$(bell)) and the row label still surfaces the gate (blocked on <gate>) so the user remembers which gate is in play — but the visual weight is de-emphasised (no wait-time scream).4. Threshold reuse
Use the existing
IDLE_WAITING_THRESHOLD_MS = 5 minutes— no new tunable. A blocked builder you haven't typed to in >5 minutes promotes itself back to "truly blocked" (top bucket), which matches user intent: dialog has stalled, the gate is now really blocking.Acceptance criteria
isActivelyCommBlocked()predicate added topackages/core/src/builder-helpers.ts, alongsideisIdleWaiting.orderForDisplayinviews/builders.tsreturns four buckets in the order above.lastDataAtticks past the threshold (no extra polling — relies on existing SSE refresh cadence).lastDataAt.Out of scope
IDLE_WAITING_THRESHOLD_MS(intentionally — one threshold, one policy).