Skip to content

vscode: builder row legibility — phase prefix + gate-specific icons for at-a-glance protocol/state visibility #810

Description

@amrmelsayed

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:

  1. 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).
  2. 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:

  1. Always visible — column truncation cuts the END, not the start. Phase stays on-screen even with long titles in a narrow sidebar.
  2. Same character cost[implement] is [implement] whether prefixed or suffixed.
  3. 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.
  4. Phase visible across all 3 states — blocked and idle rows gain phase info they don't have today.
  5. 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

  • Every builder row's label includes a [<phase>] prefix immediately after the issue number, before the issue title
  • When phase is empty (transient init state), no prefix appears (no [] literal)
  • Active-state rows no longer carry a trailing [<phase>] (it's now the prefix; redundancy removed)
  • Blocked-state rows still carry blocked on <gate> [<elapsed>] after the title
  • Idle-state rows still carry waiting on input [<elapsed> silent] after the title

Change B — gate-specific icons

  • Blocked builders at spec-approval show the book codicon (still warning-yellow color)
  • Blocked builders at plan-approval show the checklist codicon (still warning-yellow color)
  • Blocked builders at dev-approval show the play codicon (still warning-yellow color)
  • Blocked builders at pr gate show the git-pull-request codicon (still warning-yellow color)
  • Blocked builders at any other gate (unknown / future) fall back to bell (still warning-yellow color) — never crash, never render without an icon
  • Color stays uniform notificationsWarningIcon.foreground across every gate variant
  • Mapping documented in a comment so future-gate additions are obvious where to edit

General

  • Idle-state icon unchanged (comment-discussion + info color)
  • Active-state icon unchanged (circle-filled + passed color)
  • Existing tooltip (Protocol: ... | Mode: ... | Progress: ...%) unchanged
  • Existing contextValue (used by menu when clauses) unchanged
  • Existing b.id stable id unchanged (still drives accordion + reveal)
  • Reactive updates work — phase prefix and blocked-gate icon both re-render on OverviewCache SSE ticks when the underlying field changes
  • Test in packages/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 small gateIconFor(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)
  • 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

Metadata

Metadata

Assignees

Labels

area/vscodeArea: VS Code extensionprojectNew project or feature

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions