Context
Intake on #17550 (Fleet activity stream) exposed a missing Runtime Engine primitive. The operator wants a real scrollable history with buffered rendering: bounded mounted DOM, not the current dead “N earlier events” fold. Current Neo offers two neighboring shapes, neither reusable for this consumer:
Neo.list.Component preserves list/listitem semantics, Store, ListModel, and component rows, but renders one VDOM item/component per store record.
Neo.grid.Body owns a fixed row pool and bufferRowRange, but Neo.grid.Container also hard-owns grid/gridcell semantics and grid chrome. A disguised one-column Grid would regress the activity stream’s existing role=log contract.
- #13045 / PR #13051 proves settled-block windowing for the markdown component. It is the gold-standard geometry pattern, but deliberately domain-specific and variable-height-estimate shaped.
The activity case has the simplifying invariant generic lists can safely require: fixed itemHeight.
The Problem
src/list/Base.mjs#createItems() iterates the full Store and writes one list item per record. src/list/Component.mjs additionally retains component instances by record index. A 500/5,000-row history therefore scales DOM and component ownership with history length.
- Reusing Grid would import the wrong semantic surface.
GridContainer.role defaults to grid; rows emit gridcell; header/view/scrollbar composition is created unconditionally. Activity/feed/list consumers need ul/li + optional log semantics, not hidden grid chrome.
- Windowing cannot key DOM identity to logical records. A pool slot must survive while different records pass through it; selection, focus, and click resolution must still resolve the current logical record.
- Prepend-heavy live feeds need a stable scroll anchor. Rebinding the Store while a reader is in history must not steal the viewport.
- Implementing these rules inside
apps/agentos would make a second virtual renderer outside the engine.
The Architectural Reality
Neo.list.Base owns Store integration, ul/li topology, Navigator subscription, ListModel selection, and the protected createItemContent(record, index) render hook.
Neo.list.Component owns component-item lifecycle and reuse; specialized consumers such as src/calendar/view/calendars/List.mjs update existing child instances through that hook.
Neo.grid.Body supplies the reusable fixed-height math precedent: available rows + symmetric buffer, stable pool slots, mounted/visible ranges, and scroll-index projection.
Neo.component.markdown.Component supplies stable spacer IDs, page-quantized range changes, and “do not rebuild inside the buffer” precedent.
- Main-thread resize observation already has the standard register/unregister path used by Grid and Helix.
The Fix
Add src/list/Buffered.mjs: Neo.list.Buffered extends Neo.list.Component and keeps the existing list consumer contract while replacing record-cardinality rendering with a fixed-height component pool.
- Fixed-height contract. A positive inherited
itemHeight is mandatory; construction fails loudly when absent/invalid. Variable-height measurement is not smuggled into this leaf.
- Mounted range. New reactive
bufferRowRange (default 3) plus observed viewport height derive availableRows, mountedRange, and a pool bounded by availableRows + 2 × bufferRowRange (clamped to Store count).
- Stable topology. The root keeps list semantics. Stable top/bottom spacer IDs represent unmounted extents; stable slot IDs represent pooled
li nodes. Logical record identity rides an explicit data field and the class’s lookup methods—not the slot id.
- Component recycling.
src/list/Base.mjs passes an optional physical poolIndex through the existing createItem() → createItemContent() hook. Buffered adds itemConfig (object or factory) + recordProperty for the generic path: one component is created per physical slot and reconfigured with the current record. Subclasses may still override createItemContent(record, logicalIndex, poolIndex) directly. Excess slots are destroyed when the viewport/pool shrinks.
- Scroll + resize. Normal captured scroll events move the mounted range only after the visible range approaches/exits its buffer; ResizeObserver deliveries recompute capacity. Scrolling within the current range is delta-free.
- Store mutations. Load/sort/filter/recordChange rebind only affected mounted slots. When records prepend/reorder while the reader is away from the leading edge, preserve the first visible logical record + pixel offset if that record survives; otherwise clamp deterministically.
- Selection/focus. Click, selection,
selectItem(), and programmatic focus resolve logical records across recycling. Targeting an unmounted record first scrolls/mounts it, then delegates to the normal Navigator/ListModel path.
- Documentation. Full JSDoc/Anchor-and-Echo coverage and a
src/list/ row in learn/benefits/ArchitectureOverview.md’s Structural Inventory. No ADR: this is a localized Runtime Engine class using existing List/Grid contracts.
Canonical unit coverage: test/playwright/unit/list/Buffered.spec.mjs, following the pooling discipline in test/playwright/unit/grid/Pooling.spec.mjs.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
Neo.list.Buffered (new) |
Neo.list.Component + this ticket |
Store-bound component list with bounded fixed-height row pool and list semantics |
invalid/missing itemHeight throws before mount |
class JSDoc + Structural Inventory |
500/5,000-record pool-bound unit witnesses |
bufferRowRange_ (new) |
Neo.grid.Body#bufferRowRange |
non-negative integer rows mounted above/below visible range; default 3 |
invalid assignment keeps prior valid value |
config JSDoc |
edge cases 0/default/runtime change |
mountedRange / pool slots (new readonly state) |
Grid pool precedent |
stable slot topology; range maps slots to current logical records |
empty Store → two zero-height spacers, zero rows |
member/method JSDoc |
slot identity + component identity across scroll |
itemConfig / recordProperty (new) |
Neo.grid.column.Component#component pooling precedent |
object/factory config creates once per slot; current record is assigned through the declared property on recycle |
invalid/absent config falls back to ordinary list text rendering |
config + class JSDoc |
pooled component identity and record-change witnesses |
createItemContent(record, logicalIndex, poolIndex) (extended protected hook) |
Neo.list.Base#createItemContent; Neo.list.Component consumers |
logical index stays Store-relative; pool index identifies the reusable component slot |
subclass may ignore the optional third argument and keep ordinary behavior |
method JSDoc |
Base compatibility + buffered subclass fixture |
| logical record mapping |
ListModel/Navigator contracts |
slot events and APIs resolve the currently bound Store record; unmounted targets scroll before focus/select |
removed target clears/clamps through current ListModel rules |
method JSDoc |
click/select/focus across recycle |
| prepend/reorder scroll anchor |
#17550 consumer requirement |
preserve first visible record + pixel offset while reading history |
missing anchor record clamps to nearest surviving logical index |
method JSDoc |
prepend/sort/filter fixtures |
Acceptance Criteria
Out of Scope
Variable-height measurement/reflow · horizontal virtualization · Neo.grid.* changes · markdown windowing changes · animation interoperability with list.plugin.Animate (#17552) · the Fleet activity consumer/count contract (#17550).
Avoided Traps
- App-local virtualizer: rejected; list windowing is Runtime Engine ownership.
- One-column hidden Grid: rejected; performance reuse does not justify semantic/chrome regression.
- Record-keyed DOM nodes: rejected; record identity and physical pool identity are different truths.
- Generic variable-height framework: rejected for this leaf; fixed
itemHeight makes the pool deterministic and one-PR-sized.
- Plugin interception stack: rejected for the first slice; a dedicated list class avoids order-sensitive method replacement beside
list.plugin.Animate.
Related
Consumer: #17550 (will be blocked by this ticket after creation). Gold standard: #13045 / PR #13051. Adjacent/disjoint: #17552. Grid precedent: src/grid/Body.mjs.
Decision Record impact: none.
Structural pre-flight: full path. Chosen src/list/Buffered.mjs over src/grid/ and apps/agentos/; Structural Inventory update required. The repo-wide structure-map command currently fails with a V8 string-length error; bounded --root src/list --files --loc succeeded (6 list classes + 1 plugin, 914 LOC).
Live latest-open sweep: checked the newest 20 open issues at 2026-08-22T16:13Z; no equivalent. Exact live search found only consumer #17550 and disjoint animation #17552. A2A last-30 sweep found no competing claim; claim MESSAGE:dee7535c-a67f-4cf9-9c96-c6707239453a owns this leaf.
Origin Session ID: f47f948b-743b-4c11-84a8-fa60a567a148
Retrieval Hint: “fixed-height buffered component list stable row pool scroll anchor list semantics”
Context
Intake on #17550 (Fleet activity stream) exposed a missing Runtime Engine primitive. The operator wants a real scrollable history with buffered rendering: bounded mounted DOM, not the current dead “N earlier events” fold. Current Neo offers two neighboring shapes, neither reusable for this consumer:
Neo.list.Componentpreserves list/listitem semantics, Store, ListModel, and component rows, but renders one VDOM item/component per store record.Neo.grid.Bodyowns a fixed row pool andbufferRowRange, butNeo.grid.Containeralso hard-owns grid/gridcell semantics and grid chrome. A disguised one-column Grid would regress the activity stream’s existingrole=logcontract.The activity case has the simplifying invariant generic lists can safely require: fixed
itemHeight.The Problem
src/list/Base.mjs#createItems()iterates the full Store and writes one list item per record.src/list/Component.mjsadditionally retains component instances by record index. A 500/5,000-row history therefore scales DOM and component ownership with history length.GridContainer.roledefaults togrid; rows emitgridcell; header/view/scrollbar composition is created unconditionally. Activity/feed/list consumers needul/li+ optionallogsemantics, not hidden grid chrome.apps/agentoswould make a second virtual renderer outside the engine.The Architectural Reality
Neo.list.Baseowns Store integration,ul/litopology, Navigator subscription, ListModel selection, and the protectedcreateItemContent(record, index)render hook.Neo.list.Componentowns component-item lifecycle and reuse; specialized consumers such assrc/calendar/view/calendars/List.mjsupdate existing child instances through that hook.Neo.grid.Bodysupplies the reusable fixed-height math precedent: available rows + symmetric buffer, stable pool slots, mounted/visible ranges, and scroll-index projection.Neo.component.markdown.Componentsupplies stable spacer IDs, page-quantized range changes, and “do not rebuild inside the buffer” precedent.The Fix
Add
src/list/Buffered.mjs:Neo.list.BufferedextendsNeo.list.Componentand keeps the existing list consumer contract while replacing record-cardinality rendering with a fixed-height component pool.itemHeightis mandatory; construction fails loudly when absent/invalid. Variable-height measurement is not smuggled into this leaf.bufferRowRange(default 3) plus observed viewport height deriveavailableRows,mountedRange, and a pool bounded byavailableRows + 2 × bufferRowRange(clamped to Store count).linodes. Logical record identity rides an explicit data field and the class’s lookup methods—not the slot id.src/list/Base.mjspasses an optional physicalpoolIndexthrough the existingcreateItem()→createItemContent()hook.BufferedaddsitemConfig(object or factory) +recordPropertyfor the generic path: one component is created per physical slot and reconfigured with the current record. Subclasses may still overridecreateItemContent(record, logicalIndex, poolIndex)directly. Excess slots are destroyed when the viewport/pool shrinks.selectItem(), and programmatic focus resolve logical records across recycling. Targeting an unmounted record first scrolls/mounts it, then delegates to the normal Navigator/ListModel path.src/list/row inlearn/benefits/ArchitectureOverview.md’s Structural Inventory. No ADR: this is a localized Runtime Engine class using existing List/Grid contracts.Canonical unit coverage:
test/playwright/unit/list/Buffered.spec.mjs, following the pooling discipline intest/playwright/unit/grid/Pooling.spec.mjs.Contract Ledger
Neo.list.Buffered(new)Neo.list.Component+ this ticketitemHeightthrows before mountbufferRowRange_(new)Neo.grid.Body#bufferRowRangemountedRange/ pool slots (new readonly state)itemConfig/recordProperty(new)Neo.grid.column.Component#componentpooling precedentcreateItemContent(record, logicalIndex, poolIndex)(extended protected hook)Neo.list.Base#createItemContent;Neo.list.ComponentconsumersAcceptance Criteria
availableRows + 2 × bufferRowRangelist rows/components; top + bottom spacers expose the full scroll extent.selectItem(), and focus resolve correct logical records before and after recycling, including an initially unmounted target.generate-docs-jsonfor touched classes and the Structural Inventory update land in the same commit/PR.Out of Scope
Variable-height measurement/reflow · horizontal virtualization ·
Neo.grid.*changes · markdown windowing changes · animation interoperability withlist.plugin.Animate(#17552) · the Fleet activity consumer/count contract (#17550).Avoided Traps
itemHeightmakes the pool deterministic and one-PR-sized.list.plugin.Animate.Related
Consumer: #17550 (will be blocked by this ticket after creation). Gold standard: #13045 / PR #13051. Adjacent/disjoint: #17552. Grid precedent:
src/grid/Body.mjs.Decision Record impact: none.
Structural pre-flight: full path. Chosen
src/list/Buffered.mjsoversrc/grid/andapps/agentos/; Structural Inventory update required. The repo-wide structure-map command currently fails with a V8 string-length error; bounded--root src/list --files --locsucceeded (6 list classes + 1 plugin, 914 LOC).Live latest-open sweep: checked the newest 20 open issues at 2026-08-22T16:13Z; no equivalent. Exact live search found only consumer #17550 and disjoint animation #17552. A2A last-30 sweep found no competing claim; claim
MESSAGE:dee7535c-a67f-4cf9-9c96-c6707239453aowns this leaf.Origin Session ID: f47f948b-743b-4c11-84a8-fa60a567a148
Retrieval Hint: “fixed-height buffered component list stable row pool scroll anchor list semantics”