Sidebar v2 beta: flat thread list with a server-backed settled lifecycle#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>
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
ApprovabilityVerdict: Needs human review Diff is too large for automated approval analysis. A human reviewer should evaluate this PR. You can customize Macroscope's approvability policy. Learn more. |
- 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>
- 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>
| // 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>
Conflict resolutions: - AppSidebarLayout: keep sidebar v2 toggle alongside main's resizable sidebar width + macOS fullscreen handling - Sidebar.tsx: main's new themed-header chrome (SidebarChromeHeader/ Footer with stage backdrop art) moved into sidebar/SidebarChrome.tsx, which this branch had already extracted so SidebarV2 shares it - ChatComposer: combine v2 styling with main's projectSelectionRequired and transition-[background-color] - HomeScreen (mobile): union of imports Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… marker Maria's design pass lands as-is: scoped graphite sidebar palette, rounded composer shell, simplified project chrome, lighter card and filter styling. Her removal of the composer project chip supersedes ours (same outcome). One addition on top: the settled boundary gets its quiet label back. Her pass replaced the SETTLED mono-caps divider with a bare 8px gap — but an unnamed gap doesn't explain why rows below it look different, and the name anchors what un-settle/settle act on. Restored minimally in her visual language: a 10px muted 'Settled' word with a soft hairline, no mono, no caps, no tracking. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Dark .app-sidebar sets --sidebar-stage-fade to its own surface: the nightly/dev header art faded to the global chrome background and showed a seam against the black panel - Failed cards render session.lastError again (one truncated line under the meta row, full text on hover) — the card refactor had reduced failures to a bare 'Failed' word - Settled slim rows: the PR badge moved out of the hover-fading slot, so it stays visible and clickable while the settle affordance shows; only the time/jump label yields on hover - New-thread tooltip resolves chat.newLocal ?? chat.new with no Electron gating, matching v1 — web users saw no shortcut at all Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codex's final-gate review found the last hole in the settle guard: a raced or stale client could settle a thread with an open approval or user-input request. The client refuses (canSettle) and effectiveSettled renders blocked threads active, but the override was still written — so the row would drop into the settled tail the moment the request resolved, hiding the transition the user cares about. The decider now derives open blocking requests from the thread's retained activities (requested minus resolved per requestId — the same accounting ProjectionPipeline uses for pending counts) and rejects thread.settle while any remain. Covered by decider tests for open, resolved, and user-input variants. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v2 showed the ⌘1..9 overlay on a naive metaKey||ctrlKey check, so a ⌘⇧4 screenshot chord kept the hints on screen. v1 already solved this: useShortcutModifierState + shouldShowThreadJumpHintsForModifiers show hints only while the held modifiers EXACTLY match a thread-jump binding — Shift or Alt breaks the match. v2 now uses the same machinery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Threads read as discrete objects: every active card gets a quiet rounded fill (white 3.5%, 2.5% for receded rows) instead of resting flush on the panel. Active/selected/hover remain brighter tones of the same treatment, so the whole interaction scale is one shape. Settled slim rows are intentionally untouched — the contrast between filled cards and bare history rows adds a second cue to the active/settled boundary. 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 1 potential issue.
There are 4 total unresolved issues (including 3 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 25b7d3f. Configure here.
- Bulk delete: grow deletedThreadKeys as deletions land instead of seeding the whole batch, so orphaned-worktree detection never counts still-alive batch mates as deleted (premature shared-worktree removal). - Decider settle invariant: clear open approval/user-input requests on provider.*.respond.failed activities whose detail marks the request stale/unknown, matching the projection's pending accounting. - Queued-turn grace: bound the age check on both sides (client + server) so a clock ahead of the evaluator reads as skew, not eternal queued work. - effectiveSettled: let a server-accepted settle (settledAt >= message time) overrule the clock-derived queued blocker so minute-floored list clocks don't pin a just-settled row active for up to 60s. - Mobile: card accessibilityHint follows the real swipe action (archive fallback); v2 empty state is self-contained — search outranks project scope and never leans on v1's projectGroups signal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
theo go to sleep it isnt worth it |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/src/components/settings/BetaSettingsPanel.tsx`:
- Around line 35-42: Update the value parsing in the auto-settle change handler
to use Number() and require Number.isInteger(parsed) before applying the
existing AUTO_SETTLE_MIN_DAYS/AUTO_SETTLE_MAX_DAYS bounds and calling onCommit,
ensuring decimal and exponent inputs are not truncated.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 40fa2463-85ff-4c99-ac3c-e3ade72158f1
📒 Files selected for processing (79)
apps/desktop/src/settings/DesktopClientSettings.test.tsapps/mobile/src/features/archive/archivedThreadList.test.tsapps/mobile/src/features/home/HomeRouteScreen.tsxapps/mobile/src/features/home/HomeScreen.tsxapps/mobile/src/features/home/homeListItems.test.tsapps/mobile/src/features/home/homeThreadList.test.tsapps/mobile/src/features/home/thread-swipe-actions.tsxapps/mobile/src/features/home/useThreadListActions.tsapps/mobile/src/features/settings/SettingsRouteScreen.tsxapps/mobile/src/features/threads/thread-list-v2-items.tsxapps/mobile/src/features/threads/threadListV2.test.tsapps/mobile/src/features/threads/threadListV2.tsapps/mobile/src/lib/repositoryGroups.test.tsapps/mobile/src/lib/threadActivity.test.tsapps/mobile/src/persistence/mobile-preferences.tsapps/mobile/src/state/use-thread-selection.tsapps/server/src/environment/ServerEnvironment.tsapps/server/src/orchestration/Layers/OrchestrationEngine.test.tsapps/server/src/orchestration/Layers/ProjectionPipeline.test.tsapps/server/src/orchestration/Layers/ProjectionPipeline.tsapps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.tsapps/server/src/orchestration/Layers/ProjectionSnapshotQuery.tsapps/server/src/orchestration/Schemas.tsapps/server/src/orchestration/commandInvariants.test.tsapps/server/src/orchestration/decider.settled.test.tsapps/server/src/orchestration/decider.tsapps/server/src/orchestration/projector.settled.test.tsapps/server/src/orchestration/projector.test.tsapps/server/src/orchestration/projector.tsapps/server/src/persistence/Layers/ProjectionRepositories.test.tsapps/server/src/persistence/Layers/ProjectionThreads.tsapps/server/src/persistence/Migrations.tsapps/server/src/persistence/Migrations/033_ProjectionThreadsSettled.tsapps/server/src/persistence/Services/ProjectionThreads.tsapps/server/src/provider/Layers/ProviderSessionReaper.test.tsapps/server/src/relay/AgentAwarenessRelay.test.tsapps/server/src/server.test.tsapps/web/src/components/AppSidebarLayout.tsxapps/web/src/components/ChatView.logic.test.tsapps/web/src/components/ChatView.logic.tsapps/web/src/components/ChatView.tsxapps/web/src/components/CommandPalette.logic.test.tsapps/web/src/components/Sidebar.logic.test.tsapps/web/src/components/Sidebar.logic.tsapps/web/src/components/Sidebar.tsxapps/web/src/components/SidebarV2.tsxapps/web/src/components/chat/ChatComposer.tsxapps/web/src/components/chat/ChatHeader.tsxapps/web/src/components/settings/BetaSettingsPanel.tsxapps/web/src/components/settings/SettingsSidebarNav.tsxapps/web/src/components/sidebar/SidebarChrome.tsxapps/web/src/hooks/useThreadActions.tsapps/web/src/index.cssapps/web/src/lib/chatThreadActions.test.tsapps/web/src/lib/chatThreadActions.tsapps/web/src/lib/threadSort.test.tsapps/web/src/newThreadPickerBus.tsapps/web/src/routeTree.gen.tsapps/web/src/routes/_chat.tsxapps/web/src/routes/settings.beta.tsxapps/web/src/state/entities.tsapps/web/src/worktreeCleanup.test.tspackages/client-runtime/package.jsonpackages/client-runtime/src/operations/commands.test.tspackages/client-runtime/src/operations/commands.tspackages/client-runtime/src/state/entities.test.tspackages/client-runtime/src/state/shellReducer.test.tspackages/client-runtime/src/state/threadCommands.tspackages/client-runtime/src/state/threadDetail.tspackages/client-runtime/src/state/threadReducer.test.tspackages/client-runtime/src/state/threadReducer.tspackages/client-runtime/src/state/threadSettled.test.tspackages/client-runtime/src/state/threadSettled.tspackages/client-runtime/src/state/threads-sync.test.tspackages/contracts/src/environment.tspackages/contracts/src/orchestration.test.tspackages/contracts/src/orchestration.tspackages/contracts/src/settings.test.tspackages/contracts/src/settings.ts
| const parsed = Number.parseInt(event.target.value, 10); | ||
| if ( | ||
| Number.isFinite(parsed) && | ||
| parsed >= AUTO_SETTLE_MIN_DAYS && | ||
| parsed <= AUTO_SETTLE_MAX_DAYS | ||
| ) { | ||
| onCommit(parsed); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject non-integer input instead of truncating it.
parseInt("3.5", 10) commits 3, and parseInt("1e2", 10) commits 1. Use Number() plus Number.isInteger() so the persisted value matches the displayed value.
Proposed fix
- const parsed = Number.parseInt(event.target.value, 10);
+ const parsed = Number(event.target.value);
if (
- Number.isFinite(parsed) &&
+ Number.isInteger(parsed) &&
parsed >= AUTO_SETTLE_MIN_DAYS &&
parsed <= AUTO_SETTLE_MAX_DAYS
) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const parsed = Number.parseInt(event.target.value, 10); | |
| if ( | |
| Number.isFinite(parsed) && | |
| parsed >= AUTO_SETTLE_MIN_DAYS && | |
| parsed <= AUTO_SETTLE_MAX_DAYS | |
| ) { | |
| onCommit(parsed); | |
| } | |
| const parsed = Number(event.target.value); | |
| if ( | |
| Number.isInteger(parsed) && | |
| parsed >= AUTO_SETTLE_MIN_DAYS && | |
| parsed <= AUTO_SETTLE_MAX_DAYS | |
| ) { | |
| onCommit(parsed); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/components/settings/BetaSettingsPanel.tsx` around lines 35 - 42,
Update the value parsing in the auto-settle change handler to use Number() and
require Number.isInteger(parsed) before applying the existing
AUTO_SETTLE_MIN_DAYS/AUTO_SETTLE_MAX_DAYS bounds and calling onCommit, ensuring
decimal and exponent inputs are not truncated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🚀 Expo continuous deployment is ready!
|
|
@t3dotgg @juliusmarminge this aesthetically looks much better but it doesn't solve a key problem which is that work happens at the level of the worktree, not an individual thread. I outlined the problem here https://cap.so/s/dvaz19nmrwjh2ja I'd really appreciate feedback here because some sort of worktree grouping, and having terminals and dev servers at the worktree level, is much more clear in my opinion. |
this problem is made most apparent when checking out a branch to review/test |
… lifecycle Rebased onto main's sidebar v2 (#4026): migration renumbered 033→034 (settled lifecycle took 033), and the pending-turn-start status resolution in ingestion merged with the roster branch. A thread waiting on background tasks is in motion, not done: - resolveSidebarV2Status shows it as 'Working' instead of the receded ready state - canSettle/effectiveSettled block settling (and override a stale settled pin), matching the running-session blockers - the thread.settle decider rejects it server-side, mirroring the active-session invariant Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cle (pingdotgg#4026) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: maria-rcks <maria@kuuro.net>
…cle (pingdotgg#4026) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: maria-rcks <maria@kuuro.net>


What
Opt-in Sidebar v2 (web) and Thread List v2 (mobile): one flat thread list ordered by creation time, replacing the project-grouped sidebar. Active threads render as status cards; finished work "settles" into a slim, paged tail at the bottom. Threads settle manually (hover ✓ / context menu / swipe), or automatically when their PR merges/closes or after N days of inactivity — and un-settle automatically on real activity (a user message, a session coming alive, a new approval/user-input request).
Settled is a first-class server-side lifecycle (this PR absorbed #4243):
thread.settle/thread.unsettlecommands,thread.settled/thread.unsettledevents, andsettled_override+settled_atcolumns onprojection_threads(migration 033). Settled ≠ archived — settled threads stay in the live shell stream, so their history stays readable without un-settling, and archive keeps its original "hidden from lists" meaning.BEFORE:

AFTER:

MOBILE:

Safety / gating
sidebarV2Enabled(web, Settings → Beta) andthreadListV2Enabled(mobile device preference). Flags off, v1 renders unchanged.PRAGMA-guarded); no backfill, no data rewritten. Threads archived before this lands stay archived.threadSettlementcapability. Clients never send the new commands to a pre-settlement server (absent capability = unsupported), and threads on such environments never classify as settled — no stranded tails with dead affordances.canSettle(running/starting sessions, pending approvals/user input, queued turn starts), the same blockers ineffectiveSettled(auto-settle can't misclassify either), and a server-side decider invariant for raced or stale clients.What was touched / review focus
Server (
apps/server):orchestration/decider.ts— settle/unsettle command handling, the live-session + queued-turn invariants, and the three activity-un-settle injection points (message send, session set, activity append). Real activity clears ANY override: settled threads wake, keep-active pins reset to neutral. Re-emissions are projected no-ops (idempotency withoutupdatedAtchurn). Best place to start.orchestration/projector.ts+Layers/ProjectionPipeline.ts— event application:reason: "user"→ the keep-active pin,reason: "activity"→ neutral.persistence/Migrations/033_ProjectionThreadsSettled.ts+ row plumbing.Shared:
packages/client-runtime/src/state/threadSettled.ts—effectiveSettled/canSettle/hasQueuedTurnStart, heavily tested (172 tests incl. a truth table over all override states).packages/contracts— commands/events (client-dispatchable unsettle reason is"user"only;"activity"is server-owned),threadSettlementcapability, two client settings.Web (
apps/web):components/SidebarV2.tsx— the new sidebar. Review focus: post-settle navigation (next card / project draft, validated against the current route), per-thread in-flight guards, capability-aware partition, bulk actions over exactly the rendered selection.hooks/useThreadActions.ts— settle/unsettle dispatch + typed errors.sidebar/SidebarChrome.tsx(v1 and v2 render identical header/footer); new/settings/betapage; minor chat-header/composer tweaks.Mobile (
apps/mobile):features/threads/threadListV2.ts+thread-list-v2-items.tsx+HomeScreen.tsx— the same model with native list anatomy (swipe actions, long-press menus, settled tail paging, ticking auto-settle clock).Known follow-ups (beta-period, not blockers)
Mobile split-view v2 wiring, mobile PR auto-settle without virtualized row mounts, a11y polish (composite row labels, ARIA patterns), v2 search parity with project titles, and the auto-settle policy knob on mobile.
🤖 Generated with Claude Code
Note
Add flat thread list Sidebar v2 with server-backed thread settle/unsettle lifecycle
SidebarV2on web and a thread list v2 on mobile, both rendering threads sorted by creation time with active threads as cards and settled threads as a paginated tail; enabled via a new Beta settings page (/settings/beta) guarded by asidebarV2Enabledclient setting.thread.settleandthread.unsettlecommands andthread.settled/thread.unsettledevents to the contracts, with full decider enforcement (no live session, no open blocking requests, no queued turn start within a grace window) and idempotent re-emit on already-settled threads.settledOverride,settledAt) through the server projection pipeline, persistence layer (migration 033 addssettled_override/settled_atcolumns toprojection_threads), in-memory projector, client-runtime reducers, and shell/detail merge.settleThread/unsettleThreadinuseThreadActions(web) anduseThreadListActions(mobile), gated by a newthreadSettlementenvironment capability flag; mobile also adds swipe-to-settle gesture support.thread.unsettledwith reasonactivitybefore the triggering event.threadSettlementcapability, so clients silently suppress settle/unsettle actions on those environments.Macroscope summarized cb57915.
Note
Medium Risk
Touches orchestration command invariants and projection schema with new user-visible lifecycle behavior, but it is gated behind beta flags default-off and capability checks with extensive test coverage.
Overview
Introduces thread settlement as a server lifecycle distinct from archive:
thread.settle/thread.unsettlecommands, matching events, and nullablesettled_override/settled_atonprojection_threads(migration 033). Settled threads remain in the live shell stream; the decider blocks settling during active sessions, open approval/user-input requests, and queued turn starts, and prependsthread.unsettledon real activity (messages, live session updates, blocking activities). Servers advertisethreadSettlementso older environments never get settle commands or settled classification.Web adds beta Sidebar v2 (
sidebarV2Enabled, Settings → Beta): creation-order flat list, active cards vs paged settled slim rows, auto-settle via inactivity days and merged/closed PR signals, with shared chrome extracted from v1. Mobile mirrors the model behindthreadListV2Enabledin device preferences—FlatListv2 path, horizontal project scope chips, settle/unsettle inuseThreadListActions, and swipe full-swipe commits the primary lifecycle action (settle/un-settle) instead of delete.Shared
effectiveSettled/canSettlein client-runtime drive partitioning; thread shells and tests across apps pick upsettledOverride/settledAtfields.Reviewed by Cursor Bugbot for commit cb57915. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit