Context
Reported by @tobiu 2026-09-01 against the Workstation on dev@2f8fc79792: "maximise => first jumps fullSize for a couple of frames, then instant jumps back to the original small size, then expands with an animation. That glitch destroys all the beauty. Broken for the other direction too, but not as bad."
Three beats, in that order, and they map one-to-one onto the current sequence.
Live latest-open sweep at 2026-09-01T17:29:17Z: 8 newest open issues plus targeted searches on maximize|maximise|FLIP|animation. Every maximize ticket is CLOSED (#17928 feature, #18003/#18008/#18021 continuity/arms, #17895 FLIP windowId routing) and none covers the arming order. No equivalent open issue.
The Problem
Mechanism corrected 2026-09-09, measured on the rendered fixture. Everything below the
horizontal rule in the previous revision blamed the App-Worker→main round trip between the
geometry flush and playFlip. That was wrong, and both Fix options it produced were shaped
around it. The exposure is not a round trip and no delay value or re-ordering across the worker
boundary was needed. The reported symptom, the ACs and the Out of Scope are unchanged.
DockFlip.play opens with a bounded outgoing-detach poll (src/main/addon/DockFlip.mjs:714):
if (!preservedMarkerSet && !landedInPlace) {
while (!playState.interrupted && frame < maxFrames && first.els.length > 0
&& first.els.every(el => el.isConnected)) {
await this.#nextFrame(playState);
frame++
}
}It waits for the OUTGOING tree's markers to disconnect, because a replacement tree's old nodes
would otherwise satisfy a naive presence-poll instantly and measure identical geometry.
A maximize has no outgoing tree. The pane paints the measured rect on the same DOM node and
nothing re-projects, so every(el => el.isConnected) never falsifies and the loop spends its whole
maxFrames budget — with the committed geometry already flushed and no inverse transform installed.
That is the exposure, in both directions.
Measured (per-frame sampler over getBoundingClientRect() + computed transform, rendered
dock-maximize fixture): 16 exposed frames on maximize (t+134→383ms) and 16 on restore
(t+84→333ms) — roughly 250ms each, against the ticket's original guess of "a couple of frames".
The invert arrived at frame 24 (maximize) and 22 (restore).
Both directions expose the same count. The reason the operator read restore as "not as bad" is
perceptual, not temporal: a maximize flashes full-screen, a restore flashes at its own small flow
slot.
The Architectural Reality
DockFlip already has two bypasses for this, and the maximize play took neither.
hasPreservedMarkerSet() (:171) — the native atomic-move path. Requires an ancestor lineage
change as its falsifier, which a same-node resize never produces. Cannot classify a maximize.
hasLandedInPlace() (:201) — written for exactly this case. Its own docblock: "A committed
same-node resize keeps every marker node AND its ancestor lineage, so the lineage-based
hasPreservedMarkerSet() can never classify it: the replacement-tree branch would burn its whole
detach poll on nodes that never detach, exposing the committed layout unanimated before the
inverse installs (the double-take)."
The defect was diagnosed in-tree, and the remedy already exists. It is gated behind a
consumer-declared geometryOnly flag, and Maximize#playFlip (:464) never passed it. The dock
refresh path does (src/dashboard/dock/Workspace.mjs:2673, from the reconciler's actual result).
The Fix
Maximize#playFlip declares geometryOnly: true.
Corrected 2026-09-09, @neo-opus-grace in review. I first wrote that the flag is "an admission
request, not an assertion — a maximize that ever does coincide with a re-projection falls back on its
own." That is false and it mattered. Against a pending swap all three hasLandedInPlace() checks
pass: the outgoing markers are still the exact set, their lineage is unchanged, and the transition's
own geometry write supplies the movement. The addon does not re-detect a swap; geometryOnly is a
consumer declaration that none can be pending, as the param's own docblock says.
The declaration holds on this path by construction, twice over — the gesture path awaits the owner's
refreshPromise before applying, and every route that can run while a swap is pending (the
refresh-owned reapply, operation-driven clears, fail-safes) lands with animate: false and never
reaches the call. That support is stronger than the claim it replaces, and it lives at the call site,
which now carries it.
It adds no mutation, so DockFlip's existing styleSnapshot cleanup keeps owning every style the
transition installs — AC-6 by construction rather than by new bookkeeping.
Prior art, which reframes this ticket. hasLandedInPlace was not built for a hypothetical: it
landed in fix(dock): classify same-node resizes as landed-in-place, skipping the stage-A double-take
(#16391 / PR #16403, @neo-kimi-phoebe, 2026-08-03) — the same symptom name, five weeks earlier. So
this is not an undiscovered mechanism; it is a consumer that never opted into a fix built for its
exact class. The engine half has been correct since August.
The two options in the previous revision — returning the resolved gap so the worker could compute
the invert, or moving the whole transition onto main — are both withdrawn. They addressed a
mechanism that does not exist, and each would have restructured the geometry-ownership contract
(rectStyle's calc() against the token is deliberate) to fix a bounded poll.
Acceptance Criteria
Out of Scope
- Whether maximize should target the viewport or the dock container, and whether it gains a gap/shadow (separate operator decision).
- Reveal/rail preview geometry.
- Cross-window drag previews (#17578).
Refs #17836 · #17928
Context
Reported by @tobiu 2026-09-01 against the Workstation on
dev@2f8fc79792: "maximise => first jumps fullSize for a couple of frames, then instant jumps back to the original small size, then expands with an animation. That glitch destroys all the beauty. Broken for the other direction too, but not as bad."Three beats, in that order, and they map one-to-one onto the current sequence.
Live latest-open sweep at 2026-09-01T17:29:17Z: 8 newest open issues plus targeted searches on
maximize|maximise|FLIP|animation. Every maximize ticket is CLOSED (#17928 feature, #18003/#18008/#18021 continuity/arms, #17895 FLIP windowId routing) and none covers the arming order. No equivalent open issue.The Problem
DockFlip.playopens with a bounded outgoing-detach poll (src/main/addon/DockFlip.mjs:714):if (!preservedMarkerSet && !landedInPlace) { while (!playState.interrupted && frame < maxFrames && first.els.length > 0 && first.els.every(el => el.isConnected)) { await this.#nextFrame(playState); frame++ } }It waits for the OUTGOING tree's markers to disconnect, because a replacement tree's old nodes would otherwise satisfy a naive presence-poll instantly and measure identical geometry.
A maximize has no outgoing tree. The pane paints the measured rect on the same DOM node and nothing re-projects, so
every(el => el.isConnected)never falsifies and the loop spends its wholemaxFramesbudget — with the committed geometry already flushed and no inverse transform installed. That is the exposure, in both directions.Measured (per-frame sampler over
getBoundingClientRect()+ computed transform, rendereddock-maximizefixture): 16 exposed frames on maximize (t+134→383ms) and 16 on restore (t+84→333ms) — roughly 250ms each, against the ticket's original guess of "a couple of frames". The invert arrived at frame 24 (maximize) and 22 (restore).Both directions expose the same count. The reason the operator read restore as "not as bad" is perceptual, not temporal: a maximize flashes full-screen, a restore flashes at its own small flow slot.
The Architectural Reality
DockFlipalready has two bypasses for this, and the maximize play took neither.hasPreservedMarkerSet()(:171) — the native atomic-move path. Requires an ancestor lineage change as its falsifier, which a same-node resize never produces. Cannot classify a maximize.hasLandedInPlace()(:201) — written for exactly this case. Its own docblock: "A committed same-node resize keeps every marker node AND its ancestor lineage, so the lineage-basedhasPreservedMarkerSet()can never classify it: the replacement-tree branch would burn its whole detach poll on nodes that never detach, exposing the committed layout unanimated before the inverse installs (the double-take)."The defect was diagnosed in-tree, and the remedy already exists. It is gated behind a consumer-declared
geometryOnlyflag, andMaximize#playFlip(:464) never passed it. The dock refresh path does (src/dashboard/dock/Workspace.mjs:2673, from the reconciler's actual result).The Fix
Maximize#playFlipdeclaresgeometryOnly: true.Corrected 2026-09-09, @neo-opus-grace in review. I first wrote that the flag is "an admission request, not an assertion — a maximize that ever does coincide with a re-projection falls back on its own." That is false and it mattered. Against a pending swap all three
hasLandedInPlace()checks pass: the outgoing markers are still the exact set, their lineage is unchanged, and the transition's own geometry write supplies the movement. The addon does not re-detect a swap;geometryOnlyis a consumer declaration that none can be pending, as the param's own docblock says.The declaration holds on this path by construction, twice over — the gesture path awaits the owner's
refreshPromisebefore applying, and every route that can run while a swap is pending (the refresh-owned reapply, operation-driven clears, fail-safes) lands withanimate: falseand never reaches the call. That support is stronger than the claim it replaces, and it lives at the call site, which now carries it.It adds no mutation, so
DockFlip's existingstyleSnapshotcleanup keeps owning every style the transition installs — AC-6 by construction rather than by new bookkeeping.Prior art, which reframes this ticket.
hasLandedInPlacewas not built for a hypothetical: it landed infix(dock): classify same-node resizes as landed-in-place, skipping the stage-A double-take(#16391 / PR #16403, @neo-kimi-phoebe, 2026-08-03) — the same symptom name, five weeks earlier. So this is not an undiscovered mechanism; it is a consumer that never opted into a fix built for its exact class. The engine half has been correct since August.The two options in the previous revision — returning the resolved gap so the worker could compute the invert, or moving the whole transition onto main — are both withdrawn. They addressed a mechanism that does not exist, and each would have restructured the geometry-ownership contract (
rectStyle'scalc()against the token is deliberate) to fix a bounded poll.Acceptance Criteria
dockMaximizeMotion; the one-shot intent isMaximize#motion, set to'instant'by operation-driven clears, fail-safes and re-projection continuity. When it is'instant',animateis false andcaptureFirst/playFlipare never reached at all.)component/dashboard/DockMaximize.spec.mjsand the continuity arms that #18003/#18021 hardened.DockFlip's existingstyleSnapshotcleanup, or by an equally idempotent path — an interrupted transition must not strand a transform.Out of Scope
Refs #17836 · #17928