Skip to content

Add workspace pages - #4766

Closed
austinywang wants to merge 51 commits into
mainfrom
issue-3297-allow-a-surface-tab-to-have-splits
Closed

austinywang wants to merge 51 commits into
mainfrom
issue-3297-allow-a-surface-tab-to-have-splits

Conversation

@austinywang

@austinywang austinywang commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Adds workspace pages under each workspace so a top-level page can own its own split/pane/surface layout.
  • Adds titlebar page controls, page-aware menus, customizable page shortcuts, CLI/socket page commands, and session persistence for inactive pages.
  • Documents the scoped first increment in docs/workspace-pages-spec.md; drag/drop page reordering and hover-only page chrome remain intentionally out of this PR.

Closes #3297

Testing

  • Not run locally per repo/user policy; CI only.

Note

Low Risk
CLI-only additions mirroring existing workspace command patterns; no auth or persistence logic in this diff, though incorrect page targeting could close or reorder the wrong page in automation scripts.

Overview
Adds legacy top-level cmux verbs for workspace page lifecycle and navigation, each calling the v2 socket page.* APIs with shared --workspace / --window context resolution.

New commands include list-pages, new-page, duplicate-page, current-page, select-page, rename-page, close-page (defaults to current page, force: true), reorder-page, and next-page / previous-page / last-page. Page targets accept UUIDs, page:N refs, or 1-based indexes via new normalizePageHandle; isHandleRef now recognizes page.

cmux tree is updated to nest pages → panes → surfaces under workspaces (with active/selected markers) and falls back to workspace-level panes when no pages exist. Per-command help, the global usage blurb, and the command allowlist document the new verbs and page handle syntax.

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


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.


Summary by cubic

Adds workspace pages so a workspace can hold multiple full layouts (splits, panes, surfaces) with fast switching. Adds page UI, page-aware menus/shortcuts, per-page session restore, CLI/socket v2 page APIs, cmux verbs, and cmux tree page output (closes #3297).

  • New Features

    • Titlebar page strip with new/rename/duplicate/close/close-others/quick-select; View menu is page-aware and localized (EN/JA).
    • Shortcuts: new/rename/close, next/previous, Option+1–8 and Option+9 for last; configurable and routed to the focused window.
    • CLI and socket v2: full page.* lifecycle (create, duplicate, rename [defaults to current; accepts positional handles and titles after --], select, close [current allowed], reorder, list, next, previous, last); cmux adds list-pages, current-page, and page handles (UUID/ref/1-based index). Pane/surface commands accept page refs and route via that page.
    • Session persistence stores per-page state and activePageId; legacy workspaces migrate into a default page. Docs in docs/workspace-pages-spec.md.
    • cmux tree prints pages under each workspace and marks the active page.
  • Bug Fixes

    • Preserved notifications across page switches; restored for inactive pages on selection and during session/runtime restores.
    • Fixed page layout restore by replacing the split tree per page; corrected reorder after anchor and titlebar strip Return behavior; on restore, selection falls back to an attached panel for better focus.
    • Fixed page reorder to be isolated within the current workspace.
    • Option-digit page selection works on symbol-first keyboard layouts.
    • Fixed v2 socket payload isolation; suppressed placeholder history on page restore.
    • CLI: close-page defaults to the current workspace and parses --workspace before page args; rename/close default to the current page; window-aware routing.
    • CLI: numeric page titles no longer conflict with 1-based index handles, including in-range numeric titles; parser disambiguates titles vs handles and honors titles after --.

Written for commit 9a84dda. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Workspace pages: create/duplicate/rename/reorder/close; next/previous/last navigation; titlebar page strip with + button, context menu, per-page selection menu, and keyboard shortcuts; CLI and socket (v2) page commands; session persistence of pages and active page.
  • Documentation

    • Added comprehensive workspace-pages spec (UX, lifecycle, commands, API).
  • Localization

    • Added page-related strings and menu labels (EN/JA).
  • Tests

    • New unit, integration, and end-to-end CLI/socket tests for page lifecycle, ordering, selection, and persistence.

@coderabbitai

coderabbitai Bot commented May 26, 2026 •

Copy link
Copy Markdown

Complex PR? Review this PR in Change Stack to move by importance, not file order.

Review Change Stack

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 workspace pages end-to-end: multi-page model and snapshots, full V2 page API and CLI commands, titlebar page-strip UI, menu/shortcut wiring, localization (EN/JA), tests, and a spec document.

Changes

Workspace Page Model and Persistence

Layer / File(s) Summary
WorkspacePage and session snapshot types
Sources/Workspace.swift, Sources/SessionPersistence.swift
Introduces WorkspacePage, StoredPageState, and codable snapshot types SessionWorkspacePageSnapshot and SessionWorkspacePageStateSnapshot.
Workspace page management API
Sources/Workspace.swift
Adds pages and activePageId, implements page lifecycle methods (create, duplicate, select, move, rename, close), manages storedPageStates, initializes initial page, and tears down stored page state.
Session snapshot and restore
Sources/Workspace.swift
Refactors sessionSnapshot and restore to persist/restore pages and activePageId, remap panel IDs, ensure non-empty panels on restore, and rework notification restoration with page-aware logic.

V2 API and CLI Surfaces

Layer / File(s) Summary
V2 handle and command infrastructure
Sources/TerminalController.swift
Adds .page V2 handle kind, updates handle bookkeeping and capabilities, extends focused/caller payloads with page fields, and prefers tab resolution by page_id.
V2 tree encoding and page methods
Sources/TerminalController.swift
Encodes workspace tree with page nodes (live/stored panes), validates/activates requested page context, and implements v2 page methods (list/create/duplicate/select/current/close/reorder/rename/next/previous/last).
CLI page commands and tree rendering
CLI/cmux.swift
Adds page commands (list-pages, new-page, duplicate-page, current-page, select-page, rename-page, close-page, reorder-page, next-page, previous-page, last-page), normalizePageHandle, runReorderPage, extends TreePath with pageHandle, and renders page branches under workspaces with updated help text.

Keyboard Shortcuts, UI, and Localization

Layer / File(s) Summary
Keyboard shortcuts and mapping
Sources/KeyboardShortcutSettings.swift, Sources/App/TerminalDirectoryOpenSupport.swift
Adds shortcut actions for page management/selection, labels/defaults/accessors, adjusts ANSI fallback matching, and maps option-digit ↔ page index.
AppDelegate routing and menu integration
Sources/AppDelegate.swift, Sources/cmuxApp.swift
Routes page shortcuts in AppDelegate.handleCustomShortcut, adds page menu commands and a Select Page submenu with per-page shortcuts.
UI titlebar page strip
Sources/ContentView.swift
Renders a horizontally scrollable workspace page strip in the titlebar with per-page buttons, selection, context menu actions, and a “+” create/select button; falls back to title text when no workspace selected.
Localization strings for page operations
Resources/Localizable.xcstrings
Adds EN/JA localization entries for page commands, dialogs, menu items, and shortcut labels.

Testing and Documentation

Layer / File(s) Summary
Unit and integration tests
cmuxTests/*
Adds tests for option-digit page selection and fallback behavior, page navigation/creation/duplication/close behaviors, V2 API coverage, persistence round-trip, and live-panel detach/reattach reuse.
CLI and socket API parity integration test
tests_v2/test_page_cli_socket_parity.py
Adds Python regression test verifying CLI and socket parity across create/select/rename/duplicate/reorder/close operations and system.tree mirroring.
Workspace pages specification
docs/workspace-pages-spec.md
Adds a spec covering hierarchy, titlebar UX, page lifecycle rules, command/shortcut surface, persistence expectations, V2/CLI API surface, non-goals, and test checklist.

Sequence Diagram (high-level page flow):

sequenceDiagram
  participant CLI as CLI/cmux
  participant Socket as V2 Socket
  participant TerminalController as TerminalController
  participant Workspace as Workspace
  CLI->>Socket: send page.* v2 request (e.g. page.create)
  Socket->>TerminalController: processV2Command
  TerminalController->>Workspace: perform page lifecycle operation
  Workspace-->>TerminalController: updated page state / snapshot
  TerminalController-->>Socket: v2 result (ok/result)
  Socket-->>CLI: prints v2 result / summary
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • manaflow-ai/cmux#4371: The main PR updates ContentView/focus routing to call the (new) TabManager.focusTab(..., dismissRestoredUnreadOnResume:) signature when switching page/workspace UI, directly aligning with the retrieved PR’s restored-unread dismissal context plumbing.
  • manaflow-ai/cmux#4445: Both PRs modify Sources/AppDelegate.swift’s handleCustomShortcut logic—main PR adds page-related shortcut actions, while retrieved PR changes the shortcut-chord routing/arming behavior that determines how such shortcuts are dispatched/consumed.

Poem

"I hop between pages, a tiny guide,
Shortcuts like carrots by my side,
Menus and trees in tidy rows,
Snapshots keep where memory goes,
Hooray — new pages for each bright stride!"


Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Algorithmic Complexity ❌ Error v2LocatePage performs nested scan: O(windows × workspaces × pages). Called from hot socket paths v2PageSelect, v2PageClose, and v2ResolveTabManager which runs for every v2 command with page routing. Cache pageUUID→workspace lookup at TabManager level or optimize v2ResolveTabManager to skip v2LocatePage when page_id is the only routing key.
Cmux Swift File And Package Boundaries ❌ Error PR adds >250 lines to three oversized files without extracting or reducing code: Workspace.swift +832, TerminalController.swift +638, CLI/cmux.swift +555. Extract page management into a SwiftPM package or refactor to reduce affected files by >200 lines through responsibility extraction.
Cmux Full Internationalization ❌ Error The string cli.page.listPages.noPages in Resources/Localizable.xcstrings is user-facing CLI output but only has translations for 2 locales (en, ja) instead of all 20 supported by the catalog. Add translations for cli.page.listPages.noPages to all 20 locales: ar, bs, da, de, en, es, fr, it, ja, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, zh-Hant.
Docstring Coverage ⚠️ Warning Docstring coverage is 2.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (15 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add workspace pages' is concise and directly summarizes the primary change of adding a page hierarchy within workspaces.
Linked Issues check ✅ Passed The PR successfully implements the core requirement from #3297 by adding workspace pages that allow tabs/pages to contain splits, enabling fullscreen and multi-pane views within the same page without manual rearrangement.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing workspace pages and related features; intentionally excluded drag/drop reordering and hover-only chrome as documented in the PR description.
Cmux Swift Actor Isolation ✅ Passed All production code has proper actor isolation: @MainActor classes, Sendable types, background tasks isolated with MainActor boundaries, no implicit MainActor models or unguarded shared state.
Cmux Swift Blocking Runtime ✅ Passed Page-related production code adds no blocking synchronization primitives: no semaphores, NSLock, sleep, Task.sleep, or DispatchQueue.main.sync in page lifecycle, V2 API, or UI rendering methods.
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift code, localization data, docs, and test code with deterministic sleeps (allowed per rule).
Cmux Swift Concurrency ✅ Passed Page APIs are synchronous, @Published pages extend existing Combine state, DispatchQueue.main.async in promptRenamePage follows allowed AppKit boundary pattern.
Cmux Swift @Concurrent ✅ Passed No new async functions added by this PR; all new page APIs (Workspace, TerminalController V2, UI code) are synchronous. Pre-existing async functions have proper isolation.
Cmux Swift Logging ✅ Passed No logging violations. Five new print() calls in FeedPreviewActions preview enum (allowed per rules), no unguarded NSLog in app code, Logger declarations properly scoped.
Cmux User-Facing Error Privacy ✅ Passed All user-facing error messages and API responses in page-related code comply with privacy rules. No vendor names, credentials, environment variables, or sensitive details are exposed.
Cmux Swiftui State Layout ✅ Passed PR adds @Published to pre-existing Workspace ObservableObject. Pages iteration uses value snapshots + closures, not store refs. No new SwiftUI state patterns introduced.
Cmux Architecture Rethink ✅ Passed Page lifecycle maintains single ownership in Workspace with proper @Published flow to views. No timing patches, duplicate state owners, or split UI lifecycle ownership detected.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds page features without new standalone windows. Page strip/buttons/menus are SwiftUI Views in existing titlebar; rename uses NSAlert. No window-closing-shortcut violations detected.
Cmux Source Artifacts ✅ Passed PR adds only hand-written source code, tests, localization catalogs, and documentation with no build outputs, caches, logs, or prohibited artifact directories.
Description check ✅ Passed PR description comprehensively covers changes, testing approach, and includes demo/review triggers, though testing is CI-only per policy.
✨ 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-3297-allow-a-surface-tab-to-have-splits

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.

@vercel

vercel Bot commented May 26, 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 6, 2026 5:30pm
cmux-staging Building Building Preview, Comment Jun 6, 2026 5:30pm

Comment thread Sources/TerminalController.swift Outdated
Comment thread Sources/TerminalController.swift
Comment thread Sources/Workspace.swift
@greptile-apps

greptile-apps Bot commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR implements workspace pages — a first-class layout container that lets each workspace hold multiple independent split/pane/surface layouts with fast runtime switching and full session persistence. The change is large (~3,300 net new lines in production Swift + 671 lines of tests) but well-scoped: Workspace.swift gains the page model, lifecycle, and runtime/session capture-restore machinery; TerminalController.swift adds the full v2 page.* socket API and wraps existing surface/pane commands in a page-activation helper; cmuxApp.swift, AppDelegate.swift, and KeyboardShortcutSettings.swift wire up UI, menu, and shortcut bindings; CLI/cmux.swift adds eleven new verbs; and Resources/Localizable.xcstrings provides EN + JA (and additional locale) translations for all new strings.

  • Page lifecycle — newPage, duplicatePage (with panel-ID remapping), selectPage (captures active page's runtime state including detached panels, restores stored state), closePage with confirmation, closeOtherPages, and movePage; runtime restore path uses withClosedPanelHistorySuppressed across layout and placeholder teardown.
  • Session persistence — SessionWorkspaceSnapshot gains optional pages + activePageId; legacy snapshots migrate into a single default page; inactive pages persist as StoredPageState (session-only until first viewed, then full runtime state on subsequent switches).
  • v2 socket API — eleven page.* methods, all v2MainSync-isolated; pane/surface commands accept an optional page_id that selects the target page before the body runs via v2ResultActivatingRequestedPage; cmux tree now nests pages under workspaces and falls back to workspace-level panes for pre-page workspaces.

Confidence Score: 5/5

Safe to merge — page lifecycle, runtime/session capture-restore, actor-isolation boundaries, and closed-panel-history suppression are all correctly handled; previous thread fixes are present in the diff.

The feature is large but follows the established v2 socket pattern precisely. Page selection is fully main-thread-gated via v2MainSync, panel detach/reattach is wrapped in withClosedPanelHistorySuppressed, session migration is backward-compatible (optional fields), and all new user-facing strings have full translations. The one finding (missing _ref fields on two reorder anchor errors) is an API cosmetic inconsistency that does not affect correctness or user data.

Sources/TerminalController.swift — the v2PageReorder anchor-not-found error responses are missing the _ref companion fields present in every other page-API error.

Important Files Changed

Filename Overview
Sources/Workspace.swift ~967 lines added: WorkspacePage model, StoredPageState (runtime+session), full page lifecycle (new/duplicate/select/move/close/closeOthers), runtime and session page restore paths, notification re-stitching on page switch, and workspace teardown extended for stored page states.
Sources/TerminalController.swift ~716 lines added: full v2 page API (list/create/duplicate/select/current/close/reorder/rename/next/previous/last), page handle kind registration, v2ResultActivatingRequestedPage wrapper for surface+pane commands, v2TreeWorkspaceNode refactored to nest pages→panes→surfaces. One minor inconsistency in reorder error data.
Sources/cmuxApp.swift Adds View-menu page controls (New/Duplicate/Rename/Close/Next/Previous/Move Left-Right/Select Page submenu) with correct .disabled guards; adds pageSelectionMenuShortcut helper mapping Option+1-9.
Sources/AppDelegate.swift Adds keyboard shortcut handlers for newPage, renamePage, closePage, nextPage, previousPage, and selectPage1-8/selectLastPage, all routing through preferredMainWindowContextForShortcutRouting.
Sources/SessionPersistence.swift Adds SessionWorkspacePageStateSnapshot, SessionWorkspacePageSnapshot, and activePageId/pages fields to SessionWorkspaceSnapshot; all three are Codable + Sendable, optional for backward compatibility.
CLI/cmux.swift Adds list-pages/new-page/duplicate-page/current-page/select-page/rename-page/close-page/reorder-page/next-page/previous-page/last-page verbs, normalizePageHandle (UUID/ref/1-based index), and page hierarchy in cmux tree.
Resources/Localizable.xcstrings Adds full-catalog translations (EN+JA and other supported locales) for all new page strings: menu items, shortcuts, dialogs, context menu, CLI strings, and common keys.

Sequence Diagram

sequenceDiagram
    participant CLI as cmux CLI / socket client
    participant TC as TerminalController (nonisolated)
    participant Main as MainActor
    participant WS as Workspace

    CLI->>TC: "page.select {page_id}"
    TC->>Main: "v2MainSync { v2LocatePage }"
    Main-->>TC: (windowId, tabManager, workspace, pageIndex)
    TC->>Main: workspace.selectPage(pageId)
    Main->>WS: captureActivePageStoredState(detachPanels:true)
    WS-->>Main: StoredPageState (runtime+session)
    Main->>WS: "storedPageStates[activePageId] = state"
    Main->>WS: restoreStoredPage(pageId)
    WS->>WS: restoreRuntimePageState OR restoreSessionPageState
    Main->>WS: "activePageId = pageId"
    TC-->>CLI: "ok {page_id, page_ref, page_index, page_title}"

    CLI->>TC: "surface.list {page_id, workspace_id}"
    TC->>TC: v2ActivateRequestedPageContext(params)
    TC->>Main: "v2MainSync { locate + selectPage if needed }"
    Main-->>TC: nil (no error)
    TC->>TC: body() → v2SurfaceList(params)
    TC->>Main: "v2MainSync { v2ResolveWorkspace }"
    Main-->>TC: surfaces on now-active page
    TC-->>CLI: "ok {surfaces: [...]}"
Loading

Reviews (12): Last reviewed commit: "Fix page reorder workspace isolation" | Re-trigger Greptile

Comment thread Sources/Workspace.swift Outdated
Comment thread Sources/Workspace.swift
Comment thread Sources/TerminalController.swift Outdated
Comment thread Sources/cmuxApp.swift
coderabbitai[bot]
coderabbitai Bot previously requested changes May 26, 2026

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

Caution

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

⚠️ Outside diff range comments (1)
docs/workspace-pages-spec.md (1)

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

Incomplete line at end of document.

Line 451 appears to end abruptly with just the line number. Please complete this section or remove the orphaned line.

🤖 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 `@docs/workspace-pages-spec.md` at line 451, The document ends with an
incomplete/orphaned line at line 451; either remove that stray line or complete
the trailing section content so the document ends cleanly—locate the end of the
last section (the paragraph or header preceding line 451) and either finish the
sentence/section or delete the lone line to prevent the abrupt end.
🤖 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 970-989: The pageSelectionMenuShortcut implementation currently
returns shortcuts via KeyboardShortcutSettings.shortcut(for:), which can bypass
menu normalization; update pageSelectionMenuShortcut to use the menu-normalized
resolver on KeyboardShortcutSettings instead (replace the final return that maps
action to KeyboardShortcutSettings.shortcut(for:) with the menu-specific
resolver, e.g. the KeyboardShortcutSettings method that provides menu-normalized
shortcuts) so Select Page items use the same menu key-equivalent normalization
as the rest of the file.

In `@Sources/ContentView.swift`:
- Around line 2339-2406: The ForEach is passing Workspace into
titlebarPageButton causing row views to capture mutable ObservableObject state;
instead create an immutable per-row snapshot and action closures in
titlebarPageStrip (e.g. let row = (id: page.id, title: page.title, isActive:
page.id == workspace.activePageId, canClose: workspace.canClosePage(page.id),
pageCount: workspace.pages.count) and closures newPage/ selectPage/
promptRenamePage/ duplicatePage/ closePage/ closeOtherPages) and change
titlebarPageButton signature to accept only these immutable values plus the
action closures (e.g. selectAction, renameAction, duplicateAction, closeAction,
closeOthersAction) so titlebarPageButton no longer references Workspace or calls
its methods directly.

In `@tests_v2/test_page_cli_socket_parity.py`:
- Around line 176-252: The cached page refs (first_page_ref, second_page_ref,
duplicate_page_ref) are stale after calling c._call("page.reorder", ...); update
the refs before reuse by re-reading current refs from page.list or page.current
(e.g. call _run_cli_json(["list-pages","--workspace", workspace_id]) or
c._call("page.current"/"page.list") and extract fresh refs) and then use those
refreshed refs in subsequent _run_cli_json calls (reorder-page --after and
close-page) or switch to using stable IDs (first_page_id/second_page_id) like
second_page_id to avoid wrong-target CLI operations.
- Line 16: The test uses SOCKET_PATH created from the wrong env var name
("CMUX_SOCKET"), causing silent fallback; update the env lookup in SOCKET_PATH
to use "CMUX_SOCKET_PATH" instead (keep the same default "/tmp/cmux-debug.sock")
so the tests_v2 runner's injected CMUX_SOCKET_PATH is respected; locate the
SOCKET_PATH assignment and replace the env key accordingly.

---

Outside diff comments:
In `@docs/workspace-pages-spec.md`:
- Line 451: The document ends with an incomplete/orphaned line at line 451;
either remove that stray line or complete the trailing section content so the
document ends cleanly—locate the end of the last section (the paragraph or
header preceding line 451) and either finish the sentence/section or delete the
lone line to prevent the abrupt end.
🪄 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: ab494aa8-50cf-42bd-a9d0-cdbcfec36bb6

📥 Commits

Reviewing files that changed from the base of the PR and between 1b22695 and d7916bc.

📒 Files selected for processing (16)
  • CLI/cmux.swift
  • Resources/Localizable.xcstrings
  • Sources/App/TerminalDirectoryOpenSupport.swift
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/KeyboardShortcutSettings.swift
  • Sources/SessionPersistence.swift
  • Sources/TerminalController.swift
  • Sources/Workspace.swift
  • Sources/cmuxApp.swift
  • cmuxTests/AppDelegateShortcutRoutingTests.swift
  • cmuxTests/SessionPersistenceTests.swift
  • cmuxTests/WorkspaceContentViewVisibilityTests.swift
  • cmuxTests/WorkspaceUnitTests.swift
  • docs/workspace-pages-spec.md
  • tests_v2/test_page_cli_socket_parity.py

Comment thread Sources/cmuxApp.swift
Comment thread Sources/ContentView.swift
Comment thread tests_v2/test_page_cli_socket_parity.py Outdated
Comment thread tests_v2/test_page_cli_socket_parity.py
…rface-tab-to-have-splits

# Conflicts:
#	Sources/TerminalController.swift
#	Sources/Workspace.swift
Comment thread Sources/Workspace.swift
Comment thread Sources/Workspace.swift
Comment thread Sources/Workspace.swift
Comment thread Sources/TerminalController.swift Outdated
Comment thread Sources/TerminalController.swift Outdated
Comment thread Sources/Workspace.swift Outdated
@austinywang

Copy link
Copy Markdown
Contributor Author

Addressed the Greptile maintenance observations in d7b7e39: removed the redundant v2PageDuplicate source-page guard with the misleading impossible-state payload, and added a concise note at StoredPageState.RuntimeState documenting the capture/restore boundary for future runtime-only page state.

Comment thread CLI/cmux.swift
Comment thread CLI/cmux.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

Caution

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

⚠️ Outside diff range comments (1)
cmuxTests/WorkspaceContentViewVisibilityTests.swift (1)

161-247: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Migrate this new suite to Swift Testing.

WorkspacePageLifecycleTests is a brand new test suite. Per coding guidelines, new unit and integration tests should default to Swift Testing rather than XCTest.

♻️ Refactor to Swift Testing
+import Testing
+
-@MainActor
-final class WorkspacePageLifecycleTests: XCTestCase {
-    func testSwitchingPagesPreservesLivePanelIdentityAcrossDetachAndReattach() throws {
+@Suite(.serialized)
+@MainActor
+struct WorkspacePageLifecycleTests {
+    `@Test` func switchingPagesPreservesLivePanelIdentityAcrossDetachAndReattach() throws {
         let workspace = Workspace()
         let firstPageId = workspace.activePageId
-        let firstPaneId = try XCTUnwrap(workspace.bonsplitController.allPaneIds.first)
+        let firstPaneId = try `#require`(workspace.bonsplitController.allPaneIds.first)
 
-        XCTAssertNotNil(workspace.newTerminalSurface(inPane: firstPaneId, focus: false))
+        `#expect`(workspace.newTerminalSurface(inPane: firstPaneId, focus: false) != nil)
         let firstPagePanelIds = Set(workspace.panels.keys)
-        XCTAssertEqual(firstPagePanelIds.count, 2)
+        `#expect`(firstPagePanelIds.count == 2)
 
         let secondPage = workspace.newPage(select: true)
-        XCTAssertEqual(workspace.activePageId, secondPage.id)
+        `#expect`(workspace.activePageId == secondPage.id)
 
         let secondPagePanelIds = Set(workspace.panels.keys)
-        XCTAssertEqual(
-            secondPagePanelIds.count,
-            1,
-            "A fresh page should mount its own placeholder terminal"
-        )
-        XCTAssertNotEqual(firstPagePanelIds, secondPagePanelIds)
+        `#expect`(secondPagePanelIds.count == 1, "A fresh page should mount its own placeholder terminal")
+        `#expect`(firstPagePanelIds != secondPagePanelIds)
 
         workspace.selectPage(firstPageId)
-        XCTAssertEqual(workspace.activePageId, firstPageId)
-        XCTAssertEqual(
-            Set(workspace.panels.keys),
-            firstPagePanelIds,
-            "Returning to the first page should reattach the parked live panels"
-        )
+        `#expect`(workspace.activePageId == firstPageId)
+        `#expect`(Set(workspace.panels.keys) == firstPagePanelIds, "Returning to the first page should reattach the parked live panels")
 
         workspace.selectPage(secondPage.id)
-        XCTAssertEqual(workspace.activePageId, secondPage.id)
-        XCTAssertEqual(
-            Set(workspace.panels.keys),
-            secondPagePanelIds,
-            "Returning to the second page should reuse its parked live panel instead of rebuilding a new one"
-        )
+        `#expect`(workspace.activePageId == secondPage.id)
+        `#expect`(Set(workspace.panels.keys) == secondPagePanelIds, "Returning to the second page should reuse its parked live panel instead of rebuilding a new one")
     }
 
-    func testRuntimePageRestoreReplacesPreviousPagePaneSkeleton() throws {
+    `@Test` func runtimePageRestoreReplacesPreviousPagePaneSkeleton() throws {
         let workspace = Workspace()
         let firstPageId = workspace.activePageId
-        let firstPanelId = try XCTUnwrap(workspace.focusedPanelId)
-        XCTAssertNotNil(workspace.newTerminalSplit(from: firstPanelId, orientation: .horizontal))
+        let firstPanelId = try `#require`(workspace.focusedPanelId)
+        `#expect`(workspace.newTerminalSplit(from: firstPanelId, orientation: .horizontal) != nil)
         let firstPagePanelIds = Set(workspace.panels.keys)
         let firstPagePaneCount = workspace.bonsplitController.allPaneIds.count
-        XCTAssertEqual(firstPagePaneCount, 2)
+        `#expect`(firstPagePaneCount == 2)
 
         let secondPage = workspace.newPage(select: true)
-        let secondPanelId = try XCTUnwrap(workspace.focusedPanelId)
-        XCTAssertNotNil(workspace.newTerminalSplit(from: secondPanelId, orientation: .vertical))
-        XCTAssertEqual(workspace.bonsplitController.allPaneIds.count, 2)
+        let secondPanelId = try `#require`(workspace.focusedPanelId)
+        `#expect`(workspace.newTerminalSplit(from: secondPanelId, orientation: .vertical) != nil)
+        `#expect`(workspace.bonsplitController.allPaneIds.count == 2)
 
         workspace.selectPage(firstPageId)
 
-        XCTAssertEqual(workspace.activePageId, firstPageId)
-        XCTAssertEqual(Set(workspace.panels.keys), firstPagePanelIds)
-        XCTAssertEqual(
-            workspace.bonsplitController.allPaneIds.count,
-            firstPagePaneCount,
-            "Runtime page restore should replace the leaving page's empty pane skeleton"
-        )
-        XCTAssertNotEqual(workspace.activePageId, secondPage.id)
+        `#expect`(workspace.activePageId == firstPageId)
+        `#expect`(Set(workspace.panels.keys) == firstPagePanelIds)
+        `#expect`(workspace.bonsplitController.allPaneIds.count == firstPagePaneCount, "Runtime page restore should replace the leaving page's empty pane skeleton")
+        `#expect`(workspace.activePageId != secondPage.id)
     }
 
-    func testRuntimePageRestoreDoesNotRecordPlaceholderPanelsAsClosedItems() throws {
+    `@Test` func runtimePageRestoreDoesNotRecordPlaceholderPanelsAsClosedItems() throws {
         ClosedItemHistoryStore.shared.removeAll()
         defer { ClosedItemHistoryStore.shared.removeAll() }
 
         let workspace = Workspace()
         let firstPageId = workspace.activePageId
-        let firstPanelId = try XCTUnwrap(workspace.focusedPanelId)
-        XCTAssertNotNil(workspace.newTerminalSplit(from: firstPanelId, orientation: .horizontal))
+        let firstPanelId = try `#require`(workspace.focusedPanelId)
+        `#expect`(workspace.newTerminalSplit(from: firstPanelId, orientation: .horizontal) != nil)
 
         let secondPage = workspace.newPage(select: true)
-        let secondPanelId = try XCTUnwrap(workspace.focusedPanelId)
-        XCTAssertNotNil(workspace.newTerminalSplit(from: secondPanelId, orientation: .vertical))
+        let secondPanelId = try `#require`(workspace.focusedPanelId)
+        `#expect`(workspace.newTerminalSplit(from: secondPanelId, orientation: .vertical) != nil)
 
         workspace.selectPage(firstPageId)
         workspace.selectPage(secondPage.id)
 
-        XCTAssertFalse(
-            ClosedItemHistoryStore.shared.canReopen,
-            "Runtime page restore should not expose synthetic placeholder panels in recently closed items"
-        )
+        `#expect`(!ClosedItemHistoryStore.shared.canReopen, "Runtime page restore should not expose synthetic placeholder panels in recently closed items")
     }
 }

As per coding guidelines: "Swift Testing is the current Apple-supported primitive for tests on this codebase... Default to Swift Testing for all unit and integration tests."

🤖 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 `@cmuxTests/WorkspaceContentViewVisibilityTests.swift` around lines 161 - 247,
The new test suite WorkspacePageLifecycleTests is written with XCTest; migrate
it to Swift Testing by converting the `@MainActor` final class
WorkspacePageLifecycleTests: XCTestCase into the Swift Testing suite style
(create the Swift Test suite type and register tests for
testSwitchingPagesPreservesLivePanelIdentityAcrossDetachAndReattach,
testRuntimePageRestoreReplacesPreviousPagePaneSkeleton, and
testRuntimePageRestoreDoesNotRecordPlaceholderPanelsAsClosedItems), replace
XCTest assertions (XCTUnwrap, XCTAssertEqual, XCTAssertNotNil, XCTAssertFalse,
etc.) with the Swift Testing assertion APIs, preserve the setup/teardown
behavior (ClosedItemHistoryStore.shared.removeAll() and defer cleanup) and keep
references to workspace, bonsplitController, panels,
newTerminalSurface/newTerminalSplit, selectPage, activePageId, focusedPanelId so
the logic and intent remain identical while using Swift Testing idioms.

Source: Coding guidelines

🤖 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 `@CLI/cmux.swift`:
- Around line 15190-15259: The parsing helpers currently re-parse tokens after a
"--" terminator; update parsing to split args at the first "--" and only inspect
the pre-terminator slice. Concretely: when implementing page command handling,
compute let pre = args.split(whereSeparator: { $0 == "--" }).first ?? args and
pass that pre slice into parsePageCommandContextOptions,
firstPositionalPageHandle, splitOptionalPositionalPageHandle and any call sites
that call positionalArgsAfterOptionalTerminator so option/handle detection
(workspace/window/page) only looks at pre; leave the post-terminator tail
untouched and forwarded as title/remaining text. Ensure
parsePageCommandContextOptions uses only the pre slice for parseOption and that
positionalArgsAfterOptionalTerminator no longer treats the full args array as
mutable input.

In `@Sources/Workspace.swift`:
- Around line 12025-12046: The code chooses entry.snapshot.selectedPanelId if it
exists in desiredPanelIds even when attachDetachedSurface failed, which leads to
no selected tab; change the selection logic to prefer a selectedPanelId only if
it is present in attachedPanelIds (i.e. actually attached) rather than
desiredPanelIds. Concretely, update the computation that sets selectedPanelId
(currently using desiredPanelIds.contains) to check attachedPanelIds.contains
(or map the original selected id to an attachedPanelId), then proceed to call
surfaceIdFromPanelId(selectedPanelId) and bonsplitController.selectTab only when
that selectedPanelId is one of attachedPanelIds so the pane always selects a
valid attached tab.

---

Outside diff comments:
In `@cmuxTests/WorkspaceContentViewVisibilityTests.swift`:
- Around line 161-247: The new test suite WorkspacePageLifecycleTests is written
with XCTest; migrate it to Swift Testing by converting the `@MainActor` final
class WorkspacePageLifecycleTests: XCTestCase into the Swift Testing suite style
(create the Swift Test suite type and register tests for
testSwitchingPagesPreservesLivePanelIdentityAcrossDetachAndReattach,
testRuntimePageRestoreReplacesPreviousPagePaneSkeleton, and
testRuntimePageRestoreDoesNotRecordPlaceholderPanelsAsClosedItems), replace
XCTest assertions (XCTUnwrap, XCTAssertEqual, XCTAssertNotNil, XCTAssertFalse,
etc.) with the Swift Testing assertion APIs, preserve the setup/teardown
behavior (ClosedItemHistoryStore.shared.removeAll() and defer cleanup) and keep
references to workspace, bonsplitController, panels,
newTerminalSurface/newTerminalSplit, selectPage, activePageId, focusedPanelId so
the logic and intent remain identical while using Swift Testing idioms.
🪄 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: cf1f4e97-a0dd-40bc-99f0-46b536a3a88e

📥 Commits

Reviewing files that changed from the base of the PR and between 5ee3d6e and d7b7e39.

📒 Files selected for processing (7)
  • CLI/cmux.swift
  • Resources/Localizable.xcstrings
  • Sources/TerminalController.swift
  • Sources/Workspace.swift
  • cmuxTests/WorkspaceContentViewVisibilityTests.swift
  • docs/workspace-pages-spec.md
  • tests_v2/test_page_cli_socket_parity.py

Comment thread CLI/cmux.swift
Comment thread Sources/Workspace.swift
@austinywang

Copy link
Copy Markdown
Contributor Author

Addressed the current CodeRabbit findings in the latest push: migrated the new WorkspacePageLifecycleTests suite to Swift Testing, added a regression seam/test for runtime page restore selection fallback, changed restore to select only an actually attached panel, and fixed page CLI parsing so options after -- remain title/positional text instead of being reparsed.

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

♻️ Duplicate comments (1)
CLI/cmux.swift (1)

4316-4324: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

-- still is not a hard stop for page-command parsing.

Line 4316 still feeds the full commandArgs array into parsePageCommandContextOptions, and this helper only treats -- specially when it is the first remaining token. So cmux rename-page page:1 -- --window draft can still have --window draft consumed as routing instead of title text. Split once at the first --, run all page/workspace/window parsing on the pre-terminator slice only, then forward the tail untouched into the title args.

Based on learnings, pre-socket CLI parsing must stop at the first -- and forward everything after it untouched.

Also applies to: 15252-15271

🤖 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 `@CLI/cmux.swift` around lines 4316 - 4324, The current parsing passes the
entire commandArgs into parsePageCommandContextOptions so `--` is not enforced
as a hard stop; change to split commandArgs at the first "--" (keep head =
tokens before first "--", tail = tokens after the first "--" untouched), call
parsePageCommandContextOptions(head, windowOverride: windowId), then run
splitOptionalPositionalPageHandle on the remaining from that parse and finally
construct title from the explicit tail plus any titleArgs (do not let parsing
consume tokens from the tail). Apply the same pre-terminator split fix for the
other occurrence range referenced (lines 15252-15271) using the same functions:
parsePageCommandContextOptions and splitOptionalPositionalPageHandle so
everything after the first "--" is forwarded as raw title text.

Source: Learnings

🤖 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 `@tests_v2/test_page_cli_socket_parity.py`:
- Around line 268-315: The test exercises numeric-title parsing with
out-of-range values ("2024" and "5") so it never detects ambiguity with valid
1-based page handles; update the test cases (the calls to _run_cli_json in the
numeric_title / numeric_title_terminator / numeric_duplicate sections) to use at
least one in-range numeric title (e.g., "2") instead of an out-of-range value
before the subsequent rename to "editor" so the parser’s handling of a valid
page handle vs numeric title is exercised (modify the arguments passed to
["rename-page", ...] and ["duplicate-page", ...] where numeric_title,
numeric_title_terminator, and numeric_duplicate are created).

---

Duplicate comments:
In `@CLI/cmux.swift`:
- Around line 4316-4324: The current parsing passes the entire commandArgs into
parsePageCommandContextOptions so `--` is not enforced as a hard stop; change to
split commandArgs at the first "--" (keep head = tokens before first "--", tail
= tokens after the first "--" untouched), call
parsePageCommandContextOptions(head, windowOverride: windowId), then run
splitOptionalPositionalPageHandle on the remaining from that parse and finally
construct title from the explicit tail plus any titleArgs (do not let parsing
consume tokens from the tail). Apply the same pre-terminator split fix for the
other occurrence range referenced (lines 15252-15271) using the same functions:
parsePageCommandContextOptions and splitOptionalPositionalPageHandle so
everything after the first "--" is forwarded as raw title text.
🪄 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: 8d7cec7f-e1f4-4d2c-9a07-a300a29394d9

📥 Commits

Reviewing files that changed from the base of the PR and between d7b7e39 and 41d5c5b.

📒 Files selected for processing (2)
  • CLI/cmux.swift
  • tests_v2/test_page_cli_socket_parity.py

Comment thread tests_v2/test_page_cli_socket_parity.py
Comment thread Sources/TerminalController.swift Outdated

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

Comment thread CLI/cmux.swift
) -> (page: String?, titleArgs: [String]) {
guard args.first != "--" else {
return (nil, args)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Post-terminator page handles ignored

Medium Severity

splitOptionalPositionalPageHandle bails out when the combined argument list starts with --, so tokens after the terminator are never treated as a page handle. duplicate-page and rename-page use this helper, while select-page, close-page, and reorder-page use firstPositionalPageHandle, which strips -- first. Invocations like duplicate-page -- page:2 or rename-page -- page:2 new title therefore target the current page and fold handle tokens into the title instead of the intended page.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9a84dda. Configure here.

@levontumanyan

levontumanyan commented Jun 19, 2026 •

Copy link
Copy Markdown

@austinywang The failing tests job looks like a transient infrastructure flake — the CI log shows it died at the "Setup Bun" step with two consecutive Unexpected HTTP response: 504 errors when downloading bun-v1.3.14 from GitHub releases. Nothing in the PR's code caused it. Could you rerun the failed job? The rest of CI is green.

this feature would be an awesome addition!

@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 — 9a84dda8 Deployed Jun 6, 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.

Allow a surface (tab) to have splits within it

4 participants