Repository navigation
Freeform 2D canvas layout for workspace panes - #5987
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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. ChangesCanvas mode end-to-end
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)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
|
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
Greptile SummaryThis PR adds a freeform 2D canvas layout mode to cmux workspaces, implemented across two new SwiftPM packages (
Confidence Score: 4/5Well-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
Sequence DiagramsequenceDiagram
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]
Reviews (5): Last reviewed commit: "Canvas: fix stranded/blank browser webvi..." | Re-trigger Greptile |
| "surfacesBlurb": "Surfaces are tabs inside a pane.", | ||
| "splitPanes": "Split Panes", |
There was a problem hiding this comment.
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)
|
|
||
| /// 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 | ||
| } | ||
|
|
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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
📒 Files selected for processing (72)
CLI/cmux.swiftPackages/CmuxCanvas/Package.swiftPackages/CmuxCanvas/Sources/CmuxCanvas/CanvasAligner.swiftPackages/CmuxCanvas/Sources/CmuxCanvas/CanvasAlignmentCommand.swiftPackages/CmuxCanvas/Sources/CmuxCanvas/CanvasDirection.swiftPackages/CmuxCanvas/Sources/CmuxCanvas/CanvasGuide.swiftPackages/CmuxCanvas/Sources/CmuxCanvas/CanvasLayout.swiftPackages/CmuxCanvas/Sources/CmuxCanvas/CanvasMetrics.swiftPackages/CmuxCanvas/Sources/CmuxCanvas/CanvasPane.swiftPackages/CmuxCanvas/Sources/CmuxCanvas/CanvasPaneID.swiftPackages/CmuxCanvas/Sources/CmuxCanvas/CanvasPlacer.swiftPackages/CmuxCanvas/Sources/CmuxCanvas/CanvasPoint.swiftPackages/CmuxCanvas/Sources/CmuxCanvas/CanvasRect.swiftPackages/CmuxCanvas/Sources/CmuxCanvas/CanvasResizeEdges.swiftPackages/CmuxCanvas/Sources/CmuxCanvas/CanvasSize.swiftPackages/CmuxCanvas/Sources/CmuxCanvas/CanvasSnapEngine.swiftPackages/CmuxCanvas/Sources/CmuxCanvas/CanvasSnapResult.swiftPackages/CmuxCanvas/Sources/CmuxCanvas/CanvasSpatialNavigator.swiftPackages/CmuxCanvas/Sources/CmuxCanvas/CanvasViewportMath.swiftPackages/CmuxCanvas/Tests/CmuxCanvasTests/CanvasAlignerTests.swiftPackages/CmuxCanvas/Tests/CmuxCanvasTests/CanvasLayoutTests.swiftPackages/CmuxCanvas/Tests/CmuxCanvasTests/CanvasPlacerTests.swiftPackages/CmuxCanvas/Tests/CmuxCanvasTests/CanvasRectTests.swiftPackages/CmuxCanvas/Tests/CmuxCanvasTests/CanvasSnapEngineTests.swiftPackages/CmuxCanvas/Tests/CmuxCanvasTests/CanvasSpatialNavigatorTests.swiftPackages/CmuxCanvas/Tests/CmuxCanvasTests/CanvasViewportMathTests.swiftPackages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCanvasActionResolution.swiftPackages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCanvasAlignCommand.swiftPackages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCanvasContext.swiftPackages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCanvasFrame.swiftPackages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCanvasInfoSnapshot.swiftPackages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCanvasPaneSummary.swiftPackages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCommandCoordinator+Canvas.swiftPackages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/ControlCommandContext.swiftPackages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/ControlCommandCoordinator.swiftPackages/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swiftPackages/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorCanvasTests.swiftPackages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swiftPackages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swiftResources/Localizable.xcstringsSources/AppDelegate.swiftSources/Canvas/CanvasAction.swiftSources/Canvas/CanvasDocumentView.swiftSources/Canvas/CanvasGeometryBridging.swiftSources/Canvas/CanvasGuidesView.swiftSources/Canvas/CanvasHostedPanelContentView.swiftSources/Canvas/CanvasLayoutSettings.swiftSources/Canvas/CanvasPaneContent.swiftSources/Canvas/CanvasPaneDescriptor.swiftSources/Canvas/CanvasPaneHitRegion.swiftSources/Canvas/CanvasPaneTitleBarView.swiftSources/Canvas/CanvasPaneView.swiftSources/Canvas/CanvasRootView.swiftSources/Canvas/CanvasScrollView.swiftSources/Canvas/CanvasViewportControlling.swiftSources/Canvas/ContentView+CanvasCommandPalette.swiftSources/Canvas/Workspace+CanvasLayout.swiftSources/Canvas/WorkspaceCanvasHostView.swiftSources/Canvas/WorkspaceCanvasModel.swiftSources/Canvas/WorkspaceLayoutMode.swiftSources/ContentView.swiftSources/KeyboardShortcutSettings.swiftSources/SessionPersistence.swiftSources/TerminalController+ControlCanvasContext.swiftSources/Workspace.swiftSources/WorkspaceContentView.swiftSources/cmuxApp.swiftcmux.xcodeproj/project.pbxprojdocs/canvas-layout-design.mdweb/data/cmux-shortcuts.tsweb/messages/en.jsonweb/messages/ja.json
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 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) + phaseYThis 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.
| Group { | ||
| if workspace.layoutMode == .canvas { | ||
| WorkspaceCanvasHostView( | ||
| workspace: workspace, | ||
| isWorkspaceVisible: isWorkspaceVisible, | ||
| isWorkspaceInputActive: isWorkspaceInputActive, | ||
| portalPriority: workspacePortalPriority, | ||
| appearance: appearance | ||
| ) | ||
| } else { | ||
| bonsplitView | ||
| } | ||
| } | ||
| .ignoresSafeArea(.container, edges: (isMinimalMode && !isFullScreen) ? .top : []) |
There was a problem hiding this comment.
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.
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.
| let routing = routingSelectors(params) | ||
| let resolution = context?.controlCanvasToggleOverview(routing: routing) | ||
| ?? .tabManagerUnavailable | ||
| return canvasActionResult(resolution) | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 winAvoid 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 winFix occlusion polarity and restore symmetric terminal visibility teardown.
Line 84 currently occludes when
rendering == true, which inverts the documented lifecycle. Also, unmount never restoressetVisibleInUI(false)after mount set it totrue, 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
📒 Files selected for processing (24)
Packages/CmuxCanvasUI/Package.swiftPackages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasDocumentView.swiftPackages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasGeometryBridging.swiftPackages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasGuidesView.swiftPackages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasHostCallbacks.swiftPackages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasModel.swiftPackages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasPaneChrome.swiftPackages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasPaneContentMounting.swiftPackages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasPaneDescriptor.swiftPackages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasPaneHitRegion.swiftPackages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasPaneTitleBarView.swiftPackages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasPaneView.swiftPackages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasRootView.swiftPackages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasScrollView.swiftPackages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasTheme.swiftPackages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasViewportControlling.swiftPackages/CmuxCanvasUI/Tests/CmuxCanvasUITests/CanvasModelTests.swiftSources/Canvas/CanvasAction.swiftSources/Canvas/CanvasPaneContent.swiftSources/Canvas/Workspace+CanvasLayout.swiftSources/Canvas/WorkspaceCanvasHostView.swiftSources/TerminalController+ControlCanvasContext.swiftSources/Workspace.swiftcmux.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
| CA53A005CA53A005CA53A005 /* CmuxCanvasUI */ = { | ||
| isa = XCSwiftPackageProductDependency; | ||
| productName = CmuxCanvasUI; | ||
| }; |
There was a problem hiding this comment.
🧩 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)
PYRepository: 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.
There was a problem hiding this comment.
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
📒 Files selected for processing (15)
CLI/cmux.swiftPackages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasRootView.swiftPackages/CmuxCanvasUI/Sources/CmuxCanvasUI/CanvasViewportControlling.swiftPackages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCanvasContext.swiftPackages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Canvas/ControlCommandCoordinator+Canvas.swiftPackages/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swiftPackages/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorCanvasTests.swiftPackages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swiftPackages/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swiftResources/Localizable.xcstringsSources/Canvas/CanvasAction.swiftSources/Canvas/ContentView+CanvasCommandPalette.swiftSources/KeyboardShortcutSettings.swiftSources/TerminalController+ControlCanvasContext.swiftweb/data/cmux-shortcuts.ts
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
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
… 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
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/CanvasPanewith orderedpanelIds+ 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 realNSScrollView(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-localizedCanvasPaneChrome,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;canvasInlineBrowserHostingDisableddefaults-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
CanvasActionExecutorserves 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, thecanvas.*socket domain (infowith tab lists,set_mode,set_frame,align,reveal,overview,zoom,join,break,select_tab), and thecmux canvasCLI namespace. All shortcuts live inKeyboardShortcutSettings, mirror into the CmuxSettingsShortcutActioncatalog (cmux.json bindings), are localized for all 19 locales, and are documented on the web shortcuts + configuration pages.canvas.paneGap/canvas.snappingEnabledare 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
🤖 Generated with Claude Code