Context
The native-titlebar drop is geometry-only: no pointer event reaches the page while the OS drags a popup, so the coordinator infers intent from the popup's position stream (dwell 450 ms on one claimed target + settle 250 ms), and there is no release event at all. #18112 (PR #18114) repairs the main-window case, where the park's focus step could never be granted, by making the move the strict gate: the drop then lands when the user releases, because the owner-routed move is refused while the OS holds the window and succeeds the instant it lets go.
That makes the drop work. It does not make it feel right, and the operator chose a different interaction on 2026-09-02 (in session, after reading the live receipts together):
the hold is the gesture — the drop area fills up while the popup rests on it, the transfer commits when the fill completes, the popup closes afterwards; no popup placeholder text; if the platform refuses the close while the titlebar is still held, even better — the close that finally succeeds tells us indirectly when the user dropped, and nothing visible waited on it.
Two facts frame the design. Nothing in the target's landing needs the OS: the transfer is app-side and the embodiment renders in the target regardless of focus. And the only platform verb left, the close, runs after the pane is already on screen.
The Problem
- During the hold the user gets no feedback that waiting does anything: the highlight says "you may drop here", nothing says "keep holding and it docks".
- The landing is tied to the release, which we cannot see. With PR #18114 it arrives at the next park retry after release — up to the current backoff step late.
- The main-target path still runs a park (move the popup behind main) that exists only so a rejected drop can restore the popup; the target has already accepted before the timer completes, so the park buys nothing there.
The Architectural Reality
src/manager/DragCoordinator.mjs — onWindowPositionChange arms/refreshes the candidate (firstSeenAt, dwellRemaining, one setTimeout for commitNativeWindowDrop); updateNativeHover calls the target's onRemoteDragMove({embodyProxy: false, …}) on every geometry update; commitNativeWindowDrop → suspendWindowDrag (true strict park, false refusal → retryNativeWindowPark, any other answer non-strict → straight to onRemoteDragMove({embodyProxy: true}) → awaitRemoteDragEmbodiment → handoff nativeWindowDropHandoffMs 180 → onRemoteDrop → settleNativeWindowDisposition(committed) → the source's onRemoteDropOut); settleNativeWindowDisposition retries close/restore with nativeWindowDispositionRetryMs doubling.
apps/workstation/view/Workspace.mjs — suspendNativeWindowDrag (main realm) → nativeVesselParkHandlers.onConversionIn → parkTearOutVessel; retireNativeWindowDrag → onGestureTerminal → Neo.Main.windowNativeClose.
src/Main.mjs#windowNativeClose — entry.win.close() then return true, unverified; #focusWindow is the verified-Boolean pattern (poll the platform, answer what happened).
src/dashboard/dock/interaction/Preview.mjs — the previewed drop region: neo-dock-preview-affordance, neo-dock-preview-region, neo-dock-preview-accepted; painted by resources/scss/src/dashboard/dock/interaction/Preview.scss with a border colour and a much lighter background. src/dashboard/dock/interaction/Rail.mjs#revealDwellMs is the precedent for a dwell as a named, overridable constant.
test/playwright/e2e/workstation/WorkstationNativeTitlebarDragNL.spec.mjs (popup → main) and WorkstationNativePopupOverPopupNL.spec.mjs (popup → popup) drive geometry with CDP and poll for the commit; headless grants focus and close freely, so neither can witness a human hold.
The Fix
1. Dwell is the commit, and it is visible.
nativeWindowDropDwellMs 450 → 1200, configurable; the rehearsal tunes it. nativeWindowDropSettleMs 250 stays: the timer runs only on a still popup over a claimed zone, so passing over a zone never triggers anything.
- The coordinator hands the target the timer with the hover:
onRemoteDragMove gains dwell: {armedAt, durationMs} (from firstSeenAt and the constant) while a native candidate is armed, and onRemoteDragLeave ends it.
- Affordance (operator's choice): the previewed drop area fills — its light background rises toward the border colour over exactly the dwell duration. Implemented as a CSS transition on the region driven by a
--dock-native-dwell-ms custom property the preview publishes from the payload, so paint and commit share one number. Leaving the zone drains the fill in ~150 ms.
- The fill is a SHAPE, not only a colour — a growing area — so the cue survives colour-vision deficits without a second channel (the hover contract keeps the popup visible during the hold,
embodyProxy: false, so there is no proxy to ramp). prefers-reduced-motion shows the armed state as a static tint. Other ideas stay open for the rehearsal: a border sweep around the region’s perimeter, a fill that starts at the edge nearest the popup.
2. Main-window target: commit, then close. No park.
- For a main-window target the Workstation’s strict park answers satisfied without a platform effect:
parkTearOutVessel (nativeTitlebar && targetIsMain) returns true after the source-route authority check, writes parked: true, physical: false to the receipt, and never asks for focus or a move. The conversion bookkeeping around the park handler (VesselPark.onConversionIn) runs exactly as for a parked vessel, so the coordinator’s existing strict branch embodies, hands off and commits transferItem into main while the user may still hold the titlebar: the pane appears at the previewed slot the moment the fill completes. (Measured first with a null "no park needed" answer to the coordinator: that skipped the bookkeeping and the embodiment never settled — the NL spec ended with an empty candidate map and no receipt.)
- The popup is retired through the owner handle.
windowNativeClose becomes verified-Boolean like #focusWindow (poll win.closed briefly; answer what happened). If the platform defers the close while the titlebar is held, the disposition retry re-attempts at a fixed ~250 ms cadence (no backoff — the release is expected within seconds), bounded at 20 s; the close that succeeds is the indirect release signal. The popup shows its empty vessel meanwhile — no placeholder text (operator).
- A
lastNativeRetireReceipt records closeAttempts, closedAt, deferredWhileHeld, so a hand test is readable through the Neural Link.
- PR #18114’s best-effort-focus branch was scoped to the same conjunction (
targetIsMain && nativeTitlebar) that the satisfied park now returns on, so it became unreachable and is REMOVED by this ticket’s PR rather than left dead (Grace’s and Mnemosyne’s reading); #18114’s tearOutParkAttempts accounting stays, and the satisfied park resets it like a landed park. Popup targets keep the physical park.
3. Popup target: mechanics unchanged, same affordance and dwell constant. The strict park (windowNativeFocus on the handle) keeps working there.
4. Witnesses.
- Unit, Workstation: a native-titlebar park onto the main window resolves
true with zero platform calls (focus and move stubs refuse and are never reached), receipt {parked: true, physical: false}, source route still checked — red on dev (the park dies at focus); a popup target keeps the physical park (existing arm, retargeted to a popup). Unit, coordinator: the hover payload carries dwell with the constant and a stable armedAt across frames — red on dev.
- Unit,
Main.mjs: windowNativeClose returns false while win.closed stays false, true once it flips — red-first (today it returns true unconditionally).
- Component (dock preview fixture): an armed native candidate stamps the fill on the previewed region with the dwell duration; leaving removes it;
prefers-reduced-motion yields the stepped variant.
- Both NL specs green under the Brain root at the new dwell (their polls allow it).
- Hand test, operator-owned Post-Merge, three runs: hold still over main — the area fills over ~1.2 s, the pane lands, the popup vanishes (at once, or at release); release before the fill completes — the pane still lands ~1 s after the popup stops; pass over main without stopping — nothing triggers.
Acceptance Criteria
Out of Scope
- The keyboard-command focus return path — unchanged.
- Pointer-path cross-window drags (#17578).
- A mid-drag cancel after the commit; the user tears out again.
- Reverting PR #18114’s
parkAttempts accounting — it stays; its main-target focus relaxation is superseded here, not layered.
Avoided Traps
- Gating the visible result on a platform verb. Focus is never granted on this path and the move is refused while held. The transfer needs neither; only the close touches the OS, and it runs after the result is already on screen.
- Two clocks. The paint and the commit read one constant; a CSS duration typed by hand drifts from the coordinator's timer the first time someone tunes one of them.
- A colour-only timer. The fill is a rising AREA, not only a hue, so the cue survives colour-vision deficits (WCAG 1.4.1); the hover contract keeps the popup visible during the hold (
embodyProxy: false), so there is no proxy to ramp alongside it.
- Backoff on the close retry. After release the user expects the popup gone within a frame or two; exponential backoff would leave it hanging for seconds. Fixed cadence, bounded.
- Reading a green rig as proof of the human path. Headless grants focus and close freely; the witnesses stub the refusals, and the hand test stays owned.
Related
- #18112 / PR #18114 — the main-target park fix this layers on (Mnemosyne).
- #18054 / PR #18055 — the park retry.
- #18059 — popup-over-popup native drop (kept as is).
- #17578 — the pointer-path coordinator defect.
Live latest-open sweep: checked the latest open issues at 2026-09-02T15:14Z and the queue again at 15:30Z (newest #18112, PR #18114); keyword sweeps dwell spring drop affordance, native titlebar drop commit close, drop area fill hold — no equivalent. A2A: Mnemosyne holds #18112 with PR #18114; this ticket is announced to her as the layer above it.
Origin Session ID: 87c91db1-7fcd-4987-9932-2bf78d3dda25
Retrieval Hint: "hold is the gesture native titlebar drop dwell fill commit then close spring-loaded dock"
Context
The native-titlebar drop is geometry-only: no pointer event reaches the page while the OS drags a popup, so the coordinator infers intent from the popup's position stream (dwell 450 ms on one claimed target + settle 250 ms), and there is no release event at all. #18112 (PR #18114) repairs the main-window case, where the park's focus step could never be granted, by making the move the strict gate: the drop then lands when the user releases, because the owner-routed move is refused while the OS holds the window and succeeds the instant it lets go.
That makes the drop work. It does not make it feel right, and the operator chose a different interaction on 2026-09-02 (in session, after reading the live receipts together):
Two facts frame the design. Nothing in the target's landing needs the OS: the transfer is app-side and the embodiment renders in the target regardless of focus. And the only platform verb left, the close, runs after the pane is already on screen.
The Problem
The Architectural Reality
src/manager/DragCoordinator.mjs—onWindowPositionChangearms/refreshes the candidate (firstSeenAt,dwellRemaining, onesetTimeoutforcommitNativeWindowDrop);updateNativeHovercalls the target'sonRemoteDragMove({embodyProxy: false, …})on every geometry update;commitNativeWindowDrop→suspendWindowDrag(truestrict park,falserefusal →retryNativeWindowPark, any other answer non-strict → straight toonRemoteDragMove({embodyProxy: true})→awaitRemoteDragEmbodiment→ handoffnativeWindowDropHandoffMs180 →onRemoteDrop→settleNativeWindowDisposition(committed)→ the source'sonRemoteDropOut);settleNativeWindowDispositionretries close/restore withnativeWindowDispositionRetryMsdoubling.apps/workstation/view/Workspace.mjs—suspendNativeWindowDrag(main realm) →nativeVesselParkHandlers.onConversionIn→parkTearOutVessel;retireNativeWindowDrag→onGestureTerminal→Neo.Main.windowNativeClose.src/Main.mjs#windowNativeClose—entry.win.close()thenreturn true, unverified;#focusWindowis the verified-Boolean pattern (poll the platform, answer what happened).src/dashboard/dock/interaction/Preview.mjs— the previewed drop region:neo-dock-preview-affordance,neo-dock-preview-region,neo-dock-preview-accepted; painted byresources/scss/src/dashboard/dock/interaction/Preview.scsswith a border colour and a much lighter background.src/dashboard/dock/interaction/Rail.mjs#revealDwellMsis the precedent for a dwell as a named, overridable constant.test/playwright/e2e/workstation/WorkstationNativeTitlebarDragNL.spec.mjs(popup → main) andWorkstationNativePopupOverPopupNL.spec.mjs(popup → popup) drive geometry with CDP and poll for the commit; headless grants focus and close freely, so neither can witness a human hold.The Fix
1. Dwell is the commit, and it is visible.
nativeWindowDropDwellMs450 → 1200, configurable; the rehearsal tunes it.nativeWindowDropSettleMs250 stays: the timer runs only on a still popup over a claimed zone, so passing over a zone never triggers anything.onRemoteDragMovegainsdwell: {armedAt, durationMs}(fromfirstSeenAtand the constant) while a native candidate is armed, andonRemoteDragLeaveends it.--dock-native-dwell-mscustom property the preview publishes from the payload, so paint and commit share one number. Leaving the zone drains the fill in ~150 ms.embodyProxy: false, so there is no proxy to ramp).prefers-reduced-motionshows the armed state as a static tint. Other ideas stay open for the rehearsal: a border sweep around the region’s perimeter, a fill that starts at the edge nearest the popup.2. Main-window target: commit, then close. No park.
parkTearOutVessel(nativeTitlebar && targetIsMain) returnstrueafter the source-route authority check, writesparked: true, physical: falseto the receipt, and never asks for focus or a move. The conversion bookkeeping around the park handler (VesselPark.onConversionIn) runs exactly as for a parked vessel, so the coordinator’s existing strict branch embodies, hands off and commitstransferIteminto main while the user may still hold the titlebar: the pane appears at the previewed slot the moment the fill completes. (Measured first with anull"no park needed" answer to the coordinator: that skipped the bookkeeping and the embodiment never settled — the NL spec ended with an empty candidate map and no receipt.)windowNativeClosebecomes verified-Boolean like#focusWindow(pollwin.closedbriefly; answer what happened). If the platform defers the close while the titlebar is held, the disposition retry re-attempts at a fixed ~250 ms cadence (no backoff — the release is expected within seconds), bounded at 20 s; the close that succeeds is the indirect release signal. The popup shows its empty vessel meanwhile — no placeholder text (operator).lastNativeRetireReceiptrecordscloseAttempts,closedAt,deferredWhileHeld, so a hand test is readable through the Neural Link.targetIsMain && nativeTitlebar) that the satisfied park now returns on, so it became unreachable and is REMOVED by this ticket’s PR rather than left dead (Grace’s and Mnemosyne’s reading); #18114’stearOutParkAttemptsaccounting stays, and the satisfied park resets it like a landed park. Popup targets keep the physical park.3. Popup target: mechanics unchanged, same affordance and dwell constant. The strict park (
windowNativeFocuson the handle) keeps working there.4. Witnesses.
truewith zero platform calls (focus and move stubs refuse and are never reached), receipt{parked: true, physical: false}, source route still checked — red ondev(the park dies at focus); a popup target keeps the physical park (existing arm, retargeted to a popup). Unit, coordinator: the hover payload carriesdwellwith the constant and a stablearmedAtacross frames — red ondev.Main.mjs:windowNativeClosereturnsfalsewhilewin.closedstaysfalse,trueonce it flips — red-first (today it returnstrueunconditionally).prefers-reduced-motionyields the stepped variant.Acceptance Criteria
transferIteminto main with no release, focus or move required; the park answers satisfied without a platform effect and the receipt records it. Unit witness red-first ondev.WorkstationNativePopupOverPopupNLandWorkstationNativeTitlebarDragNLgreen under the Brain root at the new dwell.unit/manager+component/dashboardgreen.Out of Scope
parkAttemptsaccounting — it stays; its main-target focus relaxation is superseded here, not layered.Avoided Traps
embodyProxy: false), so there is no proxy to ramp alongside it.Related
Live latest-open sweep: checked the latest open issues at 2026-09-02T15:14Z and the queue again at 15:30Z (newest #18112, PR #18114); keyword sweeps
dwell spring drop affordance,native titlebar drop commit close,drop area fill hold— no equivalent. A2A: Mnemosyne holds #18112 with PR #18114; this ticket is announced to her as the layer above it.Origin Session ID: 87c91db1-7fcd-4987-9932-2bf78d3dda25
Retrieval Hint: "hold is the gesture native titlebar drop dwell fill commit then close spring-loaded dock"