Repository navigation
feat(ribbon): run the proxy overlay on every display as the only motion system - #152
Conversation
Each display gets its own presenter, panel, wallpaper, motion token and hidden border set. The daemon publishes every monitor's managed windows and its own workspace switch shift before each layout. With several displays, a display the layout does not move presents nothing. A window dropped on another display leaves its source overlay at once. Displays that disappear drop their presenter and panel, and new or reshaped displays are warmed before their first motion.
Remove the animation.window_proxies and animation.duration_ms options and the settings row: the overlay is always on while animations are enabled, and its spring never read the duration. The daemon now asks the platform for motion with a flag instead of AX animation timing, and its branches for the overlay being off are gone, including the live mouse reorder during a drag and the native outgoing workspace slide. The platform still compiles its per-frame AX path, now unreachable.
Every native window now moves with one Accessibility write per layout, under the overlay when it draws the motion. Remove FrameAnimationDriver, the coordinator's animation clock, process lanes, spring sampling, intermediate latency budgets, reentry staging, ribbon offset planning and deferred parking, with the DefiCore spring helpers that only fed them and the tests that only protected them. Background snapshot reads now wait for the overlay's motion, the only motion left, instead of the AX animation. Keyboard navigation keeps its horizontal-only, position-only writes on a single display. The daemon status keeps axPending and animating, now reporting the overlay.
Document that animation.enabled is the only animation option, that each display has its own overlay, and that windows move at once without Screen Recording, with Reduce Motion, or before the overlay can draw.
A layout limited to the monitors a command moves reuses the other monitors' cached plans. Their windows had no ribbon target in that layout, so the overlay read them as leaving and presented on the idle display, hiding and reshowing its borders. The daemon now marks those monitors as not laid out: they keep their ribbon history and present nothing.
On a display that clamps windows away from their ribbon place (a notched built-in), a dropped window's expected landing dropped the clamp: its own offset is the pointer's, and learned placements never carried the clamp because they were measured against the already clamped target. The overlay then covered for the full timeout. Learn each app's placement against what Defi asked for, so it carries the clamp; let it survive a column reflowed a point narrower; and give a dropped window without one the clamp its neighbours show. The landing check now expects the planned target as is.
Every hidden managed window, inactive workspace or offscreen column, keeps its size and leaves a one-point sliver on its own display, at the first of TrellisWM's five corner and edge spots that stays clear of every other display, else the one with the least overlap. macOS keeps a window's title bar between the menu bar and the visible bottom of the display under most of its width. Spots therefore sit at that visible bottom, and a spot mostly under another display counts as landing on it: on a side-by-side desk the external display parks in its bottom-left corner instead of leaking into the built-in one. The spot depends only on display frames and window size, so a parked window stays put across layouts.
An app with a taller title bar stops a parked window up to 60 pt above its predicted spot, inside the region parking already checked. Parking verification and snapshot drift both accept that spot, so the window is not rewritten on every check. A window parked below its display shows a one-point row, not a column; treat it as offscreen. Horizontal navigation keeps only an app's small vertical clamp, so a column leaving its parked corner reaches the ribbon.
Defi no longer reconfigures display origins. Delete the arrangement controller, pointer routing and warps, desk coordinates, the saved display-arrangement.json state, the shutdown restore, and the displayArrangement and displayPointerWarps status keys. Commands and pointer navigation use native display coordinates, and the active event tap only sees key events again.
This reverts commit da43598.
The staircase is a fixed point of isolatedDisplayArrangement, so once macOS commits it to the session and Defi has no usable desk record, reconcile took it as a new native arrangement and saved desk == technical. Keep a known desk instead, or report displayArrangement=ambiguous without routing.
script/test_coverage.py runs the non-desktop suites with coverage in a separate scratch path and writes per-module and per-file line coverage.
…intake as unit tests These InputMonitor outcomes were only proven by desktop tests that posted real events; the unit tests assert the same routed actions and commands.
Neither needs Accessibility or a fixture window, so they no longer wait for the desktop reservation.
…ests Dispatching events to the window directly removes the app activation race that made the desktop version fail three times in ten runs.
A skip counts only when the test prints a lane marker; result.json lists it under desktop.not_run. The run fails unless exactly one defi-daemon exists before the tests and after the session restore.
Replace 47 desktop tests with 12. Each opens and closes its own fixtures, one preflight fails loudly without Accessibility, Screen Recording, or with Reduce Motion on, and two-display and perf tests skip only as named lanes. Every hotkey manager now stops in a defer: leaked Carbon registrations from earlier tests made the reservation tests order-dependent. The observing platform stops its event monitor, whose unretained display callback crashed xctest after deallocation.
A window moved to a taller display grew only its height, so the resize ran at the source position and macOS clamped it to the smaller display. A busy app then kept that height. Height growth now defers like width growth. Found by the two-display desktop test, which failed in 1 of 8 runs.
Cooperative activation refuses the test runner while another app is active, which made the Carbon and Settings tests depend on the user's frontmost app. The fixture yields activation, AX focus brings fixtures forward, and the waits pump AppKit events, which carry activation. Overview captures retry the way discovery ticks do.
LaunchServices can return -600 for open just after the previous instance exits; restore-session failed this way in 10 runs across 7 worktrees. start() relaunches only while no daemon runs, so a retry cannot create a second instance.
The global frontmost app can change outside the desktop reservation, so the unit test checks this process's activation instead.
Defi re-submits a window whose app keeps a size it did not ask for. The commit read during that write's quarantine is deferred: the clamped frame lies between where the window was and its target. It still counted as a fresh observation, which cleared the mismatch history, so the width learning never saw the clamp persist and the window was re-submitted every 0.85 s forever. A deferred or stale commit read no longer counts as a fresh frame observation.
Native size constraints are window widths, but the layout applied them to the column slot, which also holds the gaps. A window with a 1500 pt minimum was asked for 1490 pt; the app kept 1500, the 10 pt overshoot was learned as width rounding, the compensated request dropped to 1480, and Defi re-submitted the window forever. Learned bounds were slot widths measured at one column position, so they also drifted when a column moved between an outer and an inner gap. Width bounds are now the window's own width everywhere: learning stores the observed window width and the layout adds the column's gaps.
A width rounding learned before a window's minimum was known (System Settings: 12 pt, persisted across restarts) narrowed every request below that minimum. The app widened it back each time and Defi re-submitted the window forever.
An offscreen column's native target is an anchor one point inside the display. macOS keeps some windows a title bar higher there (Xcode by 20 pt), which the parking check accepts but the landing check did not: the overlay covered the sliver for 2 s and logged a timeout after every motion on that display. The landing now waits only for targets showing more than a sliver; the others are out of view, like a leaving window.
While a native focus change is pending (a window opening on one display takes focus), a layout skips every window on the other displays. Those windows had no ribbon target, so the idle display read them as leaving its ribbon and presented in the same millisecond as the moving display, hiding and restoring its borders for nothing. A skipped window no longer counts as leaving.
There was a problem hiding this comment.
All reported issues were addressed across 112 files
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
Swift 6.3 rejects capturing a local Mutex in closures isolated to the navigation actor. Share a Sendable command log instead.
ApprovabilityVerdict: Needs Human Approval Macroscope's review found this PR requires human approval — This PR replaces native window motion with a default-on, per-display captured overlay and adds display staircase, parking, pointer-routing, and restoration behavior across the runtime. The breadth and impact on existing paths require human review rather than routine approval. Notes:
You can add or adjust custom eligibility rules. Learn more. |
Apply the per-window hidden state when a border hands off to a new selection, and read the ring's visibility from the overlay itself.
A window moving from view to a sliver anchor is neither awaited nor leaving, so a slow app could show it when the overlay lifts. It now counts as leaving and the landing waits until it is out of view.
Validate the stored desk once when it loads, so the staircase fallback never adopts a corrupted record either.
Make two AppKit tests independent of the environment: the cheatsheet test waits for the fitted frame without a fade (Reduce Motion), and the settings search test restores the activation policy it changes.
Restore coverage for the keyboard focus input guards, which no test exercised, and check that inactive borders skip hidden windows.
Document the bottom-edge and least-overlap parking fallbacks and the observation-mismatch arrangement status.
On a screen too small for the shortcut list the panel keeps its largest frame. Wait for the frame to hold still and allow that largest frame.
NSPanel reads the content rect relative to the screen it is given, so a global frame placed the cheatsheet off its display whenever that display's origin is not zero, such as a secondary display or the main one in the display staircase. A test now opens it on every connected display.
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
You’re at about 94% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
#152 made minimumTiledWidth and maximumTiledWidth the window's own width, with the layout adding the column's gaps. Native resize learning still compared them with column widths, so it dropped a minimum the window honored and ignored sibling bounds by one gap.
Hidden windows waited in the bottom-right corner since #152, chosen to leave other displays clear without the staircase. The staircase came back, and any native motion the overlay misses now crossed the screen diagonally. Park at the side edge nearest the window, at its own height, as before #152 and as Rift, OmniWM and paneru do, so a missed motion is a short horizontal slide. Corners and the bottom edge remain the fallback when both side edges touch another display.
* fix(runtime): keep app width bounds across native resize * fix(daemon): keep gesture widths out of constraint learning Restart the width-mismatch stability clock for the gesture window so a user-held width cannot age into an app constraint after release, and ignore post-release frames that match a recent Defi write. * fix(daemon): ignore only pending Defi writes after a resize release A completed write equals the pre-gesture frame, so dragging a window back to its start width must still count as the user's width. * docs(verify-defi): add native column resizing journey * fix: address PR review feedback (#145) Match a late post-release frame only against in-flight write targets, and document the helper build step in the column-resizing journey. * fix: address PR review feedback (#145) Size-only in-flight writes keep the window at its completed origin, so match late frames against that origin rather than the requested point. * fix: address PR review feedback (#145) Apply an overlay drop on release only when the window moved, so a preview left by an interrupted drag can never turn an edge resize into a drop. * fix(runtime): read width bounds as window widths after #152 #152 made minimumTiledWidth and maximumTiledWidth the window's own width, with the layout adding the column's gaps. Native resize learning still compared them with column widths, so it dropped a minimum the window honored and ignored sibling bounds by one gap. * fix(ribbon): keep the overlay stretch for the whole edge drag Learning the width on every sync tick laid the workspace out mid-drag. Each layout restarted the overlay motion: neighbors sprang to the latest layout instead of following the edge, and a window entering the view slid in from its parked corner. While the overlay holds the gesture, the width is now learned once, from the released frame. Cycle width also steps from a width off the presets, such as a native resize, to the next wider or narrower preset instead of restarting. * fix(runtime): keep a dragged left edge where it was released The overlay scrolls the ribbon with a dragged left edge, but the learned width grew the window to the right on release, so the columns before it sprang back. The ribbon now scrolls by the edge's travel. * fix(daemon): read the released frame from the WindowServer Desktop reads wait while the overlay draws, so the snapshot taken at release could hold a frame from before the drag and the width was lost. * fix(daemon): end a held gesture when the button reads up The button state can read up one snapshot before the gesture's release event lands. That snapshot finished the gesture as inactive, so the release lost its start frame and the overlay-held width was never learned. * fix(ribbon): keep the stretch on the ribbon and within the window's widths A parked window joined the stretch at its corner's height, so it slid in along the bottom. The stretch now clamps to the window's own width bounds and masks the start picture, which spilled into the gap once narrowed. A request just under a window's minimum is no longer learned as rounding, and a user resize clears stale rounding: both centred the window and widened its gaps. * fix(ribbon): start the stretch at the first resizing drag event The stretch waited for a snapshot to see the resize, so on a fast drag the native edge moved while its neighbours stood still. A drag event now starts it as soon as the WindowServer shows the pressed tiled window resizing. The stretch measures shifts from the window's ribbon place instead of a native frame already resized, takes the held edge from the pointer, and keeps a window still travelling to or from its parking corner at its ribbon place. * fix(ribbon): start the stretch when a side edge is dragged A terminal resizes in whole cells, so waiting for its native edge to move held the neighbours still for over 100 ms. A press on a tiled window's side edge now starts the stretch at the first horizontal move of 2 points. * fix(ribbon): keep the stretch smooth and other workspaces still on release During a stretch the daemon kept snapshotting the desktop. Its window list waited on the WindowServer, busy with the live resize, while holding the connection the overlay commits through, so the stretch froze for half a second. The daemon now skips snapshots until release, which learns the width. A release re-parks other workspaces' windows. Without ribbon history they started shifted by the ribbon's motion and crossed the screen into their corner; they now stay where they are. * fix(parking): park hidden windows at the nearest side edge again Hidden windows waited in the bottom-right corner since #152, chosen to leave other displays clear without the staircase. The staircase came back, and any native motion the overlay misses now crossed the screen diagonally. Park at the side edge nearest the window, at its own height, as before #152 and as Rift, OmniWM and paneru do, so a missed motion is a short horizontal slide. Corners and the bottom edge remain the fallback when both side edges touch another display. * fix: address PR review feedback (#145) - Drop a contradicted width bound even when the column already has the released width, so a stacked window resized past its maximum keeps it. - Advance a settling release instead of restarting it on every button-up snapshot, so the settlement times out with animations off. - Release the gesture scroll anchor once a released resize is learned, so it no longer undoes the left-edge scroll shift. - Read the side-edge press at the event's location, not when the queue runs. - Keep the stretch width range under a native maximum below 240 pt.
Why
The proxy overlay from #150 only ran with one display. With two displays every motion jumped, drags had no placeholder, and the per-frame Accessibility animation stayed as a second motion system. This makes the overlay the only motion system on every display, and fixes the multi-display problems found while testing it on a real two-display desk.
What changed
animation.duration_msandanimation.window_proxies. Native windows get one write per motion. Without Screen Recording, with Reduce Motion, or withanimation.enabled = false, windows move at once.displayArrangement=ambiguousand nothing is routed.script/test_coverage.pyreports coverage per module.Scope
Motion, parking, display arrangement, border hiding during motion, width learning for app-enforced sizes, and the test suite. Overview rendering, keyboard bindings and workspace routing rules are unchanged.
Tradeoffs
Blast Radius
Every animated motion, parking of hidden windows, multi-display pointer routing and the display arrangement file. Config files that still set
duration_msorwindow_proxieskeep loading, since unknown keys are ignored.Verification
python3 script/verify.py localpassed on the final commit.python3 script/verify.py desktoppassed on the final build: 11 tests on two displays, session restored, one daemon before and after. The rewritten desktop suite passed three full runs in a row earlier on the branch.settling=0anddrift=0[]. Workspace switches fade in 334 to 348 ms with no landing timeout.Note
Run the ribbon proxy overlay on every display as the only motion system
AnimationConfigin ConfigModels.swift keeps onlyenabled;duration_ms,window_proxies, the settings search entry, and the appearance UI control are removed. Workspace transitions, mouse drops, and lifecycle reflows now animate purely from the enabled flag.parkedFrameutility in Parking.swift with new display-staircase detection in DisplayArrangement.swift.duration_ms = 0to get instant movement now animate wheneverenabledis true; checkAnimationConfigdecoding in ConfigModels.swift and callers in DaemonAnimation.swift. Public API removals inAnimation.swift,AXFrameCoordinator, andFrameCommit(spring samples, lane plans,ParkingSide,resolveParkingPlacement) break any out-of-tree consumers.Macroscope summarized bab2202.