Context
Found by #18532's witness arm (the fourth test of test/playwright/e2e/workstation/WorkstationNativePopupOverPopupNL.spec.mjs, on its branch) on dev 42b80fee81, 2026-10-02, headless Chrome on a real display with 67 px of window chrome. The source vessel is parked by the real native-titlebar gesture (dwell → commitNativeWindowDrop → the host's parkTearOutVessel, receipt parked: true); the coordinator then refuses the handoff (clearNativeWindowDropCandidate — the path a declined drop, a failed embodiment and a cancelled gesture all take); the restore receipt reads:
rect : {x: 796, y: 123, width: 320, height: 240} // the frame the park took the window from
frame : {x: 796, y: 56} // what moveTo was handed
moved : true, admitted: true, terminal: true, addonRestored: falseThe window re-shows at y 56 — exactly chrome.top (67) above where the user left it. The arm's assertion against the parked-from frame fails by 67 on every cycle.
The Problem
A refused native handoff, and every non-commit terminal of a pointer-path conversion while the vessel is parked (cancel, reject, a host-routed disconnect — VesselPark's compensating arm), restores the user's window one title bar above where it was. On the native path the user never sees the park, so what they see is the window they were holding jump upward when the drop is declined. The engine reports success (admitted: true), so nothing downstream learns the restore was wrong.
The Architectural Reality
Four lines carry one rect through two coordinate planes — the hazard #18532 named:
| step |
source |
plane |
| sample |
apps/workstation/view/VesselWorkspace.mjs resolveVesselConversionSourceRect → record.outerRect ?? record.innerRect |
frame, since 48591f31b6 (#19225, PR #19232, 2026-09-26: the conversion admission must read the frame the pointer rides) |
| record |
the same value reaches VesselPark#onConversionIn as sourceRect — pointer path VesselWorkspace.mjs:504 (data.record?.sourceRect), native path :558 (resolveVesselConversionSourceRect directly) — and is kept as preConversionRect |
frame |
| restore |
VesselPark#finishTerminal → restore(vessel, vessel.preConversionRect, true) → NativeVesselTransaction reshowVessel({rect, terminal: true}) |
handed on as rect |
| convert |
src/dashboard/dock/window/NativeVesselTransaction.mjs:710 toFrameOrigin(rect, windowId) = rect − chrome, documented at :238–244 as converting a content rect into the frame origin moveTo consumes; pinned by test/playwright/unit/apps/workstation/Workspace.spec.mjs ("large-over-small park … restores the same exact popup": rect {420, 240} → frame {420, 173} with chrome.top 67) |
content → frame |
Before #19225 the recorded rect was the window's innerRect (content), so the conversion was right, and #18515 had fixed the terminal path for exactly that content contract. #19225 moved the sample to the frame for the admission — correctly: ADR 0029 §2.8.6 reads the park against the source's outer frame — and the restore anchor moved with it, uncorrected. The same section's witness states the restore's obligation: "restored the exact original frame".
toFrameOrigin cannot tell which plane it was handed: a frame rect and a content rect are both {x, y}.
The Fix
The park RECORD is a content rect; the admission SAMPLE stays the frame.
VesselWorkspace.mjs: one host helper (name at implementation) that snapshots Neo.manager.Window.get(windowId).innerRect for a tear-out vessel, used as sourceRect by both onConversionIn call sites (:504, :558). resolveVesselConversionSourceRect is untouched — the sensor keeps the frame #19225 gave it.
VesselPark#onConversionIn's docblock names the plane of sourceRect (content), so the next author does not re-derive it.
- Unit arm beside the existing park/restore arms in
Workspace.spec.mjs: with a source window publishing chrome.top 67, a park record taken from it and a terminal restore hand moveTo the frame the park took the window from — red on dev (frame 67 px high), green with the fix.
- The #18532 e2e arm is this ticket's end-to-end witness: red by 67 on
dev, green with the fix; it lands under #18532 once this merges.
Contract Ledger
| Surface |
Authority |
Behavior |
Edge case |
Docs |
Evidence |
VesselPark#onConversionIn sourceRect — the restore anchor |
the transaction's content-rect restore contract (toFrameOrigin); ADR 0029 §2.8.6 ("restored the exact original frame") |
the host records the vessel's inner rect; a terminal restore re-shows the frame where the park took it from |
a window publishing no chrome: inner = outer, zero offset, behaviour unchanged |
onConversionIn + helper JSDoc |
unit arm red-first; the #18532 e2e arm |
conversion admission sample — resolveVesselConversionSourceRect |
#19225 / ADR 0029 §2.8.6 |
unchanged: the frame |
— |
existing |
#19225's unit arm stays green |
Decision Record impact: aligned-with ADR 0029 §2.8.6 — no amendment; the record already states both the admission plane and the restore's obligation.
Acceptance Criteria
Out of Scope
The admission plane (#19225 stands). The addon's drag follow and the non-terminal resumeWindowDrag route. Making toFrameOrigin plane-aware (Avoided Traps). The witness arm itself — #18532 lands it.
Avoided Traps
- Teaching the transaction the plane (
rectPlane: 'outer' ⇒ skip toFrameOrigin): the same reshowVessel receives content rects from conversion-out and from the host's own unit contract; a plane switch there fixes one caller by breaking the others. The record site is the one place that knows what it recorded.
- Subtracting the chrome back at the record site: equivalent to reading
innerRect, but it re-derives what manager.Window already publishes (outerRect.x = innerRect.x − chrome.left, src/manager/Window.mjs:113).
Related
#18532 (the witness; blocked by this) · #19373 (the conversion-out half, split from AC-3) · #19225 / PR #19232 (the plane switch) · #18514 / #18515 (the content-rect terminal fix this restores) · #15396 / #15436 (the park-never-close contract) · #19278 (the clear park) · ADR 0029 §2.8.6
Live latest-open sweep: latest 20 open issues read 2026-10-02T20:25Z — no equivalent; the only hits for re-show / toFrameOrigin / preConversionRect are #18532 and closed predecessors. A2A in-flight sweep: last 30 messages, no claim on the restore plane. Memory sweep: the 2026-09-09 chain recorded sourceRect = innerRect as the restore anchor — this ticket is its regression.
Origin Session ID: 31c0406b-2b5c-42e7-89af-35cbecea62ac
Retrieval Hint: query_raw_memories("parked vessel terminal re-show chrome too high preConversionRect frame plane toFrameOrigin regression")
Context
Found by #18532's witness arm (the fourth test of
test/playwright/e2e/workstation/WorkstationNativePopupOverPopupNL.spec.mjs, on its branch) ondev42b80fee81, 2026-10-02, headless Chrome on a real display with 67 px of window chrome. The source vessel is parked by the real native-titlebar gesture (dwell →commitNativeWindowDrop→ the host'sparkTearOutVessel, receiptparked: true); the coordinator then refuses the handoff (clearNativeWindowDropCandidate— the path a declined drop, a failed embodiment and a cancelled gesture all take); the restore receipt reads:rect : {x: 796, y: 123, width: 320, height: 240} // the frame the park took the window from frame : {x: 796, y: 56} // what moveTo was handed moved : true, admitted: true, terminal: true, addonRestored: falseThe window re-shows at y 56 — exactly
chrome.top(67) above where the user left it. The arm's assertion against the parked-from frame fails by 67 on every cycle.The Problem
A refused native handoff, and every non-commit terminal of a pointer-path conversion while the vessel is parked (cancel, reject, a host-routed disconnect — VesselPark's compensating arm), restores the user's window one title bar above where it was. On the native path the user never sees the park, so what they see is the window they were holding jump upward when the drop is declined. The engine reports success (
admitted: true), so nothing downstream learns the restore was wrong.The Architectural Reality
Four lines carry one rect through two coordinate planes — the hazard #18532 named:
apps/workstation/view/VesselWorkspace.mjsresolveVesselConversionSourceRect→record.outerRect ?? record.innerRectVesselPark#onConversionInassourceRect— pointer pathVesselWorkspace.mjs:504(data.record?.sourceRect), native path:558(resolveVesselConversionSourceRectdirectly) — and is kept aspreConversionRectVesselPark#finishTerminal→restore(vessel, vessel.preConversionRect, true)→NativeVesselTransactionreshowVessel({rect, terminal: true})rectsrc/dashboard/dock/window/NativeVesselTransaction.mjs:710toFrameOrigin(rect, windowId)=rect − chrome, documented at:238–244as converting a content rect into the frame originmoveToconsumes; pinned bytest/playwright/unit/apps/workstation/Workspace.spec.mjs("large-over-small park … restores the same exact popup":rect {420, 240}→frame {420, 173}withchrome.top67)Before #19225 the recorded rect was the window's
innerRect(content), so the conversion was right, and #18515 had fixed the terminal path for exactly that content contract. #19225 moved the sample to the frame for the admission — correctly: ADR 0029 §2.8.6 reads the park against the source's outer frame — and the restore anchor moved with it, uncorrected. The same section's witness states the restore's obligation: "restored the exact original frame".toFrameOrigincannot tell which plane it was handed: a frame rect and a content rect are both{x, y}.The Fix
The park RECORD is a content rect; the admission SAMPLE stays the frame.
VesselWorkspace.mjs: one host helper (name at implementation) that snapshotsNeo.manager.Window.get(windowId).innerRectfor a tear-out vessel, used assourceRectby bothonConversionIncall sites (:504,:558).resolveVesselConversionSourceRectis untouched — the sensor keeps the frame #19225 gave it.VesselPark#onConversionIn's docblock names the plane ofsourceRect(content), so the next author does not re-derive it.Workspace.spec.mjs: with a source window publishingchrome.top67, a park record taken from it and a terminal restore handmoveTothe frame the park took the window from — red ondev(frame 67 px high), green with the fix.dev, green with the fix; it lands under #18532 once this merges.Contract Ledger
VesselPark#onConversionInsourceRect— the restore anchortoFrameOrigin); ADR 0029 §2.8.6 ("restored the exact original frame")onConversionIn+ helper JSDocresolveVesselConversionSourceRectDecision Record impact: aligned-with ADR 0029 §2.8.6 — no amendment; the record already states both the admission plane and the restore's obligation.
Acceptance Criteria
screenX/Y+outerWidth/Heightand manager.WindowouterRect, within 2 px, three cycles: the #18532 arm, red bychrome.topondev42b80fee81, green with the fix.Workspace.spec.mjsunit arm above is red ondevand green with the fix; the existing park/restore arms and #19225's sampling arm stay green.data.logicalRect ?? record.sourceRect,VesselWorkspace.mjs:520) is #19373's question — split out because its origin is the coordinator's pointer-minus-grablogicalSourceRect(DragCoordinator.mjs:1040–1044), whose plane only a headed pointer-drag measurement settles; this ticket leaves that site byte-identical and states the finding there, not here.resolveVesselConversionSourceRectand the conversion admission are byte-identical todev.Out of Scope
The admission plane (#19225 stands). The addon's drag follow and the non-terminal
resumeWindowDragroute. MakingtoFrameOriginplane-aware (Avoided Traps). The witness arm itself — #18532 lands it.Avoided Traps
rectPlane: 'outer'⇒ skiptoFrameOrigin): the samereshowVesselreceives content rects from conversion-out and from the host's own unit contract; a plane switch there fixes one caller by breaking the others. The record site is the one place that knows what it recorded.innerRect, but it re-derives what manager.Window already publishes (outerRect.x = innerRect.x − chrome.left,src/manager/Window.mjs:113).Related
#18532 (the witness; blocked by this) · #19373 (the conversion-out half, split from AC-3) · #19225 / PR #19232 (the plane switch) · #18514 / #18515 (the content-rect terminal fix this restores) · #15396 / #15436 (the park-never-close contract) · #19278 (the clear park) · ADR 0029 §2.8.6
Live latest-open sweep: latest 20 open issues read 2026-10-02T20:25Z — no equivalent; the only hits for re-show /
toFrameOrigin/preConversionRectare #18532 and closed predecessors. A2A in-flight sweep: last 30 messages, no claim on the restore plane. Memory sweep: the 2026-09-09 chain recordedsourceRect = innerRectas the restore anchor — this ticket is its regression.Origin Session ID: 31c0406b-2b5c-42e7-89af-35cbecea62ac Retrieval Hint:
query_raw_memories("parked vessel terminal re-show chrome too high preConversionRect frame plane toFrameOrigin regression")