Skip to content

Add collapsed-project status badge to the sidebar#2340

Open
kaspesi wants to merge 11 commits into
getpaseo:mainfrom
kaspesi:fix/collapsed-project-status-badge-sizing
Open

Add collapsed-project status badge to the sidebar#2340
kaspesi wants to merge 11 commits into
getpaseo:mainfrom
kaspesi:fix/collapsed-project-status-badge-sizing

Conversation

@kaspesi

@kaspesi kaspesi commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Linked issue

Closes #

Type of change

  • Bug fix
  • New feature (with prior issue + design alignment)
  • Refactor / code improvement
  • Docs

What does this PR do

Collapsing a project in the sidebar hides its workspaces and, on main, leaves no signal
about what's happening inside them — a project could have a workspace waiting on your input or
one that just failed, and the collapsed row looks identical to an idle one. This PR surfaces the
single most urgent hidden workspace status as a small corner badge on the project icon, so a
collapsed project still tells you when it needs attention.

Every surfaced status renders inside the same badge shell — a 14pt circle with a 1pt
sidebar-colored ring, fixed at a −7/−7 offset on the icon's bottom-right corner. Only the glyph
inside changes; the loader, the alert, and the dot never draw a differently-sized or
differently-positioned circle. Priority order for the collapsed aggregate is
needs_input > failed > running > attention > done — a working project keeps its loader even if
another of its workspaces is also awaiting review. An expanded project shows no badge at all;
the individual workspace rows speak for themselves.

What changed

  • components/sidebar/project-leading-visual.tsx (new) — the project row's leading icon,
    now rendering ProjectStatusIndicator / ProjectStatusBadge: one shared badge shell for every
    status. Glyph sizes are even (8pt dot, 10pt alert) so they center in the 12pt shell interior
    without fractional-pixel correction (see the sizing proof below). The running loader is nudged by
    STATUS_BADGE_LOADER_NUDGE_X so its visual center lands on the circle center.
  • utils/project-status-badge-content.ts (new) — the pure glyph-selection function that maps
    a status bucket to badge content, extracted so it's unit-testable without a DOM.
  • hooks/sidebar-workspaces-view-model.ts, hooks/use-sidebar-workspaces-list.ts,
    utils/sidebar-agent-state.ts, components/sidebar-workspace-list.tsx — the aggregation
    the badge is driven by: deriveProjectStatusBucket, useSidebarProjectStatusBucket, and the
    bucket rollup, with the list component slimmed down as logic moved into the hook/util layer.
  • docs/glossary.md — new authoritative term "Project status bucket."
  • e2e/project-status-badge-gallery.spec.ts (new) — an on-demand capture/measurement spec
    (details under How did you verify it).
  • .gitignore — ignores ad-hoc verification capture output (**/.verification/).
  • .github/pr-assets/collapsed-project-status-badge/ — the images embedded below; safe to
    delete after merge.

How did you verify it

Platform coverage, stated plainly: all evidence below was captured on web through the
Playwright capture spec, at both a desktop width and a compact (390×844) width. I did
not test on native iOS, native Android, or the Electron desktop wrapper. The badge is
plain layout (a positioned View + Lucide glyphs, no web-only APIs), so it should behave
identically there, but I'm calling out that I haven't run it on those targets.

All states, one shared circle

all states

running → amber loader, needs_input → amber alert, failed → red dot, attention → green
dot — all four in the identical ringed circle. done has no active work, so the project icon
carries no badge at all.

Per status

running needs_input failed attention done
running needs-input failed attention done

Sizing & centering — why the glyphs are even

sizing proof

The shell's interior is 12pt (14pt minus the 1pt ring on each side). A centered glyph of size N
sits at a (12 − N) / 2 offset — fractional for odd N, which the browser snaps to a
device-pixel boundary and renders visibly off-center (an odd size measures ~1.5 device px right
and down at 3×, with asymmetric ring gaps between opposite sides). Even sizes divide the interior
into whole pixels and land dead center with no correction.

So the glyphs are sized even: the dot is 8pt and the alert 10pt (Lucide's circle-alert
paints ~83% of its nominal size, so a 10pt alert draws a ~8.3pt disc that matches the 8pt dot — the
two read as the same diameter). The red crosshair marks each glyph's center; the ring gap is
symmetric on all sides, and DOM geometry confirms a 0.00px offset from the shell center for both.

Spinner, in the shell

spinner

The running state's dot-comet loader, nudged left by STATUS_BADGE_LOADER_NUDGE_X (−0.67pt) so its
visual center lands on the circle center — the play-button-triangle trick. Assembled from native
deviceScaleFactor: 3 frames, not an upscaled video crop.

Priority aggregation — statuses resolving up to the project badge

The collapsed row must reduce every hidden workspace down to one badge. This recording seeds three
projects, each with several same-directory workspaces in different states, and shows each resolving
to a different winner — needs_input beating running+attention, failed beating running,
running beating attention — then expands two of them to reveal the constituent workspace rows that
rolled up:

propagation

(Crisp — assembled from deviceScaleFactor: 3 frames, not upscaled video.
▶ higher-quality mp4.)

The same rules, captured statically per-row (each row proves one rule, including both failed
cases: needs_input+failed → alert, failed+running → red dot, running+attention → running,
attention+done → attention, all done → no badge), plus a control project left expanded:

edge cases

The expanded control is the inverse of everything else — an expanded project shows no badge on
the project row; status rides the individual workspace rows instead:

expanded control

Dark mode

dark mode

The ring is sidebar-colored, so the badge separates cleanly from the icon underneath in both themes.

Compact (narrow) width

compact

The same shell, offset, and sizing at a 390px-wide (mobile web) viewport.

Automated checks & the capture spec

  • Unit tests: utils/project-status-badge-content.test.ts (pure glyph selection per
    bucket), hooks/sidebar-workspaces-view-model.test.ts (aggregation / priority rollup), and
    utils/sidebar-agent-state.test.ts (bucket derivation).
  • e2e/project-status-badge-gallery.spec.ts is an on-demand capture/measurement spec, not part
    of CI
    — it drives the real running app via the mock provider, asserts each row's testid before
    capturing, and dumps DOM geometry proving every badge shell shares the same size/offset and every
    even-size glyph centers at a 0.00px offset. It produced every image above (including the new
    propagation recording). The archiving spinner is a transient state that could not be caught
    deterministically, so its capture is intentionally test.skip'd with an inline investigation
    note rather than left flaky.
  • npm run typecheck, npm run lint, and npm run format are all clean (also enforced by the
    repo's pre-commit hook on every commit here).

Checklist

  • One focused change. Unrelated cleanups split out.
  • npm run typecheck passes
  • npm run lint passes
  • npm run format ran (Biome)
  • UI changes include screenshots or video for every affected platform — web only (desktop +
    compact widths); native iOS/Android and Electron desktop were not tested (see the platform note
    above)
  • Tests added or updated where it made sense

@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a status badge to collapsed project rows in the sidebar, surfacing the most urgent hidden workspace's status as a 14pt ringed badge on the project icon. All status states share a single badge shell whose only variable is the glyph inside — a dot-comet loader for running, an amber alert for needs_input, a colored dot for failed/attention, and nothing for done.

  • Badge rendering (project-leading-visual.tsx, project-status-badge-content.ts): the old inline component bundle is extracted into a proper sidebar sub-module; a new pure-function utility maps the aggregate bucket to a typed descriptor (loader | alert | dot | null), keeping React out of the selection logic and making it directly testable.
  • Aggregation logic (sidebar-agent-state.ts, sidebar-workspaces-view-model.ts, use-sidebar-workspaces-list.ts): aggregateSidebarStateBuckets folds N workspace buckets into one via a single priority array (needs_input > failed > running > attention > done); deriveProjectStatusBucket reuses the existing agent-activity index pipeline; useSidebarProjectStatusBucket returns a primitive so status churn only re-renders the project row when the aggregate actually changes.
  • E2E capture spec (project-status-badge-gallery.spec.ts): an on-demand (non-CI) spec that seeds every state and edge case via the mock provider and dumps DOM geometry to verify badge-shell uniformity by measurement.

Confidence Score: 5/5

Safe to merge. The feature is well-scoped, the aggregation logic is centralized and thoroughly unit-tested, and the rendering path uses a shared badge shell verified by DOM geometry in the E2E capture spec.

All production paths (badge rendering, aggregation, hook subscription) are correct and well-tested. The two open findings are both in the on-demand capture spec: residual if/continue conditionals in two assertion loops that the earlier fix didn't reach, and a skipped archiving test that flags a possible but unconfirmed bug. Neither affects the shipping feature.

packages/app/e2e/project-status-badge-gallery.spec.ts — two assertion loops still have if/continue conditionals, and the archiving test is left as test.skip with an open bug note.

Important Files Changed

Filename Overview
packages/app/src/components/sidebar/project-leading-visual.tsx New file: extracts the project row's leading icon into a clean module with a shared 14pt badge shell for all statuses; uses getProjectStatusBadgeContent to keep rendering logic separate from selection logic.
packages/app/src/utils/sidebar-agent-state.ts Adds aggregateSidebarStateBuckets with a single priority array (needs_input > failed > running > attention > done); handles unknown buckets via indexOf guard; well-tested.
packages/app/src/utils/project-status-badge-content.ts New pure-function utility mapping a SidebarStateBucket to a typed badge descriptor (loader
packages/app/src/hooks/sidebar-workspaces-view-model.ts Adds deriveProjectStatusBucket and ProjectStatusSession interface; builds one agent-activity index per server (not per workspace), then delegates aggregation to aggregateSidebarStateBuckets.
packages/app/src/hooks/use-sidebar-workspaces-list.ts Adds useSidebarProjectStatusBucket hook returning a primitive string, so status churn only re-renders the project row when the aggregate actually changes; disabled while the project is expanded.
packages/app/src/components/sidebar-workspace-list.tsx Cleans up the old inline ProjectLeadingVisual, ProjectIcon, ProjectLeadingVisualStatus, StatusDotOverlay, and ProjectInlineChevron definitions; integrates useSidebarProjectStatusBucket in ProjectBlock.
packages/app/e2e/project-status-badge-gallery.spec.ts New on-demand capture spec; two tests still have if/continue conditionals in assertion loops (not fixed by the earlier split), and the archiving test is left as test.skip with an unresolved bug note.
packages/app/src/utils/project-status-badge-content.test.ts Plain vitest unit test for getProjectStatusBadgeContent; no mocks, no JSDOM, covers all five buckets and null.
packages/app/src/hooks/sidebar-workspaces-view-model.test.ts Adds thorough unit tests for deriveProjectStatusBucket covering cross-server aggregation, partial hydration, agent-lift, archived/sub-agent exclusion, and all priority-order pairs.
packages/app/src/utils/sidebar-agent-state.test.ts Extends existing tests with full aggregateSidebarStateBuckets coverage including order-independence and all adjacent-priority pairs.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    PB["ProjectBlock\n(collapsed=true)"]
    HOOK["useSidebarProjectStatusBucket\n(workspaces, enabled)"]
    STORE["SessionStore\n(sessions, agents)"]
    DERIVE["deriveProjectStatusBucket\n(workspaces, sessions)"]
    AGG["aggregateSidebarStateBuckets\n(buckets[])"]
    BUCKET["SidebarStateBucket\n(needs_input | failed | running | attention | done | null)"]
    CONTENT["getProjectStatusBadgeContent\n(statusBucket)"]
    PLV["ProjectLeadingVisual"]
    PSI["ProjectStatusIndicator"]
    PSB["ProjectStatusBadge"]
    RENDER["Render: loader / alert / dot / nothing"]

    PB -->|"project.workspaces"| HOOK
    HOOK -->|"selector"| STORE
    STORE -->|"state.sessions"| DERIVE
    DERIVE -->|"per-workspace bucket"| AGG
    AGG --> BUCKET
    BUCKET --> HOOK
    HOOK -->|"statusBucket"| PLV
    PLV --> PSI
    PSI --> PSB
    PSB --> CONTENT
    CONTENT --> RENDER
Loading

Reviews (5): Last reviewed commit: "Add higher-quality mp4 of the status pro..." | Re-trigger Greptile

Comment thread packages/app/src/components/sidebar/project-leading-visual.tsx Outdated
Comment thread packages/app/src/components/sidebar/project-leading-visual.test.tsx Outdated
Comment thread packages/app/e2e/project-status-badge-gallery.spec.ts Outdated
kaspesi added 5 commits July 22, 2026 21:56
Collapsed project rows now surface the most urgent hidden workspace's
status as a corner badge on the project icon, instead of giving no
signal at all: a loader for running, an alert for needs_input, a dot
for attention/failed, nothing for done. Priority order (needs_input >
failed > running > attention > done) means a working project keeps
its loader even if another workspace is also awaiting review.

Every surfaced status shares one badge shell (size, ring, offset) so
the loader, alert, and dot never draw a differently-sized circle -
previously the loader/alert used a 12pt ring while the dot used a
7-9pt one with a different corner offset. The running loader is also
nudged half a point left so its visual center lands on the circle's
center rather than reading as shifted right.

Adds an e2e capture spec (run on demand, not part of CI) that seeds
every state and edge case through the mock provider and dumps DOM
geometry, for re-verifying the badge shell stays uniform.
Temporary evidence assets for pull request review, embedded via blob
URLs in the PR description. Safe to delete after merge.
This feature never shipped on main, so there is no real "before" to
compare against for a reviewer — the earlier before/after images were
an artifact of local dev iteration, not a regression fix. Replaced
with one clean still per status.
Lucide's circle-alert only paints ~83% of its nominal size, so the
prior 10pt glyph read as smaller than the loader inside the same
14pt shell. Sized both up so all three glyphs draw the same visual
footprint against the shared ring.
Greptile flagged getStatusDotColorStyle's needs_input/running cases as
dead (unreachable — shouldRenderSyncedStatusLoader and an explicit
check already intercept those buckets earlier). Extracting the
selection logic into a plain-TS descriptor function
(getProjectStatusBadgeContent) makes that structural: the "dot"
variant is typed to failed|attention only, so there's no switch left
with unreachable branches.

That extraction also resolves the JSDOM component test Greptile
flagged (project-leading-visual.test.tsx used vi.hoisted/vi.mock/
createRoot mounting, all banned by docs/testing.md's two test
categories) — the pure selector is now tested directly with a plain
vitest file, no mounting needed. Dropped that test file and
use-sidebar-project-status-bucket.test.tsx (same banned pattern); the
real coverage was already redundant with sidebar-workspaces-view-model
.test.ts's existing priority-order assertions and the e2e capture
spec's real-app gating checks.

Also split a conditional branch in the e2e capture spec's assertion
loop into two deterministic loops (badge-expected vs no-badge cases)
per Greptile's third note.
@kaspesi
kaspesi force-pushed the fix/collapsed-project-status-badge-sizing branch from aad0ad1 to 135541e Compare July 23, 2026 02:03
kaspesi added 2 commits July 23, 2026 14:40
The badge shell's interior is 12pt (14pt minus the 1pt ring on each
side). A centered glyph of size N sits at a (12 - N) / 2 offset, which
is fractional for odd N and gets snapped to a device-pixel boundary,
rendering visibly off-center (~1.5 device px at 3x). Drop the dot to 8pt
and the alert to 10pt so both divide the interior into whole pixels and
land dead center; Lucide's circle-alert paints ~83% of nominal, so 10pt
draws a ~8.3pt disc that matches the 8pt dot.

Also extends the on-demand capture spec to cover the failed bucket, the
priority aggregation cases, dark mode, hover, a compact viewport, and a
recorded running-to-expand transition, dumping DOM geometry that proves
every shell shares one size/offset and every even glyph centers at 0px.
Recaptures every state at the approved 8pt-dot / 10pt-alert sizing,
adds the failed bucket to the per-status set, a crosshair sizing/
centering proof, dark-mode and compact-form-factor shots, and replaces
the soft upscaled spinner GIF with a crisp one assembled from native
3x-scale frames. Drops the stale pre-sizing images.
@kaspesi
kaspesi force-pushed the fix/collapsed-project-status-badge-sizing branch from 90afdfc to 4328146 Compare July 23, 2026 18:46
kaspesi added 4 commits July 23, 2026 15:11
Seeds three collapsed projects, each with several same-directory
workspaces in different buckets, so each resolves to a different
highest-priority winner (needs_input / failed / running). Films the
collapse and expand so the aggregation is provable as motion, not a
single frame.
@kaspesi kaspesi changed the title Unify collapsed-project status badge sizing Add collapsed-project status badge to the sidebar Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant