Skip to content

fix(web): keep settled threads reachable when opened directly#4413

Merged
t3dotgg merged 8 commits into
mainfrom
sidebar-v2-settled-thread-affordances
Jul 24, 2026
Merged

fix(web): keep settled threads reachable when opened directly#4413
t3dotgg merged 8 commits into
mainfrom
sidebar-v2-settled-thread-affordances

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 24, 2026

Copy link
Copy Markdown
Member

Problem

Opening an old (settled) thread via search left you stuck:

  • The thread's row sits below the "Show more" cutoff in the sidebar's settled tail, so there's no visible highlight and no reachable un-settle affordance.
  • There was no indication in the thread view that the thread is settled, and no way to un-settle it without sending a message.
image

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 ComposerBannerStack shows "This thread is settled — sending a message moves it back to Active in the sidebar" with an Un-settle button. The button issues the same thread.unsettle command (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, the threadSettlement capability gate, and PR state), so the banner and sidebar row can't disagree. Server threads only — never drafts.

Verification

  • bun run typecheck — clean
  • bun run lint — clean for touched files
  • apps/web unit 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 effectiveSettled inputs as the sidebar—shell, sidebarAutoSettleAfterDays, settlement capability, PR state), an info banner in ComposerBannerStack explains settled state and offers Un-settle via threadEnvironment.unsettle (reason: "user"), with per-thread pending state and error toasts. Banner ordering keeps system/branch notices above the settled banner.

Shared clock: New useNowMinute hook 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

  • When a settled thread is opened directly, SidebarV2 now always includes it in the visible settled section, even if it would otherwise be hidden behind "Show more".
  • ChatViewContent shows an informational banner in the composer when the active thread is settled, with an Un-settle button that calls threadEnvironment.unsettle and shows an error toast on failure.
  • A new shared useNowMinute hook provides a minute-quantized UTC timestamp via a single module-level timer, replacing a local interval in SidebarV2.

Macroscope summarized 54b1618.

Summary by CodeRabbit

  • New Features

    • Added settled-thread indicators and an Un-settle action for active threads.
    • Added an informational banner when a thread is settled, while preserving higher-priority warnings.
  • Bug Fixes

    • Active settled threads now remain visible in the sidebar, even when outside the current display limit.
    • Updated the sidebar’s hidden-thread count to accurately reflect visible routed threads.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main fix: keeping settled threads reachable when opened directly.
Description check ✅ Passed The description covers the problem, what changed, why, a UI image, and verification, though it doesn't match the template headings exactly.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sidebar-v2-settled-thread-affordances

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:L 100-499 changed lines (additions + deletions). labels Jul 24, 2026
Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread apps/web/src/components/ChatView.tsx Outdated
@macroscopeapp

macroscopeapp Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Approvability

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/web/src/components/ChatView.tsx (1)

3828-3835: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated minute-quantized clock polling — extract a shared hook. Both files implement the identical nowMinute state + 60s setInterval pattern, 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 shared useNowMinute() hook.
  • apps/web/src/components/SidebarV2.tsx#L793-L800: replace with the same shared useNowMinute() 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

📥 Commits

Reviewing files that changed from the base of the PR and between 67a7b1a and 8eb4efe.

📒 Files selected for processing (2)
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/SidebarV2.tsx

Comment thread apps/web/src/components/ChatView.tsx Outdated
t3dotgg and others added 3 commits July 23, 2026 18:25
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>
Comment thread apps/web/src/hooks/useNowMinute.ts Outdated
@t3dotgg
t3dotgg force-pushed the sidebar-v2-settled-thread-affordances branch from 2e7b0cf to 671013e Compare July 24, 2026 01:26
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>
Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/web/src/components/ChatView.tsx
t3dotgg and others added 2 commits July 23, 2026 18:40
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
Comment thread apps/web/src/hooks/useNowMinute.ts
Comment thread apps/web/src/hooks/useNowMinute.ts Outdated
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>

@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 1 potential issue.

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 bd0b8cb. Configure here.

Comment thread apps/web/src/hooks/useNowMinute.ts
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>
@t3dotgg
t3dotgg merged commit bb38c33 into main Jul 24, 2026
16 checks passed
@t3dotgg
t3dotgg deleted the sidebar-v2-settled-thread-affordances branch July 24, 2026 02:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 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