Sidebar v2 without server changes#4026
Conversation
…ifecycle
A new thread sidebar behind a Settings → Beta toggle (sidebarV2Enabled,
default off). One flat recency-sorted list across projects: active threads
render as two-line cards (favicon, title, time, status word · branch ·
provider glyph + model · machine), settled threads collapse to slim
one-liners. Approval-blocked threads pin to the top with wait time; failed
sessions (session.lastError) finally get a visible state.
Settled is an event-sourced lifecycle state mirroring the archived pattern:
- settledOverride ("settled" | "active" | null) + settledAt on the thread,
thread.settle/thread.unsettle commands, thread.settled/thread.unsettled
events, projection columns via migration 033.
- effectiveSettled() computes the visible state: manual override beats
auto rules (PR merged/closed, N days of inactivity — client setting,
default 3, nullable). Idempotency is by event re-emission since the
engine rejects zero-event commands.
- Activity auto-unsettles server-side: turn start, live session start,
approval/user-input requests. Session stop/error status writes do not.
Manually settling a thread whose worktree is orphaned offers a one-click
worktree removal (re-validated at click time, never forced).
The v2 component is a sibling swapped at the single AppSidebarLayout mount
point; shared chrome moved to sidebar/SidebarChrome.tsx. The data model is
flag-independent and ships dark for v1 users, so toggling is a pure view
swap.
Plan: .plans/21-sidebar-v2-beta.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…reation picker Design iteration after testing the beta over Tailscale: - Settled is the only collapse rule. Reverted the "seen Ready threads render slim" heuristic — it silently turned settled back into a derived state. Every unsettled thread is a full card; density is earned by settling. - Cards are bordered islands (rounded-xl, state-colored border: amber approval / sky working / red failed), status dot replaces the favicon on stateful cards, live elapsed timer on working. Slim rows grew to 34px with 13px titles; a hairline divider separates cards from the settled tail. - Harness identity is the provider glyph only (model name in tooltip). The full-row harness tint experiment is removed. - Hover actions (Settle/Un-settle) are absolutely-positioned overlays that fade over the timestamp — no layout shift. - Project scope chips above the list (All / per-project / + add): filters the flat list and informs the new-thread default. Session-local. - New thread opens a project picker dialog: contextual default preselected and focused (Enter confirms), all projects with workspace paths, Add project row. Single-project setups skip the dialog. chat.new (mod+shift+o) routes through the picker via newThreadPickerBus since the shortcut is handled in the _chat route layout. Arrow/Home/End navigation in the list. - chat.newLocal still bypasses the picker as the explicit escape hatch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI: add settledOverride/settledAt + new client settings to desktop and mobile test fixtures and the mobile threadDetailToShell mapping. Review fixes (cursor bugbot + macroscope): - failed status now requires session.status === "error" instead of any lingering lastError on a stopped/ready session - effectiveSettled: pending approvals or user input always win — blocked work can never collapse into the settled tail, even manually settled - decider unsettle-on-activity guards check settledOverride === "settled" so activity no longer erases a user's explicit keep-active override - repeat thread.settle re-emits with updatedAt = original settledAt, so double-settles no longer bump recency ordering - sidebarAutoSettleAfterDays bounded 1..90 in schema and patch - approval wait sort/label use shell.updatedAt (bumped by the approval activity in the projection pipeline) instead of latestUserMessageAt / turn start, with the approximation documented - v2 rows: Enter/Space keyboard activation; double-click inline rename ported from v1 plus a context-menu Rename entry - new-thread picker: cross-project creation no longer inherits the active thread's branch/worktree; preselection consults the active draft via resolveThreadActionProjectRef; stale project scope resets to All - settle worktree prompt: vcs status checked before offering (and again at click time) — dirty or ahead worktrees are never offered Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…led tail - Cards are square hard-edged blocks on a darker shell; active row reads by a near-white border instead of a blue-adjacent wash - Status moves from tinted border + dot to a solid 3px left edge strip (duty-cycled pulse while working), so the project favicon always shows - Card titles clamp to two lines instead of truncating one - Settled rows grey out (desaturated favicon, dimmed title) and restore on hover; section gets an explicit SETTLED label - Project chips are square mono tabs; selected All chip inverts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Each card is a tiny window: a thin title bar carries the project favicon + mono uppercase name with the timestamp, separated from the thread content by a hairline — project identity reads as chrome - Active card brightens its title bar on top of the near-white border - Thread list is statically ordered by creation time, newest first; activity never reorders rows — only settling moves them - Project identity mirrored on the open thread: favicon+name chip leads the chat header and the composer's run-context strip (drafts included) - Settled rows keep dimmed favicons; chips/picker favicons restored - Composer squared to match: 12px radius, higher-contrast border, neutral focus ring Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the web sidebar v2 design to React Native behind a device-local Settings → Beta toggle (threadListV2Enabled — mobile has no client-settings sync, so the flag lives in mobile preferences): - threadListV2.ts: v2 status model (approval/working/failed/ready), static creation-order sort, and the effectiveSettled partition into an active card block + settled recency tail (shared @t3tools/client-runtime/state/thread-settled, web-default 3-day auto-settle) - thread-list-v2-items.tsx: square title-bar cards — project favicon + mono uppercase name as chrome, status edge strip, two-line titles, branch/status meta — and dimmed slim rows under a SETTLED divider - Settle (cards) / un-settle (slim) as the primary swipe action and in the long-press menu; useThreadListActions grows settle/unsettle via the shared threadEnvironment atoms - HomeScreen renders the flat v2 list (no grouping, rows never reorder on activity) when the toggle is on; v1 list untouched otherwise Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first mobile pass copied the web card anatomy (outlined translucent box, thin header strip) — which is exactly iOS push-banner anatomy, so rows read as information rather than buttons. Rebuild the card around native list-app touch signals: - Solid raised bg-card surface, no border, 18pt continuous corners - Leading 38pt favicon tile as the touch anchor; mono project name becomes an eyebrow line above the title - Trailing chevron affordance; timestamp joins the eyebrow row - Spring press feedback (scale to 97%) via reanimated - Status edge strip survives as a full-height bar on the left edge Settled rows, swipe actions, and long-press menus unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hanges Makes the new sidebar/thread-list testable against existing unupgraded servers by removing every backend dependency of the settled feature: - apps/server, contracts orchestration schemas, and client-runtime command/reducer plumbing revert to main baseline (no thread.settle commands, no settled events/fields, no migration 033) - effectiveSettled() is now archive-based: archivedAt is the explicit settled signal; PR merged/closed and inactivity auto-settle keep working client-side. Blocked work (pending approval/input, live session) still never settles - settle/unsettle actions ride threadEnvironment.archive/unarchive on web and mobile — UI and UX unchanged - Archived threads stay visible as the settled tail: the live shell stream drops archived threads (thread.archived → thread-removed), so both clients merge them back from the archived shell snapshot and refresh it after settle/unsettle/archive actions - Client settings (sidebarV2Enabled, sidebarAutoSettleAfterDays) are kept — they persist client-side only Trade-offs vs the event-sourced model (still on t3code/sidebar-v2-mobile): no activity-driven auto-un-settle for archived threads, and no "keep active" override distinct from unarchiving. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
ApprovabilityVerdict: Needs human review 1 blocking correctness issue found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR. You can customize Macroscope's approvability policy. Learn more. |
| // and is identical for both sidebars — so v1 stays mounted there. | ||
| const pathname = useLocation({ select: (location) => location.pathname }); | ||
| const isOnSettings = pathname === "/settings" || pathname.startsWith("/settings/"); | ||
| const useSidebarV2 = sidebarV2Enabled && !isOnSettings; |
There was a problem hiding this comment.
New-thread picker missing on settings
Medium Severity
With Sidebar V2 enabled, the chat.new shortcut fails to open the new thread/project picker when on /settings/* routes. This occurs because the event listener for this action is only implemented in SidebarV2, but the layout renders SidebarV1 on settings pages, leaving no active listener for the shortcut.
Reviewed by Cursor Bugbot for commit 351a34b. Configure here.
- settleThread inherits archive's running-turn guard on web and mobile: settling can never interrupt an active turn - unsettleThread no-ops on auto-settled (unarchived) threads instead of sending an unarchive the server would reject - Mobile v2 partition now receives per-row PR state (rows report up via onChangeRequestState, mirroring web) so merged/closed PRs auto-settle - Mobile empty state accounts for archived-only workspaces when v2 is on - Optimistic settled hold on web + mobile: the shell being settled is kept visible (marked archived) between the live stream dropping it and the archived snapshot returning it — no flicker, and holds are created only at settle time so deletes are never resurrected - Hover-only settle buttons in web SidebarV2 get focus-visible:opacity-100 - v2 full swipe commits the advertised primary action (Settle/Un-settle), with the stretch animation following; delete remains a tap target only. v1 keeps full-swipe delete via the new fullSwipeAction prop default - Android long-press on v2 cards works: PressableScaleCard is the menu's direct child and forwards the injected onLongPress to its Pressable Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| }); | ||
| }, | ||
| [], | ||
| ); |
There was a problem hiding this comment.
Off-screen PR auto-settle stale
Medium Severity
PR state for partitioning is updated only from mounted ThreadListV2Row effects. On the virtualized mobile list, threads off-screen may never report merged/closed PRs to changeRequestStateByKey, so effectiveSettled can keep them as active cards until the row mounts again.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit af880a2. Configure here.
- Worktree-removal offer works after settle: archived threads leave the live shell store, so the orphan check re-includes the settled shell and click-time revalidation treats absence-from-live as still-settled - Un-settle is only offered when there is an archive to undo: auto-settled rows (inactivity / merged PR, archivedAt null) get Settle instead, on web (hover button + context menu) and mobile (swipe + long-press menu) - Mobile optimistic settled hold rolls back when the settle is blocked or fails (settleThread now reports success); web and mobile both drop holds on delete and un-settle so ghosts can't linger - Archived snapshot refreshes after delete too, clearing stale settled rows sourced from the snapshot - v2 mobile list renders queued offline tasks above the cards (they were dropped entirely, leaving a blank screen for pending-task-only projects) On the chat.new/settings finding: not fixed here — the shortcut routes to the v2 picker only inside chat routes by design; on /settings it falls back to contextual create, matching v1's behavior on those routes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| </PressableScaleCard> | ||
| ) : ( | ||
| <Pressable | ||
| accessibilityHint="Opens the thread. Swipe left to un-settle." |
There was a problem hiding this comment.
🟡 Medium threads/thread-list-v2-items.tsx:317
The slim row's accessibilityHint is hardcoded to "Opens the thread. Swipe left to un-settle.", but auto-settled slim rows (where canUnsettle is false) advertise Settle as their swipe action. Screen-reader users are told the swipe performs the opposite lifecycle action. The hint should reflect whether the swipe settles or un-settles based on canUnsettle.
| accessibilityHint="Opens the thread. Swipe left to un-settle." | |
| accessibilityHint={ | |
| canUnsettle | |
| ? "Opens the thread. Swipe left to un-settle." | |
| : "Opens the thread. Swipe left to settle." | |
| } |
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/features/threads/thread-list-v2-items.tsx around line 317:
The slim row's `accessibilityHint` is hardcoded to `"Opens the thread. Swipe left to un-settle."`, but auto-settled slim rows (where `canUnsettle` is `false`) advertise **Settle** as their swipe action. Screen-reader users are told the swipe performs the opposite lifecycle action. The hint should reflect whether the swipe settles or un-settles based on `canUnsettle`.
| // Archived threads leave the live store entirely, so absence | ||
| // IS the settled state; a live shell means it woke back up | ||
| // (unarchive puts it back in the stream). | ||
| const stillSettled = currentShell === null || currentShell.archivedAt != null; |
There was a problem hiding this comment.
Deleted thread stillSettled check
High Severity
The worktree-removal toast's re-validation for stillSettled incorrectly treats a null currentShell (e.g., when the thread is deleted) as a settled state. This allows the 'Remove worktree' action to proceed for a thread that no longer exists.
Reviewed by Cursor Bugbot for commit 5354496. Configure here.
| next.delete(threadKey); | ||
| return next; | ||
| }); | ||
| } |
There was a problem hiding this comment.
In-flight settle clears hold
Medium Severity
The optimistic settledHolds rollback in handleSettleThread incorrectly removes a thread's hold when onSettleThread resolves false for an in-flight duplicate action. This causes the thread to temporarily disappear from the UI until the archived snapshot loads.
Reviewed by Cursor Bugbot for commit 5354496. Configure here.
Settling is a high-frequency lifecycle action; announcing it with a notification (plus a disk-cleanup offer) was noise. Settle is silent now. Worktree cleanup still exists on the delete path, which asks explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Settling the active thread on web navigates to the next remaining card (wrapping, never a settled row), or to the new-thread screen when it was the last active one. Background settles never navigate. - The settled tail renders 10 rows, then an explicit Show more reveals 25 at a time (web + mobile). Expansion resets when scope, environment, or search changes. Mobile's builder counts hidden rows instead of building them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
There are 7 total unresolved issues (including 4 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit fc5065c. Configure here.
| // Settle = archive: the shell stream drops the thread, so pull the | ||
| // archived snapshot immediately to keep it visible as a settled row. | ||
| refreshArchivedThreadsForEnvironment(threadRef.environmentId); | ||
| navigateAfterSettle?.(); |
There was a problem hiding this comment.
Bulk settle navigates to co-settling thread
Medium Severity
When multiple threads are settled in parallel, the post-settle navigation for the active thread uses a pre-settle snapshot of the sidebar. This can cause navigation to a thread that is also being settled, leaving the user on an archiving or already archived thread.
Reviewed by Cursor Bugbot for commit fc5065c. Configure here.
| // Settle = archive: the shell stream drops the thread, so pull the | ||
| // archived snapshot immediately to keep it visible as a settled row. | ||
| refreshArchivedThreadsForEnvironment(threadRef.environmentId); | ||
| navigateAfterSettle?.(); |
There was a problem hiding this comment.
Interrupted settle still navigates away
Low Severity
After settleThread, only non-interrupted failures roll back the optimistic hold and return early. Interrupt-only failures fall through to refreshArchivedThreadsForEnvironment and navigateAfterSettle, so the UI can leave the current thread even when settle did not finish successfully.
Reviewed by Cursor Bugbot for commit fc5065c. Configure here.
| const nextThread = nextCardKey ? threadByKeyRef.current.get(nextCardKey) : null; | ||
| navigateAfterSettle = nextThread | ||
| ? () => navigateToThread(scopeThreadRef(nextThread.environmentId, nextThread.id)) | ||
| : () => void router.navigate({ to: "/" }); |
There was a problem hiding this comment.
Last settle skips project draft
Medium Severity
When settling the open thread leaves no other active cards, navigateAfterSettle routes to / (NoActiveThreadState). Archiving the same last active thread via archiveThread opens a new draft in that project instead, so settle and archive diverge for the same lifecycle action.
Reviewed by Cursor Bugbot for commit fc5065c. Configure here.


Client-only variant of #3960 for test-driving against existing servers: settle/unsettle ride the archive lifecycle, all server/contract/migration changes reverted to main. Archived threads render as the settled tail. UI unchanged.
🤖 Generated with Claude Code
Note
Medium Risk
Large new sidebar surfaces and thread lifecycle behavior (settle ↔ archive, navigation after settle, archived snapshot merging); defaults keep v1, but mistaken settle/archive mapping could confuse users or drop threads from the live list briefly.
Overview
Adds an opt-in Sidebar v2 on web (
sidebarV2Enabled/sidebarAutoSettleAfterDaysin client settings) and Thread List v2 on mobile (threadListV2Enabledin device preferences), plus planning docs under.plans/.Settle is client-only: manual settle/unsettle map to archive/unarchive (no new server commands).
effectiveSettledinclient-runtimealso treats merged/closed PRs and inactivity as settled. Archived threads are merged back from snapshots so they render as a slim “settled” tail below full card rows for active work.Web:
AppSidebarLayoutswapsSidebarV2when the beta flag is on (v1 stays on settings routes). New flat list uses creation-time ordering, project filter chips, four visual states (approval/working/failed/ready), settle affordances, and shared chrome moved toSidebarChrome. Mobile mirrors the model withThreadListV2Row, optimistic settle holds, paged settled tail, and swipe-primary Settle instead of delete.Smaller UI:
BranchToolbar/ chat header show project favicon + name; desktop settings tests pick up the new client-setting fields.Reviewed by Cursor Bugbot for commit fc5065c. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add Sidebar v2 beta with thread settling, status-driven cards, and beta settings
SidebarV2on web (enabled viasidebarV2Enabledclient setting) with newest-first thread ordering, status-driven card edge colors, and project favicons; settings routes always fall back to the legacy sidebar.effectiveSettledandthreadLastActivityAtutilities inclient-runtimeto compute settled state from inactivity threshold, archived status, session state, and change request state.settleThreadandunsettleThreadtouseThreadActions(web) anduseThreadListActions(mobile); settling is blocked while a thread turn is active and returnsThreadArchiveBlockedError.threadListV2.ts,thread-list-v2-items.tsx) with swipeable settle/un-settle, status badges, pagination of the settled tail, and optimistic hold logic during archive snapshot sync.sidebarV2EnabledandsidebarAutoSettleAfterDays(default 3, range 1–90) toClientSettingsSchema, with UI controls in a new Beta settings panel on both web (/settings/beta) and mobile.startNewThreadInProjectFromContextnow omits contextual branch/worktree options when the target project differs from the current project.Macroscope summarized fc5065c.