Skip to content

Add snooze/hide workspace - #4265

Closed
austinywang wants to merge 58 commits into
mainfrom
issue-4261-snooze-hide-workspace
Closed

austinywang wants to merge 58 commits into
mainfrom
issue-4261-snooze-hide-workspace

Conversation

@austinywang

@austinywang austinywang commented May 17, 2026 •

Copy link
Copy Markdown
Contributor

Closes #4261

Summary

  • Add persisted workspace hidden state alongside existing workspace metadata, plus shared visible/hidden projections in TabManager.
  • Add sidebar context-menu snooze/wake actions and a collapsed bottom accordion showing hidden workspace count.
  • Add CLI/socket parity: hide-workspace, show-workspace, and list-workspaces --include-hidden.
  • Keep agent-stop notifications firing for hidden workspaces: notify_target_async still queues directly by workspace/surface IDs through TerminalMutationBus and is not filtered by sidebar visibility.

Notes

  • Hiding the focused workspace selects the next visible workspace when possible and refuses to hide the last visible workspace.
  • Hidden workspaces remain directly targetable by ID/ref for tree and other socket operations.

Verification

  • Added unit coverage for visible filtering, unhide order, focus handoff, last-visible guard, and session restore persistence.
  • Per task instructions, local tests were not run; CI will validate.

Note

Medium Risk
Changes core workspace navigation, sidebar selection, and CLI/socket contracts across a large surface area, though behavior is guarded (e.g. last-visible workspace) and mostly additive.

Overview
Introduces workspace snooze/hide: workspaces can be removed from the sidebar without closing them or stopping notifications, with state persisted in session snapshots (isHidden).

CLI / socket: New hide-workspace / show-workspace (and workspace hide / show) call v2 workspace.hide / workspace.show; list-workspaces and workspace list gain --include-hidden. Workspace handle resolution can pass include_hidden for UUID/ref targeting while numeric indices stay on the visible list. Hide/show commands skip pre-dispatch window focus. Socket mapper emits workspace.visibility_changed and workspace.reordered.

App UI: Sidebar lists visible workspaces only; a collapsible Hidden Workspaces section shows snoozed rows (non-draggable, click-to-wake). Context menus and the command palette add Snooze/Wake with guards so the last visible workspace cannot be hidden. Number shortcuts, reorder/move/close batch actions, command-palette switcher, and drag-drop use visible indices, with mapping back to raw order for inserts.

Supporting fixes: SidebarWorkspaceSelectionSyncPolicy reconciles multi-select after visibility changes; keyboard shortcut matching allows shifted ⌘ punctuation via physical key fallback; find shortcut bails out before focusing the window when there is no target.

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


Summary by cubic

Adds snooze/hide for workspaces and a collapsible “Hidden Workspaces (n)” section. All menus, shortcuts, history, drag/drop, and socket actions resolve against the visible list; selecting by click, ID/ref, or focus jumps reveals a hidden workspace and keeps selection stable.

  • New Features

    • UI: Snooze/Wake actions; Hidden Workspaces accordion; hidden rows aren’t draggable; clicking a hidden row wakes it.
    • Navigation + APIs: Number shortcuts, View > “Workspace N”, history, and drag/drop operate on the visible list; reordering maps visible indices back to raw order.
    • CLI/Socket: hide-workspace/show-workspace, list-workspaces --include-hidden; v2 workspace.hide/workspace.show; emit workspace.visibility_changed and workspace.reordered; hide/show skip pre-dispatch window focus.
    • Persistence/i18n/tests: Persist/restore isHidden; localized Snooze/Wake and Hidden section titles; tests cover visibility filtering, session restore, visible-index resolution, hidden ID/ref selection, reorder clamping, and group/anchor behavior.
    • Dependencies: Advance ghostty submodule to main SHA for latest editor link, tmux bridge, and surface registry fixes.
  • Bug Fixes

    • Groups: Suppress group headers when the anchor workspace is hidden; render visible children as plain rows until woken.
    • Selection/focus: Reveal-on-select moved into the shared selection path so focus jumps and notifications can’t activate a hidden workspace; history skips hidden; context-menu Wake focuses the workspace.
    • Guards: Cannot hide the last visible workspace; hiding the active selects the next visible.
    • Reordering/drag-drop: Clamp out-of-range visible reorder indices; prevent cross-section misplacement and index drift; keep hidden slots when moving to top; fix Bonsplit empty-area drops; preserve sidebar selection anchors; clean up autoscroll on deinit.
    • Shortcuts: Number keys operate on the visible list; exclude digits from simple shortcuts; allow shifted punctuation physical fallback and block mismatches; find shortcut avoids focusing the window when no target.
    • Socket: workspace.action move_up/move_down and close_* now operate over the visible list to avoid acting on hidden workspaces.

Written for commit 315c6c3. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • CLI: hide-workspace / show-workspace and list-workspaces --include-hidden.
  • UI
    • Snooze (hide) / Wake workspace actions in context menus and command palette; context-sensitive labels.
  • Sidebar
    • Collapsible "Hidden Workspaces" section, hidden badges, and adjusted drag/drop behavior (hidden rows non-draggable/non-reorderable).
  • Navigation
    • Number shortcuts, selection, reordering and history operate over visible workspaces only.
  • Persistence
    • Workspace hidden state saved/restored across sessions.
  • Localization
    • Added labels/messages for hide/snooze/wake and last-visible errors.
  • Tests
    • Added workspace visibility tests.

Review Change Stack

@vercel

vercel Bot commented May 17, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 10, 2026 10:52pm
cmux-staging Building Building Preview, Comment Jun 10, 2026 10:52pm

@coderabbitai

coderabbitai Bot commented May 17, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds persistent workspace hidden state, TabManager visible-only APIs and invariants, sidebar hidden-workspaces UI and drag/drop handling, V2 and CLI hide/show/include_hidden semantics, localization, and tests.

Changes

Workspace Visibility (Hide/Snooze)

Layer / File(s) Summary
Workspace model & session persistence
Sources/Workspace.swift, Sources/SessionPersistence.swift
Adds @Published var isHidden, persists/restores hidden state in session snapshots and includes it in session fingerprinting.
TabManager visibility logic & selection invariants
Sources/TabManager.swift
Introduces visibleWorkspaceTabs/hiddenWorkspaceTabs, workspaceTabs(includeHidden:), canHideWorkspaces, hide/show APIs, visible-only reordering/select (selectVisibleWorkspace), navigation/back-forward skipping hidden entries, and restore/ensure-one-visible invariants.
Sidebar UI & action bundle
Sources/ContentView.swift (and many sidebar helpers)
Extends render context with hidden metadata, adds SidebarWorkspaceActionBundle, renders a hidden-workspaces accordion, rewires selection/actions to use visible ordering, updates context menus and command-palette snooze/wake commands, and ensures selecting a hidden workspace first unhides it.
Sidebar drop delegates & Bonsplit overlay
Sources/SidebarWorkspaceDropHandling.swift, Sources/ContentView+MoveTabToNewWorkspace.swift
Adds SidebarEmptyArea, DropDelegate implementations for bonsplit and sidebar reorders that compute visible target indices, reorders via visible reordering APIs, and refactors the bonsplit overlay to accept an injected move closure.
TerminalController V2/V1 handlers & payloads
Sources/TerminalController.swift
Adds workspace.hide/workspace.show v2 handlers (v2WorkspaceSetHidden), include_hidden parsing/enumeration for system tree/list, includes hidden in workspace payloads, refines move-to-window last-visible checks, and updates V1 list/select to use visible-only semantics.
CLI hide/show & list-workspaces
CLI/cmux.swift
Adds hide-workspace/show-workspace subcommands via runWorkspaceVisibilityCommand, extends list-workspaces --include-hidden, propagates includeHidden into normalization, and updates help/output to mark hidden workspaces.
Socket event mapping
Sources/CmuxSocketEventMapper.swift
Publishes workspace.visibility_changed domain events when v2 workspace.hide/workspace.show occur.
App menu, numeric shortcuts, and cmuxApp integration
Sources/cmuxApp.swift, Sources/AppDelegate.swift
Numeric workspace shortcuts, movement, and close-other/above/below operations now index into visibleWorkspaceTabs and use visible-aware enablement and reordering.
Localization, tests, and Xcode project wiring
Resources/Localizable.xcstrings, cmux.xcodeproj, cmuxTests/WorkspaceVisibilityTests.swift
Adds English/Japanese localization keys for snooze/wake and last-visible errors, adds WorkspaceVisibilityTests exercising hide/unhide, selection, reordering, history, session restore, and wires new source files into the project.

Sequence Diagram(s)

(omitted — changes are primarily internal UI/model/handler updates; no new external multi-component sequential flow that needs visualization)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#3430: Sidebar Bonsplit drop overlays and move-to-new-workspace insertion/indexing are code-level related to the sidebar drop handling refactor in this PR.

Poem

"I'm a rabbit in the tree, softly I hide,
Tucking workspaces where quiet ideas bide.
Folded like leaves in a cozy small row,
Tap once to wake them — let bright thoughts grow.
🐇🌿 — cheers to tidy branches!"


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Swift Concurrency ❌ Error Introduces SelectedWorkspaceDirectoryObserver: new ObservableObject with @Published and Combine publishers (.combineLatest, .sink). Should use @Observable and async/await. Replace SelectedWorkspaceDirectoryObserver with @Observable macro and convert Combine chains to async/await per concurrency modernization rules.
Cmux Swift File And Package Boundaries ❌ Error ContentView.swift (+664 lines) and TabManager.swift (+255 lines) violate the >250 lines to oversized file rule. Neither satisfies extraction exceptions or allowed cases. Extract visibility filtering to SwiftPM package or restructure to reduce additions below 250-line threshold while shrinking files by >200 lines.
Docstring Coverage ⚠️ Warning Docstring coverage is 4.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Swiftui State Layout ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
✅ Passed checks (12 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add snooze/hide workspace' clearly summarizes the main feature added in the changeset.
Linked Issues check ✅ Passed All coding requirements from issue #4261 are fully implemented: context-menu snooze/wake actions, collapsible hidden workspaces section, persistence across sessions, ID-based addressability, visible-only operations with focus handoff, and last-visible guards.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the workspace visibility feature from #4261; no unrelated modifications were introduced.
Cmux Swift Actor Isolation ✅ Passed No Swift 6 actor isolation violations found. MainActor classes properly annotated, new value types immutable and correctly scoped, no implicit isolation or Sendable issues.
Cmux Swift Blocking Runtime ✅ Passed No new blocking/timing primitives introduced. All workspace visibility code uses pure data mutations or existing patterns like v2MainSync.
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift and config file changes. Check applies to TypeScript, JavaScript, shell, and build/runtime scripts. Swift timing is out of scope per rule.
Cmux Swift @Concurrent ✅ Passed No violations found. PR adds synchronous @MainActor methods and properties without missing @concurrent, invalid @concurrent, or heavy async without proper boundaries.
Cmux Swift Logging ✅ Passed No logging violations in new workspace visibility code. All new functions lack print/NSLog. CLI output allowed.
Cmux User-Facing Error Privacy ✅ Passed All new user-facing error messages and alerts comply with privacy rules. Uses generic product terminology without exposing vendor names, internal details, credentials, or raw upstream messages.
Cmux Architecture Rethink ✅ Passed PR implements workspace visibility (hide/show) using clean, single-source-of-truth architecture. No timing patches, locks, polling, or split UI lifecycle ownership. Invariants well-enforced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds workspace hide/snooze functionality with no new standalone windows, NSWindow/NSPanel objects, or SwiftUI Window/WindowGroup declarations. Only drop-handling UI components added.
Description check ✅ Passed The pull request description includes the required Summary and Testing sections, along with additional context from auto-generated summaries.
✨ 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 issue-4261-snooze-hide-workspace

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread Sources/TabManager.swift Outdated
Comment thread Sources/TabManager.swift Outdated

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/TabManager.swift (1)

5698-5710: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Skip hidden workspaces in history without deleting them.

These loops currently treat hidden workspaces like closed workspaces and remove them from tabHistory. After that workspace is woken again, back/forward navigation can no longer return to it because its history entry was discarded. Only prune entries when the workspace no longer exists.

Suggested fix
     func navigateBack() {
         guard historyIndex > 0 else { return }

         // Find the previous valid tab in history (skip closed tabs)
         var targetIndex = historyIndex - 1
         while targetIndex >= 0 {
             let tabId = tabHistory[targetIndex]
-            if tabs.contains(where: { $0.id == tabId && !$0.isHidden }) {
-                isNavigatingHistory = true
-                historyIndex = targetIndex
-                selectedTabId = tabId
-                isNavigatingHistory = false
-                return
+            if let workspace = tabs.first(where: { $0.id == tabId }) {
+                if !workspace.isHidden {
+                    isNavigatingHistory = true
+                    historyIndex = targetIndex
+                    selectedTabId = tabId
+                    isNavigatingHistory = false
+                    return
+                }
+                targetIndex -= 1
+                continue
             }
             // Remove closed tab from history
             tabHistory.remove(at: targetIndex)
             historyIndex -= 1
             targetIndex -= 1
@@
     func navigateForward() {
         guard historyIndex < tabHistory.count - 1 else { return }

         // Find the next valid tab in history (skip closed tabs)
-        let targetIndex = historyIndex + 1
+        var targetIndex = historyIndex + 1
         while targetIndex < tabHistory.count {
             let tabId = tabHistory[targetIndex]
-            if tabs.contains(where: { $0.id == tabId && !$0.isHidden }) {
-                isNavigatingHistory = true
-                historyIndex = targetIndex
-                selectedTabId = tabId
-                isNavigatingHistory = false
-                return
+            if let workspace = tabs.first(where: { $0.id == tabId }) {
+                if !workspace.isHidden {
+                    isNavigatingHistory = true
+                    historyIndex = targetIndex
+                    selectedTabId = tabId
+                    isNavigatingHistory = false
+                    return
+                }
+                targetIndex += 1
+                continue
             }
             // Remove closed tab from history
             tabHistory.remove(at: targetIndex)
             // Don't increment targetIndex since we removed the element
         }

Also applies to: 5718-5730

🤖 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 `@Sources/TabManager.swift` around lines 5698 - 5710, The loop that walks
tabHistory currently treats hidden workspaces as closed by removing their
entries; change the logic in the block using tabHistory/targetIndex/historyIndex
to skip entries where a tab exists but isHidden (i.e., if tabs.contains(where: {
$0.id == tabId }) then just decrement targetIndex/historyIndex without
removing), and only remove an entry when no tab with that id exists
(tabs.contains(where: { $0.id == tabId }) == false). Preserve the existing state
updates (isNavigatingHistory = true/false and selectedTabId = tabId) when you
find a non-hidden tab, and apply the same fix to the other analogous loop
handling history around the code that also manipulates tabHistory and
historyIndex.
🤖 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 `@Sources/cmuxApp.swift`:
- Around line 1070-1074: The Close Other Workspaces button currently uses
manager.tabs and manager.tabs.count and thus can act on hidden workspaces;
update the enablement and action to operate only on visible tabs and ignore
hidden workspaces: change the disabled check to use workspace == nil ||
workspace?.isHidden == true || manager.visibleTabs.count <= 1 (or equivalent API
that returns only non-hidden tabs) and modify or replace the call to
closeOtherSelectedWorkspacePeers(in:) so that it either filters manager.tabs for
only non-hidden/visible peers before closing or create a new helper (e.g.,
closeOtherSelectedVisibleWorkspacePeers) that resolves peers from
manager.visibleTabs and only closes those. Ensure the Button label still calls
the new/updated helper so hidden workspaces are not affected.

In `@Sources/ContentView.swift`:
- Around line 9818-9833: The ForEach in workspaceRows still lets views created
below it (e.g., TabItemView, SidebarEmptyArea) capture live ObservableObjects
like TabManager/NotificationStore via renderContext, so convert the data passed
into immutable snapshot structs and closure bundles: create a lightweight value
type (e.g., WorkspaceRowSnapshot and DropGapSnapshot) with only the fields the
row needs plus action closures (select, close, openContextMenu, onDrop), change
workspaceRow(...) to accept these snapshots instead of
Workspace/WorkspaceListRenderContext, and update TabItemView and
SidebarEmptyArea to read only from those snapshots and invoke closures for
mutations; ensure no ObservableObject instances or NotificationStore references
are captured below the ForEach boundary.
- Around line 9909-9917: Replace the hard-coded plural header by adding ICU
plural entries (one/other) under a new key like "sidebar.hiddenWorkspaces" in
your .stringsdict, then call the pluralized localization with the numeric
argument instead of formatting manually: switch the current String(localized:
"sidebar.hiddenWorkspaces.title", defaultValue: "Hidden Workspaces (%d)") usage
to String(localized: "sidebar.hiddenWorkspaces", locale: .current,
renderContext.hiddenWorkspaceCount) (keeping renderContext.hiddenWorkspaceCount
as the numeric parameter) so the OS picks the correct plural form for all
locales.

In `@Sources/TabManager.swift`:
- Around line 4311-4318: The current
visibleWorkspaceIdForFocus(afterRemovingAt:) falls back to the first visible tab
overall, causing focus to jump to a non-adjacent workspace; change the fallback
to search backward from just before the removed index for the nearest visible
workspace. In function visibleWorkspaceIdForFocus(afterRemovingAt removedIndex:
Int), after trying the forward search (startIndex..<tabs.count), add a backwards
search (e.g., stride(from: startIndex - 1, through: 0, by: -1) or a reverse
loop) to find the first tabs[i] where !tabs[i].isHidden and return its id; only
if that also fails keep the existing tabs.first(where: { !$0.isHidden })?.id (or
return nil if no visible tabs).
- Around line 4054-4069: The fallback search in nextVisibleWorkspaceId currently
uses beforeRange.prefix(hiddenIndex).first(where:) which finds the first visible
tab from the start of the list instead of the nearest previous visible tab;
change the before-range search to iterate backward from hiddenIndex so it
returns the closest visible workspace before hiddenIndex (e.g., use a
reversed/prior-indices search such as reversing the prefix or using last(where:)
on the prefix) and return tabs[thatIndex].id; refer to nextVisibleWorkspaceId,
hiddenIndex and beforeRange to locate the site to change.

---

Outside diff comments:
In `@Sources/TabManager.swift`:
- Around line 5698-5710: The loop that walks tabHistory currently treats hidden
workspaces as closed by removing their entries; change the logic in the block
using tabHistory/targetIndex/historyIndex to skip entries where a tab exists but
isHidden (i.e., if tabs.contains(where: { $0.id == tabId }) then just decrement
targetIndex/historyIndex without removing), and only remove an entry when no tab
with that id exists (tabs.contains(where: { $0.id == tabId }) == false).
Preserve the existing state updates (isNavigatingHistory = true/false and
selectedTabId = tabId) when you find a non-hidden tab, and apply the same fix to
the other analogous loop handling history around the code that also manipulates
tabHistory and historyIndex.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 38a20f72-bbbd-43ea-9d05-b1dabbfa1d08

📥 Commits

Reviewing files that changed from the base of the PR and between 789d188 and 96bbc5c.

📒 Files selected for processing (12)
  • CLI/cmux.swift
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/CmuxSocketEventMapper.swift
  • Sources/ContentView.swift
  • Sources/SessionPersistence.swift
  • Sources/TabManager.swift
  • Sources/TerminalController.swift
  • Sources/Workspace.swift
  • Sources/cmuxApp.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/WorkspaceVisibilityTests.swift

Comment thread Sources/cmuxApp.swift
Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/TabManager.swift Outdated
Comment thread Sources/TabManager.swift
Comment thread Sources/ContentView.swift

@cubic-dev-ai cubic-dev-ai 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.

5 issues found across 12 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/TabManager.swift">

<violation number="1" location="Sources/TabManager.swift:4317">
P2: Close/detach focus fallback jumps to the first visible workspace instead of the nearest previous visible workspace.</violation>
</file>

<file name="Sources/TerminalController.swift">

<violation number="1" location="Sources/TerminalController.swift:4326">
P2: `workspace.list` should reject malformed `workspace_id` values; it currently treats them as absent and returns the normal list.</violation>
</file>

<file name="Sources/ContentView.swift">

<violation number="1" location="Sources/ContentView.swift:14951">
P2: After drag/drop, the sidebar stores selection index against `tabManager.tabs` instead of visible tabs, which breaks subsequent Shift-range selection when hidden workspaces exist.</violation>
</file>

<file name="Sources/cmuxApp.swift">

<violation number="1" location="Sources/cmuxApp.swift:1073">
P2: Use `visibleWorkspaceTabs.count` for this disable condition so hidden workspaces do not affect whether “Close Other Workspaces” is enabled.</violation>
</file>

<file name="Resources/Localizable.xcstrings">

<violation number="1" location="Resources/Localizable.xcstrings:3088">
P3: Define this label with pluralized localization variants (e.g., `.one` / `.other`) instead of a single hard-coded plural string so counts like 1 render correctly across locales.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Re-trigger cubic

Comment thread Sources/TabManager.swift Outdated
Comment thread Sources/TerminalController.swift
Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/cmuxApp.swift Outdated
Comment thread Resources/Localizable.xcstrings Outdated
@greptile-apps

greptile-apps Bot commented May 17, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces workspace snooze/hide across the full stack: a new isHidden property on Workspace, persisted in session snapshots, drives visibility filtering throughout TabManager, the sidebar, CLI, and socket API. The implementation is large but well-guarded — the last-visible-workspace invariant is enforced at every close, detach, and hide site, and selectWorkspaceId acts as a shared choke point that reveals hidden workspaces on any selection.

  • TabManager: Adds visibleWorkspaceTabs/hiddenWorkspaceTabs projections and a full family of reorderVisibleWorkspace helpers; all reorder, close-batch, cycle-next/prev, and selection operations are ported to visible-index space with hidden slots preserved in raw order via applyVisibleWorkspaceOrder.
  • ContentView / sidebar: Adds the collapsible Hidden Workspaces accordion, snooze/wake context-menu items and command-palette contributions, and updates lastSidebarSelectionIndex and multi-select anchors to use visible-index space throughout.
  • CLI / socket: New hide-workspace/show-workspace and workspace hide/show commands call workspace.hide/workspace.show; workspace.list gains include_hidden; workspace.reorder and workspace.action move_up/move_down now operate and respond in visible-index space.

Confidence Score: 4/5

Safe to merge with one socket API inconsistency to fix: workspace.action move_top returns a raw all-tabs index while move_up/move_down now return visible indices.

The workspace.action move_top handler was not updated alongside move_up/move_down: it still calls tabManager.tabs.firstIndex (raw position) while the other two actions now call visibleWorkspaceTabs.firstIndex (visible position). Any script or extension that chains move_top with move_up/move_down or compares the result against workspace.list indices will get inconsistent workspace positions. All other previously flagged issues (workspace.reorder, workspace.list workspace_id filtering, workspace.move_to_window last-visible error code, lastSidebarSelectionIndex raw-index overwrite, and translation coverage) are addressed in this iteration.

Sources/TerminalController.swift — the workspace.action move_top case at line 7141.

Important Files Changed

Filename Overview
Sources/TerminalController.swift Adds workspace.hide/workspace.show socket endpoints, fixes workspace.list include_hidden filtering, move_to_window last-visible guard, and reorder visible-index semantics; move_top response still returns raw all-tabs index while move_up/move_down now return visible index.
Sources/TabManager.swift Core workspace visibility logic: visibleWorkspaceTabs/hiddenWorkspaceTabs projections, setWorkspaceHidden, canHideWorkspaces, ensureAtLeastOneVisibleWorkspace, canRemoveWorkspacePreservingVisibleInvariant, reorderVisibleWorkspace family, and selectWorkspaceId reveal-on-select choke point all look well-structured and correct.
Sources/ContentView.swift Sidebar hidden-workspaces accordion, context menu snooze/wake, command palette contributions, and selection index updates all consistently use visibleWorkspaceTabs; reconcileSidebarSelectionAfterWorkspaceVisibilityChange correctly delegates to SidebarWorkspaceSelectionSyncPolicy.
CLI/cmux.swift Adds hide-workspace/show-workspace CLI commands and workspace hide/show subcommands with correct numeric-index vs ref/UUID resolution, include-hidden flag for list, and skipping window focus for hide/show dispatch.
Sources/Workspace.swift Adds isHidden @published Bool property and snapshot serialization (nil for false, true for true) to avoid inflating existing session files.
Sources/SessionPersistence.swift Adds optional isHidden to SessionWorkspaceSnapshot with nil default for backward-compatible decoding.
Sources/SidebarWorkspaceRenderItem.swift Correctly suppresses group headers whose anchor is hidden and removes the skip-anchor logic that depended on anchor visibility.
Sources/AppDelegate.swift Find-shortcut bails out before window focus when no target, workspace number shortcuts use visibleWorkspaceTabs, and the new-workspace flow correctly restores selection by ID instead of raw index.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI / Socket caller
    participant TC as TerminalController
    participant TM as TabManager
    participant WS as Workspace

    CLI->>TC: "workspace.hide {workspace_id}"
    TC->>TM: canHideWorkspaces([id])
    TM-->>TC: false → last_visible_workspace error
    TM-->>TC: true → proceed
    TC->>TM: setWorkspaceHidden(workspace, hidden: true)
    TM->>WS: "isHidden = true"
    TM->>TM: nextVisibleWorkspaceId(afterHiding:)
    TM->>TM: selectWorkspaceId(nextId)
    TM-->>TC: true
    TC-->>CLI: "{hidden: true, selected_workspace_id: nextId, index: nil}"

    CLI->>TC: "workspace.show {workspace_id}"
    TC->>TM: setWorkspaceHidden(workspace, hidden: false)
    TM->>WS: "isHidden = false"
    TM-->>TC: true
    TC-->>CLI: "{hidden: false, index: visibleIndex}"

    CLI->>TC: "workspace.list {include_hidden: false}"
    TC->>TM: workspaceTabs(includeHidden: false)
    TM-->>TC: visible workspaces with 0-based visible indices
    TC-->>CLI: "{workspaces: [...], include_hidden: false}"
Loading

Reviews (27): Last reviewed commit: "Route socket workspace.action move/close..." | Re-trigger Greptile

Comment thread Sources/TabManager.swift
Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/ContentView.swift
Comment thread Sources/cmuxApp.swift

@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: 2

🤖 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 `@Sources/TabManager.swift`:
- Around line 5409-5441: When selectedTabId refers to a hidden workspace the
current logic falls back to visibleTabs.first, causing jumps to the start
instead of the adjacent visible workspace; update selectNextTab() and
selectPreviousTab() so that if currentId is not found in visibleWorkspaceTabs
you locate currentId in the full ordered tab list (the app's master tab array,
e.g. workspaceTabs/allTabs), then search forward (for next) or backward (for
prev) from that position wrapping around until you find the next
visibleWorkspaceTabs element, and set selectedTabId to that id (instead of
visibleTabs.first). Keep using activateWorkspaceCycleHotWindow() and preserve
the DEBUG debugPrepareWorkspaceSwitch calls.
- Around line 4031-4051: The setWorkspaceHidden(_ tab: Workspace, hidden: Bool)
function mutates the passed Workspace instance which may be a stale detached
object after restoreSessionSnapshot() replaces tabs with fresh Workspace
instances; instead, resolve the live workspace from the tabs array by finding
the workspace with the same id (use tabs.first(where: { $0.id == tab.id }) or an
index lookup) and perform all checks and mutations (isHidden,
sidebarSelectedWorkspaceIds, selectedTabId update, nextVisibleWorkspaceId call)
on that resolved live instance before assigning tabs = tabs and returning;
ensure you still respect canHideWorkspaces([tab.id]) by checking against the
resolved workspace id and preserve the existing early returns and nextSelection
logic but operate on the live workspace object.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 879e403a-0cd2-4585-a512-ab3f6e8f1a0d

📥 Commits

Reviewing files that changed from the base of the PR and between 96bbc5c and 1d18aad.

📒 Files selected for processing (6)
  • Resources/Localizable.xcstrings
  • Sources/ContentView+MoveTabToNewWorkspace.swift
  • Sources/ContentView.swift
  • Sources/TabManager.swift
  • Sources/cmuxApp.swift
  • cmuxTests/WorkspaceVisibilityTests.swift

Comment thread Sources/TabManager.swift Outdated
Comment thread Sources/TabManager.swift
Comment thread Sources/ContentView.swift
Comment thread Sources/ContentView.swift Outdated
Comment thread Sources/ContentView.swift Outdated
Comment thread CLI/cmux.swift
Comment thread CLI/cmux.swift

@cursor cursor 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.

Cursor Bugbot has reviewed your changes 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 dec325c. Configure here.

Comment thread Sources/ContentView.swift
austinywang and others added 4 commits June 10, 2026 14:23
…e-workspace

# Conflicts:
#	CLI/cmux.swift
#	Sources/CmuxSocketEventMapper.swift
#	Sources/ContentView.swift
#	Sources/TabManager.swift
#	Sources/Workspace.swift
The snooze/hide workspace feature adds lines to existing files
(ContentView, TabManager, CLI/cmux, TerminalController, etc.); refresh
the tracked budget to the post-merge actuals. Also picks up reductions
for files that shrank on main (TerminalNotificationStore, FeedCoordinator).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e-workspace

# Conflicts:
#	.github/swift-file-length-budget.tsv
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
austinywang and others added 3 commits June 10, 2026 14:52
The visible-index reorder plan rejected out-of-range indices before
clampedVisibleReorderIndex could clamp them, regressing CLI/socket
reorder callers (e.g. workspace.reorder --index 999 meaning 'move to
bottom') that historically clamped via workspaceReorderPlan(toIndex:).
Remove the bounds guard and rely on the clamp; add regression coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two interactions surfaced after merging main's workspace-groups subsystem:

1. Snoozing a group's anchor left the anchor visible: the visible-only
   render still emitted a group header keyed on the hidden anchor id, so
   the snoozed workspace stayed in the sidebar as the header. renderItems
   now suppresses a group header when its anchor is not in the visible
   set and renders the visible children as plain rows until woken.

2. Reveal-on-select only ran in selectWorkspace(_:), so focus paths that
   route through selectWorkspaceId directly (focusTab / notification
   jump-to-unread, move-with-focus) could make a hidden workspace the
   active terminal with no sidebar row. Move the reveal into the shared
   selectWorkspaceId choke point.

Adds regression coverage for both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
austinywang and others added 2 commits June 10, 2026 15:31
The feature branch was pinned to an older ghostty (f78189a, an ancestor
of main's 34cbf18); the main merges failed to fast-forward the gitlink.
Point it at main's current SHA so builds pick up the cmd-click link, tmux
bridge, and surface registry fixes already on main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
workspace.action move_up/move_down and close_others/above/below operated
on raw tabManager.tabs, so a snoozed (hidden) workspace between visible
rows made move_down a visual no-op and close-others/above/below could
close snoozed workspaces that should keep running. Route them through the
visible workspace list, matching the UI context-menu/keyboard path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment on lines 7139 to 7141
case "move_top":
tabManager.moveTabToTop(workspace.id)
finish(["index": v2OrNull(tabManager.tabs.firstIndex(where: { $0.id == workspace.id }))])

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.

P1 move_top response index not updated to visible space

move_up and move_down were both updated to return visibleWorkspaceTabs.firstIndex (visible index) in this PR, but move_top still returns tabManager.tabs.firstIndex (raw all-tabs index). After any workspace.action move_top, the response index diverges from the index returned by workspace.list (default) and from the index returned by move_up/move_down — all three actions now promise a consistent visible-space index, but move_top silently breaks that contract.

Concrete failure: [HiddenX(0), A(1), B(2)]. workspace.action workspace_id=B action=move_top moves B to visible slot 0 and raw slot 1. The response reports index: 1 (raw). A follow-up workspace.list shows B at index: 0 (visible). A script that feeds the move_top response index back into a workspace.reorder or workspace.action move_up/move_down call will target the wrong workspace.

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 315c6c3b Deployed Jun 10, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Snooze/Hide a workspace.

3 participants