🤖 feat: snooze workspaces from sidebar, menu, and /snooze command#3343
🤖 feat: snooze workspaces from sidebar, menu, and /snooze command#3343ammar-agent wants to merge 4 commits into
Conversation
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 -->
|
@codex review |
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 |
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 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 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 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 -->
|
@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". |
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.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".partitionWorkspacesBySnoozepeels snoozed workspaces out of the input list beforepartitionWorkspacesByAgeruns, preserving parent/child inheritance so descendants follow their parent through the snooze partition.renderAgeTiersinProjectSidebarrenders an additional collapsible 💤 Snoozed (N) button below the age tiers, styled identically. Expansion state is persisted per-tierKeyPrefixso each project and sub-project section get their own state./snooze <duration>accepts15m,2h,3d,1w, oroff. The parser lives in a sharedsrc/common/utils/snooze.ts(parseHumanDurationMs,formatDurationShort,isWorkspaceSnoozed,MAX_SNOOZE_MS) so the registry, modal preset list, and timestamp derivation all use the same definitions.WorkspaceSnoozeModal(preset buttons + custom duration input) is lazy-mounted on demand and echoes the equivalent/snooze <duration>command live as you choose. Wired throughWorkspaceActionsMenuContent(a newonSnoozeChatslot rendered alongside Archive) from bothAgentListItemandWorkspaceMenuBar.SNOOZE_WORKSPACE→Cmd/Ctrl+Shift+Z(Z mnemonic, no existing collision). Handler lives inWorkspaceMenuBarso it still resolves when the left sidebar is collapsed (same pattern asSHARE_TRANSCRIPT). Listed in the Settings → Keybinds panel."Try /snooze 1d to hide this chat under the Snoozed section until tomorrow".Validation
make typecheck— green.make static-check— green (eslint, prettier, link checks, shellcheck, hadolint).src/common/utils/snooze.test.ts— duration parser, isWorkspaceSnoozed auto-drain, MAX_SNOOZE_MS.src/browser/utils/ui/workspaceFiltering.test.ts— newpartitionWorkspacesBySnoozecases for current/expired snooze plus parent inheritance.src/node/services/workspaceService.snooze.test.ts— persist, clear-via-null, past-timestamp normalization, malformed input, and the 52-week ceiling.AgentListItem,WorkspaceMenuBar,ProjectSidebar,contexts/— all still pass (snooze modal is conditionally mounted so it doesn't require WorkspaceProvider context in lightweight tests).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.snoozedUntilround-trip unchanged; downgrade is safe.Generated with
mux• Model:anthropic:claude-opus-4-7• Thinking:xhigh