Skip to content

feat(ribbon): run the proxy overlay on every display as the only motion system - #152

Merged
qeude merged 86 commits into
mainfrom
feat/multi-display-proxies
Oct 24, 2026
Merged

qeude merged 86 commits into
mainfrom
feat/multi-display-proxies

Conversation

@qeude

@qeude qeude commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

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

  • One overlay per display. Each display presents, lands and hides its own borders on its own, exactly as a single display does. A display where nothing moves presents nothing.
  • The per-frame Accessibility animation is deleted, with animation.duration_ms and animation.window_proxies. Native windows get one write per motion. Without Screen Recording, with Reduce Motion, or with animation.enabled = false, windows move at once.
  • Hidden windows park in a corner of their own display, chosen so the parked frame overlaps no other display. The model accounts for where macOS really keeps a window.
  • The display staircase stays and is hardened. Defi never adopts its own staircase as the user's desk. When only the staircase is known, status shows displayArrangement=ambiguous and nothing is routed.
  • Window close is detected as fast as on main. The moving flag clears when the spring stops, and a closed window counts as landed.
  • An app that enforces its own size or position converges instead of being re-submitted forever. Width bounds are the window's own width, rounding never asks below the minimum, and a deferred check no longer resets the mismatch history. This idle loop also exists on main.
  • A window moved to a taller display moves before it grows, so macOS no longer clamps its height to the smaller display.
  • The test suite is smaller and deterministic. Desktop tests use Defi-owned fixture windows only, behind one preflight, with a separate two-display lane. Unit tests were merged into tables, and tests that restated constants or internals were removed. script/test_coverage.py reports 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

  • A single step settles in about 0.24 to 0.30 s on the spring instead of the old 125 ms duration. Rapid commands keep their velocity.
  • Without Screen Recording there is no animation. This is intended.
  • A drop on another display lands at the end of the ribbon. Dropping at the pointer is in feat: drop dragged windows at the gap under the pointer on any display #147.
  • At rest the daemon uses about 1.5 % CPU, all in window discovery polling. The loop that cost 3.7 % is gone.
  • The first landing of a new window whose size limit is not reported yet can still wait about 1 s.

Blast Radius

Every animated motion, parking of hidden windows, multi-display pointer routing and the display arrangement file. Config files that still set duration_ms or window_proxies keep loading, since unknown keys are ignored.

Verification

  • python3 script/verify.py local passed on the final commit.
  • python3 script/verify.py desktop passed 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.
  • On the real desk: no parked window or partial column touches the other display, the pointer crosses the desk edge, and stopping Defi restores the side-by-side arrangement.
  • With a fixed-width window and System Settings managed, 30 s at rest show 0 re-submits, settling=0 and drift=0[]. Workspace switches fade in 334 to 348 ms with no landing timeout.
  • Window close latency matches main: median 480 ms versus 486 ms over 6 closes each.

View guided diff Turn on auto-fix

Note

Run the ribbon proxy overlay on every display as the only motion system

  • Replaces the single shared presenter and panel with one proxy presenter, overlay panel, and motion token per display in RibbonProxyRenderer.swift, RibbonProxyPresenter.swift, and MacOSPlatform.swift. Each display independently decides whether its overlay moves, and carries, background captures, and border hiding are display-scoped.
  • Removes the native Accessibility animation pipeline: interpolated frames, animation lanes, retargeting velocities, spring clock types in Animation.swift, and the staged write path in AXFrameCoordinatorWrites.swift are deleted. Frame writes now apply final positions and sizes directly and verify after a short fixed delay.
  • AnimationConfig in ConfigModels.swift keeps only enabled; 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.
  • Layout width constraints now count inter-window gaps outside each window's own width in Layout.swift, and parking moves to the display-relative parkedFrame utility in Parking.swift with new display-staircase detection in DisplayArrangement.swift.
  • Adds verification tooling: source-coverage reporting in test_coverage.py, not-run desktop lane accounting plus a daemon-count guard in verify.py, and launch retry in desktop_session.py. Many test suites are consolidated into parameterized cases.
  • Risk: configs that used duration_ms = 0 to get instant movement now animate whenever enabled is true; check AnimationConfig decoding in ConfigModels.swift and callers in DaemonAnimation.swift. Public API removals in Animation.swift, AXFrameCoordinator, and FrameCommit (spring samples, lane plans, ParkingSide, resolveParkingPlacement) break any out-of-tree consumers.

Macroscope summarized bab2202.

qeude added 30 commits October 23, 2026 21:25
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.
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.
qeude added 7 commits October 24, 2026 01:57
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.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment thread Sources/DefiDaemon/DaemonAnimation.swift
Comment thread Sources/DefiMacOS/MacOSPlatform.swift
Comment thread Sources/DefiCore/Parking.swift
Comment thread Tests/DefiRuntimeTests/PointerFocusTests.swift
Comment thread Sources/DefiDaemon/DefiDaemon.swift
Comment thread CONFIGURATION.md
Comment thread Tests/DefiDaemonTests/DaemonCommandPolicyTests.swift
Comment thread Tests/DefiDaemonTests/DaemonCommandPolicyTests.swift
Comment thread Tests/DefiMacOSTests/WindowBorderTests.swift
Comment thread Tests/DefiMacOSTests/PlatformEventTests.swift
Swift 6.3 rejects capturing a local Mutex in closures isolated to the
navigation actor. Share a Sendable command log instead.
@macroscopeapp

macroscopeapp Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Approvability

Verdict: 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:

  • No code objects were reviewed. Approvability was decided on eligibility alone.

You can add or adjust custom eligibility rules. Learn more.

qeude added 8 commits October 24, 2026 03:06
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.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment thread Tests/DefiMacOSTests/CheatsheetTests.swift
@qeude
qeude merged commit d31f1bd into main Oct 24, 2026
4 checks passed
@qeude
qeude deleted the feat/multi-display-proxies branch October 24, 2026 11:24
qeude added a commit that referenced this pull request Oct 24, 2026
Adapt the PR to #152 and #153: keep the release-time translation guard on
the #153 drop preview, keep both verify-defi journeys, and drop the
removed animatesSize and isReentering arguments from the motion write
test helper.
qeude added a commit that referenced this pull request Oct 24, 2026
#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.
qeude added a commit that referenced this pull request Oct 25, 2026
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.
qeude added a commit that referenced this pull request Oct 25, 2026
* 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant