Sidebar v2 — beta plan

One flat, recency-sorted thread list where row size is earned by state — and “settled” becomes a real lifecycle stage users control. Familiar mechanics only: nothing here requires learning a new gesture.

beta · toggle in settings plan: .plans/21-sidebar-v2-beta.md original 5 concepts ↗

01What we're building

The hybrid the concepts page predicted, filtered through “meet users where they are”: concept 4's adaptive density as the skeleton, concept 1's card layout (trimmed to two structured lines — no generated text) for active rows, and concept 3 reduced to a sort rule (approval-blocked threads pin above the recency flow — no tiers, no inline approve).

Changes

  • Flat recency list — project group headers gone; project becomes a chip. This is how Claude Code, Codex, and Cursor already present sessions.
  • Settled lifecycle state — explicit, user- or rule-triggered. Settled rows collapse to slim one-liners (≈ today's density).
  • Four states, three colors — Needs approval (amber, pinned with wait time), Working (sky, pulsing), Ready (no color, no label — the normal resting state; bold title until visited), Failed (red).
  • Structured metadata only — the card's second line is status word · branch · harness + model · machine. No summaries or free text to generate; every field is data the shell already carries.
  • Project identity = favicon — the existing ProjectFavicon per-row, not a text chip. Harness (Claude Code / Codex / other) is a tinted glyph on the model; machine shows only when the thread lives on another computer.
  • Failed sessions visiblesession.lastError finally gets a red state; today a dead session shows nothing.
  • Settle → worktree cleanup — settling an orphaned-worktree thread offers a one-click remove.

Deliberately not

  • Inline approve/reject in the sidebar — needs a safety story first.
  • Message snippets (concept 2) — streaming churn unsolved.
  • Ops-grid density mode (concept 5).
  • Auto-archive of long-settled threads (future: settled ≥ 30d).
  • Removing v1 — it stays the default; v2 is opt-in.

02The mock

Hover any card and hit “Settle” — it collapses into the slim tail. That height change is the whole design: it happens only at lifecycle transitions, never from streaming updates, which is what keeps the list calm.

Card anatomy (active threads, ~52px)

Line 1 — project favicon · title (bold = unread) · diff stats when present · time Line 2 — structured metadata only: status word (only when colored) · branch · harness glyph + model · machine (only when not this one). No generated text — everything on the card is data we already have.

Four states, three colors

APPROVAL — blocked mid-run, pinned to top with “waiting Xm” WORKING — pulsing rail, live elapsed timer Ready — the default; needs no label. Stopped, waiting on you. Bold title until visited. FAILED — session error, invisible today

Harness · model · machine

Claude Code · Codex · a neutral glyph for “other” — one tinted glyph before the model name, deliberately quiet. ⏻ machine appears only on threads running somewhere other than the current computer; local threads show nothing.

Slim row (settled, ~28px)

favicon · title · PR when notable · time. Visually ≈ today's v1 row, so density for history is preserved.

Sort

Approval first (by wait time) → then pure recency. No sections, no headers, no “show more” — the tail just scrolls, virtualized.

03The settled model

Concept 4 derived “settled” passively. This version makes it an acknowledgment with an act attached — Gmail's archive, GitHub notifications' “Done” — which is what makes row heights stable and the list trustworthy.

Active rich card · in rollups Settled slim row · out of rollups Archived existing flow, unchanged user settles · PR merged/closed · inactive ≥ 3d any real activity · user un-settles manual (future: 30d auto)

Storage is one override, everything else is computed. No background jobs, no sweeper:

effectiveSettled(thread) =
  override === "settled"true   // manual settle
  override === "active"false  // manual keep-active (beats auto rules)
  pr.state ∈ {merged, closed}                      → true   // auto — strongest signal
  lastActivityAt < now − inactivityThreshold       → true   // auto — backstop, default 3d, configurable
  otherwise                                        → false
Inbox-zero hedge: manual settling must feel like optional satisfaction, not homework. The auto rules guarantee a user who never touches the affordance still gets a tidy list. Beta telemetry decides how prominent the affordance stays.

04Swap boundary

Whole-component swap, one seam. Today's Sidebar.tsx (~3,750 lines) takes zero props and mounts at exactly one place. Everything that must behave identically in both sidebars already lives outside the component in shared hooks and stores — and what lives inside (project grouping, drag-and-drop, show-more) is precisely what v2 deletes.

AppSidebarLayout.tsx:93 {sidebarV2Enabled ? <ThreadSidebarV2/> : <ThreadSidebar/>} Sidebar.tsx (v1 — untouched, default) project groups · dnd · show-more · collapse ignores new settled fields entirely SidebarV2.tsx (new, beta) flat virtualized list · card/slim rows · settle no imports from Sidebar.tsx, ever Shared — both components call the same modules Sidebar.logic.ts · ThreadStatusIndicators · useThreadActions · threadSelectionStore · uiStateStore · threadSort.ts · keybindings anything both need moves here first — never cross-imported

05Phases

P1Settled data modelships dark · no UI change
  • contracts/settings.ts — add sidebarV2Enabled, sidebarAutoSettleAfterDays.
  • contracts/orchestration.tssettledOverride + settledAt on the thread (decoding defaults → old data decodes unchanged); thread.settled / thread.unsettled events; thread.settle / thread.unsettle commands.
  • server/orchestration/decider.ts — handle both commands (settle is idempotent for bulk); activity paths (user message, session start, approval request) emit thread.unsettled when an override is set.
  • client-runtime/threadReducer.ts — reduce both events (archive cases at :87–101 are the template).
  • Pure effectiveSettled(shell, {now, autoSettleAfterDays}) in client-runtime; unit-test the full truth table (override × PR state × inactivity).

Verify: typecheck · reducer + predicate tests · zero visible change.

P2SidebarV2 component + beta togglethe visible feature
  • “Beta features” settings panel + route; swap seam in AppSidebarLayout.tsx.
  • SidebarV2.tsx: flat virtualized list, sort = needs-approval (by wait) → recency; single SidebarV2Row with card / slim variants from the same shell data. Rows lead with the existing ProjectFavicon component (environmentId + cwd are already on the shell).
  • Consolidate status derivation in Sidebar.logic.ts to four visual states: Needs approval, Working (incl. connecting), Ready (merges today's Awaiting Input / Plan Ready / Completed-unseen — same user action, so one unlabeled state), and new Failed (session.lastError, non-running) — kept shared so v1 can adopt Failed later.
  • Meta line: harness glyph + model from the shell's provider/model selection; machine label from the thread's environment vs. the current one (render only when they differ). Both are display-only lookups, no new data plumbing.
  • Unread = bold-until-visited from existing uiStateStore.threadLastVisitedAtById.
  • Height changes only on settle/unsettle transitions (auto-animate); streaming never resizes rows.
  • useThreadActions: settleThread / unsettleThread mirroring archiveThread, minus navigation — settling never navigates away. Affordances: row hover button, context menu, bulk multi-select. No keyboard shortcut yet.

Verify: toggle on → v2; toggle off → v1 pixel-identical to before · settle round-trips across a desktop+remote pair · killed session → Failed card.

P3Worktree cleanup hooksettle = reclaim disk
  • On manual settle where the worktree is orphaned (getOrphanedWorktreePathForThread): non-blocking “Worktree kept · Remove?” prompt → vcsEnvironment.removeWorktree. Never automatic, never blocks the settle.
  • Skip the prompt when the worktree has uncommitted changes or unpushed commits (from vcs status the sidebar already has).
  • Auto-settle never touches worktrees. Follow-up (tracked, out of scope): settings “Storage” view listing orphaned worktrees of settled threads with sizes + bulk remove.
P4Telemetry & beta exitfind the balance
  • Count settles by source: manual / auto-PR / auto-inactivity / bulk. Count un-settles: manual vs. activity-driven.
  • Activity-driven un-settles of manual settles = the model fighting users → retune threshold or triggers.
  • Manual settling ≈ 0% → shrink the affordance, lean on auto rules. High → a habit worth building on (keyboard shortcut, etc.).
  • Track toggle-off rate after trying v2.

06Open questions

QuestionCurrent thinking
Server-side activity observationIf decider activity paths are too scattered for unsettle-on-activity, fall back to client-side: compute with activity timestamps, override only stores manual state (activity newer than settledAt wins). Decide in P1.
Shell projectionsettledOverride/settledAt must be in the thread shell stream — the sidebar never loads details. Verify first thing in P1.
Diff stats on cardsRequires checkpoint data in the shell; if absent, defer diff stats rather than loading details per row.
Project grouping in v2Not planned; v1 stays available for users who want groups. Revisit only if beta feedback demands it.
T3 Code · Sidebar v2 beta · plan doc pair: .plans/21-sidebar-v2-beta.md (implementation detail) + this page (design + rationale). Mock data mirrors the original concepts page (from live state.sqlite, Jul 13).