Skip to content

feat(sidebar-v2): thread snooze with wake-loud return#4311

Open
t3dotgg wants to merge 2 commits into
mainfrom
t3/sidebar-v2-snooze
Open

feat(sidebar-v2): thread snooze with wake-loud return#4311
t3dotgg wants to merge 2 commits into
mainfrom
t3/sidebar-v2-snooze

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 22, 2026

Copy link
Copy Markdown
Member

Summary

Adds an inbox-zero snooze lifecycle to Sidebar v2: hide a thread from the inbox until a chosen time, then bring it back loudly. Design follows the approved plan (option B popover entry + option C collapsed shelf + option F context-menu parity, "Woke" pill for returns): https://2udjpqoxpjoz.postplan.dev

Snooze is an overlay on the active state, not a fourth destination — crisply distinct from Settle ("done, tuck below") and Archive ("out of the sidebar"): snooze is "not now, guaranteed to come back."

Contracts + server

  • New thread.snooze / thread.unsnooze commands and thread.snoozed / thread.unsnoozed events; snoozedUntil / snoozedAt on OrchestrationThread + shell (optional fields, so payloads from pre-snooze servers still decode)
  • Decider invariants: wake time must be in the future; blocked-on-you work (open approval/user-input request) cannot be snoozed; duplicate snoozes re-emit idempotently (same pattern as settle); a user message on a snoozed thread spends the return ticket (thread.unsnoozed, reason activity)
  • Migration 034 adds snoozed_until / snoozed_at to projection_threads; projector, projection pipeline, and every snapshot/shell query thread the fields through
  • New threadSnooze capability flag, version-skew gated exactly like threadSettlement — clients never send the commands to older servers

Shared logic (client-runtime)

  • effectiveSnoozed: hidden while the wake time is ahead and the thread hasn't raised its hand. Timer wakes are derived — no server event fires; a passed snoozedUntil simply stops classifying as snoozed
  • threadRaisedHandWhileSnoozed: pending approval/input, a failure newer than the snooze, or a run completed after the snooze wake the thread early (classification only — the snooze fields stay put, mirroring how effectiveSettled treats blocked work). Snoozing a thread that already failed stays snoozed: that snooze was "I saw it, not now"
  • threadWokeAt feeds the woke indicator until the user visits
  • Snooze never pauses the agent — a working thread keeps working; only visibility changes

Sidebar v2 UI

  • Entry: hover clock button beside Settle opens a preset popover — In 1 hour / This evening / Tomorrow 9:00 / Next week — with "Until PR merges" / "Until next review" visible as SOON teasers (the event-based schema hook lands next)
  • Destination: collapsed "Snoozed · N" shelf between Active and Settled — snoozed threads are never gone without a trace. Expanded rows show wake countdowns (2h, 18h) and a hover wake-now button; the shelf disappears entirely at count 0
  • Wake: amber Woke pill (alarm-clock icon) + unread-weight title, in the thread's original position — the calm static sort is untouched; the pill carries the signal. Clears on visit
  • Parity: context-menu Snooze › submenu on single rows and Snooze (N) on multi-select (only when every selected thread can take it); undo toast after every snooze

Tests

  • decider.snoozed.test.ts — invariants, idempotent re-emission, activity wake on turn.start
  • threadSnoozed.test.ts — effectiveSnoozed / raised-hand / canSnooze / threadWokeAt edge cases (incl. pre-snooze failures staying snoozed)
  • Sidebar.snooze.test.ts — preset boundaries (evening suppression, Monday next-week), wake labels
  • Existing settled/projection fixtures extended with the new columns

pnpm typecheck, pnpm lint (no new warnings), and full test suites for server (1571 passed), web (1444), client-runtime (467), contracts (187) all green. Codex review ran on the diff; its one finding (pre-existing failures instantly un-hiding a fresh snooze) is fixed and covered by a test.

🤖 Generated with Claude Code


Note

Medium Risk
Touches orchestration decider, projections, and sidebar partitioning/navigation; invariants mirror settlement but new lifecycle paths (activity unsnooze, client-derived timer wake) need careful rollout behind capability flags.

Overview
Adds thread snooze end-to-end: hide inbox threads until a chosen time, then surface them again with a visible Woke signal—distinct from settle/archive and gated by a new threadSnooze capability like settlement.

Stack: New thread.snooze / thread.unsnooze commands and thread.snoozed / thread.unsnoozed events; optional snoozedUntil / snoozedAt on thread shells; migration 034 on projection_threads; projector, pipeline, and snapshot queries updated. The decider enforces future wake times, blocks snooze when approvals/user-input or queued turn starts are pending, idempotent re-snooze to the same time, and auto-unsnoozes on user turn start (activity). Queued-turn detection is extracted to shared threadHasQueuedTurnStart for settle and snooze.

Client-runtime: effectiveSnoozed, canSnooze, threadRaisedHandWhileSnoozed, and threadWokeAt drive visibility (timer wake needs no server event; pending work / fresh failures / post-snooze completions can wake early without clearing snooze fields).

Sidebar v2: Preset popover (hour / evening / tomorrow / next week), collapsible Snoozed · N shelf with countdowns and wake-now, context-menu and bulk snooze, success toast with undo; settle navigation skips snoozed rows.

Tests: Decider, client-runtime snooze logic, and preset/label helpers; fixtures extended for new columns.

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

Note

Add thread snooze and wake functionality to SidebarV2 with server-side enforcement

  • Adds snooze/unsnooze commands end-to-end: new contract schemas, server decider logic, projection updates, DB migration (migration 034), and client-runtime dispatch.
  • Snoozed threads are grouped in a collapsible 'Snoozed' shelf in SidebarV2, sorted by earliest wake time, with per-row wake countdowns and hover actions to snooze or wake immediately.
  • Threads wake early when they raise their hand (pending approvals, user input, session errors, or turn completion after snooze time); sending a user message also auto-unsnoozes.
  • Server enforces snooze invariants: thread must not be archived, wake time must be in the future, no pending blocking requests, and no queued turn start.
  • useThreadActions exposes snoozeThread/unsnoozeThread with structured errors (ThreadSnoozeUnsupportedError, ThreadSnoozeBlockedError) when capability or thread state blocks the action.
  • Risk: partition recomputation runs on a JS timer keyed to the nearest wake boundary; clock skew or tab suspension may delay reclassification.

Macroscope summarized c56b487.

Adds an inbox-zero snooze lifecycle to Sidebar v2: hide a thread until a
chosen time, then bring it back loudly.

Contracts + server:
- thread.snooze / thread.unsnooze commands and thread.snoozed /
  thread.unsnoozed events; snoozedUntil/snoozedAt on thread + shell
  (optional, so pre-snooze payloads still decode)
- decider invariants: wake time must be in the future; blocked-on-you
  work (open approval/user-input) cannot be snoozed; duplicates re-emit
  idempotently; a user message on a snoozed thread spends the return
  ticket (thread.unsnoozed reason "activity")
- migration 034 adds snoozed_until/snoozed_at to projection_threads;
  projector, projection pipeline, and all snapshot/shell queries thread
  the fields through
- threadSnooze capability flag, version-skew gated like threadSettlement

Shared logic (client-runtime):
- effectiveSnoozed: hidden while the wake time is ahead AND the thread
  hasn't raised its hand; timer wakes are derived (no server event)
- threadRaisedHandWhileSnoozed: pending approval/input, a failure newer
  than the snooze, or a run completed after the snooze wake early —
  classification only, without clearing the server-side snooze
- threadWokeAt drives the woke indicator until the user visits

Sidebar v2 UI:
- hover clock button beside Settle opening a preset popover (In 1 hour /
  This evening / Tomorrow / Next week; event-based options teased as SOON)
- collapsed "Snoozed · N" shelf between Active and Settled — never hides
  threads without a trace; expanded rows show wake countdowns and a
  wake-now button; shelf disappears at count 0
- amber "Woke" pill + unread-weight title when a snooze ends; the static
  sort is untouched, the pill carries the signal
- context-menu Snooze submenu (single + multi-select), undo toast

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 22, 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: e131bea2-b7a4-479e-8efa-f39dde202a83

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 t3/sidebar-v2-snooze

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 22, 2026
Comment thread apps/web/src/components/SidebarV2.tsx Outdated
Comment thread apps/server/src/orchestration/decider.ts
Comment thread apps/server/src/orchestration/decider.ts
Comment thread packages/client-runtime/src/state/threadSettled.ts
Comment thread apps/web/src/components/Sidebar.snooze.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

3 blocking correctness issues found. New feature adding thread snooze functionality across server, client, and UI layers with new database schema, commands, events, and business logic. Multiple unresolved medium-severity comments identify potential bugs in the snooze invariant checks, wake timestamp handling, and DST date arithmetic.

You can customize Macroscope's approvability policy. Learn more.

Merge resolves the SidebarV2 restyle (#4268) by re-applying snooze onto
the new surface model (sidebar tokens, thread tooltips, restyled action
buttons), and folds in serverSelfUpdate capability alongside threadSnooze.

Review fixes:
- Snooze classification now uses a precise clock instead of the
  minute-quantized partition time, plus a timeout armed at the earliest
  upcoming wake — the shelf empties the moment a snooze expires instead
  of up to a minute late (Cursor)
- thread.snooze now rejects queued turn starts exactly like
  thread.settle, server-side (shared threadHasQueuedTurnStart helper)
  and client-side (canSnooze gains the hasQueuedTurnStart check) (Cursor)
- threadWokeAt keeps an early hand-raise wake authoritative after the
  scheduled wake time passes, so a visited-and-cleared Woke pill cannot
  resurface when snoozedUntil elapses (Macroscope)
- resolveSnoozePresets advances calendar days with setDate instead of
  DAY_MS offsets, fixing tomorrow/next-week presets across DST
  transitions (Macroscope)
- Documented why hasOpenBlockingRequest's scan of the 500-capped
  activities array is sound for the snooze invariant (Macroscope)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

Updated the branch (merged main, incl. the #4268 sidebar restyle — snooze UI re-applied onto the new surface tokens) and addressed all four review findings:

  • Mismatched clocks (Cursor): snooze classification now reads a precise clock instead of the minute-quantized partition time, and a timeout is armed at the earliest upcoming wake — the shelf empties the moment a snooze expires, and wake labels can't show "now" while still hidden.
  • Queued turns (Cursor): thread.snooze now rejects queued turn starts with the same detection and clock-skew bounds as thread.settle (extracted into a shared threadHasQueuedTurnStart helper); canSnooze mirrors it client-side via hasQueuedTurnStart. Covered by new decider + shared-logic tests.
  • threadWokeAt resurfacing a cleared pill (Macroscope): early hand-raise wakes now stay authoritative after the scheduled wake time passes — the hand-raise branch is checked before the timer branch, with a regression test.
  • DST preset bug (Macroscope): resolveSnoozePresets advances calendar days with setDate instead of DAY_MS offsets.
  • Capped-activities scan (Macroscope): documented on hasOpenBlockingRequest why the 500-entry cap is sound — an open request blocks its turn, so a request can't scroll out of the window while still open; the projection's pendingApprovalCount reads the same stream and stays consistent.

All suites green: server 1597, web 1450, client-runtime 469, contracts 187; repo-wide typecheck + lint clean.

🤖 Generated with Claude Code

@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 c56b487. Configure here.

// shows the pill; visiting clears it.
const isWoke =
props.wokeAt !== null &&
(lastVisitedAt === undefined || Date.parse(lastVisitedAt) < Date.parse(props.wokeAt));

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.

Invalid visit time hides Woke

Medium Severity

The new isWoke check and snooze label helpers (snoozeWakeLabel, snoozeWakeDescription) bypass the project's parseTimestampDate utility. This leads to inconsistent handling of unparseable ISO timestamps, causing the "Woke" pill to not display and potentially misclassifying thread status.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by learned rule: Use parseTimestampDate for ISO string parsing, return null for invalid

Reviewed by Cursor Bugbot for commit c56b487. 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