LearnNewsExamplesServices
Frontmatter
id15395
titleDual-window conversion geometry: popup→proxy over a target vessel
stateClosed
labels
enhancementai
assigneesneo-gpt
createdAtJul 18, 2026, 4:21 AM
updatedAtJul 19, 2026, 11:06 AM
githubUrlhttps://github.com/neomjs/neo/issues/15395
authorneo-fable-clio
commentsCount4
parentIssue15239
subIssues
15557 Bind popup adoption to exact product owner grants
subIssuesCompleted1
subIssuesTotal1
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[x] 15396 In-gesture vessel lifecycle on conversion: park, don''t close
closedAtJul 19, 2026, 11:06 AM

Dual-window conversion geometry: popup→proxy over a target vessel

Closed Backlog/active-chunk-7 enhancementai
neo-fable-clio
neo-fable-clio commented on Jul 18, 2026, 4:21 AM

Context

Epic #15239 leaf (G3 family) — operator exploration 2026-07-18: drag one tab out into a popup, resize it, drag a second tab out — when the second popup crosses the first, at what point does it convert into a semi-transparent drag proxy inside the target? Gut: it should depend on the heights and widths of BOTH windows. The gut is provably right, and this leaf turns it into a measured contract. #15246 owns G3's claim-arbitration story; no leaf owns the conversion-entry mechanic — this one does.

The Problem

The landed tear-out grammar uses direction-aware intersection-ratio hysteresis against ONE window (src/draggable/container/SortZone.mjs:270 checkWindowBoundary, defaults 0.8 detach / 0.6 reattach, #8160 lineage). A fixed single-denominator ratio is mathematically broken for popup-over-popup:

  • Denominator = dragged window B: a large B over a small target A caps at area(A)/area(B) — for B twice A's size, the ratio can never exceed 0.5, so a 0.8 threshold is unreachable and conversion never fires.
  • Denominator = target A: a small B can never cover enough of a large A — unreachable in the mirror case.

Both windows are freely resizable mid-session (the operator's scenario resizes A before the second drag), so any creation-time or single-window basis is wrong twice.

The Architectural Reality

  • The conversion is the tear-out's inverse: boundary-exit converts proxy→popup (src/dashboard/Container.mjs:218 onDragBoundaryExit:243 openWidgetInPopup); this leaf converts popup→proxy on vessel-overlap. Same hysteresis philosophy, new geometry.
  • The pointer (not the rect) is the intent signal — the dragged window follows the pointer (main/addon/DragDrop.mjs pointer-follow), but a large B's rect can overlap A while the pointer is nowhere near it. Claim/menu/preview single-target semantics are #15240's protocol (ADR 0029 amendment, closed) + #15246's arbitration; this leaf feeds them a conversion event, never re-implements claiming.
  • The calibration harness already exists in-flight: the #15243 seven-row headed matrix suite — this contract's witness rides the same harness as an extension row (same instrumentation, new cells), so the geometry ships with calibration receipts, never numbers from memory (G1's #15244 discipline).

The Fix

One PR: the dual-window conversion contract + its headed witness.

  1. Metric: per-axis overlap ratios normalized by the SMALLER extent per axis — rx = overlapWidth / min(width(A), width(B)), ry = overlapHeight / min(height(A), height(B)) — composed (e.g. min(rx, ry) or product; decided empirically), so the threshold is reachable for every size pair in both directions. Computed from LIVE rects every gesture frame.
  2. Hysteresis pair: convert-in threshold > convert-out threshold (flicker-free at the boundary), calibrated on the workstation density with receipts.
  3. Pointer gate: conversion arms only while the pointer is inside the target's dock-accepting region — rect overlap alone never converts (intent stays pointer-owned; feeds the #15240 claim protocol unchanged).
  4. Headed witness cells on the #15243 harness: small-over-large, large-over-small, near-equal, post-resize asymmetric — each asserting reachability, hysteresis stability (no convert/revert oscillation across a slow crossing), and single-claim (at most one target exposes menu+preview, G1 AC parity).

Acceptance Criteria

  • The conversion metric is reachable for ANY window-size pair in both directions (small↔large), proven by witness cells, computed from live post-resize rects.
  • In/out hysteresis pair calibrated empirically on the workstation density; chosen values carry calibration receipts in the PR.
  • Pointer-inside-target gates arming; rect-overlap alone never converts.
  • Conversion emits through the #15240 outcome machine (a new state, if required, lands as an ADR 0029 amendment in the same PR — see Decision Record impact).
  • A slow boundary-crossing witness proves zero convert/revert flicker.
  • Flicker ownership (the #15434 review's non-blocking challenge, Ada): the sensor's pointer gate is deliberately undamped (rect overlap must never pin a conversion), so the wire-in NAMES where pointerInTarget stability lives — the #15246 claim feed's expiry/hysteresis semantics or an explicit debounce at the binding site — and witnesses a boundary-flicker sequence producing bounded conversion flips.

Out of Scope

The vessel's OS-window lifecycle during conversion (companion leaf, filed alongside) · claim arbitration itself (#15246) · whole-stack reintegration (#15247) · the tear-out grammar (#15244 G1).

Decision Record impact

depends-on ADR 0029 (outcome machine, detachItem §2.1); may amend it if conversion requires a new gesture state — resolved at design time, amendment rides the same PR if so.

Related

Parent #15239 · #15246 (G3 claim arbitration — this feeds it) · companion: in-gesture vessel lifecycle leaf (filed in the same pass) · #15243 (the witness harness this extends) · #15244/#15245 (the grammar + acquisition it inverts/consumes) · #15240 (outcome machine).

Live latest-open sweep: latest open checked at 2026-07-18T02:16Z; no equivalent (epic leaf map fetched fresh — no conversion-geometry leaf exists). A2A sweep: no competing claim; QT reassignment ruling broadcast 6e7cb4af is the active coordination frame.

Origin Session ID: abce4d75-7dcb-4145-8afc-b0ff2cdc51e6

Retrieval Hint: query_raw_memories("dual-window conversion geometry popup proxy min-axis hysteresis unreachable threshold")

Contract Ledger Matrix

Live intake fold (2026-07-19): the sensor contract is already delivered by #15434. This ticket remains the owner of production composition, calibration, and the headed matrix. The canonical journey in #15551 / PR #15552 additionally falsified the current vessel join: a competing ?popout=workbench child can overwrite the already-owned cross-window target because onWindowConnect() currently treats itemId plus entry existence as authority.

Target surface Source of authority Proposed behavior Fallback / edge case Docs Evidence
commitCrossWindowTransfer() workspace-target publication ADR 0029 §§2.8.1, 2.8.3; crossWindowStageGeneration + crossWindowTargetWindowId A committed main→popup transfer records the already-mounted workspace target as owner; it never grants a later ?popout= child permission to adopt the pane. A superseded generation or disconnected target fails closed and cannot repaint or reparent. Method summary + owner-field prose name the distinction. Existing settlement/disconnect unit witnesses plus the competing-vessel falsifier.
popOutPane() click-popout admission The opener-created, one-use owner grant minted before windowOpen The expected click child consumes its exact grant once, preserving connect-before-open-settlement. Failed open revokes the grant and rolls both documents back; replay/stale connections are ignored. Entry JSDoc names grant lifetime and consumption. Unit witness for fast connect, replay, and rollback.
openTearOutVessel() gesture admission The active gesture's separately minted, one-use owner grant The expected tear-out child alone may populate tearOutConnects or adopt a post-terminal tearOutPanes entry. Cancel, failed admission, or stale/reused child revokes/loses; click bookkeeping remains untouched. Tear-out seam prose names the grant and the two race orders. Unit witnesses for connect-before-terminal and terminal-before-connect.
onWindowConnect() adoption join Exact host + item + flow + unconsumed owner grant; not item id, URL shape, arrival order, or entry existence Dispatches the connection to exactly one flow and consumes its authority before any reparent. Unmatched/consumed grants are inert and never overwrite windowId or create stray tear-out bookkeeping. Method contract carries the fail-closed rule. Unit wrong-owner, stale/replay, click-race, and tear-out-race falsifiers.
Conversion sensor → claim/outcome composition #15434 sensor contract; #15246 claim arbitration; ADR 0029 §§2.8.1–2.8.3 Live dual-rect + pointer samples feed the existing claim/outcome machine; pointer stability lives at the claim-feed binding. Invalid geometry and ambiguous/no claim remain non-converting; no legacy first-intersection fallback. Binding-site JSDoc names threshold and flicker ownership. Calibrated small↔large, near-equal, resize, and slow-cross cells.
Canonical headed journey #15243 harness contract; #15551 / PR #15552 exact target assertion The intended workspace target keeps the same live pane while a competing G1 vessel exists. A narrow drag-only green is not a substitute; the extra vessel must remain present. PR receipts link exact heads and thresholds. Full round-trip red→green plus size-pair/flicker matrix.

Intake classification: valid as written, ADR-aligned, pre-stale, and high ROI. The immediate owner-grant repair is inside this ticket's named production wire-in; it does not move #15396's async park/re-show lifecycle scope or #15552's headed evidence ownership.