fix(web): keep settled threads reachable when opened directly#4413
Conversation
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
ApprovabilityVerdict: Approved This is a self-contained UX bug fix ensuring settled threads remain reachable when opened directly. The changes refactor timer logic into a shared hook and add appropriate UI feedback. Limited scope with no security or infrastructure implications. You can customize Macroscope's approvability policy. Learn more. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/web/src/components/ChatView.tsx (1)
3828-3835: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated minute-quantized clock polling — extract a shared hook. Both files implement the identical
nowMinutestate + 60ssetIntervalpattern, and the ChatView comment explicitly notes it must stay "on the same tick" as the sidebar's. Duplicating the logic risks the two copies drifting apart on a future edit.
apps/web/src/components/ChatView.tsx#L3828-L3835: replace with a shareduseNowMinute()hook.apps/web/src/components/SidebarV2.tsx#L793-L800: replace with the same shareduseNowMinute()hook.🤖 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/ChatView.tsx` around lines 3828 - 3835, Extract the duplicated minute-quantized clock polling into a shared useNowMinute() hook, preserving the existing initial value, 60-second interval, and cleanup behavior. Replace the local nowMinute state/effect in apps/web/src/components/ChatView.tsx lines 3828-3835 and apps/web/src/components/SidebarV2.tsx lines 793-800 with the shared hook, ensuring both components use the same implementation and tick behavior.
🤖 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/ChatView.tsx`:
- Around line 3853-3875: Reset isUnsettling whenever the active thread changes,
matching the existing isRevertingCheckpoint reset pattern. Add an effect keyed
to the active thread identity near the isUnsettling state or
handleUnsettleActiveThread logic, so navigation clears the pending UI state for
the newly selected thread without changing the mutation behavior.
---
Nitpick comments:
In `@apps/web/src/components/ChatView.tsx`:
- Around line 3828-3835: Extract the duplicated minute-quantized clock polling
into a shared useNowMinute() hook, preserving the existing initial value,
60-second interval, and cleanup behavior. Replace the local nowMinute
state/effect in apps/web/src/components/ChatView.tsx lines 3828-3835 and
apps/web/src/components/SidebarV2.tsx lines 793-800 with the shared hook,
ensuring both components use the same implementation and tick behavior.
🪄 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: d35e22ed-b162-4ff0-9819-488fade13187
📒 Files selected for processing (2)
apps/web/src/components/ChatView.tsxapps/web/src/components/SidebarV2.tsx
Opening an old thread via search left it invisible in the sidebar (below "Show more") with no way to un-settle it short of sending a message. Two changes: - SidebarV2: the route thread is force-revealed in the settled tail when it falls below the paging cutoff, so the active row and its un-settle affordance are always visible. - ChatView: a composer banner on settled threads states the settled state, explains that replying re-activates the thread, and offers an explicit Un-settle action (thread.unsettle, reason "user"). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Drive the settled computation off a minute-quantized clock (same tick pattern as the sidebar partition) so inactivity auto-settle updates as time passes instead of freezing at mount. - Order the settled info banner last in the composer stack so the branch-mismatch warning and its repair actions stay front-most. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Reset isUnsettling when the active thread changes so a mid-flight un-settle on thread A never renders thread B's banner as pending (mirrors the isRevertingCheckpoint reset pattern). - Extract the duplicated minute-quantized clock into useNowMinute, shared by the sidebar partition and the composer banner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2e7b0cf to
671013e
Compare
A mount-time-offset interval let two consumers hold different minute values for up to a minute after a boundary crossing, so the sidebar partition and composer banner could briefly disagree on settled state. Tick on UTC minute boundaries instead so all consumers cross together. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A boolean isUnsettling raced across navigation: un-settling thread A, navigating to settled thread B, and un-settling B let A's finally clear B's pending state mid-flight. Track the un-settling thread's key instead — the banner derives its pending state per thread, and a resolving request can only clear its own key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…read-affordances # Conflicts: # apps/web/src/components/ChatView.tsx
Per-instance state and timers meant two consumers could still hold different minute values (remount timing, throttled background timers) despite boundary alignment. A single module-level clock behind useSyncExternalStore gives every consumer the same value by construction; ticks re-read the clock so late timers self-correct, and the timer stops when the last subscriber leaves. 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.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit bd0b8cb. Configure here.
After the last consumer unmounted, the module-level minute froze and a remount's first render served the stale value (subscribe refreshed it without notifying React). getSnapshot now re-reads the clock whenever no timer is running, so a fresh mount renders the current minute; while subscribed, the cached snapshot stays stable as React requires. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Problem
Opening an old (settled) thread via search left you stuck:
Changes
SidebarV2 — force-reveal the open thread. When the route thread is settled and falls below the settled-tail paging cutoff, its row is appended to the bottom of the visible settled section (just above "Show more"). The active highlight and the hover un-settle action are therefore always reachable; the hidden count adjusts to match. No change when the thread is already visible or active.
ChatView — settled banner above the composer. When the open thread is settled, a banner in the existing
ComposerBannerStackshows "This thread is settled — sending a message moves it back to Active in the sidebar" with an Un-settle button. The button issues the samethread.unsettlecommand (reason: "user") the sidebar uses, so it pins the thread active against auto-settle; failures surface as an error toast.Settled state is resolved with the exact same inputs as the sidebar partition (
effectiveSettled,sidebarAutoSettleAfterDays, thethreadSettlementcapability gate, and PR state), so the banner and sidebar row can't disagree. Server threads only — never drafts.Verification
bun run typecheck— cleanbun run lint— clean for touched filesapps/webunit tests — 1465 passed🤖 Generated with Claude Code
Note
Low Risk
UI-only behavior around settled-thread visibility and un-settle; reuses existing settlement APIs and mirrors sidebar logic to avoid state mismatch.
Overview
Fixes the case where opening a settled thread via search or deep link left no sidebar highlight and no way to un-settle without sending a message.
Sidebar: If the routed thread is settled but past the settled-tail “Show more” cutoff, its row is appended to the visible settled list so selection and hover un-settle stay reachable; the hidden count is derived from what is actually shown.
Chat: When the open server thread is settled (same
effectiveSettledinputs as the sidebar—shell,sidebarAutoSettleAfterDays, settlement capability, PR state), an info banner inComposerBannerStackexplains settled state and offers Un-settle viathreadEnvironment.unsettle(reason: "user"), with per-thread pending state and error toasts. Banner ordering keeps system/branch notices above the settled banner.Shared clock: New
useNowMinutehook replaces Sidebar’s local minute interval so sidebar and chat share one UTC-aligned minute timer for auto-settle timing.Reviewed by Cursor Bugbot for commit 54b1618. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Keep settled threads visible in sidebar and show Un-settle banner in composer
threadEnvironment.unsettleand shows an error toast on failure.useNowMinutehook provides a minute-quantized UTC timestamp via a single module-level timer, replacing a local interval inSidebarV2.Macroscope summarized 54b1618.
Summary by CodeRabbit
New Features
Bug Fixes