* Add workspace pages in the titlebar * Add workspace pages UI test target entry * Relax workspace pages UI test titlebar checks * Use page close button in workspace pages UI test * Stabilize workspace pages UI test interruptions * Skip page close confirms in UI tests * Clean up superseded workspace handoffs * Tighten page hint UI assertions --------- Co-authored-by: cmux <cmux@cmuxs-Mac-mini.local>
19 KiB
Workspace Pages Spec
Last updated: March 7, 2026
Related issue: https://github.com/manaflow-ai/cmux/issues/569
Problem
Today a workspace owns exactly one Bonsplit layout. That forces users to either:
- Keep editor or database panes full-width in a separate workspace.
- Keep everything in one workspace and accept squeezed layouts.
The requested hierarchy is:
workspacepagepanesurface
A workspace stays "one project or repo". Pages let that project hold multiple full-layout views in the same workspace.
Naming
Recommended public name: page
Canonical terms:
workspace: the vertical sidebar item for a project/task.page: a titlebar-level layout inside a workspace.pane: a split region inside a page.surface: a tab inside a pane.
Why page:
tabis already overloaded in cmux for workspaces, Bonsplit tabs, and browser tabs.layoutsounds static, but this object is navigated, renamed, closed, and reordered.sceneis distinctive but reads too novel for a terminal app.pageis short, works in menus and shortcuts, and fits the titlebar text-strip UI.
Rejected names for now:
workspace tabtop-level tabscenelayout
Product Shape
Each workspace owns an ordered list of pages. Each page owns one full Bonsplit tree plus its page-local focus state.
The active page is shown in a horizontal titlebar strip. Switching pages swaps the active Bonsplit layout without changing the selected workspace.
The sidebar continues to represent workspaces only. V1 does not add a second sidebar layer.
Implementation Status
Implemented on this branch:
Workspacenow owns an orderedpageslist plus an active page selection.- Page order, titles, active selection, and page-local layouts persist through session restore.
- The fake titlebar now shows a horizontal page strip instead of the folder icon.
- The page
+is hover-only, pinned on the far right, and does not steal drag space while hidden. - Page close-button visibility follows the active/hover rules in the titlebar strip.
- Page context menus support create, duplicate, rename, close, close others, move left, and move right.
- Page switching detaches inactive Ghostty and WKWebView-backed panels from the live hierarchy instead of killing PTYs or browser state.
- Holding Option reveals direct-select page shortcut badges in the titlebar strip, using the existing shortcut-hint pattern.
- Customizable page shortcuts exist in
KeyboardShortcutSettings, and the default bindings are wired through app-level shortcut handling. Cmd+Shift+Pexposes page create, duplicate, rename, close, close others, next/previous, move left/right, and direct page selection commands.- The app menu exposes page create, duplicate, rename, close, close others, move left/right, next/previous, and direct page selection actions.
- The page strip supports drag-and-drop reordering with horizontal auto-scroll.
- Socket v2 page APIs exist for list/current/create/duplicate/select/rename/close/reorder/next/previous/last.
- CLI page commands exist for
list-pages,new-page,duplicate-page,current-page,select-page,rename-page,close-page,reorder-page,next-page,previous-page, andlast-page. system.identifynow includes focused page identity viapage_id,page_ref,page_index, andpage_title.system.treeandcmux treenow renderworkspace -> page -> pane -> surface, while keeping the selected page mirrored into the legacy workspace-levelpanesfield for older consumers.- Unit coverage now exists for page drag-drop planner behavior, page-strip autoscroll planning, page persistence round-trips, shortcut routing, duplicate-page structure preservation, active-page close-neighbor selection, runtime page detach/reattach identity across switches, and the v2 JSON page/tree path for
page.list,page.select,page.current, andsystem.tree. - Dedicated UI automation now exists for the titlebar page strip create/select/close and shortcut-hint flow.
- A
tests_v2regression now exists for external CLI and socket page parity across create, select, reorder, current, last, and close flows.
Not implemented yet:
- The deeper model refactor where each page owns its own
bonsplitControllerand live panel map directly. - CI execution and stabilization for the new page UI automation and external page API regressions still needs to be wired and kept green on this branch.
Titlebar UX
Replace the current folder icon and single titlebar label area with a text-only page strip.
V1 strip rules:
- Page items render as text only.
- The active page is visually distinct and keeps its close button visible.
- Inactive pages reveal their close button on hover.
- When a workspace has only one page, the active page still reserves the close slot, but the close button is disabled so a workspace never reaches zero pages.
- A page
+control sits at the far right of the fake titlebar lane, outside the scrollable page list. - Right click on a page opens its context menu.
- Empty titlebar space remains draggable.
- Holding Option should reveal the direct-select shortcut labels for visible pages, using the existing shortcut-hint pattern instead of adding permanent chrome.
- The page
+control is only visible while hovering the fake titlebar.
The current titlebar folder icon goes away in V1. Open Folder remains available through existing menu, command palette, and shortcut paths.
Page UI Detail
The page strip should feel like part of the macOS titlebar, not like a second toolbar.
Visual direction:
- Text-first, not boxed tabs.
- No persistent pill backgrounds, segmented control borders, or folder/file chrome.
- Typography should be close to the current titlebar label treatment, with the active page using stronger weight and opacity.
- Hover can add a very light background wash, but the default state should read as text in the titlebar.
Titlebar layout:
- Traffic lights stay where they are now.
- The page strip replaces the current folder-icon-plus-title area.
- Existing titlebar controls on the trailing side stay separate from the page strip.
- The strip should consume available width before squeezing the trailing controls.
- Any leftover titlebar gap outside page hit targets remains window-drag space.
- The page list itself is a scrollable lane.
- The page
+control is pinned to the far right of the fake titlebar lane and is not part of the scrolling content.
Page item anatomy:
- Page title text.
- Reserved close-button slot on the trailing edge of the item.
- Hover/active hit area large enough to be easy to target in the titlebar.
Page item state rules:
- Active page:
- stronger text weight
- higher contrast
- close button always visible
- Inactive page:
- lighter text treatment
- close button hidden until hover
- Hovered page:
- subtle background wash is allowed
- close button becomes visible
- Pressed page:
- same layout, just a stronger hover/pressed wash
- Single remaining page:
- keeps the close slot visible for layout stability
- close button is disabled
Close-button behavior:
- Use an
xor close glyph sized for titlebar density, not a large filled control. - The close button must not shift page text when it appears.
- Clicking the close button closes only that page.
- Closing the active page selects the nearest surviving neighbor, preferring the page to the right.
Sizing and truncation:
- Single-line titles only.
- Tail truncation when a title is too long.
- Each page item keeps a stable minimum clickable width even for short names.
- The active page gets slightly higher layout priority before truncation.
Overflow behavior:
- The strip stays single-row and never wraps.
- When pages exceed available width, the strip becomes horizontally scrollable.
- Selecting, creating, or moving to a page should auto-scroll it into view.
- The pinned page
+control stays visible on the far right while the page list scrolls underneath its own lane. - Leading and trailing fade hints are acceptable if needed, but V1 should avoid adding heavy chrome.
Interaction details:
- Left click selects the page.
- Right click opens the page context menu for the clicked page.
- Right click should not require activating the page first.
- Double click rename can wait until later; V1 can use menu, command palette, and shortcut-driven rename only.
- The context menu and close button must not break titlebar drag behavior outside their hit regions.
- The fake titlebar should still drag the window anywhere that is not an actual page hit target or the visible page
+hit target.
Creation affordance:
- The page
+control is pinned to the far right of the fake titlebar lane. - It should visually match the text-first style instead of looking like a toolbar button.
- It is hidden by default.
- It fades in only while hovering the fake titlebar region.
- When hidden, that area should behave like normal titlebar drag space rather than a dead zone.
- Only the visible glyph and its small padded hit target become clickable.
- It should stay easy to hit without competing with the existing
New Workspacetitlebar control.
Tooltips and hints:
- Hovering a page should show the full page title when truncated.
- Hovering the
+affordance should showNew Pageplus its effective shortcut. - Holding Option should show page-index shortcut hints in the strip, following the same “hold modifier to reveal hints” idea already used elsewhere in cmux.
Page Behavior
Each page preserves its own:
- Split topology.
- Surface order inside each pane.
- Focused pane.
- Selected surface per pane.
- Scrollback and restore state already tracked by the current workspace/session model.
Workspace-level state remains shared:
- Sidebar row identity and ordering.
- Workspace name and color.
- Notification aggregation and unread state.
- Workspace-level commands such as rename, move, and close workspace.
For single-value sidebar metadata in V1, use the active page as the source of truth. We can revisit cross-page aggregation later if this feels misleading.
Efficiency And Lifecycle
Pages should not behave like multiple fully mounted workspaces stacked on top of each other.
Lifecycle policy:
- Only the active page in the selected workspace keeps its Ghostty terminal views and WKWebViews mounted in the live window hierarchy.
- When a page becomes inactive, its terminal portal views and browser portal views should be hidden or detached through the same kind of unmount path cmux already uses for workspace switches.
- Switching pages must not kill PTYs, throw away scrollback, or reload browser state just because the page is inactive.
- Re-activating a page should reattach its existing panels instead of reconstructing the whole layout from scratch.
- Hidden pages should not keep participating in hit testing, layout, or display-driven redraw work.
- Rapid workspace switching must also hide portal-hosted views for superseded retiring workspaces immediately, so deferred handoff cleanup cannot leave stale terminal or browser portals alive after churn.
Performance rule:
- There should never be more than one visible page worth of portal-hosted Ghostty surfaces or WKWebViews for a workspace at once.
- The selected page should remount fast enough that page switches feel like view changes, not restore flows.
- If later measurement shows browser-heavy workspaces still consume too much memory, add a follow-on cold-parking policy for long-idle pages instead of forcing that complexity into the first implementation.
Commands And Shortcuts
Required page actions:
New PageRename PageClose PageClose Other PagesNext PagePrevious PageSelect Page 1throughSelect Page 8Select Last PageDuplicate PageMove Page LeftMove Page Right
Default shortcuts:
Command+Option+N: new page.Command+Option+R: rename page.Command+Option+W: close page.Option+1throughOption+8: select page by index.Option+9: select the last page.Option+]: next page.Option+[: previous page.
All page shortcuts must be first-class KeyboardShortcutSettings actions so they appear in Settings and can be customized.
The same actions should also appear in the command palette and the app menu.
Implementation note:
Direct page selection should route by physical digit intent, not by text produced after Option modifies the character, so Option+digit keeps working across keyboard layouts.
Cmd+Shift+P Commands
Cmd+Shift+P should expose page actions as first-class commands, not as hidden side effects.
Required command-palette entries:
New PageDuplicate PageRename Page…Close PageClose Other PagesNext PagePrevious PageMove Page LeftMove Page RightSelect Page <title>
Command-palette behavior:
Rename Page…should use the same inline rename flow style already used for rename-oriented palette actions.- Page commands should resolve against the active window, active workspace, and selected page unless the command explicitly targets another page.
- Palette results should show current shortcut hints where they exist.
- Dynamic
Select Page <title>results should make it easy to jump directly to any page even when there are more than nine.
Context Menu
Right-clicking a page should expose:
New PageRename Page…Move LeftMove RightClose PageClose Other Pages
Current branch status:
- Implemented.
Drag And Drop Reordering
The page strip should support drag-and-drop reordering, not just menu-based movement.
Required behavior:
- Dragging starts from the page item, not from its close button.
- The reorder indicator should be a single insertion gap, similar to the sidebar workspace reordering model.
- If the strip is horizontally scrolled, dragging near the left or right edge should auto-scroll it.
- Dragging a page must never drag the window.
- Reordering stays within the current workspace in V1.
- Context-menu move actions remain as keyboard and accessibility fallback.
Current branch status:
- Implemented.
Page Naming
V1 default names:
Page 1Page 2Page 3
User rename is the primary naming path. Automatic labels based on the active process or focused surface can be added later if the default names feel too generic.
Model Direction
The current Workspace object in Sources/Workspace.swift still mixes project-level identity with page-level layout state.
Long-term direction:
- Keep
Workspaceas the sidebar/project container. - Add a
WorkspacePagemodel underWorkspace. - Move
bonsplitControllerintoWorkspacePage. - Move page-local
panelsintoWorkspacePage. - Move page-local focus and selected-surface state into
WorkspacePage. - Move page-local session snapshot data into
WorkspacePage. - Keep workspace-level sidebar and metadata state on
Workspace.
This is the cleanest long-term shape for workspace -> page -> pane -> surface.
Current branch status:
- Only the first two steps are implemented.
- The branch intentionally keeps the existing single
bonsplitControlleronWorkspaceand swaps page state in and out around it.
Persistence
Session restore should persist:
- page order
- selected page per workspace
- each page's Bonsplit snapshot
- page custom titles
Workspace restore should reopen the last selected page, then restore page-local focus within that page.
Current branch status:
- Implemented.
Socket And CLI APIs
Pages need first-class API support because cmux is scriptable and page state will sit between workspace and pane.
Implemented v2 API surface:
page.listpage.currentpage.createpage.duplicatepage.selectpage.renamepage.closepage.reorderpage.nextpage.previouspage.last
Identity and targeting:
system.identifyincludesfocused.page_id,focused.page_ref,focused.page_index, andfocused.page_title.- Short refs support
page:<n>. - Commands that target panes or surfaces without an explicit page should resolve against the currently selected page in the targeted workspace.
Implemented CLI surface:
list-pages [--workspace <id|ref>]current-page [--workspace <id|ref>]new-page [--workspace <id|ref>] [--title <text>]duplicate-page [--workspace <id|ref>] [--page <id|ref>] [--title <text>]select-page --page <id|ref|index> [--workspace <id|ref>]rename-page [--workspace <id|ref>] [--page <id|ref>] <title>close-page [--page <id|ref>] [--workspace <id|ref>]reorder-page --page <id|ref|index> (--index <n> | --before <id|ref|index> | --after <id|ref|index>) [--workspace <id|ref>]next-page [--workspace <id|ref>]previous-page [--workspace <id|ref>]last-page [--workspace <id|ref>]
Non-Goals For V1
- Page-level badges, git metadata, or notification chips in the titlebar strip.
- Cross-workspace page moves.
- Nested page groups.
- Aggressively destroying inactive PTYs or browser sessions on every page switch.
Acceptance Criteria
The first implementation should feel complete if all of this is true:
- A workspace can hold multiple pages with independent pane/tab layouts.
- The titlebar strip replaces the folder icon area and is usable with mouse only.
Option+1..9works by default and is customizable in Settings.- Right click works on page items without breaking window dragging or terminal focus.
- Active-page close button visibility matches the rules above.
- Inactive pages unmount from the live UI so only the active page's terminal and browser views stay mounted.
- Drag-and-drop page reordering works, including edge auto-scroll for overflowed strips.
Cmd+Shift+Pexposes page commands and inline rename behavior.- Socket and CLI page APIs exist, including
system.identifypage context. - App relaunch restores page order, selection, and layout.
- Existing workspace and pane navigation continue to behave as before.
Current branch status:
- The V1 acceptance list is implemented.
- The remaining work is follow-on coverage and the deeper per-page controller refactor described above.
Test Expectations
Once implementation starts, add coverage for:
- titlebar hit testing, page item interaction, and empty-space drag behavior
- page switching preserving per-page Bonsplit state
Option+1..9routing, including9 -> last- custom shortcut overrides for page actions
Cmd+Shift+Ppage commands and rename flow- page context menu actions
- inactive-page terminal and browser unmount behavior
- page drag reordering, including overflow auto-scroll
- session restore of page order and selected page
- socket and CLI page commands, including
system.identifypage fields
Current branch status:
- Unit coverage now exists for page persistence round-trips and page shortcut routing, including
Option+9 -> last page,Option+],Cmd+Option+N, and symbol-first layout fallback for page shortcuts. - Unit coverage also exists for duplicate-page structure preservation and active-page close-neighbor selection.
- UI and end-to-end coverage for titlebar hit testing, drag behavior, and page lifecycle still needs to be added.