feat: keep automatic stacks within application size constraints

QE
19h ago
+1038-39

Why

Stacked windows got equal slots even when an app could not shrink that far. The app refused the height, Defi retried it on every desktop sync, and the window overflowed its slot. With the proxy overlay from #150 the refusal is also visible: the overlay draws the picture at the asked height, then fades to a native window of another height.

Closes #93.

What changed

This PR stacks on #150 (feat/window-proxy-ribbon).

  • Window keeps a verified minimumTiledHeight from the existing optional private constraint read. Without that read, stacks behave as before.
  • Sources/DefiCore/StackConstraints.swift splits a column by water-filling within each window's verified minimum and maximum height. Unconstrained stacks are split exactly as before.
  • join-window refuses a stack that cannot fit and leaves the workspace untouched. The CLI prints cannot join: stack needs 1650pt but only 1367pt is available.
  • When a stack stops fitting after a monitor, reserved-area or constraint change, the longest top run stays and the rest moves into new columns to its right. Columns never merge back on their own.
  • A single window taller than the viewport is targeted at its minimum, top-aligned, and listed as oversized-stack-windows= in defi status.
  • New on top of #150: a mouse drag only offers a stack when that stack fits. Otherwise the drop reorders the column. The overlay's stack preview uses the same slot heights the layout will apply.

Scope

Stack heights, join-window, and the drag-to-stack target from #150. Overview drops into a stack are unchanged. If such a stack cannot fit, the separation rule splits it on the next sync.

Tradeoffs

  • Targets now match what the app accepts, so the overlay's per-window settle learning only sees small residual adjustments. It never has to absorb a refused height.
  • Allocation ignores per-request TiledSizeAcceptance. Changing a request from it would start a resize battle.
  • The window server reports a minimum no larger than the current frame. A window that opens below its own minimum is read with a smaller minimum until the next topology read. Separation then catches up.

Blast Radius

Pure layout and runtime code, plus one more field from a constraint read that already ran. No new Accessibility writes, no per-frame work, no overlay or border code. The drag change only adds a feasibility check and a slot-height computation per pointer update, both pure math over one column.

Verification

  • python3 script/verify.py local passes on this head. One earlier run failed FrameGeometryReadTests on a 3 s timing wait outside this diff. The suite passed alone and the full rerun passed.
  • On the previous #150 head (7f6d7608), python3 script/verify.py desktop <run> --wait --filter 'DesktopE2ETests/(testAppliedTargetConvergesWithRealWindowFrame|testManagedResizeAnimationConvergesWithAdaptiveSizeWrites|testReenteringWindowJoinsFirstAnimatedRibbonSample|testHorizontalAnimationFrameWritesPositionWithoutSize)' ran 4 tests, 4 passed, 0 skipped. Accessibility was available, and the session restore passed. One earlier run passed the same 4 tests but its restore hit a LaunchServices open error -600. A manual restore under the desktop lock then succeeded.
  • On that same head, python3 script/desktop_lock.py --wait python3 .agents/skills/verify-defi/stacks_check.py on the installed build: all six scenarios passed, with one daemon left afterwards.
  • New unit tests cover a drag into a stack that cannot fit and a preview that keeps the dragged window's minimum.
  • Not done: Computer Use with a real drag over constrained apps.
  • The latest #150 commits only change AGENTS.md and the duration_ms = 0 workspace switch. They do not touch stacks or drag, so the desktop checks were not repeated on this head.

[!NOTE]

Add automatic stack separation and window height constraints to Defi layout

  • Windows now carry a discovered minimumTiledHeight (Window.swift, WindowBorderPrivateProviders.swift), and stack slots are allocated with a water-filling algorithm that honors minimum and maximum heights per window (StackConstraints.swift).
  • Stacks that cannot fit are handled at three points: joinFocusedWindow rejects infeasible joins with a StackRejection error before mutating state, mouse drops hide stack targets that cannot fit, and synchronizeScrollOffsets automatically separates infeasible columns into right-adjacent columns while preserving window order, focus, and scroll anchors (ScrollReconciliation.swift).
  • Separated columns are never re-merged, even after the viewport grows. Single windows whose minimum exceeds the viewport remain unchanged and are reported as oversized-stack-windows in defi status.
  • Adds an executable end-to-end verifier (stacks_check.py) with a ClampFixture DEFI_FIXTURE_MIN mode, plus unit tests for allocation, rejection, separation, and mouse previews.
  • Risk: existing workspaces with height-constrained windows may separate on first reconciliation after this change; check separateInfeasibleStacks in StackConstraints.swift and the computeLayout / acceptedTiledSizes changes in Layout.swift, which now use constrained slot geometry instead of equal-height slots.

Macroscope summarized 488a223.