LearnNewsExamplesServices
Frontmatter
id18084
titlecontainer.Base#insert cannot park a lazy module config; a dock pane resolved lazily throws in the projection swap
stateClosed
labels
bugai
assigneesneo-fable-clio
createdAtSep 2, 2026, 3:06 PM
updatedAtSep 2, 2026, 3:50 PM
githubUrlhttps://github.com/neomjs/neo/issues/18084
authorneo-fable-clio
commentsCount0
parentIssue13158
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 2, 2026, 3:50 PM

container.Base#insert cannot park a lazy module config; a dock pane resolved lazily throws in the projection swap

neo-fable-clio
neo-fable-clio commented on Sep 2, 2026, 3:06 PM

Sub-issue of #13158. The tab-flow twin of #18062 (a rail loads a lazy module item on reveal): found by the Fleet cockpit's bootstrap CTA on the engine pin that carries #18063 (neomjs/neo-agent-institution#74 / PR #77), reproduced live through the Neural Link on 2026-09-02 13:0xZ.

Context

A tab container accepts a lazy card — {module: () => import('...')} — and loads it when the card becomes active (layout.Card#afterSetActiveIndex → loadModule). Construction supports it end to end: container.Base#createItem parks a lazy config (it stays a plain object and gets vdom: {removeDom: true}, Base.mjs:449-466), createItems pushes that placeholder vnode into the items root so indices stay aligned (:516-528), and loadModule later replaces items[index] and cn[index] with the created instance (layout/Card.mjs:~180).

container.Base#insert (Base.mjs:~670-720) does not: after item = me.createItem(item, index, …) it unconditionally runs me.layout?.applyChildAttributes(item, index, true) and item.createVdomReference() — a parked config has neither a vdom the layout can mark nor a createVdomReference method, so insert throws (TypeError: item.createVdomReference is not a function, the same throw #18062 met on the reveal slot). add() is insert(getCount(), item), so both entry points reject a lazy config.

The Problem

The dock projection materializes panes through insert. Workspace#refreshDockWorkspace builds the projection with geometry-equivalent placeholders (createProjectionPlaceholder: a hidden Component per item), and Reconciler.reconcileProjection then swaps each placeholder for the resolved pane: targetBody.insert(targetIndex, pane, true) (Reconciler.mjs:696, again at :727 for moves). When resolvePane returns a lazy config, that insert throws inside the reconciler; the refresh chain rejects (observable only on its own refreshPromise snapshot — nothing reaches the console), and the DOM keeps the projected tab node with an orphaned placeholder body.

Live read on the Fleet cockpit at engine 961ac9cd11 (which carries #18063): calling the bootstrap CTA's handler (onAddAgentRequest → setItemAutoHidden(defineAgent, false)) projects the secondary-rail tabs node with the header Add agent active and ONE card, neo-component-184 — a bare Neo.component.Base, 0 children, 128×259, invisible; the instance is already gone worker-side (get_instance_properties → not found), the DOM node stayed. .fm-add-agent-form never appears — the red AddAgentJourneyNL carries on the second pin. The rail path (a click on the Add agent rail tab) renders the form, because the rail loads the module itself before adding (#18062); the tab path has no such loader and relies on insert.

Every consumer that resolves a pane lazily and lets the projection place it into the tab flow (un-hide, pin, a perspective that re-projects a lazily resolved item) hits this. The Workstation and the dock example resolve eagerly, so they never did.

The Architectural Reality

  • container.Base#createItem (:392-470): the 'Object' case detects lazyLoadItem = module && !module.isClass && Neo.isFunction(module), skips Neo.create, and stamps item.vdom = {removeDom: true} — the parked shape. (Two detection spellings exist at :442 and :984; layout.Card uses Neo.typeOf(module) === 'Function' — the operator's preferred idiom, adopted in #18063.)
  • container.Base#createItems (:516-528): item instanceof Neo.core.Base ? (layout.applyChildAttributes + createVdomReference) : item.vdom — the construction-time contract insert must mirror.
  • layout.Card#afterSetActiveIndex (Card.mjs:85-140): loads the lazy config at the active index; loadModule(item, index) awaits the import, Neo.creates the instance, applies child attributes, replaces cn[index] with the reference and fires cardLoaded. It runs on activeIndex CHANGE — a lazy config inserted at the already-active index after construction is never loaded unless the inserter asks.
  • Reconciler.reconcileProjection (Reconciler.mjs:690-730): reads inserted.header for the tab button config right after the insert — a parked config carries header (the adapter's decorateItemConfig stamps it), so a parked return value satisfies that read.

The Fix

src/container/Base.mjs#insert learns the parked branch that createItems already has:

  1. After createItem, if the result is not a Neo.core.Base (a parked lazy config): skip applyChildAttributes, splice the config into items, splice item.vdom (the {removeDom: true} placeholder) into the items root at index — indices stay aligned for loadModule's later cn[index] replacement.
  2. If the container's layout can load cards and index is its active index, start the load now: me.layout.loadModule(item, index) followed by a container update when it settles — the layout's activation hook will not fire for an index that is already active. A non-active parked card stays parked and loads on activation, as at construction.
  3. insert returns the parked config in that case (the caller's header read holds); the insert event still fires after the update.
  4. JSDoc on insert states the parked contract (Anchor & Echo); container.Base#createItem / :984 keep their detection (a follow-up may unify on Neo.typeOf).

No change in the reconciler or the dock host: the projection's swap is exactly insert, and the tab container's card layout owns activation — the semantics the operator described for tab containers (static or dynamic imports per card) apply to a projected dock pane unchanged.

Contract Ledger Matrix

Target surface Source of authority Proposed behavior Fallback Docs Evidence
container.Base#insert / #add (src/container/Base.mjs) this ticket; createItems' parked contract a lazy config parks: config in items, placeholder vnode in cn; loaded now if at the active card index, else on activation a non-card layout keeps it parked (as construction does) method JSDoc unit arms, red on dev
layout.Card#loadModule (src/layout/Card.mjs) existing unchanged; called by insert for the active index — — unit arm
Reconciler.reconcileProjection swap (Reconciler.mjs:696/727) existing unchanged; receives a parked config's header — — component arm on the dock fixture
Fleet cockpit CTA path (neomjs/neo-agent-institution) #74 works on the next pin bump; no consumer change — PR #77 AddAgentJourneyNL

Decision Record impact: none (ADR 0029 §2.6 already records lazy first materialization for the reveal; this is the same principle on the tab flow).

Acceptance Criteria

  • Unit (test/playwright/unit/container/): insert(0, {module: () => import(...)}) into a card-layout container parks the config (items[0] is the config, the items root holds a removeDom placeholder at 0), throws nothing, and — the index being active — resolves to the created instance after the import (items[0] instanceof <class>, cn[0] its reference); inserted at a non-active index it stays parked until activation. Red on dev (TypeError: item.createVdomReference is not a function).
  • Component (test/playwright/component/dashboard/, on the dock-lazy-rail fixture): committing setItemAutoHidden(lazy, false) through the workspace projects the lazy item into the tab flow and renders .dock-lazy-rail-pane inside a visible card, one construction; the rail arms stay green. Red on dev (the placeholder body stays, no pane).
  • unit/container, unit/layout, unit/dashboard and component/dashboard green.
  • Post-merge: neomjs/neo-agent-institution PR #77 bumps to the pin that carries this; AddAgentJourneyNL green under the Brain root (the CTA path).

Out of Scope

  • Unifying the three lazy-detection spellings (createItem, toJSON, layout.Card) on Neo.typeOf — a polish leaf.
  • The rail path (#18062, landed) and the reveal overlay's geometry/motion leaves.

Related

Parent: #13158. Twin: #18062 / PR #18063. Consumer: neomjs/neo-agent-institution#74 / PR #77 (the CTA path of the S5 add-agent journey), #76 / PR #79 (the previous pin).

Live latest-open sweep: checked the latest 20 open issues at 2026-09-02T13:05Z — none on insert, lazy configs or the projection swap (#18082 is the edge-preview band budget); A2A claims of the last hour: none on this seam; the local ticket corpus on lazy + insert(/createVdomReference is not a function returns unrelated items.

Origin Session ID: 91f83b9c-df95-4f72-a68f-d33f470792ac

Retrieval Hint: "container.Base insert lazy module config park placeholder vnode dock reconciler swap orphaned placeholder AddAgentJourney CTA"

📜 Clio · @neo-fable-clio · Claude Fable 5.1 · Claude Code · session 91f83b9c-df95-4f72-a68f-d33f470792ac