Skip to content

Sidebar v2 without server changes#4026

Open
t3dotgg wants to merge 15 commits into
mainfrom
t3code/new-sidebar-client-only
Open

Sidebar v2 without server changes#4026
t3dotgg wants to merge 15 commits into
mainfrom
t3code/new-sidebar-client-only

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 16, 2026

Copy link
Copy Markdown
Member

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 / sidebarAutoSettleAfterDays in client settings) and Thread List v2 on mobile (threadListV2Enabled in device preferences), plus planning docs under .plans/.

Settle is client-only: manual settle/unsettle map to archive/unarchive (no new server commands). effectiveSettled in client-runtime also 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: AppSidebarLayout swaps SidebarV2 when 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 to SidebarChrome. Mobile mirrors the model with ThreadListV2Row, 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

  • Introduces SidebarV2 on web (enabled via sidebarV2Enabled client setting) with newest-first thread ordering, status-driven card edge colors, and project favicons; settings routes always fall back to the legacy sidebar.
  • Adds effectiveSettled and threadLastActivityAt utilities in client-runtime to compute settled state from inactivity threshold, archived status, session state, and change request state.
  • Adds settleThread and unsettleThread to useThreadActions (web) and useThreadListActions (mobile); settling is blocked while a thread turn is active and returns ThreadArchiveBlockedError.
  • Introduces a mobile v2 thread list (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.
  • Adds sidebarV2Enabled and sidebarAutoSettleAfterDays (default 3, range 1–90) to ClientSettingsSchema, with UI controls in a new Beta settings panel on both web (/settings/beta) and mobile.
  • startNewThreadInProjectFromContext now omits contextual branch/worktree options when the target project differs from the current project.
  • Risk: settling reuses the archive/unarchive mutations client-side; the settled state is purely client-local and not persisted server-side.

Macroscope summarized fc5065c.

t3dotgg and others added 9 commits July 15, 2026 02:30
…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>
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: bfe630dc-76fa-4a6c-adf2-6cc632d38f16

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/new-sidebar-client-only

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 16, 2026
Comment thread apps/web/src/components/SidebarV2.tsx
Comment thread apps/web/src/components/SidebarV2.tsx Outdated
Comment thread apps/mobile/src/features/threads/threadListV2.ts Outdated
Comment thread apps/web/src/components/SidebarV2.tsx Outdated
Comment thread apps/mobile/src/features/threads/thread-list-v2-items.tsx
Comment thread apps/web/src/components/SidebarV2.tsx
Comment thread apps/mobile/src/features/home/HomeScreen.tsx
Comment thread apps/web/src/hooks/useThreadActions.ts
Comment thread apps/web/src/hooks/useThreadActions.ts
Comment thread apps/mobile/src/features/threads/thread-list-v2-items.tsx
@macroscopeapp

macroscopeapp Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

Comment thread apps/web/src/hooks/useThreadActions.ts Outdated
Comment thread apps/web/src/hooks/useThreadActions.ts
// 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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>
Comment thread apps/mobile/src/features/home/HomeScreen.tsx
Comment thread apps/mobile/src/features/home/HomeScreen.tsx
Comment thread apps/mobile/src/features/home/HomeScreen.tsx
});
},
[],
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit af880a2. Configure here.

Comment thread apps/mobile/src/features/home/useThreadListActions.ts
Comment thread apps/web/src/hooks/useThreadActions.ts
- 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."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Suggested change
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`.

Comment thread apps/web/src/hooks/useThreadActions.ts Outdated
// 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5354496. Configure here.

next.delete(threadKey);
return next;
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5354496. Configure here.

t3dotgg and others added 3 commits July 16, 2026 00:17
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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Fix All in Cursor

❌ 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?.();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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?.();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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: "/" });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fc5065c. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant