Skip to content

Freeform 2D canvas layout for workspace panes - #5987

Merged
azooz2003-bit merged 59 commits into
mainfrom
feat-canvas-panes
Jun 14, 2026
Merged

azooz2003-bit merged 59 commits into
mainfrom
feat-canvas-panes

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jun 12, 2026 •

Copy link
Copy Markdown
Collaborator

A new per-workspace layout mode where panes float on an infinite, scrollable 2D canvas. The existing bonsplit split tree is untouched; canvas is a presentation mode you toggle per workspace, and toggling back restores the splits exactly.

Architecture

Two SPM packages own the feature; the app target carries only glue.

Packages/CmuxCanvas (pure, no AppKit): pane frames + z-order + tabs (CanvasLayout/CanvasPane with ordered panelIds + selection), gap math and snapping (CanvasSnapEngine), placement (CanvasPlacer), align/distribute/tidy (CanvasAligner), spatial focus (CanvasSpatialNavigator), viewport math. 65 deterministic tests.

Packages/CmuxCanvasUI (AppKit rendering + input): a real NSScrollView (free 2D panning, momentum, pinch zoom from real scroll events; Cmd+Scroll routes to the canvas even over pane content), document sizing with origin compensation, workspace-pane-style tab bars (render-only SwiftUI reporting hit rects; ALL strip mouse events run on the AppKit path — drags on the fast NSEvent drag session with snap guides, deterministic tab select/close at mouse-up, Option+drag tears a tab out into its own pane and dropping a single-tab pane on another pane's tab bar joins it), keyboard zoom (1.25x steps), centered overview zoom, focused pane always raises to front, and an explicit offscreen lifecycle (visible rect + 50% margin; one mounted content per pane — its selected tab). Host seams: CanvasPaneContentMounting, CanvasTheme, pre-localized CanvasPaneChrome, onViewportGeometryChanged/onViewportSettled. 11 model tests.

App glue (Sources/Canvas/): Workspace integration (mode toggle seeds from splits; canvas state and tab groups persist per session with panel-id remap on restore — verified across relaunch), terminal mounts (Ghostty surface hosted directly; surface pixel size derives from the window backing scale so canvas magnification scales terminals visually instead of re-typesetting them), browser mounts (webviews inline-hosted inside the pane hierarchy by default — content tracks pans frame-perfectly, scales with magnification, stacks in native z-order; canvasInlineBrowserHostingDisabled defaults-key reverts to window-portal hosting), Cmd+T opens a tab in the focused canvas pane (Cmd+D / Cmd+Shift+D create new floating panes), cmd+shift+]/[ cycles a pane's tabs.

One shared CanvasActionExecutor serves every entrypoint: shortcuts (ctrl+cmd+C toggle / R reveal / O overview / T tidy; opt+cmd+= - 0 zoom; opt+cmd+arrows spatial focus), command palette, View menu, the canvas.* socket domain (info with tab lists, set_mode, set_frame, align, reveal, overview, zoom, join, break, select_tab), and the cmux canvas CLI namespace. All shortcuts live in KeyboardShortcutSettings, mirror into the CmuxSettings ShortcutAction catalog (cmux.json bindings), are localized for all 19 locales, and are documented on the web shortcuts + configuration pages. canvas.paneGap / canvas.snappingEnabled are catalog settings with Settings UI rows and cmux.json schema entries.

Verification

Socket-driven preflights on the tagged build with screenshots at every step: mode toggling both directions, seed-from-splits, tidy/align at the configured gap (gap=40 verified live), centered overview with scaled live terminals and browsers, zoom steps and round-trips (terminal grid integrity verified — the earlier zoom row-duplication is fixed at the surface-sizing root), set-frame, reveal, spatial focus, tab join/select/break/cycle (scrollback intact across tab remounts), browser content tracking through scroll/drag/zoom/overview round-trips, focused-pane z-raising, no floating fragments after full verb sweeps, and persistence of frames + tab groups across app relaunch. 300+ package tests across four packages. Tear-out/join drag gestures cannot be socket-driven and carry a hand-verification flag.

Known limitations

  • Tab bar matches split-pane anatomy (metrics, fills, hover-close) but not yet favicons/loading spinners/dirty dots.
  • Tear-out (Option+drag) and drop-to-join shipped gesture-verified-by-code-review only; needs hands-on confirmation.

🤖 Generated with Claude Code

Pane frames, canonical gap metrics, drag/resize snapping with guides,
align/distribute/tidy commands, spatial focus navigation, new-pane
placement, and viewport math. Foundation-only, deterministic, 57 tests.

Part of the freeform canvas layout for workspace panes (design in
docs/canvas-layout-design.md).
NSScrollView-based freeform canvas (CanvasRootView) hosting terminal
surfaces directly (portal-detached, no edge reflow) and other panel
kinds via NSHostingView. Pane chrome with title-bar move drag, edge/
corner resize with snap guides, explicit offscreen occlusion lifecycle,
document auto-sizing with origin compensation, overview zoom, reveal.
Workspace gains layoutMode + canvasModel; canvas-aware moveFocus;
WorkspaceContentView branches per mode.
…w menu

12 new KeyboardShortcutSettings actions (4 bound: ctrl+cmd+C/R/O/T,
8 unbound align/distribute/equalize), all routed through the shared
CanvasActionExecutor; palette commands gated on canvas mode via a new
workspace.canvasLayout context key; View menu entries.
New ControlCanvasContext seam slice (info/set_mode/set_frame/align/
reveal/overview) lifted onto the coordinator; TerminalController
witnesses route through CanvasActionExecutor so the socket shares the
single execution path. CLI: cmux canvas info|mode|set-frame|align|
reveal|overview with --workspace/--surface ref normalization.
…lization

SessionWorkspaceSnapshot gains layoutMode + canvasPanes (z-ordered,
panel-id remapped on restore; restore sets layoutMode directly to skip
seed-from-splits). Package ShortcutAction mirrors the 12 canvas actions
(cmux.json bindings + Settings UI defaults). 17 new Localizable.xcstrings
keys translated for all 19 locales.
…st stubs

Shortcut docs + configuration docs render the new canvas category from
cmux-shortcuts.ts (en/ja messages added). 8 coordinator tests cover the
canvas domain wire contract; stub file gains window + canvas defaults.
setMagnification(centeredAt:) lands off-center on large magnification
deltas; anchor by setting the clip bounds origin to contentCenter -
clipSize/2 in the same animation group.
@vercel

vercel Bot commented Jun 12, 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 14, 2026 4:20am
cmux-staging Building Building Preview, Comment Jun 14, 2026 4:20am

@coderabbitai

coderabbitai Bot commented Jun 12, 2026 •

Copy link
Copy Markdown

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 cmux canvas CLI, a CmuxCanvas package (geometry, layout, snapping, placement, navigation, aligner, viewport math), CmuxCanvasUI host/model and AppKit bridge, control-socket canvas API and handlers, workspace wiring, command-palette/shortcuts/localization, tests, docs, and project/package wiring.

Changes

Canvas mode end-to-end

Layer / File(s) Summary
End-to-end canvas feature
Packages/CmuxCanvas/*, Packages/CmuxCanvasUI/*, Packages/CmuxControlSocket/*, Sources/Canvas/*, Sources/*, CLI/cmux.swift, cmux.xcodeproj, web/*, Resources/*, docs/*, Packages/*/Tests/*
Adds geometry and layout types, snapping, placement, aligner, spatial navigation, CanvasModel and CanvasRootView, workspace layoutMode/session persistence, control-socket handlers and CLI cmux canvas, command-palette/shortcut surfaces and defaults, UI hosting/mounting, tests across packages, package/project wiring, localization, and design docs.

Sequence Diagram(s)

sequenceDiagram
  participant KeyboardShortcut as KeyboardShortcutSettings
  participant Palette as CommandPalette
  participant CLI as cmux
  participant Coordinator as ControlCommandCoordinator
  participant Workspace as Workspace
  participant Model as CanvasModel
  participant View as CanvasRootView

  KeyboardShortcut->>Palette: register canvasActions
  Palette->>Workspace: invoke CanvasAction via handler
  CLI->>Coordinator: sends canvas.* v2 method
  Coordinator->>Workspace: dispatch canvas action via ControlCanvasContext
  Workspace->>Model: update layout / apply alignment / setFrame
  Model->>View: viewport update / sync (CanvasViewportControlling)
  View->>Model: commit frame changes (on drag/end)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • manaflow-ai/cmux#4309: Related workspace/extension and ContentView wiring changes touching similar host integration areas.
  • manaflow-ai/cmux#5816: Related coordinator dispatch/handler additions that modified request routing.

Poem

🐇 I hopped through canvas, wide and free,
Panes now dance in two-dimensional glee.
Snaps and guides and tidy rows,
Shortcuts hum where sunlight flows.
A rabbit smiles at the new spree.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-canvas-panes

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@greptile-apps

greptile-apps Bot commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a freeform 2D canvas layout mode to cmux workspaces, implemented across two new SwiftPM packages (CmuxCanvas for pure geometry, CmuxCanvasUI for AppKit rendering) plus app-layer glue. The feature is toggled per-workspace and preserves the existing bonsplit tree so switching back to splits restores the original layout exactly.

  • Architecture: CanvasLayout (value type, Codable) owns geometry and z-order; CanvasModel (@MainActor class) is the durable workspace-owned state bridge; CanvasRootView is the AppKit root that manages pane views, drag/resize sessions, viewport animations, and the explicit offscreen lifecycle. All canvas entrypoints (shortcuts, command palette, View menu, socket domain, CLI) share a single CanvasActionExecutor.
  • Integration: Session persistence snapshots canvas frames in z-order and remaps them on restore; mode is restored directly (bypassing seedFromSplitFrames) so saved frames survive restarts. The canvas.* socket domain adds six verbs wired through the existing ControlCommandCoordinator pattern.

Confidence Score: 4/5

Well-isolated feature addition with clean package boundaries; three open issues from prior review threads remain unresolved and should be addressed before shipping.

The canvas geometry engine, model, and rendering are sound and follow existing codebase patterns. The three open issues from previous threads are the only defects keeping this from a clean merge: missing canvas/canvasBlurb keys cause broken section headers for 17 locales, the API error body exposes an internal class name to socket callers, and a dead restore path is a maintenance hazard. No new issues were found in this pass.

web/messages/ locale files (missing canvas/canvasBlurb keys in 17 locales), Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasModel.swift (dead restore(panes:) method), Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCommandCoordinator+Canvas.swift (TabManager name in error string)

Important Files Changed

Filename Overview
Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasModel.swift @mainactor model bridging geometry to the host. Contains dead code: restore(panes:) is an unreachable duplicate of restoreFrames(_:) (flagged in previous thread).
Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasRootView.swift 534-line AppKit root view; owns scroll view, pane lifecycle, drag sessions, command-scroll monitor, and viewport animation. NotificationCenter observer correctly uses MainActor.assumeIsolated for the always-main-thread boundsDidChangeNotification. Monitor lifecycle is guarded in teardown and viewDidMoveToWindow.
Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasPaneView.swift AppKit pane view with corner-aware resize bands and 2pt drag-activation threshold. Title-bar drag works via responder-chain propagation since SwiftUI content deliberately does not consume mouse-down events.
Sources/Canvas/Workspace+CanvasLayout.swift Canvas extension on Workspace: mode switching, split-seeding, spatial focus, persistence helpers. Correctly skips seedFromSplitFrames on restore to preserve saved frames.
Sources/Canvas/WorkspaceCanvasHostView.swift SwiftUI/AppKit bridge; descriptors are recomputed on every workspace ObservableObject publish but sync() is the sole path into the canvas view, correctly following the existing workspace observation pattern.
Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCommandCoordinator+Canvas.swift Six canvas socket verbs correctly routed. The tabManagerUnavailable error case leaks the internal class name TabManager in the API response body (flagged in previous thread).
Sources/SessionPersistence.swift Adds SessionCanvasPaneSnapshot (Codable, Sendable) and two optional fields to SessionWorkspaceSnapshot. Default nil values ensure backward compatibility with pre-canvas snapshots.
Sources/TerminalController+ControlCanvasContext.swift Implements ControlCanvasContext for all six socket verbs. Routes mutations through CanvasActionExecutor to share one execution path with shortcuts. Workspace resolution correctly handles workspace_id, surface_id, and selected tab fallbacks.
web/data/cmux-shortcuts.ts New canvas category with titleKey/blurbKey references next-intl keys that are absent from 17 locale files (flagged in previous thread). Shortcut descriptions follow existing en+ja-only inline pattern.
Resources/Localizable.xcstrings 16 new canvas string keys present but all omit the km (Khmer) locale, which is included throughout the rest of the catalog (flagged in previous outside-diff comment).

Sequence Diagram

sequenceDiagram
    participant User
    participant KBShortcut as Keyboard/Palette/Menu/Socket
    participant Executor as CanvasActionExecutor
    participant Workspace
    participant CanvasModel
    participant Viewport as CanvasRootView (Viewport)

    User->>KBShortcut: ctrl+cmd+C / canvas.set_mode
    KBShortcut->>Executor: perform(.toggleLayout)
    Executor->>Workspace: toggleCanvasLayout()
    Workspace->>Workspace: seedFromSplitFrames()
    Workspace->>CanvasModel: syncPanes(panelIds:focusedPanelId:)
    Workspace-->>Workspace: "layoutMode = .canvas"

    User->>KBShortcut: drag pane / canvas.set_frame
    KBShortcut->>Viewport: paneView(draggedTo:)
    Viewport->>CanvasModel: snapForMove(proposed:)
    CanvasModel-->>Viewport: CanvasSnapResult(frame, guides)
    Viewport->>CanvasModel: setFrame(_:for:)
    Viewport->>Workspace: noteCanvasLayoutChanged()

    User->>KBShortcut: ctrl+cmd+T / canvas.align(tidy)
    KBShortcut->>Executor: perform(.alignment(.tidy))
    Executor->>CanvasModel: applyAlignment(tidy, to:[], reference:)
    CanvasModel->>CanvasModel: CanvasAligner.tidiedFrames()
    CanvasModel-->>Executor: "changed=true"
    Executor->>Viewport: modelDidChangeExternally(animated: true)

    Workspace->>Workspace: sessionSnapshot()
    Workspace->>CanvasModel: canvasSessionPaneSnapshots()
    CanvasModel-->>Workspace: [SessionCanvasPaneSnapshot]
Loading

Reviews (5): Last reviewed commit: "Canvas: fix stranded/blank browser webvi..." | Re-trigger Greptile

Comment thread web/messages/en.json
Comment on lines 1037 to 1038
"surfacesBlurb": "Surfaces are tabs inside a pane.",
"splitPanes": "Split Panes",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Missing canvas keys in 17 locale files

canvas and canvasBlurb are added only to en.json and ja.json, but the shortcuts page uses titleKey: "canvas" and blurbKey: "canvasBlurb" to render the new canvas category header. The other 17 locale files — ar, bs, da, de, es, fr, it, ko, no, pl, pt-BR, ru, th, tr, uk, zh-CN, zh-TW — are all missing these keys. Users with any of those locales will see a broken or missing section title and blurb on the shortcuts page.

Rule Used: Flag production user-facing text that is not fully... (source)

Comment on lines +163 to +170

/// The neighboring pane in a spatial direction from the given pane.
func pane(_ direction: CanvasDirection, from panelId: UUID) -> UUID? {
CanvasSpatialNavigator()
.pane(direction, from: CanvasPaneID(rawValue: panelId), in: layout)?
.rawValue
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Dead code: restore(panes:) is an exact duplicate of restoreFrames(_:)

Both methods build a CanvasLayout from an ordered (id, frame) list and increment revision. Only restoreFrames is called (from Workspace+CanvasLayout.swift's restoreCanvasState); restore(panes:) is never invoked. Two live restore paths leave room for callers to use the wrong one later and silently diverge in behaviour.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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

🤖 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 `@Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift`:
- Around line 355-366: The new canvas shortcut labels (cases like
toggleCanvasLayout, canvasRevealFocusedPane, canvasOverview, canvasTidy,
canvasAlignLeft, canvasAlignRight, canvasAlignTop, canvasAlignBottom,
canvasEqualizeWidths, canvasEqualizeHeights, canvasDistributeHorizontally,
canvasDistributeVertically in ShortcutAction) are hardcoded English strings;
replace each return value with a localized lookup (e.g., use String(localized:
"SHORTCUT_<IDENTIFIER>", defaultValue: "…", comment: "…") or your project’s
localization API) and add corresponding keys and translations to
Resources/Localizable.xcstrings for all supported locales so every new
user-facing label is localized.

In `@Sources/AppDelegate.swift`:
- Around line 13557-13568: The canvas shortcut display names are missing
localization keys; add string-catalog entries for each canvas action key named
like shortcutAction.toggleCanvasLayout, shortcutAction.canvasRevealFocusedPane,
shortcutAction.canvasOverview, shortcutAction.canvasTidy,
shortcutAction.canvasAlignLeft, shortcutAction.canvasAlignRight,
shortcutAction.canvasAlignTop, shortcutAction.canvasAlignBottom,
shortcutAction.canvasEqualizeWidths, shortcutAction.canvasEqualizeHeights,
shortcutAction.canvasDistributeHorizontally, and
shortcutAction.canvasDistributeVertically in Resources/Localizable.xcstrings
(use an English defaultValue for each), and ensure the same keys are present in
every supported locale so UI code that looks up names for
KeyboardShortcutSettings.Action.canvasActions / matchConfiguredShortcut and
CanvasActionExecutor(canvasAction: ...) will find localized strings.

In `@Sources/Canvas/CanvasDocumentView.swift`:
- Around line 34-59: The grid start-position calculation in draw(_:) is
overcomplicated: replace the phase and manual floor+while-adjust logic that
computes x and y starts (currently using canvasToDocumentOffset,
Self.gridSpacing, Self.gridDotRadius and the while x < dirtyRect.minX / while y
< dirtyRect.minY adjustments) with direct startX/startY computations using the
given phaseX/phaseY and spacing (compute the first grid coordinate via rounding
up the division then multiplying by spacing and adding phase), remove the extra
while loops that bump the start into the rect, and keep the existing nested
loops that advance by spacing and draw the dots.

In `@Sources/Canvas/CanvasPaneContent.swift`:
- Around line 52-53: The terminal visibility lifecycle is inverted and
asymmetric: change the occlusion call in the rendering mapping to use the
inverse (e.g., hostedView.setOcclusion(!rendering) or setOcclusion(isOccluded)
where isOccluded = !rendering) so rendering==true means not occluded, and add
symmetric teardown to restore UI visibility by calling
hostedView.setVisibleInUI(false) on unmount/teardown (mirror the existing
hostedView.setVisibleInUI(true) on mount). Also ensure visibility transitions
explicitly pass through a hide-state (call setVisibleInUI(false) before changing
occlusion/visibility) to preserve scroll/visibility behavior; update the
handlers referenced (hostedView.setVisibleInUI, hostedView.setOcclusion, and the
mount/unmount/teardown block that installs hostedView.setFocusHandler)
accordingly.

In `@Sources/Canvas/ContentView`+CanvasCommandPalette.swift:
- Around line 22-39: The command-palette shows hard-coded English because canvas
action titles are returned as plain literals and the subtitle is missing the km
locale; update ShortcutAction.label to return localized strings (use
String(localized:) or NSLocalizedString) for all canvas-related cases
(toggleCanvasLayout, canvasRevealFocusedPane, canvasOverview, canvasTidy, and
all align/equalize/distribute variants) and ensure
commandPaletteCanvasCommandContributions() continues to use command.action.label
(now localized); also add the "command.canvas.subtitle" translation to the km
locale in Resources/Localizable.xcstrings so the subtitle resolves for that
locale.

In `@Sources/KeyboardShortcutSettings.swift`:
- Around line 244-255: Add Khmer ("km") translations for the new canvas shortcut
labels by updating Resources/Localizable.xcstrings to include entries for each
localization key used in KeyboardShortcutSettings
(shortcut.toggleCanvasLayout.label, shortcut.canvasRevealFocusedPane.label,
shortcut.canvasOverview.label, shortcut.canvasTidy.label,
shortcut.canvasAlignLeft.label, shortcut.canvasAlignRight.label,
shortcut.canvasAlignTop.label, shortcut.canvasAlignBottom.label,
shortcut.canvasEqualizeWidths.label, shortcut.canvasEqualizeHeights.label,
shortcut.canvasDistributeHorizontally.label,
shortcut.canvasDistributeVertically.label), providing appropriate Khmer strings
for each key so the km locale mirrors the existing default/other locales.

In `@Sources/TerminalController`+ControlCanvasContext.swift:
- Around line 95-111: controlCanvasReveal currently returns
.paneNotFound(UUID()) when both surfaceID and ws.focusedPanelId are nil,
producing a spurious random UUID; add a new resolution case (e.g.,
.noTargetSpecified) to ControlCanvasActionResolution and return that from
controlCanvasReveal when the target cannot be resolved (inside the guard let
target ... else branch), then update any downstream handling/serialization of
ControlCanvasActionResolution to recognize the new .noTargetSpecified case (also
adjust any protocol/wire mapping or tests that assert on paneNotFound behavior).

In `@Sources/Workspace.swift`:
- Around line 16911-16914: The test suite never exercises the canvas-specific
branch because Workspace.layoutMode defaults to .splits; add a unit test in
WorkspaceUnitTests that sets workspace.layoutMode = .canvas (use
WorkspaceLayoutMode.canvas), then call workspace.moveFocus(...) with the same
direction used in existing tests and assert the expected canvas-focus change (or
spy that moveCanvasFocus(direction:) on the Workspace is invoked) to cover the
early-return routing to moveCanvasFocus; reference the public API moveFocus, the
internal moveCanvasFocus method, and the layoutMode property when implementing
the test.

In `@Sources/WorkspaceContentView.swift`:
- Around line 359-372: The canvas branch is skipping the lifecycle/effect
modifiers that are currently attached only to bonsplitView; move the shared
.onAppear/.onChange/.onReceive/.onDisappear (and any other workspace lifecycle
side-effect modifiers) from bonsplitView up to the enclosing Group (or its
parent wrapper) so both WorkspaceCanvasHostView and bonsplitView execute the
same hooks; keep the existing .ignoresSafeArea(...) call and its
isMinimalMode/isFullScreen conditional, and ensure the moved modifiers still
reference workspace, appearance, isWorkspaceVisible, isWorkspaceInputActive, and
workspacePortalPriority as needed.

In `@web/data/cmux-shortcuts.ts`:
- Around line 180-190: The new shortcut category in web/data/cmux-shortcuts.ts
uses titleKey: "canvas" and blurbKey: "canvasBlurb" but those i18n keys are only
present in en.json and ja.json; add docs.keyboardShortcuts.cat.canvas and
docs.keyboardShortcuts.cat.canvasBlurb entries to all missing locale files (de,
fr, es, ko, zh-CN, zh-TW, pt-BR, ru, it, pl, uk, tr, da, no) with appropriate
translated strings (or fallback English strings) so every locale contains the
same keys referenced by the Canvas category.

In `@web/messages/ja.json`:
- Around line 997-998: Add the missing localization keys "canvas" and
"canvasBlurb" to every locale JSON listed in web/i18n/routing.ts so all
supported locales match the en/ja set; locate each locale file under
web/messages/ (e.g., the other .json files beside ja.json), add the two keys
with appropriate translations (or temporary English fallbacks) mirroring the
meaning in ja.json, and ensure the keys are valid JSON and placed consistently
with nearby UI strings to keep parity across locales.
🪄 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: 073d3575-ebd0-4388-85f0-1438ee85116d

📥 Commits

Reviewing files that changed from the base of the PR and between aeb8847 and ad98c1e.

📒 Files selected for processing (72)
  • CLI/cmux.swift
  • Packages/CmuxCanvas/Package.swift
  • Packages/CmuxCanvas/Sources/CmuxCanvas/CanvasAligner.swift
  • Packages/CmuxCanvas/Sources/CmuxCanvas/CanvasAlignmentCommand.swift
  • Packages/CmuxCanvas/Sources/CmuxCanvas/CanvasDirection.swift
  • Packages/CmuxCanvas/Sources/CmuxCanvas/CanvasGuide.swift
  • Packages/CmuxCanvas/Sources/CmuxCanvas/CanvasLayout.swift
  • Packages/CmuxCanvas/Sources/CmuxCanvas/CanvasMetrics.swift
  • Packages/CmuxCanvas/Sources/CmuxCanvas/CanvasPane.swift
  • Packages/CmuxCanvas/Sources/CmuxCanvas/CanvasPaneID.swift
  • Packages/CmuxCanvas/Sources/CmuxCanvas/CanvasPlacer.swift
  • Packages/CmuxCanvas/Sources/CmuxCanvas/CanvasPoint.swift
  • Packages/CmuxCanvas/Sources/CmuxCanvas/CanvasRect.swift
  • Packages/CmuxCanvas/Sources/CmuxCanvas/CanvasResizeEdges.swift
  • Packages/CmuxCanvas/Sources/CmuxCanvas/CanvasSize.swift
  • Packages/CmuxCanvas/Sources/CmuxCanvas/CanvasSnapEngine.swift
  • Packages/CmuxCanvas/Sources/CmuxCanvas/CanvasSnapResult.swift
  • Packages/CmuxCanvas/Sources/CmuxCanvas/CanvasSpatialNavigator.swift
  • Packages/CmuxCanvas/Sources/CmuxCanvas/CanvasViewportMath.swift
  • Packages/CmuxCanvas/Tests/CmuxCanvasTests/CanvasAlignerTests.swift
  • Packages/CmuxCanvas/Tests/CmuxCanvasTests/CanvasLayoutTests.swift
  • Packages/CmuxCanvas/Tests/CmuxCanvasTests/CanvasPlacerTests.swift
  • Packages/CmuxCanvas/Tests/CmuxCanvasTests/CanvasRectTests.swift
  • Packages/CmuxCanvas/Tests/CmuxCanvasTests/CanvasSnapEngineTests.swift
  • Packages/CmuxCanvas/Tests/CmuxCanvasTests/CanvasSpatialNavigatorTests.swift
  • Packages/CmuxCanvas/Tests/CmuxCanvasTests/CanvasViewportMathTests.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCanvasActionResolution.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCanvasAlignCommand.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCanvasContext.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCanvasFrame.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCanvasInfoSnapshot.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCanvasPaneSummary.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCommandCoordinator+Canvas.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/ControlCommandContext.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/ControlCommandCoordinator.swift
  • Packages/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift
  • Packages/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorCanvasTests.swift
  • Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift
  • Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/Canvas/CanvasAction.swift
  • Sources/Canvas/CanvasDocumentView.swift
  • Sources/Canvas/CanvasGeometryBridging.swift
  • Sources/Canvas/CanvasGuidesView.swift
  • Sources/Canvas/CanvasHostedPanelContentView.swift
  • Sources/Canvas/CanvasLayoutSettings.swift
  • Sources/Canvas/CanvasPaneContent.swift
  • Sources/Canvas/CanvasPaneDescriptor.swift
  • Sources/Canvas/CanvasPaneHitRegion.swift
  • Sources/Canvas/CanvasPaneTitleBarView.swift
  • Sources/Canvas/CanvasPaneView.swift
  • Sources/Canvas/CanvasRootView.swift
  • Sources/Canvas/CanvasScrollView.swift
  • Sources/Canvas/CanvasViewportControlling.swift
  • Sources/Canvas/ContentView+CanvasCommandPalette.swift
  • Sources/Canvas/Workspace+CanvasLayout.swift
  • Sources/Canvas/WorkspaceCanvasHostView.swift
  • Sources/Canvas/WorkspaceCanvasModel.swift
  • Sources/Canvas/WorkspaceLayoutMode.swift
  • Sources/ContentView.swift
  • Sources/KeyboardShortcutSettings.swift
  • Sources/SessionPersistence.swift
  • Sources/TerminalController+ControlCanvasContext.swift
  • Sources/Workspace.swift
  • Sources/WorkspaceContentView.swift
  • Sources/cmuxApp.swift
  • cmux.xcodeproj/project.pbxproj
  • docs/canvas-layout-design.md
  • web/data/cmux-shortcuts.ts
  • web/messages/en.json
  • web/messages/ja.json

Comment thread Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift Outdated
Comment thread Sources/AppDelegate.swift
Comment on lines +34 to +59
override func draw(_ dirtyRect: NSRect) {
GhosttyBackgroundTheme.currentColor().withSystemEffect(.none).setFill()
dirtyRect.fill()

// Dots are aligned to canvas space so they stay put when the document
// re-centers around content.
let spacing = Self.gridSpacing
let radius = Self.gridDotRadius
NSColor.tertiaryLabelColor.withAlphaComponent(0.18).setFill()
let phaseX = canvasToDocumentOffset.x.truncatingRemainder(dividingBy: spacing)
let phaseY = canvasToDocumentOffset.y.truncatingRemainder(dividingBy: spacing)
var x = (dirtyRect.minX - phaseX).rounded(.down) - (dirtyRect.minX - phaseX)
.truncatingRemainder(dividingBy: spacing) + phaseX
while x < dirtyRect.minX { x += spacing }
while x <= dirtyRect.maxX {
var y = (dirtyRect.minY - phaseY).rounded(.down) - (dirtyRect.minY - phaseY)
.truncatingRemainder(dividingBy: spacing) + phaseY
while y < dirtyRect.minY { y += spacing }
while y <= dirtyRect.maxY {
let dot = CGRect(x: x - radius, y: y - radius, width: radius * 2, height: radius * 2)
NSBezierPath(ovalIn: dot).fill()
y += spacing
}
x += spacing
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Grid rendering logic is correct but could be simplified.

The nested loops correctly render the dot grid aligned to canvas space. The phase calculation (lines 43-46, 49-50) works but is more complex than necessary. The current approach computes the floor of the starting position then advances with a while loop.

Consider simplifying to:

let startX = (((dirtyRect.minX - phaseX) / spacing).rounded(.up) * spacing) + phaseX
let startY = (((dirtyRect.minY - phaseY) / spacing).rounded(.up) * spacing) + phaseY

This directly computes the first grid position without the while x < dirtyRect.minX adjustment loop, making the intent clearer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Canvas/CanvasDocumentView.swift` around lines 34 - 59, The grid
start-position calculation in draw(_:) is overcomplicated: replace the phase and
manual floor+while-adjust logic that computes x and y starts (currently using
canvasToDocumentOffset, Self.gridSpacing, Self.gridDotRadius and the while x <
dirtyRect.minX / while y < dirtyRect.minY adjustments) with direct startX/startY
computations using the given phaseX/phaseY and spacing (compute the first grid
coordinate via rounding up the division then multiplying by spacing and adding
phase), remove the extra while loops that bump the start into the rect, and keep
the existing nested loops that advance by spacing and draw the dots.

Comment thread Sources/Canvas/CanvasPaneContent.swift
Comment thread Sources/Canvas/ContentView+CanvasCommandPalette.swift
Comment thread Sources/TerminalController+ControlCanvasContext.swift
Comment thread Sources/Workspace.swift
Comment on lines +359 to +372
Group {
if workspace.layoutMode == .canvas {
WorkspaceCanvasHostView(
workspace: workspace,
isWorkspaceVisible: isWorkspaceVisible,
isWorkspaceInputActive: isWorkspaceInputActive,
portalPriority: workspacePortalPriority,
appearance: appearance
)
} else {
bonsplitView
}
}
.ignoresSafeArea(.container, edges: (isMinimalMode && !isFullScreen) ? .top : [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Canvas branch bypasses workspace lifecycle/effect hooks.

At Line 360, rendering WorkspaceCanvasHostView skips the .onAppear/.onChange/.onReceive/.onDisappear chain currently attached to bonsplitView, so canvas mode misses theme-refresh and presentation-state synchronization side effects. Move shared side-effect modifiers to the common Group (or a parent wrapper) so both layout modes execute the same workspace lifecycle logic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/WorkspaceContentView.swift` around lines 359 - 372, The canvas branch
is skipping the lifecycle/effect modifiers that are currently attached only to
bonsplitView; move the shared .onAppear/.onChange/.onReceive/.onDisappear (and
any other workspace lifecycle side-effect modifiers) from bonsplitView up to the
enclosing Group (or its parent wrapper) so both WorkspaceCanvasHostView and
bonsplitView execute the same hooks; keep the existing .ignoresSafeArea(...)
call and its isMinimalMode/isFullScreen conditional, and ensure the moved
modifiers still reference workspace, appearance, isWorkspaceVisible,
isWorkspaceInputActive, and workspacePortalPriority as needed.

Comment thread web/data/cmux-shortcuts.ts
Comment thread web/messages/ja.json
Sources/Canvas kept only app glue (Workspace integration, panel content
mounts, palette/menu/socket surfaces). The package owns the scroll view,
document, pane chrome, drag/resize sessions, lifecycle, and CanvasModel,
behind three seams: CanvasPaneContentMounting (panel content), CanvasTheme
(colors), and pre-localized CanvasPaneChrome strings. 7 CanvasModel tests
cover sync/seed/restore/z-order/alignment.
Comment on lines +139 to +143
let routing = routingSelectors(params)
let resolution = context?.controlCanvasToggleOverview(routing: routing)
?? .tabManagerUnavailable
return canvasActionResult(resolution)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Internal class name leaks into socket API error body

"TabManager not available" exposes TabManager, an internal cmux class name, in the message field of the canvas.* API response that cmux canvas CLI commands surface to callers. Per the user-facing error rules, API error bodies must describe the failure in product terms rather than implementation terms. The concrete failure a caller experiences is that the cmux session is not yet ready, not that a specific internal service class is unavailable.

Replace with copy that describes the situation in user-visible terms, e.g. "cmux session not available" or "canvas control is unavailable — try again once a workspace is open".

Rule Used: Flag Swift fixes that patch symptoms while leaving... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

- web shortcut docs: canvas category title/blurb added to all 18 remaining
  locale catalogs (greptile P1)
- package ShortcutAction canvas display names localized via the existing
  shortcut.*.label xcstrings keys (coderabbit)
- canvas.reveal with no surface and no focused panel returns a dedicated
  invalid_state error instead of minting a random UUID (coderabbit)
- unit test covering canvas-mode moveFocus spatial routing (coderabbit)

Not changed: setOcclusion polarity — the parameter is named 'visible'
(ghostty_surface_set_occlusion(surface, visible)), so setRendering maps
correctly; reviewer inferred inverted semantics from the method name.

@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

Caution

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

⚠️ Outside diff range comments (1)
Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasModel.swift (1)

54-62: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid per-pane full layout rescans during batch pane sync.

At Line 61, layout.panes.map(\.frame) is rebuilt for each newly added pane. In bulk syncs this creates repeated full-collection scans in a hot batch path.

Proposed fix
     var added: [UUID] = []
     let placer = CanvasPlacer(metrics: metrics)
+    var occupiedFrames = layout.panes.map(\.frame)
     for panelId in panelIds where !layout.contains(CanvasPaneID(rawValue: panelId)) {
         let anchor = focusedPanelId
             .flatMap { layout.frame(of: CanvasPaneID(rawValue: $0)) }
             ?? layout.panes.last?.frame
         let frame = placer.frameForNewPane(
             size: Self.defaultPaneSize,
             near: anchor,
-            avoiding: layout.panes.map(\.frame)
+            avoiding: occupiedFrames
         )
-        layout.add(CanvasPane(id: CanvasPaneID(rawValue: panelId), frame: frame))
+        let pane = CanvasPane(id: CanvasPaneID(rawValue: panelId), frame: frame)
+        layout.add(pane)
+        occupiedFrames.append(pane.frame)
         added.append(panelId)
         changed = true
     }

As per coding guidelines, batch operations in production paths should avoid per-target rescans of full collections.

🤖 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 `@Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasModel.swift` around lines 54
- 62, The loop over panelIds rebuilds layout.panes.map(\.frame) for every new
pane causing repeated full scans; compute the avoiding frames once before the
loop (e.g., let avoidingFrames = layout.panes.map(\.frame)) and use that when
calling placer.frameForNewPane, then when you successfully create a new pane
append its frame to the avoidingFrames collection so subsequent iterations avoid
rescanning layout.panes; update references: the for panelId in panelIds where
!layout.contains(...), placer.frameForNewPane(size: Self.defaultPaneSize, near:
anchor, avoiding: ...), and ensure focusedPanelId/layout.frame(of:) logic
remains unchanged while using the cached avoidingFrames.

Source: Coding guidelines

♻️ Duplicate comments (1)
Sources/Canvas/CanvasPaneContent.swift (1)

81-98: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix occlusion polarity and restore symmetric terminal visibility teardown.

Line 84 currently occludes when rendering == true, which inverts the documented lifecycle. Also, unmount never restores setVisibleInUI(false) after mount set it to true, so terminal hide-state callbacks can be skipped.

Suggested fix
 func setRendering(_ rendering: Bool) {
     switch content {
     case .terminal(let panel):
-        panel.surface.setOcclusion(rendering)
+        panel.surface.setOcclusion(!rendering)
     case .hosted:
         break
     }
 }
@@
     case .terminal(let panel):
         let hostedView = panel.hostedView
         hostedView.setFocusHandler(nil)
+        hostedView.setVisibleInUI(false)
         panel.surface.setOcclusion(true)
         hostedView.removeFromSuperview()

Based on learnings: terminal visibility transitions in this repo should pass through explicit hide-state handling to preserve expected scroll/visibility behavior across UI transitions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Canvas/CanvasPaneContent.swift` around lines 81 - 98, Invert the
occlusion polarity in setRendering(_ rendering: Bool) by calling
panel.surface.setOcclusion(!rendering) so that rendering == true => not
occluded, and in unmount() restore the terminal's UI-visible state symmetrically
by calling hostedView.setVisibleInUI(false) before tearing down (after
hostedView.setFocusHandler(nil) and before hostedView.removeFromSuperview());
keep the existing panel.surface.setOcclusion(true) call or change it to match
the inverted polarity if needed so occlusion and visible flags remain
consistent.

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 `@cmux.xcodeproj/project.pbxproj`:
- Around line 4271-4274: The XCSwiftPackageProductDependency entry for
CA53A005CA53A005CA53A005 (productName = CmuxCanvasUI) is missing its required
package pointer; update that block to include a package = <PACKAGE_REF_ID> /*
<PackageName> */; entry that points to the corresponding XCSwiftPackageReference
used elsewhere in the project (find the XCSwiftPackageReference whose
products/include CmuxCanvasUI or whose comment matches the package), so the
XCSwiftPackageProductDependency for CmuxCanvasUI correctly references the
package ID.

---

Outside diff comments:
In `@Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasModel.swift`:
- Around line 54-62: The loop over panelIds rebuilds layout.panes.map(\.frame)
for every new pane causing repeated full scans; compute the avoiding frames once
before the loop (e.g., let avoidingFrames = layout.panes.map(\.frame)) and use
that when calling placer.frameForNewPane, then when you successfully create a
new pane append its frame to the avoidingFrames collection so subsequent
iterations avoid rescanning layout.panes; update references: the for panelId in
panelIds where !layout.contains(...), placer.frameForNewPane(size:
Self.defaultPaneSize, near: anchor, avoiding: ...), and ensure
focusedPanelId/layout.frame(of:) logic remains unchanged while using the cached
avoidingFrames.

---

Duplicate comments:
In `@Sources/Canvas/CanvasPaneContent.swift`:
- Around line 81-98: Invert the occlusion polarity in setRendering(_ rendering:
Bool) by calling panel.surface.setOcclusion(!rendering) so that rendering ==
true => not occluded, and in unmount() restore the terminal's UI-visible state
symmetrically by calling hostedView.setVisibleInUI(false) before tearing down
(after hostedView.setFocusHandler(nil) and before
hostedView.removeFromSuperview()); keep the existing
panel.surface.setOcclusion(true) call or change it to match the inverted
polarity if needed so occlusion and visible flags remain consistent.
🪄 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: b6ce8f5f-86aa-484a-bdff-eafb969782a9

📥 Commits

Reviewing files that changed from the base of the PR and between ad98c1e and 8b5f824.

📒 Files selected for processing (24)
  • Packages/CmuxCanvasUI/Package.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasDocumentView.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasGeometryBridging.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasGuidesView.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasHostCallbacks.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasModel.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasPaneChrome.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasPaneContentMounting.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasPaneDescriptor.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasPaneHitRegion.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasPaneTitleBarView.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasPaneView.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasRootView.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasScrollView.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasTheme.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasViewportControlling.swift
  • Packages/CmuxCanvasUI/Tests/CmuxCanvasUITests/CanvasModelTests.swift
  • Sources/Canvas/CanvasAction.swift
  • Sources/Canvas/CanvasPaneContent.swift
  • Sources/Canvas/Workspace+CanvasLayout.swift
  • Sources/Canvas/WorkspaceCanvasHostView.swift
  • Sources/TerminalController+ControlCanvasContext.swift
  • Sources/Workspace.swift
  • cmux.xcodeproj/project.pbxproj
💤 Files with no reviewable changes (3)
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasPaneHitRegion.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasGuidesView.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasGeometryBridging.swift

Comment on lines +4271 to +4274
CA53A005CA53A005CA53A005 /* CmuxCanvasUI */ = {
isa = XCSwiftPackageProductDependency;
productName = CmuxCanvasUI;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re, sys

text = Path("cmux.xcodeproj/project.pbxproj").read_text()

m = re.search(r'CA53A005CA53A005CA53A005 /\* CmuxCanvasUI \*/ = \{(.*?)\n\t\t\};', text, re.S)
if not m:
    print("FAIL: CmuxCanvasUI product dependency block not found.")
    sys.exit(1)

block = m.group(1)
print("CmuxCanvasUI dependency block:\n")
print(block.strip(), "\n")

if "package = CA53A004CA53A004CA53A004" in block:
    print("OK: package linkage is present.")
else:
    print("FAIL: package linkage is missing.")
    sys.exit(1)
PY

Repository: manaflow-ai/cmux

Length of output: 198


Link CmuxCanvasUI product dependency to its package reference in cmux.xcodeproj/project.pbxproj

CA53A005CA53A005CA53A005 /* CmuxCanvasUI */ is missing the required package = ... pointer in the XCSwiftPackageProductDependency block (lines 4271-4274), unlike other local Swift package product wiring.

🔧 Proposed fix
 		CA53A005CA53A005CA53A005 /* CmuxCanvasUI */ = {
 			isa = XCSwiftPackageProductDependency;
+			package = CA53A004CA53A004CA53A004 /* XCLocalSwiftPackageReference "CmuxCanvasUI" */;
 			productName = CmuxCanvasUI;
 		};
🤖 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 `@cmux.xcodeproj/project.pbxproj` around lines 4271 - 4274, The
XCSwiftPackageProductDependency entry for CA53A005CA53A005CA53A005 (productName
= CmuxCanvasUI) is missing its required package pointer; update that block to
include a package = <PACKAGE_REF_ID> /* <PackageName> */; entry that points to
the corresponding XCSwiftPackageReference used elsewhere in the project (find
the XCSwiftPackageReference whose products/include CmuxCanvasUI or whose comment
matches the package), so the XCSwiftPackageProductDependency for CmuxCanvasUI
correctly references the package ID.

Source: Coding guidelines

Cmd+Scroll routes scroll events to the canvas via a local event monitor
regardless of what is under the cursor, so terminal scrollback no longer
interrupts panning. Zoom in/out (1.25x steps) and actual-size, anchored
at the viewport center, through the full shared pipeline: shortcuts
(opt+cmd+= / opt+cmd+- / opt+cmd+0), palette, canvas.zoom socket verb,
cmux canvas zoom CLI, ShortcutAction mirror, 19-locale labels, web docs.

@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

🤖 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/TerminalController`+ControlCanvasContext.swift:
- Around line 134-141: Update the switch on the direction string so only
recognized values mutate state: add an explicit case "reset" that calls
executor.perform(.zoomReset) and handle unknown values by returning a non-OK
resolution (do not call executor.perform). Keep the existing cases for "in" ->
.zoomIn and "out" -> .zoomOut, reference the direction variable and
executor.perform(.zoomIn/.zoomOut/.zoomReset) when making the changes, and
ensure the code returns a failure/non-OK result for the default branch instead
of silently resetting.
🪄 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: 2c8b3790-b835-40eb-904f-d5b96c3b09bb

📥 Commits

Reviewing files that changed from the base of the PR and between bff5d90 and 2806fd8.

📒 Files selected for processing (15)
  • CLI/cmux.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasRootView.swift
  • Packages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasViewportControlling.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCanvasContext.swift
  • Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCommandCoordinator+Canvas.swift
  • Packages/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift
  • Packages/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorCanvasTests.swift
  • Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift
  • Packages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift
  • Resources/Localizable.xcstrings
  • Sources/Canvas/CanvasAction.swift
  • Sources/Canvas/ContentView+CanvasCommandPalette.swift
  • Sources/KeyboardShortcutSettings.swift
  • Sources/TerminalController+ControlCanvasContext.swift
  • web/data/cmux-shortcuts.ts

Comment thread Sources/TerminalController+ControlCanvasContext.swift
Two root causes. (1) The window portal only re-synced webview geometry
on split-layout events; canvas scrolls/zooms/drags moved anchors with no
such event, leaving webviews at stale window coordinates. CanvasRootView
now fires a coalesced onViewportGeometryChanged callback from every
geometry path (scroll, zoom, drag frame, layout, animated alignment) and
the host nudges BrowserWindowPortalRegistry.scheduleExternalGeometry-
Synchronize. (2) Workspace.renderedVisiblePanelIdsForCurrentLayout was
purely bonsplit-based, so focusing another canvas pane deselected the
browser's bonsplit tab and force-hid its webview (visibleInUI=0, never
restored). Canvas mode now reports every panel as rendered (the portal's
frame clamp handles offscreen), and setLayoutMode reconciles portal
visibility immediately in both directions.

Verified: reveal/tidy/overview round-trips keep webview content glued to
its pane; splits mode unaffected.
canvas.paneGap (0-64, default 16) and canvas.snappingEnabled join the
settings catalog (CanvasCatalogSection), the Settings UI (App section:
stepper + toggle, localized for 19 locales), cmux.json (supported paths,
web schema + en/ja schema descriptions). CanvasLayoutSettings accepts
any numeric UserDefaults representation since the catalog writes Int.
Verified live: gap=40 changes tidy pitch to 680/460.

canvas.zoom direction crosses the seam as a typed
ControlCanvasZoomDirection so the app witness switch is exhaustive
(review finding: unknown strings previously fell through to reset).
CanvasPane now hosts ordered panelIds + selectedPanelId (CanvasPanelID
distinct from CanvasPaneID; single-tab panes reuse the founding panel's
UUID so existing behavior and persistence stay identical). CanvasLayout
gains tab operations: pane(containing:), addPanel (join/move primitive),
removePanel (pane dies with last tab, selection moves to neighbor),
breakOutPanel, selectPanel. CanvasModel resolves all panel-keyed APIs
through hosting panes (frame/setFrame/bringToFront/snap exclusion/
spatial focus -> neighbor's selected tab) and adds joinPanel/breakOut-
Panel/selectPanel. Session snapshots carry panelIds + selectedPanelId
(pre-tab snapshots decode as single-tab panes; pane identity follows the
first surviving panel through the restore id remap).

12 new tests across both packages; single-tab canvas behavior verified
unchanged on the tagged build. UI tab strip + join/break surfaces come
next.
Pane views are now keyed by CanvasPaneID and host a SwiftUI tab strip
(single tab reads as a title bar; multiple tabs render selectable pills
with hover close). One content mount per pane — exactly the selected
tab — switching unmounts/remounts through the existing seam. The root
reconciles panes+mounts+chrome from cached descriptors on external
model mutations, so socket verbs update the UI without a SwiftUI pass.
focusPanel selects the tab in canvas mode so focus and visibility never
diverge. New verbs: canvas.join {surface,target_surface}, canvas.break,
canvas.select_tab; CLI: cmux canvas join/break/select-tab; 2 new
coordinator tests (146 green).

Preflight on tagged build: join renders a two-tab strip, select-tab
switches mounted terminals (scrollback intact), break returns the tab
to its own pane.
# Conflicts:
#	.github/swift-file-length-budget.tsv
# Conflicts:
#	.github/swift-file-length-budget.tsv
#	Sources/Workspace.swift
#	cmux.xcodeproj/project.pbxproj
azooz2003-bit and others added 3 commits June 13, 2026 15:33
Aziz preference: the one-time pill teaching Cmd+scroll-pans reads better
anchored near the top of the canvas. Switch the pill constraint from
bottomAnchor (-24) to topAnchor (+24). Scale+fade animation is
non-directional so top placement needs no other change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three tab-drag improvements from Aziz dogfood (implemented by a subagent,
reviewed + Task A socket-verified here):

1. First/founding tab can now tear out. CanvasModel.breakOutPanel minted the
   new pane id from the panel UUID; a single-tab pane's id equals its founding
   panel's UUID, so for the founding tab that id collided with the source
   pane and layout.breakOutPanel's !contains guard rejected it — the first
   tab could never tear out. Mint a fresh UUID for the torn-out pane instead.
   Verified via canvas.break on a founding panel (45->46 panes, tab tears out).
   Adds a CanvasLayout founding-panel break-out unit test.

2. Cmd+drag a tab moves the whole pane (skips tear-out), mirroring Cmd+scroll
   targeting the canvas instead of pane content. Guarantees a move handle even
   when the tab bar overflows and has no empty title-bar region.

3. Drop indicator during a drag: CanvasGuidesView draws an accent highlight on
   the target pane's tab bar when the drag would join there, computed from the
   same predicate that commits the join on drop. Clears over empty canvas and
   on drag end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	.github/swift-file-length-budget.tsv
#	cmux.xcodeproj/project.pbxproj
azooz2003-bit and others added 2 commits June 13, 2026 17:40
… in info

Closes the two CLI-coverage gaps Aziz flagged. Both flow through the typed
canvas.* seam (context protocol -> coordinator -> CLI), 1:1 with the socket.

- canvas.set_viewport / `cmux canvas set-viewport --x --y [--zoom]`: centers
  the viewport on a canvas point and optionally sets magnification (explicit
  clip-origin math, mirrors setMagnification). Makes Cmd+scroll pan and
  pinch/option-scroll zoom scriptable.
- canvas.info now reports `magnification` and `viewport_center` (omitted in
  splits), so zoom/center are inspectable — and pan/zoom become socket-testable
  instead of eyeball-only.
- canvas.new_pane / `cmux canvas new-pane [--type terminal|browser]`: opens a
  standalone canvas pane via Workspace.openNewCanvasPane (creates the surface,
  does NOT join as a tab, placer positions it, focus+reveal), returns the new
  surface ref. New .created(mode:surfaceID:) resolution case.

Socket-verified: info shows magnification/center; set-viewport --x 600 --y 400
--zoom 0.5 reads back 0.5 @ (599.86,399.78); new-pane terminal+browser bump the
pane count and return refs; invalid type rejected. 155 control-socket tests
pass (+8). Adds coordinator tests + context stubs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	.github/swift-file-length-budget.tsv
#	Sources/Workspace.swift
#	cmux.xcodeproj/project.pbxproj
# Conflicts:
#	.github/swift-file-length-budget.tsv
# Conflicts:
#	.github/swift-file-length-budget.tsv
# Conflicts:
#	.github/swift-file-length-budget.tsv
# Conflicts:
#	.github/swift-file-length-budget.tsv
# Conflicts:
#	.github/swift-file-length-budget.tsv

This branch was successfully deployed

1 active deployment
Preview – cmux — 8891a4f1 Deployed Jun 14, 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.

1 participant