LearnNewsExamplesServices
Frontmatter
id18177
titleAn edge-zone home records no extent, so a restored pane loses its width
stateClosed
labels
bugaidashboard
assigneesneo-opus-grace
createdAtSep 3, 2026, 11:04 AM
updatedAtSep 3, 2026, 11:37 AM
githubUrlhttps://github.com/neomjs/neo/issues/18177
authorneo-opus-vega
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 3, 2026, 11:37 AM

An edge-zone home records no extent, so a restored pane loses its width

Closed Backlog/active-chunk-20 bugaidashboard
neo-opus-vega
neo-opus-vega commented on Sep 3, 2026, 11:04 AM

Context

#18164 / PR #18165 made a torn-out pane come home to its own node instead of the first one, including when the node collapsed while the pane was out. That fix is doing its job. What it does not restore is the home's size: an edge-zone slot whose only pane was torn out is rebuilt at the projection's default width rather than the width it had.

Measured on a consumer dock host at 862edb31b3 (which carries #18165), a single-pane right edge zone, host-relative:

left from   x 1281   width 318   height 919
came home   x 1321   width 272   height 900

The side is correct — that is #18165 — so this is the narrower property left over. It is user-visible and it costs work: a user who widens a docked panel loses that width by popping the panel out and closing it.

Not a symptom of #18175. restoreTab reaching the dispatch table without a change class was a map-completeness red; changeClassFor already fell through to topology, so runtime behaviour was correct throughout. I checked that first precisely because restoreTab is the shared surface, and its body settles it — the two are independent.

Live latest-open sweep: checked the latest 20 open issues at 2026-09-03T09:03:51Z; no equivalent found.

The Problem

Document.captureNodeHome records what is needed to put a node back, and it records different amounts depending on the parent type:

  • split parent — orientation, size (the child's own fraction, defaulted to 0.5), and a sibling anchor.
  • edge-zone parent — {parentId, slot} and nothing else.

So a split home restores its geometry and an edge-zone home does not. The member's own JSDoc explains the asymmetry, but on a different axis than the one that bites:

An edge-zone parent needs no anchor: an edge-zone node survives losing every zone, so its id and the zone key remain resolvable.

That is true and it is about resolvability — the id and key survive, so no sibling anchor is needed. It has been generalised into "an edge-zone home needs nothing else recorded", which does not follow: the id surviving says nothing about the extent surviving.

Worth stating why this is an oversight rather than a contract, since a lone asymmetry can be either. Document.setZoneNodeId spreads the descriptor it finds and overwrites only nodeId:

edgeZoneNode.zones[edge] = {
    ...(Document.isJsonRecord(edgeZoneNode.zones[edge]) ? edgeZoneNode.zones[edge] : {}),
    nodeId
}

Preserving extent across a repoint is therefore the intended behaviour wherever the descriptor is still present — the helper is written for it, and Document documents it as "repoints one nested edge-zone descriptor while preserving its extent and policy". The gap is that on a sole-occupant tear-out there is no descriptor left to spread: the slot was cleared when the node went away, so the restore writes {nodeId} alone and the extent is simply absent.

The Architectural Reality

  • An edge zone's size is a property of the descriptor, not of the node it points at: Document.dockZoneDescriptorKeys = new Set(['nodeId', 'extent', 'resizable']), validated as a finite number in the open interval (0, 1).
  • Operations.restoreTab mints a node when the home is gone — doc.nodes[nodeId] = {activeItemId: itemId, items: [itemId], type: 'tabs'}, id-collision-safe via Document.genId — then calls Document.restoreNodeHome.
  • Document.restoreNodeHome re-points the slot through setZoneNodeId once it has established the slot is unoccupied or already this node's (the ownership check #18165 added).
  • resizeEdgeZone is what writes extent in the first place, so the value being lost is a committed user gesture, not a default someone forgot to configure.

The whole path is therefore correct except for one missing pair of fields in the record.

The Fix

Document.captureNodeHome: when the parent is an edge-zone, record the descriptor's extent and resizable alongside slot, the way the split branch already records size and orientation.

Document.restoreNodeHome: when writing the slot, re-apply the recorded extent / resizable — additively, so a descriptor that survived keeps what it has and only an emptied slot is rebuilt from the record. setZoneNodeId's spread already gives the right merge semantics; the recorded values supply what the spread cannot find.

Keep the validation boundary: an out-of-range or non-finite recorded extent must be dropped in favour of the default rather than written into the document, since Document's own validator rejects extent outside (0, 1) and a restore must not be able to author an invalid document.

Acceptance Criteria

  • AC-1 — captureNodeHome on a node whose parent is an edge-zone returns a record carrying the slot's extent; a slot with no extent yields a record with none, not an invented default.
  • AC-2 — captureNodeHome also carries resizable when the descriptor declares it, so a non-resizable edge does not come back resizable.
  • AC-3 — restoring into an emptied edge-zone slot rebuilds the descriptor with the recorded extent, not the projection default.
  • AC-4 — restoring into a slot whose descriptor survived leaves that descriptor's own extent intact; the record does not overwrite a live value.
  • AC-5 — a recorded extent that fails Document's (0, 1) finite check is discarded on restore and the document stays valid; asserted with a red-first arm that writes the invalid value.
  • AC-6 — the split branch's behaviour is unchanged: a split home still restores size and orientation.
  • AC-7 — model-level round trip: an edge-zone slot holding one tabs node at a non-default extent, item detached and restored, ends with the same extent. This is the public witness for the measurement in Context and needs no app.
  • AC-8 — the captureNodeHome JSDoc states what an edge-zone home records and why, so the next reader does not re-derive the resolvability argument as a completeness argument.

Out of Scope

  • The #18165 ownership rule and the occupied-slot refusal. Untouched; this only adds fields to the record it already writes.
  • Split-node sizes and the sibling-anchor logic.
  • resizeEdgeZone itself, and any change to how extent is validated or projected.
  • Persisting extents across a reload — a separate concern from restoring one within a session.

Avoided Traps

  • Treating the asymmetry as a bug without reading its justification. The JSDoc gives a real reason for the edge-zone branch being shorter; the defect is that the reason covers anchors and was extended to cover everything. Filing it as "the branch is incomplete" would have missed that the anchor omission is correct and only the geometry omission is not.
  • Restoring the extent unconditionally. A vessel is long-lived, and the user can resize that edge while the pane is out. Overwriting a surviving descriptor's extent from a stale record would discard a newer gesture — the same class of mistake as overwriting an occupied slot, which is what #18165 had to guard.
  • Reading the returned pane's pixel width as the assertion. The width also moves when the projection legitimately re-lays-out; the document's extent is the value under test, which is why AC-7 is model-level and the pixel figures stay in Context as the symptom that found it.
  • Blaming #18175. restoreTab is the shared surface and the red was live at the time of measurement, so the tempting inference was one cause. changeClassFor's fall-through makes them independent.

Decision Record impact

none — additive fields on an internal record, no ADR authority touched.

Related

  • #18164 / PR #18165 — restores the home node; this restores the home's size.
  • #18175 / PR #18176 — same operation, independent cause; cited above so the next reader does not re-merge them.
  • #18151 — the epic on the dock asking consumers to re-derive engine knowledge; adjacent in spirit, since a consumer cannot restore an extent the model never recorded.

Handoff Retrieval Hints

  • query_raw_memories: "edge-zone home extent not restored sole occupant tear-out"
  • Anchors: Document.captureNodeHome, Document.restoreNodeHome, Document.setZoneNodeId, Document.dockZoneDescriptorKeys, Operations.restoreTab
  • Engine head where measured: 862edb31b3

Origin Session ID: e8ea32a2-b9a8-43ea-84ba-b83081ec4b3d