🤖 feat: snooze workspaces from sidebar, menu, and /snooze command#3343
🤖 feat: snooze workspaces from sidebar, menu, and /snooze command#3343ammar-agent wants to merge 8 commits into
Conversation
|
@codex review |
1 similar comment
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0d80069798
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review Addressed both findings:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4a785b0b47
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review Addressed cross-section parent inheritance:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3be3f18c82
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review The previous P1 (cross-section parent inheritance) is already addressed by commit 3be3f18:
The function's local-slice-only behavior only kicks in when |
|
Codex Review: Didn't find any major issues. Keep it up! ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Adds a workspace "snooze" feature that hides a chat from the main sidebar list until a deadline passes, surfacing it under a dedicated 💤 Snoozed collapsible section similar to the existing "Older than X" tiers. Reachable from: - A new "Snooze chat…" item in the workspace dropdown menu - The Cmd/Ctrl+Shift+Z keybind (opens a modal) - The /snooze <duration> slash command (e.g. /snooze 1h, /snooze 2d, /snooze 1w, /snooze off) - A discovery tip in the chat input placeholder carousel The modal echoes the equivalent slash command live, so the menu/keybind flow doubles as command discovery. Snoozed workspaces drain back into the active list automatically once their deadline passes (no backend timer). --- _Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `xhigh`_ <!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=xhigh -->
Lint rule @typescript-eslint/require-await flags async functions without an await expression. The mock returns Promise.resolve() directly instead. --- _Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `xhigh`_ <!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=xhigh -->
Codex review feedback (P1 + P2): - SNOOZE_WORKSPACE keybind moves from Cmd/Ctrl+Shift+Z to Cmd/Ctrl+Shift+X. Cmd+Shift+Z is the platform-standard Redo on macOS and our global preventDefault was hijacking it from text inputs / editors. - The keybind handler now skips when focus is in an editable element so we never preventDefault a legitimate editor shortcut even with the new key. - Modal + slash command + dispatch now clamp custom durations to MAX_SNOOZE_MS (52 weeks) before calling new Date(now + ms).toISOString(). Without the clamp, very large inputs could produce an invalid Date and a RangeError that bypassed the toast UI and stranded the modal as isSaving. - Modal renders a dedicated 'cap at 52 weeks' helper message when the user types a duration beyond the maximum. --- _Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `xhigh`_ <!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=xhigh -->
Codex P2: partitionWorkspacesBySnooze only saw the local workspace slice, so a sub-agent whose snoozed parent lived in a sibling section (or the unsectioned bucket) silently slipped into the active list, breaking the 'descendants follow parent snooze state' contract. - Add an optional parentLookup map to partitionWorkspacesBySnooze that the partitioner falls back to when a parentWorkspaceId isn't present in the local slice. - ProjectSidebar's renderAgeTiers now builds a project-wide id→metadata lookup once per project render and threads it through every section's partition call. - New test asserts cross-section inheritance via parentLookup. --- _Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `xhigh`_ <!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=xhigh -->
Sending a real (non-synthetic) message into a snoozed chat now clears its snoozedUntil so the workspace releases from the Snoozed section, matching Gmail/Slack snooze semantics where any user re-engagement cancels the snooze. - WorkspaceService.sendMessage fires a best-effort clearSnoozeOnUserMessage() for any send where internal.synthetic !== true. Heartbeats, idle compaction, and goal continuations stay gated so they never drain the section. - clearSnoozeOnUserMessage fast-paths the not-snoozed case with a sync config read (no editConfig write) so per-message overhead stays minimal. - Errors are swallowed and logged at debug so a transient config write failure can never block the actual message send. - Three new tests cover: clears active snooze, fast-paths when not snoozed, swallows write errors. --- _Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `xhigh`_ <!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=xhigh -->
3be3f18 to
e49e303
Compare
|
@codex review Two updates since the last review:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e49e30395e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The App-level sortedWorkspacesByProject is wrapped in useStableReference with a comparator that only treats a metadata change as 'visible' when getWorkspaceSidebarKey() differs. snoozedUntil was missing from the key, so when setSnooze or auto-unsnooze emitted a metadata event the Map update was correctly merged into workspaceMetadata but the comparator short-circuited and handed the sidebar back the previous sorted Map reference. The 💤 partition then ran on stale data and the workspace only moved between sections after a full reload (which rebuilt the sorted Map from scratch). Archive happens to work without being in the key because the metadata subscription deletes the entry from the Map entirely, which trips the comparator's length check. Snooze keeps the entry in place, so we have to surface the field-level change explicitly here.
|
@codex review Fixes the "must reload to see snooze move workspaces between sections" report by adding |
Codex P2 catch: the previous placement next to updateRecencyTimestamp fired before downstream validation, so any failed manual send (blank input, pricing gate rejection, queued-task block, requireIdle on busy session, session.sendMessage preflight error) would still drain the snooze even though no user turn was accepted. Move the call to fire on both real success paths instead: right before the queue-path Ok return and after session.sendMessage resolves with success. Same synthetic gate as before so backend-initiated maintenance sends never drain the Snoozed section.
|
@codex review Addressed P2: moved Also fixed the original "must reload to see workspace move between snooze states" report by adding |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e97a47d428
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Codex P2 catch: setSnooze already falls back to a path lookup when the config entry has no `id` yet (legacy format), but clearSnoozeOnUserMessage only searched by id. That meant a legacy workspace could be snoozed successfully but never auto-unsnoozed — the fast-path would treat it as "not snoozed" and skip the editConfig write, leaving the workspace stuck in the Snoozed section after re-engagement. Mirror setSnooze's id-or-path fallback so the fast-path recognizes both modern and legacy entries. Test covers the legacy path-only case.
|
@codex review Addressed second P2: mirrored |
|
Codex Review: Didn't find any major issues. Bravo. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Summary
Adds a workspace "snooze" feature that hides a chat from the main sidebar list until its deadline passes, surfacing it instead under a dedicated 💤 Snoozed collapsible section (rendered like the existing "Older than N days" tiers, but with its own collapsed state). Snooze is reachable from the workspace dropdown menu, a keybind, and a
/snooze <duration>slash command — and is discoverable via the chat-input tips carousel. Snoozes also auto-clear on user re-engagement (sending a message), matching Gmail/Slack semantics.Background
Chats accumulate in the sidebar even when the user has consciously paused them for a few hours/days (e.g. waiting on something, or context-switching out for a sprint). Archiving is too permanent and triggers worktree cleanup; the user just wants them out of sight for a while. Snooze fills that gap with a timestamp-based hide that auto-expires.
Implementation
snoozedUntilISO timestamp toWorkspaceConfigSchema,WorkspaceMetadataSchema, and the node config builder (NEW/LEGACY/fallback paths plusaddWorkspace). The persisted form is the absolute deadline, mirroring how archive usesarchivedAt/unarchivedAt— the live "is snoozed?" state is derived at render time.WorkspaceService.setSnooze(workspaceId, snoozedUntil)validates the timestamp (rejects malformed input and snoozes >52 weeks out, treats past deadlines as explicit unsnooze), updates the entry, then emits metadata so the sidebar reacts via the normal subscription path. Newapi.workspace.snoozeroute withnullsemantics for "clear".WorkspaceService.sendMessagefires a best-effortclearSnoozeOnUserMessagefor any non-synthetic send. Heartbeats, idle compaction, and goal continuations stay gated (they already passsynthetic: true) so they can never drain the Snoozed section by themselves. The helper fast-paths the common "not snoozed" case with a sync config read, so per-message overhead is negligible, and swallows errors so a transient config write failure can never block the actual message send.partitionWorkspacesBySnooze(with optionalparentLookup) cleanly peels snoozed workspaces out before age-tier partitioning, with parent inheritance that works across sub-project sections.WorkspaceSnoozeModalwith preset durations (1h, 4h, 1d, 3d, 1w) + a custom input. Shows the equivalent/snooze <duration>command live. Clamps custom durations toMAX_SNOOZE_MS(52 weeks) to avoidDateRangeErrors. Includes an "Unsnooze" button when already snoozed./snooze <duration>|offaccepts15m,2h,3d,1w. Registered as workspace-only.Cmd/Ctrl+Shift+X("eXclude from sidebar"). Handler skips editable-element targets so it never steals editor shortcuts.WorkspaceActionsMenuContent, wired through bothAgentListItemandWorkspaceMenuBar."Try /snooze 1d to hide this chat under the Snoozed section until tomorrow".Validation
make typecheck,make lint,make static-check— all green locally on the rebased branch.src/common/utils/snooze.test.ts— duration parser,isWorkspaceSnoozedauto-drain,MAX_SNOOZE_MS.src/browser/utils/ui/workspaceFiltering.test.ts—partitionWorkspacesBySnooze(current/expired snooze, local parent inheritance, cross-section parent inheritance viaparentLookup).src/node/services/workspaceService.snooze.test.ts—setSnooze(persist, clear-via-null, past-timestamp normalization, malformed input, max horizon) andclearSnoozeOnUserMessage(clears active snooze, fast-paths not-snoozed, swallows write errors).AgentListItem,WorkspaceMenuBar,ProjectSidebar,contexts/) — all still pass.Risks
partitionWorkspacesByAgekeeps its existing return shape, so no callers were forced to change. Regression risk is contained to the sidebar render path insiderenderAgeTiers.snoozedUntilis treated as unsnoozed byisWorkspaceSnoozedand normalized away on the nextsetSnoozewrite, so a stale persisted timestamp can never strand a workspace.synthetic. All four backend-initiated synthetic sends (heartbeat, heartbeat-with-compaction, idle compaction, goal continuation) already passsynthetic: true, and the helper is fire-and-forget with error swallowing, so the new path can't regress message-send latency or reliability.snoozedUntilround-trip unchanged; downgrade is safe.Generated with
mux• Model:anthropic:claude-opus-4-7• Thinking:xhigh