LearnNewsExamplesServices
Frontmatter
id17948
titleEngine-owned reload header action with a per-pane delegation contract
stateClosed
labels
enhancementaitestingarchitecturecore
assigneesneo-opus-vega
createdAtAug 31, 2026, 3:50 PM
updatedAtAug 31, 2026, 11:13 PM
githubUrlhttps://github.com/neomjs/neo/issues/17948
authorneo-opus-vega
commentsCount3
parentIssue13158
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[x] 17928 Dock layouts have no maximize — a first-class pane maximize for tabs nodes
blocking[]
closedAtAug 31, 2026, 11:13 PM

Engine-owned reload header action with a per-pane delegation contract

Closed v13.2.0/archive-v13-2-0-chunk-17 enhancementaitestingarchitecturecore
neo-opus-vega
neo-opus-vega commented on Aug 31, 2026, 3:50 PM

Context

Part of the pane-header parity family under epic #13158: engine-owned default header actions on dock tabs nodes, measured against a native Qt-class reference UX for the desktop-to-web migration audience. Merged siblings: #17952 (pin), #17953 (maximize), #17960 (overflow-as-action). Family ordering contract (shared, frozen): [tab overflow, only while overflowing] → [host actions] → [engine set: lock · reload · pin · pop-out · maximize — focus-gated] → [close — always visible, always last]; each engine action behind its own enableDock<X>Action boolean with the reserved-name guard (now a table, LayoutAdapter.mjs) extended. Anchors verified at Engine dev@4f9b9e392b.

Intake re-scope (2026-08-31, @neo-gpt-emmy, IC 5482415936): this leaf is the delegation-only reload. The originally-prescribed universal destroy-and-recreate fallback conflicts with ADR 0029 §2.6 (resolved panes are moved, never destroyed) and cannot honestly promise rollback against cache-backed resolvers; it moved to successor #17966 with the two-phase transaction and ADR amendment it requires.

The Problem

Dock panes have no reload affordance. The one meaning an engine can ship without guessing is the one the pane's author declares: a pane implementing the reload contract owns what "refresh" means for itself — its stores, its caches, its re-render. The engine must never guess among N stores or unsaved edits, and (per the intake findings now recorded on #17966) it must not destroy panes it cannot promise to replace.

The Architectural Reality

  • Action substrate (shipped): the reserved-name guard table and the action router (Workspace#onDockHeaderAction — one handler per action) landed with #17952/#17953; reload is a table row plus a router row plus one handler.
  • No result channel exists on the action wire: TabContainer#onHeaderAction and Workspace#onDockHeaderAction re-fire events, and Observable.fire() discards listener returns — a sync throw bubbles, an async rejection is unobserved. The leaf therefore OWNS its settlement channel (ledger row below).
  • Nothing persists: reload is runtime-only. The committed document, perspectives and topology diffs never observe it.

The Fix

enableDockReloadAction: false on dashboard.dock.Workspace. When on, a focus-gated reload action leads the engine set (frozen order). Dispatch, per active pane, delegation-only:

  1. The contract: dockReload(): void | Promise<*> — explicitly promise-aware. Present ⇒ invoked; the widget owns the meaning. Absent ⇒ the action is hidden for that pane (a pure typeof probe on the live card — never a resolver call, which could be side-effectful).
  2. Single-flight per item: while one invocation is in flight, the stable action instance is disabled; a second activation cannot start a second invocation. Settlement re-enables.
  3. One named settlement channel: the workspace fires dockReloadSettled {dockNodeId, itemId, errors} on every completion — sync throw, async rejection and async success all settle through it; errors carry the contract method's failure text. A throwing/rejecting dockReload() keeps the pane, always.
  4. Teardown mid-flight: a pane, tab container or workspace destroyed while an invocation is in flight settles without further mutations (guards on the destroyed state; no re-enable on retired chrome).
  5. No committed operation, ever — the document stays byte-identical through every path.

Avoided Traps

  • Store-detection heuristic — rejected. The engine cannot pick among N stores or know about unsaved edits; the author declares meaning by implementing the contract.
  • A workspace-level mode enum — rejected as the wrong grain: reload meaning is per-widget.
  • Universal destroy-and-recreate in this leaf — removed at intake: it crosses ADR 0029 §2.6, the reconciler can re-serve a destroyed instance (Reconciler.mjs:573-590), and cache-backed resolvers make post-destruction rollback impossible. #17966 owns it, with the two-phase fresh-candidate transaction that can honestly promise rollback.
  • Eager resolver probing for visibility — rejected: resolvePane implementations may be side-effectful; availability is decided by a pure method probe on the live card only.

Contract Ledger (consumed cells)

Cell Producer Behavior Fallback Docs Evidence
action surface guard-table row + router row + one handler (the shipped #17952/#17953 shape) Default off; on = focus-gated reload leading the engine set, reserved-name guard covering it Off by default Config jsdoc Guard table + router shipping on dev
delegation contract NEW: dockReload(): void | Promise<*> on the pane component, detected by pure typeof probe on the live card Present ⇒ invoked exactly once per settlement window; the widget owns the meaning; no dock operation either way Absent ⇒ the action is hidden for that pane Config jsdoc + the contract documented on the method Duck-typing precedent: the engine's opt-in augmentation rule
single-flight NEW per-item in-flight state on the Workspace One invocation per item at a time; the stable action instance is disabled during flight (the hidden-sync discipline, second axis); settlement re-enables Disabled = safe default Config jsdoc Stable-instance hidden sync shipping for close/pin/reload visibility
settlement channel NEW: workspace event dockReloadSettled {dockNodeId, itemId, errors} Every path settles exactly once — sync throw, async reject, async success; errors carry the failure text; the pane is kept on every failure Hosts that ignore the event lose nothing but observability Event jsdoc Observable.fire discards returns — the event IS the channel (intake finding)
teardown Destroyed-state guards on the settlement path Mid-flight destroy of pane/tab/workspace settles without mutations; no re-enable on retired chrome Settle-only Handler jsdoc The maximize lane's destroyed-guard precedent
visibility policy syncDockReloadAction per active item (the syncDockCloseAction idiom), recomputed on active-item change and post-reconcile Hidden while no active item OR the live card lacks the contract; initial projection ships hidden and the sync reveals Hidden = safe default Config jsdoc Idiom shipping

Decision Record impact

none — delegation never destroys, re-parents or persists; ADR 0029 §2.6 is untouched. The successor #17966 carries amends ADR 0029 §2.6 for the recreate capability.

Acceptance Criteria

  • Default off: byte-identical projection. On: focus-gated reload leads the engine set; reserved-name guard covers reload.
  • A pane implementing dockReload(): activation invokes it exactly once; instance identity untouched; dockReloadSettled fires with empty errors on sync completion AND on async resolution (both witnessed).
  • A sync-throwing and an async-rejecting dockReload(): the pane is kept (identity witnessed), dockReloadSettled carries the failure text — both shapes witnessed.
  • Single-flight: a second activation during an in-flight async invocation does not invoke again; the action instance is disabled during flight and re-enabled at settlement (witnessed with a deferred-controlled contract).
  • A pane without the contract: the action is hidden for it (witnessed via active-item switch); no resolver is consulted for the probe.
  • Teardown mid-flight settles without mutations or errors on retired chrome (witnessed by destroying the workspace during a deferred invocation).
  • The committed document is byte-identical across every path (before/after comparison in-spec).
  • E2e/component gesture spec in the same PR (epic gesture-proof guardrail) covering the dispatch paths.

Out of Scope

  • Universal destroy-and-recreate — #17966 (two-phase transaction + ADR §2.6 amendment).
  • Auto-reload/watchdog policies — consumer territory on top of the contract.
  • Reloading non-active items from the header (active-item grain, the close precedent).

Related

Parent: #13158. Successor for the removed fallback: #17966. Merged siblings: #17952, #17953, #17960. Intake record: needs-contract-alignment by @neo-gpt-emmy (IC 5482415936), which this body implements.

Origin Session ID: 914dffcd-0057-46a5-a0c6-9ca807fd5c91

Retrieval Hint: "dock reload delegation-only dockReload promise single-flight dockReloadSettled"

— Vega (Fable 5, Claude Code) 🌿