Skip to content

🤖 feat: snooze workspaces from sidebar, menu, and /snooze command#3343

Open
ammar-agent wants to merge 4 commits into
mainfrom
chat-snooze-6rdj
Open

🤖 feat: snooze workspaces from sidebar, menu, and /snooze command#3343
ammar-agent wants to merge 4 commits into
mainfrom
chat-snooze-6rdj

Conversation

@ammar-agent
Copy link
Copy Markdown
Collaborator

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

  • Schema. Adds an optional snoozedUntil ISO timestamp to WorkspaceConfigSchema, WorkspaceMetadataSchema, and the node config builder (NEW/LEGACY/fallback paths plus addWorkspace). The persisted form is the absolute deadline, mirroring how archive uses archivedAt/unarchivedAt — the live "is snoozed?" state is derived at render time.
  • Backend. 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. New api.workspace.snooze route with null semantics for "clear".
  • Frontend partition. New partitionWorkspacesBySnooze peels snoozed workspaces out of the input list before partitionWorkspacesByAge runs, preserving parent/child inheritance so descendants follow their parent through the snooze partition.
  • Sidebar render. renderAgeTiers in ProjectSidebar renders an additional collapsible 💤 Snoozed (N) button below the age tiers, styled identically. Expansion state is persisted per-tierKeyPrefix so each project and sub-project section get their own state.
  • Slash command. /snooze <duration> accepts 15m, 2h, 3d, 1w, or off. The parser lives in a shared src/common/utils/snooze.ts (parseHumanDurationMs, formatDurationShort, isWorkspaceSnoozed, MAX_SNOOZE_MS) so the registry, modal preset list, and timestamp derivation all use the same definitions.
  • Modal + menu. New WorkspaceSnoozeModal (preset buttons + custom duration input) is lazy-mounted on demand and echoes the equivalent /snooze <duration> command live as you choose. Wired through WorkspaceActionsMenuContent (a new onSnoozeChat slot rendered alongside Archive) from both AgentListItem and WorkspaceMenuBar.
  • Keybind. SNOOZE_WORKSPACECmd/Ctrl+Shift+Z (Z mnemonic, no existing collision). Handler lives in WorkspaceMenuBar so it still resolves when the left sidebar is collapsed (same pattern as SHARE_TRANSCRIPT). Listed in the Settings → Keybinds panel.
  • Tip. Adds a tip after position 0 (Chromatic-pinned lead tip is preserved): "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).
  • Targeted bun tests:
    • src/common/utils/snooze.test.ts — duration parser, isWorkspaceSnoozed auto-drain, MAX_SNOOZE_MS.
    • src/browser/utils/ui/workspaceFiltering.test.ts — new partitionWorkspacesBySnooze cases 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.
    • Component suites: AgentListItem, WorkspaceMenuBar, ProjectSidebar, contexts/ — all still pass (snooze modal is conditionally mounted so it doesn't require WorkspaceProvider context in lightweight tests).

Risks

  • Sidebar partition output type unchanged. Snooze is a new partitioner; partitionWorkspacesByAge keeps its existing return shape, so no callers were forced to change. Regression risk is contained to the sidebar render path inside renderAgeTiers.
  • Self-healing auto-drain. A past snoozedUntil is treated as unsnoozed by isWorkspaceSnoozed and normalized away on the next setSnooze write, so a stale persisted timestamp can never strand a workspace.
  • Schema change is additive + optional. Existing configs without snoozedUntil round-trip unchanged; downgrade is safe.

Generated with mux • Model: anthropic:claude-opus-4-7 • Thinking: xhigh

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 -->
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@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 -->
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment thread src/browser/utils/ui/keybinds.ts Outdated
Comment thread src/browser/components/WorkspaceSnoozeModal/WorkspaceSnoozeModal.tsx Outdated
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 -->
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Addressed both findings:

  • P1 (Redo conflict): SNOOZE_WORKSPACE moved to Cmd/Ctrl+Shift+X ("eXclude from sidebar"); handler also now skips when focus is in an editable element so the chord can never preventDefault an editor shortcut.
  • P2 (Range bypass): Modal + slash command + dispatcher all clamp custom durations against MAX_SNOOZE_MS (52 weeks) and verify the resulting milliseconds are finite before calling new Date(...).toISOString(). Modal also surfaces a "cap at 52 weeks" helper when the user types over the maximum.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment thread src/browser/utils/ui/workspaceFiltering.ts
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 -->
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Addressed cross-section parent inheritance:

  • partitionWorkspacesBySnooze now accepts an optional parentLookup map and falls back to it when a parentWorkspaceId isn't present in the local slice.
  • ProjectSidebar.renderAgeTiers builds a project-wide id→metadata map once per project render and threads it through every section's partition call, so a child stays under the Snoozed section even if its parent lives in a sibling section / the unsectioned bucket.
  • Added a test covering the cross-section case.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment thread src/browser/utils/ui/workspaceFiltering.ts
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

The previous P1 (cross-section parent inheritance) is already addressed by commit 3be3f18:

  • partitionWorkspacesBySnooze now accepts an optional parentLookup (project-wide id→metadata map) and falls back to it when a parentWorkspaceId isn't in the local slice.
  • ProjectSidebar.renderAgeTiers (the single production callsite) builds a project-wide lookup once per project render and threads it through every section's partition call.
  • New test walks the parentLookup when the parent lives outside the partition slice covers exactly this case.

The function's local-slice-only behavior only kicks in when parentLookup is omitted (e.g. in legacy tests); production always passes it. Please re-check the callsite at ProjectSidebar.tsx:2490-2503.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

ℹ️ 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".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant