Skip to content

[WIP] Sidebar v2 beta: flat adaptive-density thread list with settled lifecycle#3960

Closed
t3dotgg wants to merge 5 commits into
mainfrom
t3code/sidebar-familiar-beta
Closed

[WIP] Sidebar v2 beta: flat adaptive-density thread list with settled lifecycle#3960
t3dotgg wants to merge 5 commits into
mainfrom
t3code/sidebar-familiar-beta

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 14, 2026

Copy link
Copy Markdown
Member

Warning

🚧 WIP — not ready to merge. Actively iterating on the design with live feedback. The data model is stable; the UI is still moving. Do not merge without a final review pass.

What

A new thread sidebar behind Settings → Beta → Sidebar v2 (sidebarV2Enabled, default off). Design doc: .plans/21-sidebar-v2-beta.md · concept mocks: https://icjy2209spm2.postplan.dev

One flat thread list across projects, statically ordered by creation time — activity never reorders rows; only settling moves them. Unsettled threads render as bordered cards; settled threads collapse to slim rows. Settled is an explicit, event-sourced lifecycle state — users settle work manually (hover ✓ / context menu / bulk), or it auto-settles (PR merged/closed, N days inactive; default 3, configurable). Any real activity un-settles automatically.

Sidebar v2 overview

Card design: per-card project title bar

The current design direction (several live-feedback rounds in) is brutalist: square hard-edged cards on a darker shell, high-contrast neutral borders, color reserved for meaning. Each card is a tiny window — a thin title bar carries the project favicon + name with the timestamp, separated from the thread content by a hairline. Project identity reads as chrome, on every row, with zero grouping. Status is a solid 3px left edge strip (sky = working with a duty-cycled pulse, amber = needs approval, red = failed) plus a mono status word; the active card brightens its border and title bar. Card titles clamp to two lines instead of truncating one.

Sidebar v2 cards

Project identity on the open thread

The same favicon + name chip leads the chat header and the composer's run-context strip (drafts included), so "which project am I in / about to submit to" is answered at both ends of the screen:

Chat header project chip

Composer with project chip

Project scope chips

The flat list drops per-project group headers; scope chips replace them — filtering the list and informing where new threads go. The + chip opens the add-project flow.

New-thread project picker

Creating a thread opens a picker with the contextual project preselected (Enter confirms — ⇧⌘O ↵ is still two keystrokes), all projects listed with paths, and an Add project row. Arrow/Home/End navigation. Single-project setups skip the modal; ⌘⇧N (chat.newLocal) bypasses it as the explicit escape hatch.

Settle interaction

Hover actions are overlay-positioned in the title bar (no layout shift). Settling an orphaned-worktree thread offers a one-click worktree removal (re-validated at click time, never forced). Settled rows grey out (dimmed favicon + title) and restore on hover.

Settle hover

The settled data model (ships dark, flag-independent)

Mirrors the archivedAt / thread.archived pattern end-to-end:

  • settledOverride: "settled" | "active" | null + settledAt on thread + shell; thread.settle / thread.unsettle commands; thread.settled / thread.unsettled events; projection columns via migration 033.
  • effectiveSettled() (client-runtime, pure, truth-table tested): manual override → live session guard → PR merged/closed → inactivity threshold.
  • Activity auto-unsettles server-side: turn start, live session start (starting/running only — stop/error status writes do not revert a manual settle), approval and user-input requests.
  • Idempotency by event re-emission (the engine rejects zero-event commands), so double-clicks and bulk settles are silent no-ops.
  • The toggle is a pure view swap — v1 ignores the new fields entirely; flipping back and forth never migrates data.

Review & verification

Reviewed by a multi-agent adversarial-verify workflow (fable-5) plus an independent gpt-5.6-sol pass; all confirmed findings fixed (zero-event command rejection, session-stop unsettling manual settles, approvals not waking settled threads, PR-state partition wiring, dropped traversal shortcuts, multi-select context menu, stale worktree toast, memo-defeating props, settings-route prefix collision, and more — see commit messages).

  • vp run typecheck clean across contracts / client-runtime / server / web (one pre-existing unrelated failure in infra/relay)
  • vp check clean · contracts 184 ✓ · client-runtime 322 ✓ · server orchestration+persistence 182 ✓ · web 1288 ✓
  • Manually tested over Tailscale against a copy of real userdata; screenshots above are from that instance.

Known open items (why this is WIP)

  • Design still iterating (several rounds of live feedback so far; more expected)
  • Approval-blocked threads no longer pin to the top (static order won that trade-off) — may want a non-reordering "N waiting" affordance if it bites
  • Settle affordance discoverability — hover-only on desktop; no touch story yet
  • No keyboard shortcut for settle yet (deliberate for the first cut)
  • List is not virtualized (content-visibility only) — fine at ~100 threads, unproven beyond
  • Beta telemetry (settle source counts, toggle-off rate) not yet implemented — plan Phase 4
  • Diff stats on cards blocked on checkpoint data reaching the shell projection

Out of scope (per plan)

Inline approve/reject from the sidebar, message snippets, ops-grid density mode, auto-archive of long-settled threads, removing v1.

🤖 Generated with Claude Code

Note

Add thread settle/unsettle lifecycle and flat-density Sidebar v2 beta

  • Introduces a thread settlement model: new thread.settle and thread.unsettle commands, events, and projections propagate settled state through contracts, server orchestration, projection pipeline, and client runtime.
  • Adds effectiveSettled and threadLastActivityAt utilities in threadSettled.ts that determine settled state from overrides, session status, PR status, and an autoSettleAfterDays inactivity threshold (default 3 days).
  • Adds a new SidebarV2.tsx with a flat adaptive-density thread list, gated behind a sidebarV2Enabled client setting toggled from a new /settings/beta panel.
  • Exposes settleThread and unsettleThread in useThreadActions; settling a thread may offer a toast to remove an orphaned worktree if VCS status is clean.
  • Fixes startNewThreadInProjectFromContext so branch/worktree context is not carried when starting a thread in a different project.
  • Risk: database migration 033 adds settled_override and settled_at columns to projection_threads; sending a user message, starting a session, or appending certain activities to a settled thread now emits an implicit thread.unsettled event before the primary event.

Macroscope summarized 23d7a3d.


Note

Medium Risk
Orchestration commands can now emit extra leading thread.unsettled events on activity, and the new sidebar is a large UI surface, but behavior is beta-gated and mirrors the existing archive pattern.

Overview
Adds an opt-in Sidebar v2 (sidebarV2Enabled) that swaps in at AppSidebarLayout (v1 stays on settings routes). The new flat list uses card vs slim rows driven by a settled lifecycle: manual settle/unsettle, auto-settle from merged/closed PRs and configurable inactivity, with activity-driven un-settle on the server for turn starts, live session starts, and approval/user-input requests.

The settled model ships flag-independent end-to-end—contracts, thread.settle / thread.unsettle, projection migration 033 (settled_override / settled_at), shell queries, client effectiveSettled(), reducer/projector/decider tests—while v1 ignores the new fields.

SidebarV2 adds project scope chips, status-colored cards (resolveSidebarV2Status), creation-time ordering for active threads, settle affordances (hover, context menu, bulk), and a multi-project new-thread picker. Shared chrome moves to SidebarChrome; chat surfaces show project favicon + name in the header/toolbar. Design rationale lives in .plans/21-sidebar-v2-beta.*.

Reviewed by Cursor Bugbot for commit 23d7a3d. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Jul 14, 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: 04c7dab5-9bd2-4667-bd2e-33202030d9ce

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/sidebar-familiar-beta

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 14, 2026
Comment thread apps/web/src/hooks/useThreadActions.ts
Comment thread apps/web/src/components/Sidebar.logic.ts
Comment thread packages/client-runtime/src/state/threadSettled.ts
Comment thread apps/web/src/components/Sidebar.logic.ts Outdated
Comment thread apps/web/src/components/SidebarV2.tsx
Comment thread apps/server/src/orchestration/decider.ts
rangeSelectTo(threadKey, orderedThreadKeysRef.current);
return;
}
if (isTrailingDoubleClick(event.detail)) {

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 components/SidebarV2.tsx:595

Double-clicking a thread in the v2 sidebar navigates on the first click and then suppresses the second click via isTrailingDoubleClick, but no row exposes an onDoubleClick handler (or any rename action) — so inline rename never starts. The v2 sidebar drops the existing double-click-to-rename behavior instead of preserving it. Consider wiring an onDoubleClick handler on each row (e.g. an onRename callback) that the trailing-click guard was meant to protect, or remove the guard if rename is not shipping in v2.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/SidebarV2.tsx around line 595:

Double-clicking a thread in the v2 sidebar navigates on the first click and then suppresses the second click via `isTrailingDoubleClick`, but no row exposes an `onDoubleClick` handler (or any rename action) — so inline rename never starts. The v2 sidebar drops the existing double-click-to-rename behavior instead of preserving it. Consider wiring an `onDoubleClick` handler on each row (e.g. an `onRename` callback) that the trailing-click guard was meant to protect, or remove the guard if rename is not shipping in v2.

Comment thread apps/server/src/orchestration/decider.ts Outdated
Comment thread packages/contracts/src/settings.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

6 blocking correctness issues 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.

},
scopeProjectRef(project.environmentId, project.id),
);
},

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.

Cross-project worktree seed on create

High Severity

The new-thread picker's createThreadInProject function calls startNewThreadInProjectFromContext, which incorrectly copies the active thread's branch, worktreePath, and envMode to the new draft. This can lead to new threads being created with an incorrect worktree or branch when the chosen project differs from the active thread's project.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4441a7a. Configure here.

Comment thread apps/web/src/components/SidebarV2.tsx Outdated
})();
const newThreadTargetRef = scopedProject
? scopeProjectRef(scopedProject.environmentId, scopedProject.id)
: (activeProjectRef ?? newThreadContext.defaultProjectRef);

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.

Picker default skips draft project

Medium Severity

newThreadTargetRef resolves the picker default from the scoped project, then the open server thread in threadByKey, then defaultProjectRef. It never consults activeDraftThread, unlike resolveThreadActionProjectRef and the command palette. On a draft with multiple projects, the preselected row (and Enter) can target the wrong project.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4441a7a. Configure here.

: (projects.find(
(project) => `${project.environmentId}:${project.id}` === projectScopeKey,
) ?? null),
[projectScopeKey, projects],

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.

Stale scope after project removal

Low Severity

projectScopeKey is never cleared when its project disappears. scopedProject becomes null so the list shows all threads, but the All chip still keys selection off projectScopeKey === null, so no chip appears selected despite an unscoped list.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4441a7a. Configure here.

@t3dotgg t3dotgg changed the title Sidebar v2 beta: flat adaptive-density thread list with settled lifecycle [WIP] Sidebar v2 beta: flat adaptive-density thread list with settled lifecycle Jul 14, 2026
@t3dotgg
t3dotgg marked this pull request as draft July 14, 2026 10:16
@t3dotgg
t3dotgg marked this pull request as ready for review July 14, 2026 10:21
t3dotgg added a commit that referenced this pull request Jul 14, 2026
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>
Comment on lines +653 to +660
useEffect(() => {
if (
projectScopeKey !== null &&
!projects.some((project) => `${project.environmentId}:${project.id}` === projectScopeKey)
) {
setProjectScopeKey(null);
}
}, [projectScopeKey, projects]);

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 components/SidebarV2.tsx:653

When the user selects threads across projects and then applies a project scope, bulk actions silently skip every selected thread that is no longer visible. handleMultiSelectContextMenu reads all keys from selectedThreadKeys (so the menu count is correct), but resolves each thread through threadByKeyRef, which only contains the currently scoped rows. For any hidden key, threadByKeyRef.current.get(threadKey) returns undefined, the if (!thread) continue skips it, and settle/delete/mark-unread affects only the visible subset. The loop then calls clearSelection() or removeFromSelection(threadKeys) on the entire selection, so the skipped threads lose their selected state without ever being acted on. Consider clearing the selection when the project scope changes, or resolving selected threads from the unfiltered thread collection rather than the scoped view.

  useEffect(() => {
-    if (
-      projectScopeKey !== null &&
-      !projects.some((project) => `${project.environmentId}:${project.id}` === projectScopeKey)
-    ) {
-      setProjectScopeKey(null);
+    const stillExists =
      projectScopeKey !== null &&
      projects.some((project) => `${project.environmentId}:${project.id}` === projectScopeKey);
+    if (!stillExists) {
+      setProjectScopeKey(null);
     }
+    clearSelection();
  }, [projectScopeKey, projects, clearSelection]);
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/SidebarV2.tsx around lines 653-660:

When the user selects threads across projects and then applies a project scope, bulk actions silently skip every selected thread that is no longer visible. `handleMultiSelectContextMenu` reads all keys from `selectedThreadKeys` (so the menu count is correct), but resolves each thread through `threadByKeyRef`, which only contains the currently scoped rows. For any hidden key, `threadByKeyRef.current.get(threadKey)` returns `undefined`, the `if (!thread) continue` skips it, and settle/delete/mark-unread affects only the visible subset. The loop then calls `clearSelection()` or `removeFromSelection(threadKeys)` on the *entire* selection, so the skipped threads lose their selected state without ever being acted on. Consider clearing the selection when the project scope changes, or resolving selected threads from the unfiltered thread collection rather than the scoped view.

);
if (confirmed._tag === "Failure" || !confirmed.value) return;
}
const deletedThreadKeys = new Set(threadKeys);

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 components/SidebarV2.tsx:909

In handleMultiSelectContextMenu, the bulk-delete loop passes the full deletedThreadKeys set (every thread selected for deletion) into every deleteThread call, including threads that haven't been deleted yet. deleteThread uses that set to decide whether the current thread's worktree is orphaned by excluding all those IDs from the survivor list. When two selected threads share a worktree, the first deletion sees the other thread's ID in the exclusion set, treats the worktree as orphaned, and removes it. If a later deletion in the batch then fails, the surviving thread is left pointing at a worktree that no longer exists. Only the IDs of threads already successfully deleted — or just the current one — should be excluded, or worktree cleanup should run after the entire batch succeeds.

Also found in 1 other location(s)

apps/web/src/hooks/useThreadActions.ts:388

settleThread offers worktree deletion without checking whether the thread has a live starting/running session. The server's thread.settle command accepts such threads, and stillSettled later only checks settledOverride, so a user can settle a currently running thread and remove its clean worktree while the agent is still using it. force: false protects Git changes, not a live process relying on that directory; this can break the active session and its ongoing work. Add a live-session guard both before creating the removal toast and in the click-time revalidation.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/SidebarV2.tsx around line 909:

In `handleMultiSelectContextMenu`, the bulk-delete loop passes the full `deletedThreadKeys` set (every thread selected for deletion) into every `deleteThread` call, including threads that haven't been deleted yet. `deleteThread` uses that set to decide whether the current thread's worktree is orphaned by excluding all those IDs from the survivor list. When two selected threads share a worktree, the first deletion sees the other thread's ID in the exclusion set, treats the worktree as orphaned, and removes it. If a later deletion in the batch then fails, the surviving thread is left pointing at a worktree that no longer exists. Only the IDs of threads already successfully deleted — or just the current one — should be excluded, or worktree cleanup should run after the entire batch succeeds.

Also found in 1 other location(s):
- apps/web/src/hooks/useThreadActions.ts:388 -- `settleThread` offers worktree deletion without checking whether the thread has a live `starting`/`running` session. The server's `thread.settle` command accepts such threads, and `stillSettled` later only checks `settledOverride`, so a user can settle a currently running thread and remove its clean worktree while the agent is still using it. `force: false` protects Git changes, not a live process relying on that directory; this can break the active session and its ongoing work. Add a live-session guard both before creating the removal toast and in the click-time revalidation.

payload: {
threadId: command.threadId,
settledAt,
updatedAt: settledAt,

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.

Settle re-emit rewinds updatedAt

Medium Severity

Setting thread.settled's updatedAt to settledAt can rewind a thread's updatedAt timestamp. This causes settled threads to reorder incorrectly in the UI and resets the perceived wait time and sort order for approval-blocked threads.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2652c0b. Configure here.

// Approval activities bump shell.updatedAt in the projection pipeline.
// The shell has no dedicated request timestamp, so this is the closest
// accurate wait-start signal shared by the label and approval ordering.
return `waiting ${formatElapsedDurationLabel(thread.updatedAt)}`;

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.

Approval wait uses noisy updatedAt

Medium Severity

Approval wait labels and longest-wait pinning now key off shell updatedAt. Projection also advances updatedAt on later thread.session-set and other shell writes, so a post-approval session update shortens the waiting label and can reorder approval-blocked threads away from true wait age.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2652c0b. Configure here.

event.preventDefault();
onStartRename(threadRef, thread.title);
},
[isRenaming, onStartRename, thread.title, threadRef],

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.

Mobile rename lacks sidebar guard

Low Severity

Inline double-click rename in SidebarV2 does not skip mobile the way v1 does. On mobile the first tap already navigates and closes the sheet, so a double-tap can still enter rename on a row that is no longer usable.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2652c0b. Configure here.

t3dotgg and others added 3 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>
@t3dotgg
t3dotgg force-pushed the t3code/sidebar-familiar-beta branch from 2652c0b to 069f271 Compare July 15, 2026 09:32
},
};
return [userMessageEvent, turnStartRequestedEvent];
if (targetThread.settledOverride !== "settled") {

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 orchestration/decider.ts:522

The auto-unsettle branches in thread.turn.start, thread.session.set, and thread.activity.append emit thread.unsettled for archived threads, bypassing the archived-thread invariant that thread.unsettle enforces via requireThreadNotArchived. An archived thread whose settledOverride is "settled" will have that override cleared to "active" by these synthesized events — the same outcome a direct thread.unsettle command is explicitly rejected for. The issue is that these branches check thread.settledOverride !== "settled" but never check thread.archivedAt, so they proceed to prepend an unsettledEvent even on archived threads. Consider gating the synthesized thread.unsettled on thread.archivedAt === null (or rejecting the parent command with requireThreadNotArchived when the thread is archived).

Suggested change
if (targetThread.settledOverride !== "settled") {
if (targetThread.archivedAt === null && targetThread.settledOverride !== "settled") {
return [userMessageEvent, turnStartRequestedEvent];
}
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/orchestration/decider.ts around line 522:

The auto-unsettle branches in `thread.turn.start`, `thread.session.set`, and `thread.activity.append` emit `thread.unsettled` for archived threads, bypassing the archived-thread invariant that `thread.unsettle` enforces via `requireThreadNotArchived`. An archived thread whose `settledOverride` is `"settled"` will have that override cleared to `"active"` by these synthesized events — the same outcome a direct `thread.unsettle` command is explicitly rejected for. The issue is that these branches check `thread.settledOverride !== "settled"` but never check `thread.archivedAt`, so they proceed to prepend an `unsettledEvent` even on archived threads. Consider gating the synthesized `thread.unsettled` on `thread.archivedAt === null` (or rejecting the parent command with `requireThreadNotArchived` when the thread is archived).

Comment on lines +419 to +421
const stillSettled = currentShell?.settledOverride === "settled";
if (!stillOrphaned || !stillSettled) {
toastManager.add(

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.

🟠 High hooks/useThreadActions.ts:419

The settle toast's "Remove worktree" action only checks that the thread is still marked settledOverride === "settled" and that the worktree is still orphaned — it never verifies that the session has actually stopped. A thread whose session is starting or running can be manually marked as settled, so the click-time recheck passes and removeWorktree deletes the working directory while an active agent session is still using it. Consider checking currentShell?.session?.status is stopped (or absent) before allowing removal.

-                const stillOrphaned =
-                  getOrphanedWorktreePathForThread(currentThreads, threadRef.threadId) ===
-                  orphanedWorktreePath;
-                const stillSettled = currentShell?.settledOverride === "settled";
-                if (!stillOrphaned || !stillSettled) {
+                const stillOrphaned =
+                  getOrphanedWorktreePathForThread(currentThreads, threadRef.threadId) ===
+                  orphanedWorktreePath;
+                const stillSettled = currentShell?.settledOverride === "settled";
+                const sessionStopped =
+                  currentShell?.session == null || currentShell.session.status === "stopped";
+                if (!stillOrphaned || !stillSettled || !sessionStopped) {
Also found in 1 other location(s)

apps/web/src/worktreeCleanup.ts:51

canOfferWorktreeRemoval treats aheadCount === 0 as proof that there are no unpushed commits, but VcsStatusResult sets aheadCount to 0 when hasUpstream is false. A clean newly-created/local-only branch therefore receives the “Remove worktree” offer even though none of its commits have been pushed, contradicting the cleanup safety check. Include hasUpstream (or compare aheadOfDefaultCount/another remote ref) before offering removal.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/hooks/useThreadActions.ts around lines 419-421:

The settle toast's "Remove worktree" action only checks that the thread is still marked `settledOverride === "settled"` and that the worktree is still orphaned — it never verifies that the session has actually stopped. A thread whose session is `starting` or `running` can be manually marked as settled, so the click-time recheck passes and `removeWorktree` deletes the working directory while an active agent session is still using it. Consider checking `currentShell?.session?.status` is `stopped` (or absent) before allowing removal.

Also found in 1 other location(s):
- apps/web/src/worktreeCleanup.ts:51 -- `canOfferWorktreeRemoval` treats `aheadCount === 0` as proof that there are no unpushed commits, but `VcsStatusResult` sets `aheadCount` to `0` when `hasUpstream` is false. A clean newly-created/local-only branch therefore receives the “Remove worktree” offer even though none of its commits have been pushed, contradicting the cleanup safety check. Include `hasUpstream` (or compare `aheadOfDefaultCount`/another remote ref) before offering removal.

Comment thread apps/server/src/orchestration/decider.ts
…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>
}, [onChangeRequestState, prState, threadKey]);

const modelInstanceId = thread.session?.providerInstanceId ?? thread.modelSelection.instanceId;
const driverKind = props.providerEntryByInstanceId.get(modelInstanceId)?.driverKind ?? 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.

🟡 Medium components/SidebarV2.tsx:214

SidebarV2Row resolves driverKind from providerEntryByInstanceId, a map built exclusively from primaryServerProvidersAtom. This sidebar renders threads from all environments, so a remote-environment thread whose provider instance isn't configured on the primary server yields driverKind === null and silently hides the provider icon and tooltip. Worse, if a remote instance ID collides with a differently-configured primary instance, the row displays the wrong provider icon. The lookup should be scoped by thread.environmentId (or use provider metadata from the thread's own environment) so remote threads resolve their correct driver kind.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/SidebarV2.tsx around line 214:

`SidebarV2Row` resolves `driverKind` from `providerEntryByInstanceId`, a map built exclusively from `primaryServerProvidersAtom`. This sidebar renders threads from all environments, so a remote-environment thread whose provider instance isn't configured on the primary server yields `driverKind === null` and silently hides the provider icon and tooltip. Worse, if a remote instance ID collides with a differently-configured primary instance, the row displays the wrong provider icon. The lookup should be scoped by `thread.environmentId` (or use provider metadata from the thread's own environment) so remote threads resolve their correct driver kind.

@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 2 potential issues.

There are 9 total unresolved issues (including 7 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 ab23a82. Configure here.

Settled
</span>
<span className="h-px flex-1 bg-border" />
</div>

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.

Settled divider breaks size hint

Medium Severity

The slim sidebar rows declare contain-intrinsic-size: auto 34px, but the new "Settled" divider adds height, causing content-visibility: auto to underestimate off-screen row sizes. This results in scroll jumps when these rows become visible.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ab23a82. Configure here.

Settled
</span>
<span className="h-px flex-1 bg-border" />
</div>

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.

Settled label hidden from AT

Low Severity

The aria-hidden attribute on the "Settled" section divider prevents assistive technologies from announcing this new, visible section boundary. This means users relying on screen readers miss the context that sighted users get from the visual label.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ab23a82. Configure here.

- 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>
@t3dotgg

t3dotgg commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

Superseded by #4026, which now carries the whole Sidebar v2 beta — the flat list UI (iterated well past this branch) plus the server-backed settled lifecycle this PR pioneered (settle/unsettle commands, settled_override/settled_at, migration 033, activity auto-unsettle were ported from here and hardened through several review rounds there). Closing in its favor.

@t3dotgg t3dotgg closed this Jul 22, 2026
colonelpanic8 pushed a commit to colonelpanic8/t3code that referenced this pull request Jul 22, 2026
thread.settle / thread.unsettle commands, thread.settled/unsettled
events, settled_override + settled_at projection columns (migration
033), and activity auto-unsettle in the decider: a user message, a
session coming alive, or a new approval/user-input request emits
thread.unsettled(reason: activity) ahead of the triggering event.

Settled threads stay in the live shell stream (unlike archived ones):
settledOverride/settledAt ride the existing thread shell schema, so
readable settled history requires no separate snapshot path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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