Skip to content

Reopen workspace pages in the titlebar - #1039

Closed
lawrencecchen wants to merge 7 commits into
mainfrom
issue-569-titlebar-top-level-tabs-reopen
Closed

lawrencecchen wants to merge 7 commits into
mainfrom
issue-569-titlebar-top-level-tabs-reopen

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Mar 7, 2026 •

Copy link
Copy Markdown
Contributor

Replacement for #1030.

Addresses the remaining review comments on #569.

What changed:

  • add regression coverage first for page shortcut routing, page close scoping, reorder semantics, and CLI/socket parity
  • fix CLI page argument parsing and plain-text current-page output
  • fix page shortcut defaults and reactive menu bindings
  • fix titlebar page hinting, drag/drop sizing, accessibility, and command palette gating
  • fix workspace/page persistence and v2 page API scoping semantics
  • add the missing titlebar page reorder UTType and refresh the spec

Verification:

  • xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-issue-569-titlebar-top-level-tabs-reopen build
  • python3 -m py_compile tests_v2/test_page_cli_socket_parity.py
  • ./scripts/reload.sh --tag issue-569-titlebar-top-level-tabs-reopen
  • gh workflow run test-e2e.yml --repo manaflow-ai/cmux -f ref=issue-569-titlebar-top-level-tabs-reopen -f test_filter=WorkspacePagesUITests -f record_video=true

Summary by cubic

Reopens workspace pages as top-level tabs in the titlebar with precise drag-and-drop and cleaner restore. Tightens v2 page socket validation, scopes page lookups to the caller’s window, and keeps stored page tree surfaces aligned with runtime state.

  • New Features

    • Titlebar page strip: live width tracking with auto-scroll, accurate drop indicators, exported UTI (com.cmux.titlebar-page-reorder), hover close, and accessibility labels.
    • Shortcut hints: hold Command to reveal direct-select badges that mirror your bindings; defaults use Cmd+Option for direct-select (1–9) and Cmd+[ / Cmd+] for next/previous.
    • Page persistence: inactive pages unmount; exported state strips scrollback; restore validates all referenced panels; neighbors request background terminal start; stored page tree surfaces stay aligned with present panels.
  • Migration

    • v2 API: page.new/page.duplicate default to select=false; page.close requires force=true over sockets with stricter payload validation; page.reorder uses final-position semantics and returns the new index; page.list/system.tree include surface_ids/surface_refs and selected_surface_id; socket page operations resolve within the active window.
    • CLI: new-page/duplicate-page accept a trailing title or --title and error on unexpected trailing args when --title is used; close-page accepts a positional page; current-page prints the page ref in text mode; unknown flags error unless separated by “--”.
    • Shortcuts: direct-select uses Cmd+Option by default; next/previous use Cmd+[ and Cmd+]; on-screen hints now trigger on Command and reflect your configured modifiers.
    • Tests: parity script now requires CMUX_SOCKET or CMUX_TAG to target a running build.

Written for commit 897a578. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Live titlebar page widths, improved drag‑and‑drop indicators, per‑page persisted shortcuts, and new command palette context flags.
    • Added exported UTI for titlebar page reorder.
  • Bug Fixes

    • CLI/socket argument validation with clearer errors; more consistent page resolution, teardown and restoration.
    • New/duplicate pages now default to not auto‑select; several shortcut defaults now require the Command modifier.
  • Documentation

    • Updated workspace pages spec to Command+Option shortcuts and v2 payload notes.
  • Tests

    • Expanded v2, CLI/socket and shortcut test coverage.

@vercel

vercel Bot commented Mar 7, 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 Mar 7, 2026 9:15am

@coderabbitai

coderabbitai Bot commented Mar 7, 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

Centralizes CLI positional/trailing-text parsing; adds per-page titlebar width tracking and width-aware drag/drop; introduces a V2 workspace/page resolver and richer pane/surface payloads; changes new/duplicate page defaults to not auto-select; changes page-selection shortcuts to require Command; standardizes stored-shortcut decoding and updates tests/docs.

Changes

Cohort / File(s) Summary
CLI argument parsing
CLI/cmux.swift
Adds private helpers positionalArguments(_:command:) and trailingTextArgument(_:command:); centralizes trailing-text extraction and unknown-flag validation for page commands (new/duplicate/rename/close/current-page).
Titlebar UI & drag/drop
Sources/ContentView.swift
Adds per-page width tracking (titlebarPageWidths, PreferenceKey), passes targetPageWidth into drop delegate, uses effective widths for indicators, adds CommandPalette context keys, accessibility labels, and debug hooks.
V2 page resolution & payloads
Sources/TerminalController.swift
Introduces v2ResolveWorkspaceForPage + V2PageWorkspaceResolution; centralizes workspace/page lookup for V2 ops and enriches pane/surface payloads with surface_ids, surface_refs, and selected-surface fields.
Workspace lifecycle & defaults
Sources/Workspace.swift
Adds teardownStoredPageStates, runtime-restore guards and helpers, invokes teardown during workspace teardown, and changes newPage/duplicatePage defaults to select: false.
Shortcut defaults & storage
Sources/KeyboardShortcutSettings.swift, Sources/cmuxApp.swift, Sources/NotificationsPage.swift, Sources/WorkspaceContentView.swift
Default page-selection shortcuts now require Command; adds AppStorage-backed per-page shortcut bindings and standardizes decoding via StoredShortcut.decode(from:fallback:).
AppDelegate / shortcut routing
Sources/AppDelegate.swift, cmuxTests/AppDelegateShortcutRoutingTests.swift
Removes pageIndex(forOptionDigit:), relaxes some guard conditions to rely on pageId only, adds DEBUG logs, and updates tests/expectations to Command+Option modifiers.
Manifest / type declaration
Resources/Info.plist
Adds UTExportedTypeDeclarations entry com.cmux.titlebar-page-reorder conforming to public.data.
Tests & CLI socket handling
tests_v2/test_page_cli_socket_parity.py, cmuxTests/...
Resolves socket path via env/tag helpers, uses resolved path in CLI tests, updates CLI/test expectations to v2 flows and textual current-page output, and expands v2 page/surface tests.
Docs
docs/workspace-pages-spec.md
Updates docs to Command+Option shortcuts, v2 payload naming, CLI/API notes (force flag mentions), and identity payload shape changes.
Small call-site decodes
Sources/NotificationsPage.swift, Sources/WorkspaceContentView.swift
Replaced local decode helpers with StoredShortcut.decode(from:fallback:) calls.

Sequence Diagram(s)

sequenceDiagram
    participant User as User (Drag)
    participant UI as Titlebar UI
    participant Drop as TitlebarPageDropDelegate
    participant Width as Titlebar Width Tracker
    participant Store as Workspace/Page Store

    User->>UI: Start dragging page
    UI->>Drop: dropEntered / dropUpdated
    Drop->>Width: effectiveTargetWidth()
    Width-->>Drop: target width (from titlebarPageWidths)
    Drop->>Drop: compute indicator position (width-aware)
    Note over Drop: debugIndicator logged
    User->>UI: Drop completes
    Drop->>Store: performDrop -> reorder pages
    Store-->>UI: UI updates ordering
Loading
sequenceDiagram
    participant Client as V2 Client
    participant Controller as TerminalController
    participant Resolver as v2ResolveWorkspaceForPage
    participant TabMgr as TabManager
    participant Workspace as Workspace

    Client->>Controller: V2 page operation (rename/close/duplicate/reorder)
    Controller->>Resolver: resolve workspace/page (params, pageId)
    Resolver->>TabMgr: lookup workspace (scoped or default)
    TabMgr-->>Resolver: tabManager or nil
    alt workspace found
        Resolver->>Workspace: locate page
        Workspace-->>Resolver: page found / not found
        Resolver-->>Controller: resolved(tabManager, workspace, page?)
    else workspace not found
        Resolver-->>Controller: workspaceNotFound
    end
    Controller->>Controller: build V2 payload (include surface_ids / surface_refs)
    Controller-->>Client: reply with resolved payload or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

  • manaflow-ai/cmux-dev-artifacts#95 — Changes to ContentView titlebar/shortcut UI directly affect the titlebar page buttons and hints exercised by WorkspacePages UI tests.

Possibly related PRs

Poem

🐰 I nibbled code where titles hide,
trailing text parsed safe and wide.
Cmd+Opt hops into the dance,
widths align and pages prance.
Teardowns tidy — carrots, advance!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Reopen workspace pages in the titlebar' directly summarizes the main feature of the PR and aligns with the primary objective of reintroducing workspace pages as top-level tabs in the titlebar.
Description check ✅ Passed The pull request provides a comprehensive description covering what changed, why, and how it was tested, with clear verification commands.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-569-titlebar-top-level-tabs-reopen

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

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

2 issues found across 11 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/TerminalController.swift">

<violation number="1" location="Sources/TerminalController.swift:4260">
P2: Explicit `workspace_id` scoping can be bypassed when `workspace_id` is malformed, causing page operations to run against the selected workspace.</violation>
</file>

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

<violation number="1" location="Sources/Workspace.swift:1758">
P2: The strict detached-surface equality check can incorrectly reject valid runtime page state and fall back to capped session restore, dropping panels in large workspaces.</violation>
</file>

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

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

greptile-apps Bot commented Mar 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR completes the titlebar workspace-pages feature by addressing remaining review feedback from issue #569. It fixes keyboard shortcut defaults (all page-select shortcuts gain the Command modifier), refactors the v2 page API to use a new v2ResolveWorkspaceForPage helper that correctly enforces workspace scope and enables cross-window page lookup, adds the missing com.cmux.titlebar-page-reorder UTType export, improves drag/drop accuracy with real measured page widths, makes the ShortcutHintModifierMonitor dynamically aware of custom shortcuts, gates command-palette page commands on contextual availability, and fixes workspace/page persistence (scrollback stripping for stored pages, stored-state teardown on restore, runtime/session panel-ID parity validation).

Key changes and notable items:

  • Breaking socket API change: page.close now requires force=true; callers that omit the flag receive a new interactive_not_allowed error instead of silently force-closing. Existing socket integrations will need to be updated. The CLI handles this automatically.
  • _resolve_socket_path() in tests_v2: Called at module-import time; raises cmuxError if env vars are absent, which breaks pytest --collect-only and IDE test discovery.
  • The TitlebarPageWidthPreferenceKey implementation, v2ResolveWorkspaceForPage logic, and scrollback-stripping fix in Workspace.exportedPageStateSnapshot are all well-structured and correct.

Confidence Score: 3/5

  • Safe to merge with minor fixes — two issues need attention before shipping to production API consumers and ensuring CI test reliability.
  • The core logic (workspace-scope resolution, shortcut defaults, drag/drop sizing, persistence fixes) is well-structured and covered by new unit tests. However, two issues affect stability: (1) _resolve_socket_path() raises at module-import time, breaking test collection in CI without env vars; (2) page.close breaking default change (force=true → false) will silently affect socket API consumers. These require fixes before production release, but the UI feature itself is solid.
  • tests_v2/test_page_cli_socket_parity.py (module-level exception handling) and Sources/TerminalController.swift (breaking API default change documentation)

Last reviewed commit: 22e85ed

Comment thread tests_v2/test_page_cli_socket_parity.py Outdated
Comment thread Sources/TerminalController.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: 4

Caution

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

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

1750-1762: ⚠️ Potential issue | 🟠 Major

Tear down detached runtime panels before snapshot fallback.

On Line 1751 this code removes the stored page state from storedPageStates, but if the new parity check fails on Lines 1757-1758 the fallback path on Line 1761 restores from sessionState without closing the detached panels in runtimeState. Those panels are then orphaned permanently, which can leak Ghostty/WebKit resources and keep terminal processes alive.

Proposed fix
     private func restoreStoredPage(_ pageId: UUID) {
         let storedState = storedPageStates.removeValue(forKey: pageId) ?? StoredPageState(
             sessionState: emptyPageSessionStateSnapshot(currentDirectory: currentDirectory),
             runtimeState: nil
         )

         let expectedPanelIds = Set(storedState.sessionState.panels.map(\.id))
-        if let runtimeState = storedState.runtimeState,
-           Set(runtimeState.detachedSurfaces.keys) == expectedPanelIds {
-            restoreRuntimePageState(runtimeState)
-        } else {
-            restoreSessionPageState(storedState.sessionState)
+        if let runtimeState = storedState.runtimeState {
+            if Set(runtimeState.detachedSurfaces.keys) == expectedPanelIds {
+                restoreRuntimePageState(runtimeState)
+                return
+            }
+
+            teardownStoredPageState(storedState)
         }
+
+        restoreSessionPageState(storedState.sessionState)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 1750 - 1762, When restoring a stored
page in restoreStoredPage you currently drop storedPageStates and, if the
runtime/session parity check fails, fall back to restoreSessionPageState without
cleaning up runtimeState.detachedSurfaces; iterate over
storedState.runtimeState?.detachedSurfaces (if present) and explicitly tear down
each detached surface before calling restoreSessionPageState — use the same
teardown/remove API used elsewhere in the codebase (e.g. the method used by
restoreRuntimePageState or any existing removeDetachedSurface/teardownSurface
helper) so detached WebKit/terminal resources are closed instead of orphaned.
🧹 Nitpick comments (2)
Sources/cmuxApp.swift (1)

933-939: Consider extracting decodeShortcut to a shared utility.

This helper is duplicated identically in ContentView.swift, NotificationsPage.swift, and WorkspaceContentView.swift. Extracting it to a shared location (e.g., a StoredShortcut extension or utility file) would reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/cmuxApp.swift` around lines 933 - 939, The
decodeShortcut(from:fallback:) helper is duplicated across cmuxApp.swift,
ContentView.swift, NotificationsPage.swift, and WorkspaceContentView.swift;
extract it into one shared location by moving its logic into a single utility
(e.g., an extension on StoredShortcut or a new ShortcutsUtils.swift) and update
callers to use StoredShortcut.decode(from:fallback:) or
ShortcutsUtils.decodeShortcut(...) instead of the local private function; ensure
the new shared method signature matches the original (accepts Data and a
fallback StoredShortcut and returns StoredShortcut) and remove the duplicate
private functions from the three files.
Sources/ContentView.swift (1)

1535-1537: Gate Close Page from the active page policy, not just page count.

workspaceHasMultiplePages is broader than the per-page close rule already used by the titlebar via workspace.canClosePage(page.id). A dedicated pageCanClose flag from the active page would keep the command palette aligned with the rest of the UI and avoid future drift if close eligibility changes again.

Also applies to: 4488-4495, 4828-4836

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ContentView.swift` around lines 1535 - 1537, Replace the broad
workspaceHasMultiplePages gate with a per-page flag derived from the active
page: add a pageCanClose boolean (computed from
workspace.canClosePage(activePage.id) or the equivalent active page identifier)
and use that flag where the "Close Page" command palette entry is gated
(references: workspaceHasMultiplePages -> replace with pageCanClose; use
workspace.canClosePage(page.id) to compute it). Update all occurrences mentioned
(the main block and the other ranges) so the command palette aligns with the
titlebar's per-page close policy and avoid future drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 1144-1149: When parsing commands like "new-page",
"duplicate-page", and "close-page" the code currently only validates leftover
args when options like title/page are absent (see variables titleOpt/pageOpt and
the call to trailingTextArgument(rem1)), which lets valueless or misspelled
flags remain in trailing and silently fallback; fix by validating trailing after
parseOption even when titleOpt/pageOpt is present: after parsing options
(variables titleOpt, pageOpt and arrays like trailing or rem1), ensure
trailing.isEmpty (or trailing.count <= 1 as appropriate) and throw an error if
extra tokens exist so commands like duplicate-page and close-page fail fast
instead of using defaults.

In `@Sources/ContentView.swift`:
- Around line 1320-1322: The titlebar hint monitor currently follows page
shortcut modifiers via ContentView.titlebarPageShortcutHintModifierFlags(),
causing titlebar hints to appear when users hold page shortcuts (e.g. ⌘⌥…) even
if they disabled Command-hold hints; change the requiredModifierFlagsProvider
used by titlebarPageShortcutHintMonitor to return a Command-only modifier
(normalized to [.command]) so ShortcutHintModifierPolicy.shouldShowHints treats
it as the Command-hold case, and ensure any logic that suppresses the
TabItemView close button continues to consult showsModifierShortcutHints (or the
always-show debug flag) rather than the page-shortcut provider; refer to
titlebarPageShortcutHintMonitor, ShortcutHintModifierMonitor,
ContentView.titlebarPageShortcutHintModifierFlags(),
ShortcutHintModifierPolicy.shouldShowHints, showsModifierShortcutHints, and
TabItemView in your change.

In `@Sources/TerminalController.swift`:
- Around line 3865-3869: The failure branch after calling
resolved.workspace.movePage(pageId:toIndex:) currently leaves result as the
previous not_found value; change it so when movePage returns false because the
workspace has a single page (the guard in movePage(pageId:toIndex:) in
Workspace), you set result to the proper invalid_state response instead of
leaving it unchanged. Update the block in TerminalController where movePage is
called (the code that builds payload with v2PageResultPayload and sets result)
to detect the false return and assign result = .invalid_state (or the project's
equivalent enum/case for invalid_state) so the client receives the correct error
for single-page reorders.
- Around line 4248-4280: The code conflates "parameter omitted" with "parameter
present but malformed"; update V2PageWorkspaceResolution to include a new case
(.invalidParams) and modify v2ResolveWorkspaceForPage to detect when params
contains "page_id" or "workspace_id" but the converted UUIDs are nil (i.e.
malformed) and return .invalidParams in those branches instead of treating them
as omitted; specifically, keep using params["workspace_id"] != nil and
params["page_id"] != nil to detect explicit presence, return .invalidParams when
an explicit key is present but v2ResolveWorkspace(params:) or the pageId
argument is nil/malformed, and only fall back to v2LocatePage when the keys are
genuinely absent; update any callers to handle the new .invalidParams case.

---

Outside diff comments:
In `@Sources/Workspace.swift`:
- Around line 1750-1762: When restoring a stored page in restoreStoredPage you
currently drop storedPageStates and, if the runtime/session parity check fails,
fall back to restoreSessionPageState without cleaning up
runtimeState.detachedSurfaces; iterate over
storedState.runtimeState?.detachedSurfaces (if present) and explicitly tear down
each detached surface before calling restoreSessionPageState — use the same
teardown/remove API used elsewhere in the codebase (e.g. the method used by
restoreRuntimePageState or any existing removeDetachedSurface/teardownSurface
helper) so detached WebKit/terminal resources are closed instead of orphaned.

---

Nitpick comments:
In `@Sources/cmuxApp.swift`:
- Around line 933-939: The decodeShortcut(from:fallback:) helper is duplicated
across cmuxApp.swift, ContentView.swift, NotificationsPage.swift, and
WorkspaceContentView.swift; extract it into one shared location by moving its
logic into a single utility (e.g., an extension on StoredShortcut or a new
ShortcutsUtils.swift) and update callers to use
StoredShortcut.decode(from:fallback:) or ShortcutsUtils.decodeShortcut(...)
instead of the local private function; ensure the new shared method signature
matches the original (accepts Data and a fallback StoredShortcut and returns
StoredShortcut) and remove the duplicate private functions from the three files.

In `@Sources/ContentView.swift`:
- Around line 1535-1537: Replace the broad workspaceHasMultiplePages gate with a
per-page flag derived from the active page: add a pageCanClose boolean (computed
from workspace.canClosePage(activePage.id) or the equivalent active page
identifier) and use that flag where the "Close Page" command palette entry is
gated (references: workspaceHasMultiplePages -> replace with pageCanClose; use
workspace.canClosePage(page.id) to compute it). Update all occurrences mentioned
(the main block and the other ranges) so the command palette aligns with the
titlebar's per-page close policy and avoid future drift.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5cc575c8-b442-4fa4-91c2-131248938e0c

📥 Commits

Reviewing files that changed from the base of the PR and between 4de975e and 22e85ed.

📒 Files selected for processing (11)
  • CLI/cmux.swift
  • Resources/Info.plist
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/KeyboardShortcutSettings.swift
  • Sources/TerminalController.swift
  • Sources/Workspace.swift
  • Sources/cmuxApp.swift
  • cmuxTests/AppDelegateShortcutRoutingTests.swift
  • docs/workspace-pages-spec.md
  • tests_v2/test_page_cli_socket_parity.py

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 22e85edfa0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/TerminalController.swift
Comment thread Sources/Workspace.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.

1 issue found across 11 files (changes from recent commits).

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/ContentView.swift">

<violation number="1" location="Sources/ContentView.swift:1320">
P2: The titlebar page-hint monitor is now locked to ⌘ only, so hints no longer track the real page shortcut modifiers (default ⌘⌥ and custom bindings).</violation>
</file>

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

Comment thread Sources/ContentView.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: 4

Caution

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

⚠️ Outside diff range comments (2)
Sources/ContentView.swift (2)

2330-2343: ⚠️ Potential issue | 🟡 Minor

Hide the invisible close button from accessibility.

opacity(0) and allowsHitTesting(false) don't remove this control from the accessibility tree, so VoiceOver can still encounter an invisible “Close Page” button. Add accessibility hiding when the button is not shown, or render it conditionally.

♿ Minimal fix
                 .buttonStyle(.plain)
                 .disabled(!canClose)
                 .accessibilityLabel(String(localized: "workspace.page.context.close", defaultValue: "Close Page"))
+                .accessibilityHidden(!showCloseButton)
                 .opacity(showCloseButton ? 1 : 0)
                 .allowsHitTesting(showCloseButton)
                 .accessibilityIdentifier(titlebarPageCloseButtonAccessibilityIdentifier(pageId: page.id))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ContentView.swift` around lines 2330 - 2343, The invisible close
button remains in the accessibility tree; update the Button with either
conditional rendering or add accessibility hiding when hidden: wrap the Button
in an if showCloseButton { ... } so it is not created at all when
showCloseButton is false, or keep the Button but add
.accessibilityHidden(!showCloseButton) alongside the existing modifiers; ensure
you preserve .accessibilityLabel and .accessibilityIdentifier
(titlebarPageCloseButtonAccessibilityIdentifier(pageId: page.id)) and keep
.disabled(!canClose) behavior.

2232-2249: ⚠️ Potential issue | 🟡 Minor

Don't make the titlebar “New Page” action hover-only.

The new accessibility label helps, but the button still only exists while the pointer is hovering the titlebar, so keyboard and VoiceOver users still can't reach it. Please keep an always-focusable affordance here, or expose an equivalent non-hover-only control.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ContentView.swift` around lines 2232 - 2249, The button is only
present when isTitlebarHovered so keyboard/VoiceOver users can't reach it;
instead always render the Button that calls workspace.newPage(select:), and
toggle its visual state based on isTitlebarHovered (e.g., change .opacity,
.background, or .foregroundColor) rather than conditionally including it or
using .transition(.opacity) to remove it; keep the .accessibilityLabel and
.accessibilityIdentifier ("titlebarPageNewButton") and ensure it is not
accessibilityHidden so it remains focusable by keyboard/VoiceOver even when not
hovered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/workspace-pages-spec.md`:
- Around line 66-67: The docs incorrectly describe the chord as "Command+Option"
but the implementation uses only "Command" via
ShortcutHintModifierPolicy.shouldShowHints; update the text in the specified
locations (the three occurrences) to read "Command" instead of "Command+Option"
and ensure the description of page shortcut hints and references to
KeyboardShortcutSettings/default bindings match the implementation; search for
the phrase "Command+Option" in this document and replace it with "Command" where
it refers to page shortcut hint chord so the docs align with
ShortcutHintModifierPolicy.shouldShowHints.

In `@Sources/TerminalController.swift`:
- Line 3584: The code currently uses v2Bool(params, "select") ?? false which
treats malformed explicit values as if the key were absent; update the
validation so that if params contains the "select" key but v2Bool returns nil
you return an invalid_params error instead of defaulting to false. Concretely,
before calling v2FocusAllowed(requested: ...), check
params.keys.contains("select"); if true and v2Bool(params, "select") == nil
produce an invalid_params response, otherwise pass the unwrapped Bool (or
default false only when the key is absent) into v2FocusAllowed. Apply the same
pattern for other boolean params mentioned (e.g., "force" in page.close) and the
other call sites around v2FocusAllowed/v2Bool noted in the comment (lines
3606-3607, 3707-3719).
- Around line 4230-4338: Add distinct failure cases to V2PageWorkspaceResolution
(e.g., missingWindow, missingSurface, missingTab, missingWorkspaceRef or
noActiveWorkspace) and update v2ResolveWorkspaceForPage to return those specific
cases instead of collapsing to .workspaceNotFound: replace the current early
UUID-nil checks to return the new missing* cases (for params["window_id"],
["surface_id"], ["tab_id"], ["workspace_id"] and when pageId present but page
not found in an explicitly scoped workspace return
.pageNotFoundInScopedWorkspace as already done), and when no explicit scope and
no active workspace inferable return the new
noActiveWorkspace/missingWorkspaceRef case. Also update downstream handling (
callers of V2PageWorkspaceResolution and v2PageResolutionInvalidParamsResult )
to map the new enum cases to the correct error codes/messages so missing-scope
objects produce not_found for the specific scope and truly no active workspace
produces the existing workspace-not-found path.

In `@Sources/Workspace.swift`:
- Around line 1756-1762: The current check builds expectedPanelIds from
sessionState.panels (capped by SessionPersistencePolicy.maxPanelsPerWorkspace)
which can miss panel IDs present in runtimeState.layout; change the validation
to collect all panel UUIDs referenced by runtimeState.layout (e.g., add a helper
panelIds(in:) that traverses SessionWorkspaceLayoutSnapshot cases) and ensure
expectedPanelIds (or a new set derived from the layout) is a subset of
runtimeState.detachedSurfaces.keys before calling
restoreRuntimePageState(runtimeState); if any layout panel ID is missing, fall
back to teardownDetachedSurfaces(storedState.runtimeState?.detachedSurfaces) and
restoreSessionPageState(storedState.sessionState) instead, keeping existing
teardownDetachedSurfaces and restoreSessionPageState calls.

---

Outside diff comments:
In `@Sources/ContentView.swift`:
- Around line 2330-2343: The invisible close button remains in the accessibility
tree; update the Button with either conditional rendering or add accessibility
hiding when hidden: wrap the Button in an if showCloseButton { ... } so it is
not created at all when showCloseButton is false, or keep the Button but add
.accessibilityHidden(!showCloseButton) alongside the existing modifiers; ensure
you preserve .accessibilityLabel and .accessibilityIdentifier
(titlebarPageCloseButtonAccessibilityIdentifier(pageId: page.id)) and keep
.disabled(!canClose) behavior.
- Around line 2232-2249: The button is only present when isTitlebarHovered so
keyboard/VoiceOver users can't reach it; instead always render the Button that
calls workspace.newPage(select:), and toggle its visual state based on
isTitlebarHovered (e.g., change .opacity, .background, or .foregroundColor)
rather than conditionally including it or using .transition(.opacity) to remove
it; keep the .accessibilityLabel and .accessibilityIdentifier
("titlebarPageNewButton") and ensure it is not accessibilityHidden so it remains
focusable by keyboard/VoiceOver even when not hovered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cd96d9c2-7c36-4115-8028-cd8e5c2fa655

📥 Commits

Reviewing files that changed from the base of the PR and between 22e85ed and cdf2608.

📒 Files selected for processing (11)
  • CLI/cmux.swift
  • Sources/ContentView.swift
  • Sources/KeyboardShortcutSettings.swift
  • Sources/NotificationsPage.swift
  • Sources/TerminalController.swift
  • Sources/Workspace.swift
  • Sources/WorkspaceContentView.swift
  • Sources/cmuxApp.swift
  • cmuxTests/AppDelegateShortcutRoutingTests.swift
  • docs/workspace-pages-spec.md
  • tests_v2/test_page_cli_socket_parity.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/KeyboardShortcutSettings.swift

Comment thread docs/workspace-pages-spec.md Outdated
Comment thread Sources/TerminalController.swift Outdated
Comment thread Sources/TerminalController.swift Outdated
Comment thread Sources/Workspace.swift Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4503d5d701

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/TerminalController.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

🧹 Nitpick comments (1)
cmuxTests/AppDelegateShortcutRoutingTests.swift (1)

1389-1396: Assert exact surface_refs mapping, not just counts.

These assertions still pass if pane-level refs are duplicated or reordered. Since agentsSurfaces is already available here, compare the exact ref list instead of only checking lengths.

Suggested assertion tightening
-        XCTAssertEqual((agentsPane?["surface_refs"] as? [String])?.count, expectedAgentSurfaceIds.count)
-        XCTAssertEqual(agentsSurfaces?.compactMap { $0["ref"] as? String }.count, expectedAgentSurfaceIds.count)
+        let expectedAgentSurfaceRefs = agentsSurfaces?.compactMap { $0["ref"] as? String }
+        XCTAssertEqual(agentsPane?["surface_refs"] as? [String], expectedAgentSurfaceRefs)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/AppDelegateShortcutRoutingTests.swift` around lines 1389 - 1396,
The pane-level ref assertions only checked counts and can miss
duplicates/reordering; replace them with exact comparisons by building the
expected ref list from agentsSurfaces using expectedAgentSurfaceIds order and
then assert equality against agentsPane["surface_refs"]. Specifically, compute
expectedRefs = expectedAgentSurfaceIds.compactMap { id in
agentsSurfaces?.first(where: { ($0["id"] as? String) == id })?["ref"] as? String
} and then XCTAssertEqual(agentsPane?["surface_refs"] as? [String],
expectedRefs); remove or replace the two count-based assertions that reference
(agentsPane?["surface_refs"] as? [String])?.count and agentsSurfaces?.compactMap
{ $0["ref"] as? String }.count with this exact-list assertion (keep the
selected_surface_ref assertion as-is which already compares the selected ref).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/TerminalController.swift`:
- Around line 2674-2685: The surface summary fields are using raw pane.panelIds
instead of the filtered surfaces, causing surface_ids/selected_surface_id to
mismatch when panelSnapshotsById dropped entries; update the construction to
derive surface_ids and surface_refs (and
selected_surface_id/selected_surface_ref) from the already-computed surfaces
array (which was filtered via panelSnapshotsById) rather than pane.panelIds,
using the same v2Ref(kind:.surface, uuid: ...) and v2OrNull(...) helpers and
preserving paneIndex and focused logic so the exported keys
("surface_ids","surface_refs","selected_surface_id","selected_surface_ref")
exactly mirror the entries in surfaces and selected panel presence.
- Around line 4311-4395: v2ResolveWorkspaceForPage currently routes to a
TabManager (e.g. when window_id is provided) but then calls v2ResolveWorkspace
which only checks the selected workspace, causing pages in other workspaces of
the same TabManager to be missed; change the logic after selecting
routedTabManager in v2ResolveWorkspaceForPage so that if pageId is present you
first search all workspaces owned by that routedTabManager for the page (via an
existing helper or add a small helper like locatePage(in: TabManager, pageId:
UUID) that mirrors v2LocatePage but scoped to a TabManager), and if found return
.resolved(tabManager: routedTabManager, workspace: foundWorkspace); if not found
and hasExplicitScope return .pageNotFoundInScopedWorkspace, otherwise fall
through to the existing global locate logic. Ensure you update references in
v2ResolveWorkspaceForPage (the window_id branch and subsequent resolution block)
and reuse v2LocatePage behavior but limited to the routedTabManager.

---

Nitpick comments:
In `@cmuxTests/AppDelegateShortcutRoutingTests.swift`:
- Around line 1389-1396: The pane-level ref assertions only checked counts and
can miss duplicates/reordering; replace them with exact comparisons by building
the expected ref list from agentsSurfaces using expectedAgentSurfaceIds order
and then assert equality against agentsPane["surface_refs"]. Specifically,
compute expectedRefs = expectedAgentSurfaceIds.compactMap { id in
agentsSurfaces?.first(where: { ($0["id"] as? String) == id })?["ref"] as? String
} and then XCTAssertEqual(agentsPane?["surface_refs"] as? [String],
expectedRefs); remove or replace the two count-based assertions that reference
(agentsPane?["surface_refs"] as? [String])?.count and agentsSurfaces?.compactMap
{ $0["ref"] as? String }.count with this exact-list assertion (keep the
selected_surface_ref assertion as-is which already compares the selected ref).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 280e80b3-e76b-4226-bdc8-43d7d7a49ea9

📥 Commits

Reviewing files that changed from the base of the PR and between 4503d5d and db78f94.

📒 Files selected for processing (4)
  • Sources/TerminalController.swift
  • Sources/Workspace.swift
  • cmuxTests/AppDelegateShortcutRoutingTests.swift
  • cmuxTests/WorkspaceContentViewVisibilityTests.swift

Comment thread Sources/TerminalController.swift Outdated
Comment thread Sources/TerminalController.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.

♻️ Duplicate comments (1)
Sources/TerminalController.swift (1)

2675-2685: ⚠️ Potential issue | 🟡 Minor

Keep stored pane surface_* fields aligned with the filtered surfaces array.

Lines 2675-2685 still derive surface_ids, surface_refs, and selected_surface_* from raw pane.panelIds. If panelSnapshotsById dropped a missing snapshot, system.tree can return IDs/refs that do not exist in surfaces.

Suggested fix
             let focused = surfaces.contains { ($0["focused"] as? Bool) == true }
-            let surfaceIds = pane.panelIds.map(\.uuidString)
-            let surfaceRefs = pane.panelIds.map { v2Ref(kind: .surface, uuid: $0) }
+            let presentPanelIds = pane.panelIds.filter { panelSnapshotsById[$0] != nil }
+            let selectedPanelId = pane.selectedPanelId.flatMap { presentPanelIds.contains($0) ? $0 : nil }
+            let surfaceIds = presentPanelIds.map(\.uuidString)
+            let surfaceRefs = presentPanelIds.map { v2Ref(kind: .surface, uuid: $0) }
             return [
                 "id": NSNull(),
                 "ref": NSNull(),
                 "index": paneIndex,
                 "focused": focused,
                 "surface_ids": surfaceIds,
                 "surface_refs": surfaceRefs,
-                "selected_surface_id": v2OrNull(pane.selectedPanelId?.uuidString),
-                "selected_surface_ref": v2Ref(kind: .surface, uuid: pane.selectedPanelId),
+                "selected_surface_id": v2OrNull(selectedPanelId?.uuidString),
+                "selected_surface_ref": v2Ref(kind: .surface, uuid: selectedPanelId),
                 "surface_count": surfaces.count,
                 "surfaces": surfaces
             ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TerminalController.swift` around lines 2675 - 2685, The stored pane
"surface_*" fields are built from raw pane.panelIds which can include IDs
dropped by panelSnapshotsById; update the construction so surface_ids and
surface_refs are derived from the filtered surfaces array (not pane.panelIds),
and compute selected_surface_id/selected_surface_ref only when
pane.selectedPanelId is present in that filtered surfaces set—use the same
v2OrNull/v2Ref helpers as before but source values from the surfaces collection
used for the UI tree (and reference panelSnapshotsById/system.tree filtering) so
stored IDs/refs remain aligned with the surfaces array.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@Sources/TerminalController.swift`:
- Around line 2675-2685: The stored pane "surface_*" fields are built from raw
pane.panelIds which can include IDs dropped by panelSnapshotsById; update the
construction so surface_ids and surface_refs are derived from the filtered
surfaces array (not pane.panelIds), and compute
selected_surface_id/selected_surface_ref only when pane.selectedPanelId is
present in that filtered surfaces set—use the same v2OrNull/v2Ref helpers as
before but source values from the surfaces collection used for the UI tree (and
reference panelSnapshotsById/system.tree filtering) so stored IDs/refs remain
aligned with the surfaces array.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6106176c-dca0-463d-a96d-415a5dc79f43

📥 Commits

Reviewing files that changed from the base of the PR and between db78f94 and 67f3811.

📒 Files selected for processing (2)
  • Sources/TerminalController.swift
  • cmuxTests/AppDelegateShortcutRoutingTests.swift

@gur111

gur111 commented Jul 20, 2026

Copy link
Copy Markdown

Is this feature abandoned?

@jonsmithers

Copy link
Copy Markdown

Is this meant to be closed in favor of #4766?

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Superseded by merged #1030, which delivered the workspace-page titlebar implementation and closed #569. Closing this replacement PR to keep the stack clean.

This branch was successfully deployed

1 active deployment
Preview — 897a5781 Deployed Mar 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants