Commit b405287
Freeform 2D canvas layout for workspace panes (#5987)
* CmuxCanvas: pure canvas layout model package
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).
* Canvas layout: AppKit rendering layer + workspace integration
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.
* Canvas: shortcut actions, AppDelegate dispatch, palette commands, View 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.
* Canvas: canvas.* socket domain + cmux canvas CLI namespace
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.
* Canvas: session persistence, CmuxSettings ShortcutAction parity, localization
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.
* Canvas: web docs category, coordinator canvas tests, window/canvas test 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.
* Canvas: center overview zoom explicitly
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.
* Canvas: extract AppKit rendering layer into CmuxCanvasUI package
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.
* Canvas: address review findings
- 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.
* Canvas: Cmd+Scroll panning + zoom in/out/reset
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.
* Canvas: fix stranded/blank browser webviews
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: gap + snapping settings, typed canvas.zoom direction
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).
* Canvas: tabbed-pane model (CmuxCanvas) + persistence shape
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.
* Canvas: tab strip UI + canvas.join/break/select_tab verbs
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.
* Canvas: browser focus ring, frame-synced webview tracking, blank-webview fix
Three dogfood-reported browser flaws:
- Focus ring occluded: window-portal webviews float above the pane's
layer border; hosted canvas content now insets 2px so the ring stays
visible (terminals unaffected, they render under the layer border).
- Webview lagging pane resize: onViewportGeometryChanged now syncs each
browser anchor synchronously (BrowserWindowPortalRegistry.synchronize-
ForAnchor) per drag/scroll frame, with the coalesced scheduled pass as
settle-up, matching split-divider per-frame behavior.
- Webviews going blank: the hidden-webview discard (300s) fires when
panel-level visibility flips false (stale bonsplit pane ownership under
canvas mode) and nothing on the canvas path ever restored. The canvas
mount now drives panel-level webview visibility directly: mount/render
-> visible (restores a discarded webview), occlude/unmount -> hidden
(discard timer may reclaim offscreen browsers, correctly).
* Canvas: satisfy Swift file-length budget gate
Split CanvasRootView into core + PaneGestures + Viewport extension
files (617 -> 394/101/125; gesture and viewport conformances are
coherent units; touched members widened private -> internal within the
package). Split the window-domain test stubs out of the over-threshold
stub file. Budget bumps for in-place feature growth that belongs where
it is: CLI canvas namespace, shortcut enum cases, settings rows,
Workspace hooks, persistence snapshot fields, unit test (+17/+12/+12/+7
app entry hooks).
* Canvas: tab cycling via next/prev-surface shortcuts + tabs in canvas.info
selectNextSurface/selectPreviousSurface branch to canvas-pane tab
cycling (wrapping, focus follows) when the focused pane has multiple
tabs, falling back to bonsplit semantics otherwise — cmd+shift+]/[ now
does the expected thing on a tabbed canvas pane. canvas.info panes gain
surface_ids/surface_refs + selected_surface_id/ref, and the focused
flag uses containment so multi-tab panes report correctly. Verified
live: shortcut cycling flips selection with focus, and tab groups
persist across app relaunch.
* Canvas: Cmd+T opens a tab in the focused canvas pane
newTerminalSurfaceInFocusedPane joins the new panel into the focused
panel's canvas pane (sync first so the panel exists in the model), so
Cmd+T matches workspace-pane tab semantics; Cmd+D / Cmd+Shift+D remain
the new-floating-pane shortcuts (split path -> placer). Verified live:
focused pane [surface:3] -> [surface:3, surface:23] selected.
Also: groundwork for inline browser hosting on canvas (environment flag
cmuxCanvasInlineBrowserHosting wired into BrowserPanelView's hosting
decision) to fix webview content trailing during pans at the root.
Parked default-off: the inline slot mis-lays out under the canvas's
flipped document hierarchy (small offset rect); per-frame portal anchor
sync remains until that layout path is fixed.
* Canvas: portal/inline hosting ownership flag (inline still parked)
BrowserPanel.canvasInlineHostingActive marks webviews owned by a canvas
pane's inline host; the workspace portal-visibility reconciler and the
canvas geometry sync skip such panels so they cannot rebind the webview
into the window portal (this was the inline-hosting tug-of-war: 4 stray
portal binds after inline takeover). With the war fixed, inline content
renders live and stable, but the webview frame inside the slot still
lays out partial-width/right-aligned, so the env flag stays default-off
for dogfood; next step is the pinHostedWebView frame-reset path under
the canvas hierarchy.
* Canvas: runtime debug flag + frame diagnostics for inline browser hosting
canvasInlineBrowserHostingDebug (launch-time UserDefaults bool) flips
inline hosting on for diagnosis without a rebuild; the inline update
path logs host/slot/webview frames, inspector presence, and companions
(DEBUG only). Diagnosis so far: pinning is correct per host (web ==
slot == host bounds once settled), no phantom inspector split — the
misplacement comes from MULTIPLE live BrowserPanelView hosts competing
for one webview (orphan fragments render at dead hosts' positions).
Next: audit canvas mount lifecycle for double-mounts and the
browser-open placement=reuse flow. Flag remains default-off.
* Canvas: focused pane raises to front; portal settle-refresh + z-order
Focusing a pane through ANY entrypoint now brings it to the front:
click focus raises immediately in the gesture path, and sync() raises
the pane hosting focusedPanelId (covers keyboard, palette, socket, and
webview-click focus). Browser portal z-priorities mirror canvas z-order
(front pane's webview stacks above a back pane's), re-derived on every
layout change. New onViewportSettled callback (didEndLiveScroll/Magnify)
force-refreshes every portal-hosted browser so content cannot rest at a
stale frame after a pan — addressing dogfood screenshots where page
content stayed window-fixed while its pane scrolled.
Verified live: focusing back pane surface:8 moved it to the z-order
front. Known residual: during the pan itself, portal content still
trails (inherent to window-portal hosting; inline hosting work
continues).
* Canvas: pin hosted panel content with constraints (root cause of shifted webviews)
NSHostingView self-sizes to SwiftUI's ideal size under autoresizing
(sizingOptions), so hosted canvas content could settle at a small
intrinsic rect instead of filling its pane. The browser portal anchors
live inside that hosting view, so webview content rendered at the wrong
rect in BOTH hosting modes — this is the mechanism behind dogfood
screenshots where page content sat shifted/partial relative to the pane.
Hosted mounts now pin to the content container with constraints and set
sizingOptions = []; terminal mounts keep frame+autoresizing. Verified
inline-flag run: focused browser pane fills edge-to-edge.
* Merge origin/main; reconcile budget for main-side untracked growth
TerminalNotificationStore.swift (+96, grown on main by #5651 without a
budget bump) and FeedCoordinator.swift (+16) exceed the budgets recorded
on main itself — main's gate is currently red independent of this
branch. Bump both here so this PR's merge CI reflects only its own
debt.
* Canvas: inline browser hosting ON by default
With the hosting-view self-sizing root cause fixed (a106b6b) and the
portal ownership war resolved (50f7adf), inline hosting now verifies
clean: webviews fill their panes pixel-perfect at 1x, multiple browser
panes render simultaneously with correct clipping, z-order stacks
natively (focused pane's browser above a back pane's), content scales
with overview/zoom magnification, and pans move content in the same
CoreAnimation transaction as the pane — the window-portal trailing
Aziz reported is structurally gone. Launch-time default
canvasInlineBrowserHostingDisabled reverts to portal hosting if needed.
* Canvas: dragging a multi-tab pane by its tab strip works
Tab pills are SwiftUI Buttons and consume mouse-down, so the AppKit
title-bar drag path never engaged on panes with several tabs (dogfood:
'can't really drag a multi-tab pane'). Each pill now carries a
simultaneous DragGesture (4pt threshold) that relays its translation to
the same pane-drag delegate path (snap + guides + Command suspend
included); taps still select, hover-close still works. Translation is
pane-local, which equals document points at any magnification.
* Canvas: tab bar matches workspace split pane tabs
Rebuilt the canvas pane strip to bonsplit's tab anatomy (TabBarMetrics
parity): 30pt bar, full-height square tabs with 1px trailing separators,
selected/hover rectangle fills, 14pt icon slot that swaps to a 16pt
close button on hover, 11pt middle-truncated titles, 220pt max tab
width, active/inactive text alphas 0.82/0.62. Single-tab panes render
as a real tab bar too, matching split panes. Tab drags still relay into
the pane-drag path; clicks select; hover-close closes.
* Canvas: tab strip events back on the AppKit path (drag regression fix)
The simultaneous SwiftUI DragGesture on tab pills made dragging fight
the Button recognizers and feel slow (gesture-system delivery vs
NSEvent tracking), and the nested hover-close Button got swallowed —
dogfood: 'pane fighting against me', 'sometimes can't close tabs'.
The strip is now render-only SwiftUI: tabs report their tab/close hit
rects via PreferenceKey, CanvasPaneView owns ALL strip mouse events
(hitTest claims the title-bar region), routes drags through the
original fast AppKit drag session (snap, guides, autoscroll), and
resolves clicks at mouse-up against the reported rects (close beats
select; sub-threshold movement still counts as a click). Close hit
rect is padded for forgiving clicks.
* Canvas: fix terminal row duplication under canvas zoom
Surface pixel sizing used convertToBacking, which folds ancestor
transforms — the canvas NSScrollView magnification — into the backing
size: zooming out re-typeset the terminal at a shrunken pixel grid and
rendered duplicated prompt rows (dogfood screenshots, reproduced at 3x
zoom-out). Pixel size now derives from the window backingScaleFactor
only (identical in split mode), so terminals keep their logical pixel
density and the magnification scales them purely visually. Verified:
deep zoom-out shows single rows everywhere; zoom round-trip + live
typing keeps the grid intact.
* Canvas: rendered-panel set is each pane's selected tab
With tabbed panes, reporting ALL panels as rendered made the terminal
window portal float unmounted background-tab terminals at stale frames
(chromeless prompt slivers found in regression sweep). The canvas
branch of renderedVisiblePanelIdsForCurrentLayout now returns exactly
the selected panel of each canvas pane, matching mount reality; this
also restores correct hidden-discard eligibility for background-tab
browsers and hibernation visibility semantics. Verified: full verb
sweep + tidy + overview shows no floating fragments.
* Canvas: tab tear-out (Option+drag) and drop-to-join
Option+dragging a tab of a multi-tab pane tears it out into its own
pane whose tab bar lands under the cursor, and the drag continues
seamlessly (sessions now route by session pane, not the event-source
view, since AppKit keeps tracking on the mouse-downed view). Dropping a
single-tab pane onto another pane's tab bar joins it as a tab there —
the canvas twin of bonsplit's tab drop — resolved front-most-first
against pane bar rects. Plain drags still move the pane (Aziz's
requested behavior); Option is the tear-out modifier so the two never
conflict. Gesture paths can't be socket-driven: needs hand
verification.
* Canvas: Option+drag on a single-tab pane degrades to a pane drag
breakOutPanel correctly no-ops on single-tab panes, but the tear-out
entry then started no drag session, leaving Option+drag dead. It now
falls back to a normal pane-drag session so the modifier never inerts
the gesture.
* ci: re-trigger workflows (push events dropped during org API throttling)
* Budget: account for zoom-fix comments (+6 GhosttyTerminalView) and canvas tab hook (+1 Workspace)
* Canvas: corner resize cursors, viewport restore on workspace switch, toolbar mode toggle
Three Aziz dogfood items:
- Corner bands now show diagonal resize cursors (NW-SE / NE-SW) via the
private window-resize cursors with public fallbacks; matches the
existing horizontal/vertical edge cursors.
- Switching workspaces away and back restores the exact canvas viewport
(center + zoom) instead of snapping to a default. CanvasModel persists
savedViewport (canvas coords); the view re-applies it when becoming
visible. Fixed via instrumentation: restore must defer until the
scroll view is sized (contentSize>1) or it lands at a garbage origin,
and saves are suppressed while a restore is pending/in-flight so the
programmatic scroll can't overwrite the saved value with a transient.
- Discoverable Splits|Canvas toolbar toggle: NSSegmentedControl in
WindowToolbarController, selection driven by Workspace.layoutMode and
synced via a new .workspaceLayoutModeDidChange notification (covers
shortcut/palette/menu/toolbar entrypoints), localized 19 locales.
Viewport restore verified: workspace switch away+back lands pixel-identical.
* Browser: collapse omnibar accessories into overflow menu at narrow width
At narrow pane widths (canvas or split) the browser chrome clipped: the
address field vanished and trailing accessory buttons ran off the pane
edge. The chrome now measures its width (preference key) and below 420pt
collapses the plain-action buttons (focus mode, screenshot, React Grab,
dev tools) into a single overflow menu, keeping nav + address field +
the popover-anchored profile/theme buttons. Shared with split mode, so
narrow splits benefit too. Verified: 340pt pane shows address field +
overflow + profile/theme with no clipping; 720pt shows the full row.
* Canvas: one-time Command+scroll discovery hint
Scrolling inside a pane (content consumes it, canvas doesn't pan) is the
teachable moment for Command+scroll panning. After ~1.2s of in-pane
scrolling the canvas shows a soft scale+fade pill — 'Command+scroll pans
the canvas from anywhere' — once per session, auto-dismissing after
~3.4s. Debounce and dismiss use cancellable Tasks wired to teardown (not
the banned asyncAfter). The local scroll monitor distinguishes
command-scroll (pans), in-pane plain scroll (hint), and empty-canvas
scroll (already pans, no hint). Hint text crosses the package seam
pre-localized (19 locales). Command-scroll + hint split into
CanvasRootView+CommandScroll.swift to keep the core under the file
budget. Gesture-triggered, so needs hand-verification.
* Canvas: horizontal scrolling for overflowing pane tabs
Tabs in a narrow pane previously spilled past the pane edge. The tab
strip now clips to the bar and scrolls horizontally: CanvasPaneView
owns the title bar's scrollWheel (it already claims the region for
drag/click routing, so a SwiftUI ScrollView can't be used), tracks a
clamped offset against the measured content width, and feeds it to the
render-only strip. Hit frames are reported in the bar-anchored
coordinate space post-offset, so a scrolled-out tab reports a frame
outside the bar and clicks stay correct. Dominant-axis delta lets a
mostly-vertical trackpad swipe still scroll the strip. Verified: 5 tabs
in a 300pt pane stay clipped inside the pane; scroll gesture needs
hand-verification. Exact bonsplit color fidelity is the remaining
self-contained follow-up.
* canvas: port bonsplit tab fill/text colors for visual parity
Active/hover tab fills now derive from the pane background using the same
lighten-on-dark / darken-on-light treatment as bonsplit's TabBarColors,
instead of a flat Color.primary.opacity. Text uses system label colors.
Threads barBackground (paneBackground) through CanvasPaneTitleBarView and
CanvasPaneTabItem, rebuilding the title bar when the pane background changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* canvas: clear terminal portal on entering canvas to kill ghost artifacts
Toggling splits->canvas left split-mode terminal surfaces floating at
their old split frames over the canvas (Aziz dogfood), persisting until
some later layout event happened to reconcile portal visibility.
setLayoutMode reconciled only the browser portal, never the terminal one.
Entering canvas now calls hideAllTerminalPortalViews() so the portal layer
is cleared immediately; the canvas mount re-shows each visible pane's
selected terminal via its portal-detach path. Leaving canvas reconciles to
the split-mode rendered set. Relaxed reconcileTerminalPortalVisibility...
from private to internal so the canvas extension can call it.
Repro (3x rapid splits<->canvas, screenshot with no interaction): green
stale terminal block over empty canvas pre-fix; clean post-fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* canvas: trackpad pinch + option-scroll zoom (mouse-friendly)
Zoom was keyboard-only. Add two pointer paths that reuse the viewport
magnification machinery:
- Trackpad pinch: intercept .magnify in the canvas input monitor and
forward to scrollView.magnify(with:). Without intercepting, a pane
swallows the gesture (Ghostty font-zoom), so pinching over a terminal
never zoomed the canvas. Native forwarding anchors at the gesture point
and fires didEndLiveMagnify so portals settle through the normal path.
- Mouse option+scroll: a mouse has no pinch, so option+scroll zooms toward
the cursor via setMagnification(centeredAt:). Cmd+scroll stays pan; plain
scroll stays pane content. A debounced onViewportSettled re-anchors
portals since synthesized magnification never fires didEndLiveMagnify.
Verified shared machinery via canvas zoom out (renders all panes at smaller
scale, no ghosts). Raw pinch/scroll input is not socket-drivable; those
input paths are hand-verify.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* canvas: plain-drag tab tear-out/move for split-layout parity
Dragging a tab out of a pane required holding Option, and a plain drag on
the tab moved the whole pane — inverted from split layout, where dragging a
tab manipulates the tab. Aziz: tabs must drag out into their own pane and
move between panes with full parity.
Now a plain drag that begins on a tab (not its close glyph) of a multi-tab
pane tears that tab into its own pane and keeps dragging it; dropping on
another pane's tab bar joins it there (existing join-on-drop path). Single-
tab panes and drags on the empty title-bar area still move the whole pane.
The tear-out/join model ops are unchanged — only the input trigger moved
from Option+mouseDown to plain drag-activation in CanvasPaneView, so a plain
click still selects the tab.
Tab drag is gesture-only (not socket-drivable) = hand-verify; the underlying
tear-out/join were already verified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* canvas: don't leak internal TabManager class name in socket API error
greptile P1: the canvas.* API returned message "TabManager not available",
exposing an internal cmux class name to cmux canvas CLI callers, violating
the user-facing error rule that API error bodies describe failures in
product terms. Changed to "No active cmux window", matching the sibling
canvas error messages ("Workspace not found", "Canvas pane not found").
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* canvas: show Cmd+scroll discovery hint at top of canvas, not bottom
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>
* canvas: first-tab tear-out fix, Cmd+drag pane move, drag drop indicator
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>
* canvas: add set-viewport + new-pane CLI/socket verbs, report viewport 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>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent 33e5380 commit b405287
117 files changed
Lines changed: 11690 additions & 50 deletions
File tree
- .github
- CLI
- Packages
- CmuxCanvasUI
- Sources/CmuxCanvasUI
- Tests/CmuxCanvasUITests
- CmuxCanvas
- Sources/CmuxCanvas
- Tests/CmuxCanvasTests
- CmuxControlSocket
- Sources/CmuxControlSocket/Coordinator
- Canvas
- Tests/CmuxControlSocketTests
- CmuxSettingsUI/Sources/CmuxSettingsUI/Sections
- CmuxSettings/Sources/CmuxSettings
- Keys
- Values
- Resources
- Sources
- Canvas
- Panels
- cmux.xcodeproj
- cmuxTests
- docs
- web
- data
- messages
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
5 | | - | |
6 | | - | |
7 | | - | |
8 | | - | |
9 | | - | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
10 | 11 | | |
11 | | - | |
| 12 | + | |
12 | 13 | | |
13 | | - | |
14 | | - | |
15 | | - | |
16 | | - | |
17 | | - | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
18 | 20 | | |
19 | | - | |
20 | 21 | | |
21 | | - | |
22 | 22 | | |
23 | | - | |
| 23 | + | |
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| 28 | + | |
28 | 29 | | |
29 | | - | |
30 | 30 | | |
31 | 31 | | |
32 | | - | |
| 32 | + | |
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| 38 | + | |
| 39 | + | |
38 | 40 | | |
39 | 41 | | |
40 | 42 | | |
41 | | - | |
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
45 | 46 | | |
46 | | - | |
| 47 | + | |
47 | 48 | | |
48 | 49 | | |
49 | | - | |
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
| |||
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
64 | | - | |
| 64 | + | |
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
71 | | - | |
| 71 | + | |
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
| 84 | + | |
84 | 85 | | |
85 | 86 | | |
86 | 87 | | |
87 | 88 | | |
88 | 89 | | |
89 | 90 | | |
| 91 | + | |
90 | 92 | | |
91 | 93 | | |
92 | | - | |
93 | 94 | | |
94 | 95 | | |
95 | 96 | | |
96 | 97 | | |
97 | 98 | | |
98 | | - | |
| 99 | + | |
| 100 | + | |
99 | 101 | | |
100 | 102 | | |
101 | | - | |
102 | 103 | | |
103 | 104 | | |
104 | 105 | | |
| |||
110 | 111 | | |
111 | 112 | | |
112 | 113 | | |
| 114 | + | |
113 | 115 | | |
114 | | - | |
| 116 | + | |
115 | 117 | | |
116 | 118 | | |
117 | 119 | | |
118 | | - | |
119 | 120 | | |
120 | 121 | | |
121 | 122 | | |
| |||
143 | 144 | | |
144 | 145 | | |
145 | 146 | | |
| 147 | + | |
146 | 148 | | |
147 | 149 | | |
148 | 150 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3936 | 3936 | | |
3937 | 3937 | | |
3938 | 3938 | | |
| 3939 | + | |
| 3940 | + | |
| 3941 | + | |
| 3942 | + | |
| 3943 | + | |
| 3944 | + | |
| 3945 | + | |
| 3946 | + | |
3939 | 3947 | | |
3940 | 3948 | | |
3941 | 3949 | | |
| |||
7334 | 7342 | | |
7335 | 7343 | | |
7336 | 7344 | | |
| 7345 | + | |
| 7346 | + | |
| 7347 | + | |
| 7348 | + | |
| 7349 | + | |
| 7350 | + | |
| 7351 | + | |
| 7352 | + | |
| 7353 | + | |
| 7354 | + | |
| 7355 | + | |
| 7356 | + | |
| 7357 | + | |
| 7358 | + | |
| 7359 | + | |
| 7360 | + | |
| 7361 | + | |
| 7362 | + | |
| 7363 | + | |
| 7364 | + | |
| 7365 | + | |
| 7366 | + | |
| 7367 | + | |
| 7368 | + | |
| 7369 | + | |
| 7370 | + | |
| 7371 | + | |
| 7372 | + | |
| 7373 | + | |
| 7374 | + | |
| 7375 | + | |
| 7376 | + | |
| 7377 | + | |
| 7378 | + | |
| 7379 | + | |
| 7380 | + | |
| 7381 | + | |
| 7382 | + | |
| 7383 | + | |
| 7384 | + | |
| 7385 | + | |
| 7386 | + | |
| 7387 | + | |
| 7388 | + | |
| 7389 | + | |
| 7390 | + | |
| 7391 | + | |
| 7392 | + | |
| 7393 | + | |
| 7394 | + | |
| 7395 | + | |
| 7396 | + | |
| 7397 | + | |
| 7398 | + | |
| 7399 | + | |
| 7400 | + | |
| 7401 | + | |
| 7402 | + | |
| 7403 | + | |
| 7404 | + | |
| 7405 | + | |
| 7406 | + | |
| 7407 | + | |
| 7408 | + | |
| 7409 | + | |
| 7410 | + | |
| 7411 | + | |
| 7412 | + | |
| 7413 | + | |
| 7414 | + | |
| 7415 | + | |
| 7416 | + | |
| 7417 | + | |
| 7418 | + | |
| 7419 | + | |
| 7420 | + | |
| 7421 | + | |
| 7422 | + | |
| 7423 | + | |
| 7424 | + | |
| 7425 | + | |
| 7426 | + | |
| 7427 | + | |
| 7428 | + | |
| 7429 | + | |
| 7430 | + | |
| 7431 | + | |
| 7432 | + | |
| 7433 | + | |
| 7434 | + | |
| 7435 | + | |
| 7436 | + | |
| 7437 | + | |
| 7438 | + | |
| 7439 | + | |
| 7440 | + | |
| 7441 | + | |
| 7442 | + | |
| 7443 | + | |
| 7444 | + | |
| 7445 | + | |
| 7446 | + | |
| 7447 | + | |
| 7448 | + | |
| 7449 | + | |
| 7450 | + | |
| 7451 | + | |
| 7452 | + | |
| 7453 | + | |
| 7454 | + | |
| 7455 | + | |
| 7456 | + | |
| 7457 | + | |
| 7458 | + | |
| 7459 | + | |
| 7460 | + | |
| 7461 | + | |
| 7462 | + | |
| 7463 | + | |
| 7464 | + | |
| 7465 | + | |
| 7466 | + | |
| 7467 | + | |
| 7468 | + | |
| 7469 | + | |
| 7470 | + | |
| 7471 | + | |
| 7472 | + | |
7337 | 7473 | | |
7338 | 7474 | | |
7339 | 7475 | | |
| |||
13327 | 13463 | | |
13328 | 13464 | | |
13329 | 13465 | | |
| 13466 | + | |
| 13467 | + | |
| 13468 | + | |
| 13469 | + | |
| 13470 | + | |
| 13471 | + | |
| 13472 | + | |
| 13473 | + | |
| 13474 | + | |
| 13475 | + | |
| 13476 | + | |
| 13477 | + | |
| 13478 | + | |
| 13479 | + | |
| 13480 | + | |
| 13481 | + | |
| 13482 | + | |
| 13483 | + | |
| 13484 | + | |
| 13485 | + | |
| 13486 | + | |
| 13487 | + | |
| 13488 | + | |
| 13489 | + | |
| 13490 | + | |
| 13491 | + | |
| 13492 | + | |
| 13493 | + | |
| 13494 | + | |
| 13495 | + | |
| 13496 | + | |
| 13497 | + | |
| 13498 | + | |
13330 | 13499 | | |
13331 | 13500 | | |
13332 | 13501 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
0 commit comments