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:
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.
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.
insert returns the parked config in that case (the caller's header read holds); the insert event still fires after the update.
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.
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.
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#createItemparks a lazy config (it stays a plain object and getsvdom: {removeDom: true},Base.mjs:449-466),createItemspushes that placeholder vnode into the items root so indices stay aligned (:516-528), andloadModulelater replacesitems[index]andcn[index]with the created instance (layout/Card.mjs:~180).container.Base#insert(Base.mjs:~670-720) does not: afteritem = me.createItem(item, index, …)it unconditionally runsme.layout?.applyChildAttributes(item, index, true)anditem.createVdomReference()— a parked config has neither avdomthe layout can mark nor acreateVdomReferencemethod, soinsertthrows (TypeError: item.createVdomReference is not a function, the same throw #18062 met on the reveal slot).add()isinsert(getCount(), item), so both entry points reject a lazy config.The Problem
The dock projection materializes panes through
insert.Workspace#refreshDockWorkspacebuilds the projection with geometry-equivalent placeholders (createProjectionPlaceholder: a hiddenComponentper item), andReconciler.reconcileProjectionthen swaps each placeholder for the resolved pane:targetBody.insert(targetIndex, pane, true)(Reconciler.mjs:696, again at:727for moves). WhenresolvePanereturns a lazy config, thatinsertthrows inside the reconciler; the refresh chain rejects (observable only on its ownrefreshPromisesnapshot — 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 thesecondary-railtabs node with the headerAdd agentactive and ONE card,neo-component-184— a bareNeo.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-formnever appears — the redAddAgentJourneyNLcarries on the second pin. The rail path (a click on theAdd agentrail tab) renders the form, because the rail loads the module itself before adding (#18062); the tab path has no such loader and relies oninsert.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 detectslazyLoadItem = module && !module.isClass && Neo.isFunction(module), skipsNeo.create, and stampsitem.vdom = {removeDom: true}— the parked shape. (Two detection spellings exist at:442and:984;layout.CardusesNeo.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 contractinsertmust 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, replacescn[index]with the reference and firescardLoaded. It runs onactiveIndexCHANGE — a lazy config inserted at the already-active index after construction is never loaded unless the inserter asks.Reconciler.reconcileProjection(Reconciler.mjs:690-730): readsinserted.headerfor the tab button config right after the insert — a parked config carriesheader(the adapter'sdecorateItemConfigstamps it), so a parked return value satisfies that read.The Fix
src/container/Base.mjs#insertlearns the parked branch thatcreateItemsalready has:createItem, if the result is not aNeo.core.Base(a parked lazy config): skipapplyChildAttributes, splice the config intoitems, spliceitem.vdom(the{removeDom: true}placeholder) into the items root atindex— indices stay aligned forloadModule's latercn[index]replacement.indexis 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.insertreturns the parked config in that case (the caller'sheaderread holds); theinsertevent still fires after the update.insertstates the parked contract (Anchor & Echo);container.Base#createItem/:984keep their detection (a follow-up may unify onNeo.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
container.Base#insert/#add(src/container/Base.mjs)createItems' parked contractitems, placeholder vnode incn; loaded now if at the active card index, else on activationdevlayout.Card#loadModule(src/layout/Card.mjs)insertfor the active indexReconciler.reconcileProjectionswap (Reconciler.mjs:696/727)headerAddAgentJourneyNLDecision 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
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 aremoveDomplaceholder 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 ondev(TypeError: item.createVdomReference is not a function).test/playwright/component/dashboard/, on thedock-lazy-railfixture): committingsetItemAutoHidden(lazy, false)through the workspace projects the lazy item into the tab flow and renders.dock-lazy-rail-paneinside a visible card, one construction; the rail arms stay green. Red ondev(the placeholder body stays, no pane).unit/container,unit/layout,unit/dashboardandcomponent/dashboardgreen.AddAgentJourneyNLgreen under the Brain root (the CTA path).Out of Scope
createItem,toJSON,layout.Card) onNeo.typeOf— a polish leaf.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 onlazy+insert(/createVdomReference is not a functionreturns 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