Problem
Builder rows in the Builders tree show three pieces of info today (packages/vscode/src/views/builders.ts:89-99 and :119-123):
- Label:
#<id> <issueTitle> <stateLabel> — state-dispatched suffix
- Icon: state-dispatched (
bell blocked / comment-discussion idle / circle-filled active)
Two distinct legibility gaps at a glance:
Gap 1 — Phase isn't visible at-a-glance
The <stateLabel> is:
- Blocked:
blocked on <gate> [<elapsed>]
- Idle:
waiting on input [<elapsed> silent]
- Active:
[<phase>]
The phase name is only visible for active builders, and only as a trailing suffix. Two problems:
- Blocked rows hide the phase entirely. A row reading
#791 Startup preflight ⏳ blocked on plan-approval [12m] doesn't tell you the builder is in the plan phase (you might infer from the gate name, but dev-approval could be either implement or review phase depending on protocol).
- Long titles truncate the trailing phase off-screen. When the sidebar is narrow or the title is long (typical for our issue titles), the
[implement] suffix gets clipped — the leftmost characters survive, the rightmost (phase indicator) are first to go.
Gap 2 — Gate type isn't visible at-a-glance for blocked rows
When a builder hits a gate, all blocked rows show the same bell icon regardless of which kind of review is needed:
- A spec-approval blocked builder looks identical to
- A plan-approval blocked builder, which looks identical to
- A dev-approval blocked builder, which looks identical to
- A PR-gate blocked builder
The gate name lives only in the label text (blocked on <gate>). A user scanning the tree to triage "any plan reviews queued?" has to read each blocked row's text, or hover for tooltip. The icon — the most pre-attentively processed signal in the row — carries zero gate-type information.
Revised proposal — two compositional changes
Both changes operate on the same file (builders.ts:89-123), both make the Builders tree more legible at a glance, both are tiny. Shipping them together is one PR, one review pass, one visual-verification cycle.
Change A — phase prefix after the issue number
Move the phase indicator from a trailing suffix to a leading prefix immediately after the issue number:
#<id> [<phase>] <issueTitle> <stateLabel>
Where <stateLabel> keeps the existing state-dispatched form without the now-redundant [<phase>] active-state variant:
- Blocked:
blocked on <gate> [<elapsed>]
- Idle:
waiting on input [<elapsed> silent]
- Active: (empty — phase prefix already covers it)
Examples:
| Today |
Revised |
#882 refactor: extract gitignore... [implement] |
#882 [implement] refactor: extract gitignore... |
#791 Startup preflight blocked on plan-approval [12m] |
#791 [plan] Startup preflight blocked on plan-approval [12m] |
#794 Notification refactor waiting on input [5m silent] |
#794 [implement] Notification refactor waiting on input [5m silent] |
Why prefix beats suffix:
- Always visible — column truncation cuts the END, not the start. Phase stays on-screen even with long titles in a narrow sidebar.
- Same character cost —
[implement] is [implement] whether prefixed or suffixed.
- Pre-attentive position — the eye scans left-to-right; phase shows up in the second-most-prominent position (after the issue ID itself) instead of buried at the row's tail.
- Phase visible across all 3 states — blocked and idle rows gain phase info they don't have today.
- Removes the redundant active-state
[<phase>] suffix — phase only appears once per row, not duplicated.
Change B — gate-specific codicons for blocked builders
Today's universal bell icon (line 119-120) becomes a small mapping that picks the codicon by gate name. Color stays uniform (notificationsWarningIcon.foreground warning yellow) so "needs your attention" remains the consistent signal — the shape encodes WHAT kind of attention:
| Gate |
Codicon |
Why |
spec-approval |
book |
Document review (spec is prose-heavy) |
plan-approval |
checklist |
Plan/structure review |
dev-approval |
play |
Run-and-verify (PIR's "test before PR" semantic) |
pr |
git-pull-request |
Universal git icon — instantly recognizable |
| unknown / future gates |
bell |
Fallback so new gates don't break rendering |
Why this works well:
Why this combined proposal beats the original badge design
This issue's original framing was a 2-character FileDecorationProvider-driven badge (IA, PC, FB, etc.) replacing the existing icons. That approach was reconsidered for three reasons:
- Letter codes are harder to read than icons. Icons are pre-attentively processed; letters require sequential decoding even after learning.
- The Source Control A/M/D analogy doesn't hold. A/M/D is 3 mutually-exclusive values in one dimension; the original badge had 2 dimensions (phase × status) with combinatorial space and protocol-dependent phase letters (SPIR's
I = Implement, BUGFIX's I = Investigate).
- The original suffix already encodes phase + duration richly (e.g.
blocked on plan-approval [12m]). A 2-letter badge replacement would lose duration info that today's suffix carries.
The revised proposal keeps icons as the primary at-a-glance signal (status + now gate type), adds phase as a leading text prefix (always-visible position), and preserves the existing state-specific suffix for blocked/idle duration. Strict information gain across two dimensions (phase visibility, gate-type visibility), no loss.
Implementation
Both changes land in packages/vscode/src/views/builders.ts. Single PR, ~15 LOC total.
Change A — label construction (builders.ts:89-99)
Current:
const phaseLabel = isBlocked
? `blocked on ${b.blocked}${waitTime}`
: isIdle
? `waiting on input${idleTime}`
: `[${b.phase}]`;
const item = new BuilderTreeItem(b.id, `#${b.issueId ?? b.id} ${b.issueTitle ?? ''} ${phaseLabel}`);
Becomes:
const stateLabel = isBlocked
? ` blocked on ${b.blocked}${waitTime}`
: isIdle
? ` waiting on input${idleTime}`
: '';
const phasePrefix = b.phase ? `[${b.phase}] ` : '';
const item = new BuilderTreeItem(
b.id,
`#${b.issueId ?? b.id} ${phasePrefix}${b.issueTitle ?? ''}${stateLabel}`,
);
Handle the edge case b.phase empty (rare transient init state) by omitting the prefix — row reads #<id> <title> rather than #<id> [] <title>.
Change B — gate-icon mapping (builders.ts:119-123)
Current:
item.iconPath = isBlocked
? new vscode.ThemeIcon('bell', new vscode.ThemeColor('notificationsWarningIcon.foreground'))
: isIdle
? new vscode.ThemeIcon('comment-discussion', new vscode.ThemeColor('notificationsInfoIcon.foreground'))
: new vscode.ThemeIcon('circle-filled', new vscode.ThemeColor('testing.iconPassed'));
Becomes:
// Gate-specific icons keep the warning-yellow color uniform (still "needs your attention")
// but encode WHAT KIND of review via the codicon shape. Falls back to `bell` for unknown
// or future gates so new protocols don't break rendering.
const GATE_ICONS: Record<string, string> = {
'spec-approval': 'book',
'plan-approval': 'checklist',
'dev-approval': 'play',
'pr': 'git-pull-request',
};
const blockedIcon = (b.blocked && GATE_ICONS[b.blocked]) || 'bell';
item.iconPath = isBlocked
? new vscode.ThemeIcon(blockedIcon, new vscode.ThemeColor('notificationsWarningIcon.foreground'))
: isIdle
? new vscode.ThemeIcon('comment-discussion', new vscode.ThemeColor('notificationsInfoIcon.foreground'))
: new vscode.ThemeIcon('circle-filled', new vscode.ThemeColor('testing.iconPassed'));
No FileDecorationProvider, no custom URI scheme, no new infrastructure. The tooltip stays unchanged.
Acceptance criteria
Change A — phase prefix
Change B — gate-specific icons
General
Out of scope
FileDecorationProvider-based 2-character badge — explicitly rejected (see "Why this combined proposal beats the original badge design" above)
- Color-coding the phase prefix — keep it as monochrome bracket text; icons already carry color signal for status
- Color-coding the gate-specific icons differently — explicitly uniform yellow across all gates so "needs your attention" stays the consistent meta-signal
- Localizing the phase names or gate names — phase + gate strings come from porch state as English-only today; that's a separate concern
- Replacing the idle/active icons — only the blocked-state icon dispatches by gate; idle and active stay as today
- Showing phase or gate in the tooltip — already covered by the tooltip's
Protocol line + the row's text label; tooltip enrichment is a separate concern
- Status-bar segment per gate type (e.g.
2 plan / 1 dev / 1 pr) — a natural sibling idea, but a separate UI surface; file separately if desired
- Grouping/sorting blocked builders by gate type within the blocked bucket — a natural sibling idea, but a separate behavior change; file separately if desired
Related
Problem
Builder rows in the Builders tree show three pieces of info today (
packages/vscode/src/views/builders.ts:89-99and:119-123):#<id> <issueTitle> <stateLabel>— state-dispatched suffixbellblocked /comment-discussionidle /circle-filledactive)Two distinct legibility gaps at a glance:
Gap 1 — Phase isn't visible at-a-glance
The
<stateLabel>is:blocked on <gate> [<elapsed>]waiting on input [<elapsed> silent][<phase>]The phase name is only visible for active builders, and only as a trailing suffix. Two problems:
#791 Startup preflight ⏳ blocked on plan-approval [12m]doesn't tell you the builder is in the plan phase (you might infer from the gate name, butdev-approvalcould be either implement or review phase depending on protocol).[implement]suffix gets clipped — the leftmost characters survive, the rightmost (phase indicator) are first to go.Gap 2 — Gate type isn't visible at-a-glance for blocked rows
When a builder hits a gate, all blocked rows show the same
bellicon regardless of which kind of review is needed:The gate name lives only in the label text (
blocked on <gate>). A user scanning the tree to triage "any plan reviews queued?" has to read each blocked row's text, or hover for tooltip. The icon — the most pre-attentively processed signal in the row — carries zero gate-type information.Revised proposal — two compositional changes
Both changes operate on the same file (
builders.ts:89-123), both make the Builders tree more legible at a glance, both are tiny. Shipping them together is one PR, one review pass, one visual-verification cycle.Change A — phase prefix after the issue number
Move the phase indicator from a trailing suffix to a leading prefix immediately after the issue number:
Where
<stateLabel>keeps the existing state-dispatched form without the now-redundant[<phase>]active-state variant:blocked on <gate> [<elapsed>]waiting on input [<elapsed> silent]Examples:
#882 refactor: extract gitignore... [implement]#882 [implement] refactor: extract gitignore...#791 Startup preflight blocked on plan-approval [12m]#791 [plan] Startup preflight blocked on plan-approval [12m]#794 Notification refactor waiting on input [5m silent]#794 [implement] Notification refactor waiting on input [5m silent]Why prefix beats suffix:
[implement]is[implement]whether prefixed or suffixed.[<phase>]suffix — phase only appears once per row, not duplicated.Change B — gate-specific codicons for blocked builders
Today's universal
bellicon (line 119-120) becomes a small mapping that picks the codicon by gate name. Color stays uniform (notificationsWarningIcon.foregroundwarning yellow) so "needs your attention" remains the consistent signal — the shape encodes WHAT kind of attention:spec-approvalbookplan-approvalchecklistdev-approvalplayprgit-pull-requestbellWhy this works well:
belluntil the mapping is updated; no crash, no broken rendering.Why this combined proposal beats the original badge design
This issue's original framing was a 2-character
FileDecorationProvider-driven badge (IA,PC,FB, etc.) replacing the existing icons. That approach was reconsidered for three reasons:I= Implement, BUGFIX'sI= Investigate).blocked on plan-approval [12m]). A 2-letter badge replacement would lose duration info that today's suffix carries.The revised proposal keeps icons as the primary at-a-glance signal (status + now gate type), adds phase as a leading text prefix (always-visible position), and preserves the existing state-specific suffix for blocked/idle duration. Strict information gain across two dimensions (phase visibility, gate-type visibility), no loss.
Implementation
Both changes land in
packages/vscode/src/views/builders.ts. Single PR, ~15 LOC total.Change A — label construction (
builders.ts:89-99)Current:
Becomes:
Handle the edge case
b.phaseempty (rare transient init state) by omitting the prefix — row reads#<id> <title>rather than#<id> [] <title>.Change B — gate-icon mapping (
builders.ts:119-123)Current:
Becomes:
No
FileDecorationProvider, no custom URI scheme, no new infrastructure. The tooltip stays unchanged.Acceptance criteria
Change A — phase prefix
[<phase>]prefix immediately after the issue number, before the issue title[]literal)[<phase>](it's now the prefix; redundancy removed)blocked on <gate> [<elapsed>]after the titlewaiting on input [<elapsed> silent]after the titleChange B — gate-specific icons
spec-approvalshow thebookcodicon (still warning-yellow color)plan-approvalshow thechecklistcodicon (still warning-yellow color)dev-approvalshow theplaycodicon (still warning-yellow color)prgate show thegit-pull-requestcodicon (still warning-yellow color)bell(still warning-yellow color) — never crash, never render without an iconnotificationsWarningIcon.foregroundacross every gate variantGeneral
comment-discussion+ info color)circle-filled+ passed color)Protocol: ... | Mode: ... | Progress: ...%) unchangedcontextValue(used by menuwhenclauses) unchangedb.idstable id unchanged (still drives accordion + reveal)OverviewCacheSSE ticks when the underlying field changespackages/vscode/src/__tests__/covers: all 3 state variants of the label (active/blocked/idle), the empty-phase edge case, and at least one gate-mapping per row for the icon variants (could be a unit test on a smallgateIconFor(gateName)helper if Change B is extracted)Out of scope
FileDecorationProvider-based 2-character badge — explicitly rejected (see "Why this combined proposal beats the original badge design" above)Protocolline + the row's text label; tooltip enrichment is a separate concern2 plan / 1 dev / 1 pr) — a natural sibling idea, but a separate UI surface; file separately if desiredRelated
isActivelyCommBlockedpredicate (varies COLOR for actively-comm-blocked rows). This issue varies SHAPE for blocked rows per gate type. Orthogonal axes — they compose: adev-approval-blocked actively-comm builder would showplaycodicon in gray (or whatever color vscode: re-bucket Builders tree — actively-communicated blocked builders sort below active, truly-blocked stays on top #798 picks for the actively-comm bucket).FileDecorationProviderbadge. That design was reconsidered after reading the existingbuilders.tscode (the trailing suffix already encodes phase + duration richly) and after the visual-design argument that letter codes are harder to read than icons. The revised proposal delivers the original goal (phase + status visibility at-a-glance) with no new infrastructure and no information loss — and adds the bonus gate-type-via-icon-shape signal that the original badge didn't think to encode.