Skip to content

Neo.mjs v13.2.0 Release Notes

Latest

Choose a tag to compare

@neo-opus-grace neo-opus-grace released this 11 Oct 22:37

Release Type: The Dock Layouts Release, a massive features release: 2,195 merged pull requests, as close to a major as a minor gets, with the package's entry points unchanged
Stability: Stable
Upgrade Path: See Upgrading from 13.1 below for stable package entry points, removed files and scripts, and behavior changes that require action.

TL;DR: 13.0 and 13.1 were Agent OS releases. 13.2 belongs to the Engine, and its centerpiece is Dock Layouts: declare an application's panes and where they go, and the engine builds the splitters, tabs, edge rails and drop targets. Then the arrangement leaves the browser window. Tear a pane out into its own OS window, carry it to another window, bring it home, and undo or reset the whole journey. The panes stay the same live components, not copies, because the app's windows share one App Worker. Around it, the repositories split: the Agent OS and the Fleet Manager moved to their own repositories, so neo.mjs installs, builds and tests as the Engine alone. Grid cell editing was rebuilt for pooled rows, so an edit follows its record through scrolling. Next comes Agent Institution, the cockpit where an operator runs a standing team of AI agents, itself an application on this release's Dock Layouts.

The cover of the Neo.mjs 13.2 film, titled One App, Many Windows: the Workstation's main window, with its Metrics pane torn out into a window of its own

▶ Watch the 13.2 film (1:47): the Workstation on real windows, recorded in one continuous native take at f1fbfe5c08, 34 steps without a page or cue error (the take record).


📋 13.2 in 2 Minutes

The one line: a workspace can leave its window. Panes go out as real OS windows and come home as the same live components, under one undo history.

Three heroes:

  1. Dock Layouts. Each workspace keeps its arrangement in one document in the App Worker, and one shared history spans the windows. A pane can leave as a real OS window and come home with its state, and Undo, Redo and Reset cover the whole journey.
  2. The repositories split. The Agent OS and the Fleet Manager left the Engine's repository and release on their own lines; the Engine builds, packs and tests without them.
  3. Grid cell editing. Editing returned to the Grid, built for its pooled rows and cells: double-click, Enter or F2 starts an edit, and scrolling its cell out of view suspends the edit instead of losing it.

The scale: 2,195 pull requests merged in neomjs/neo since 13.1.0 was published on 2026-07-03, about 920 of them Agent OS, Fleet Manager and DevIndex work done here before it moved. Across the organization the count is 3,041, with 379 in the Brain's repository and 333 in Agent Institution's (GitHub search is:pr is:merged merged:>=2026-07-03T21:40:28Z, the 13.1.0 release's publish time, run 2026-10-11 15:29Z). The Full Changelog at the end groups them.

The short route: build a workspace from panes and zones with the first Dock Layout tutorial, then edit in place in the Grid cell-editing example: double-click a cell, or press Enter or F2, then type or pick a number, a date or a country. The 5,000-record Buffered List shows a bounded row pool.

Read for your work: Dock if an app needs an IDE-shaped layout or spans windows; the repositories split if you install or contribute; Grid cell editing if users change data in place; Also in 13.2 and the upgrade path if you extend or ship the Engine.

Three longer reads: Drag it to the other screen shows what Dock Layouts give a multi-window dashboard and its users, A workspace that can leave its window tells how the windows share one application, and One repository per reader tells the split and the cockpit we build next.

The limit: Canvas workers now separate unrelated roots, but App, Data and VDom workers remain origin-wide; the wider heap boundary stays open under D#18730.

What we build next: Agent Institution, the Fleet Manager's own repository: the cockpit where an operator runs a standing team of AI agents, built on this release's Dock Layouts. What we build next below says where it stands.


🧩 Dock Layouts: a workspace that can leave its window

Arrange the Workstation: drop panes into tabs from an indicator menu, split a zone, fold a pane into an edge rail. Then pull a tab past the browser window's edge, and it becomes a real OS window mid-gesture. Carry it over another window, and that window takes over the drag as a tab header whose drop zones answer the held pointer; release, and the pane joins the target. Close a popup, and all its panes return to main in one undoable step with their component, provider and store identities intact. Undo and Redo walk the arrangement across windows, and Reset to default brings back the shipped one.

The Workstation mid-drag: a drop indicator and a drop region painted over the 100k Matrix

The Metrics tab carried over the 100k Matrix, before release: the indicator offers five placements, and the region the tab would take is painted. A frame from the Workstation's film tour.

One document for the whole workspace

That continuity needed a different foundation. Before it, you could select the second tab and resize its split, and the first tab came back. An initial edge-zone band offered no resize handle, while a split made by rearranging panes did. The July choreography and perspectives demos could move a pane into a popup opened from the workspace and bring it home, but those gestures exposed a deeper problem. Selection lived in a view while geometry lived in the stored layout; the next projection could disagree with what the user just did (D#17818, #17836).

The August decision was to give the workspace one authoritative document for its current state. The App Worker owns that serialisable dock document and its perspectives. A window renders a projection; a drag or resize proposes a semantic operation, and one reducer commits it. Pointer previews and hover pixels remain temporary, while DOM geometry and physical windows stay on the main thread. Persistence receives the document, never a DOM node or a window object. ADR 0029 records that boundary.

flowchart TD
    gesture["Main thread<br/>pointer gesture<br/>live preview"]
    reducer["App Worker<br/>applyDockZoneOperation<br/>one pure reducer"]
    doc[("dockModel<br/>the committed document")]
    project["App Worker<br/>LayoutAdapter.project<br/>+ Reconciler"]
    store[("Persistence")]
    gesture -- "proposes a semantic operation" --> reducer
    reducer -- "commits" --> doc
    doc -- "re-projects" --> project
    project -. "renders panes, identity kept" .-> gesture
    doc -- "the document, never a DOM node" --> store

A window can host a workspace of its own. In the Workstation, a popup holds a workspace with its own document, and a move between windows commits both documents in one Group transaction, or neither (ADR 0029, workspace topology). That Group's history is the one Undo and Redo walk.

This was a deliberate hard cut, with a real upgrade cost. npm neo.mjs@13.1.0 shipped three experimental dock foundations — DockZoneModel, DockLayoutAdapter and DockSplitter. The new system replaces those Dock* files without a compatibility alias; a 13.1 consumer importing them must port. The Workstation and dashboard examples were updated with the engine cut (#17841). The migration is spelled out below.

The model cut landed on 2026-08-29 (#17841). The dock then stopped asking each consumer to reconstruct facts only the engine knew (#18151), and its growing Workspace class gave projection, interaction, persistence and window behavior distinct homes (#18304). Tab activation and edge extent now commit through that document; splitters preview both sides live before one terminal commit. The Workstation's edge once booted pinned at its CSS maximum, so it could not grow right and snapped back after some leftward drops. #17858 removed those caps and restored the preview floor. Selection and width now survive the next projection rather than being reconstructed from a view that disagrees with the stored layout. A commit no longer shows an empty workspace while it swaps shells, and a tabs node that crosses the root keeps its tab container on screen.

For a developer, a first arrangement is declared, not programmed (#18474). Extend Neo.dashboard.dock.Workspace and give it two configs: panes, a catalog of ordinary component configurations keyed by names you choose, and zones, which says where those names go. A pane is not a Neo.dashboard.Panel instance, and it never learns about layout (document contract):

import Component from '../../../node_modules/neo.mjs/src/component/Base.mjs';
import Workspace from '../../../node_modules/neo.mjs/src/dashboard/dock/Workspace.mjs';

class MainView extends Workspace {
    static config = {
        className: 'MyApp.view.MainView',

        panes: {
            editor : {module: Component, header: {text: 'Editor'},  html: 'Editor'},
            preview: {module: Component, header: {text: 'Preview'}, html: 'Preview'},
            console: {module: Component, header: {text: 'Console'}, html: 'Console'},
            outline: {module: Component, header: {text: 'Outline'}, html: 'Outline'},
            search : {module: Component, header: {text: 'Search'},  html: 'Search', autoHidden: true}
        },

        zones: {
            center: {
                orientation: 'horizontal',
                sizes      : [0.62, 0.38],
                children   : [
                    {items: ['editor']},
                    {items: ['preview', 'console'], activeItemId: 'preview'}
                ]
            },
            left : {items: ['outline'], extent: 0.22, resizable: true},
            right: {items: ['search']}
        }
    }
}

export default Neo.setupClass(MainView);

The engine builds the rest: a splitter between editor and a tab group of preview and console, a resizable outline band on the left, and search on the right edge rail, revealed over the content. Every tab drags, and a drop moves the existing component instead of rebuilding it, so a pane keeps its state, scroll position and focus. A pane's module can also be a function returning a dynamic import(), so a pane nobody opens never loads. The first-layout tutorial runs this layout live, and you can edit it in place.

The Workspace composes its own cross-window participation once a sort group is published, and its header actions are a plugin a host can decline. The tear-out, the vessel embodiments and the native re-show are registered classes whose host seams are configs, so a subclass or a Neo.overwrites entry replaces the code a gesture actually runs. A host that turns tear-out on without publishing that group gets a warning at projection, since none of its torn-out windows could be dragged back. Perspective selection follows reactive state (#18605). A window group owns transaction history and the topology needed to restore a saved arrangement across reload (#18303). The Workstation extends that host with its own panes and vessel lifecycle; the Dock Layouts guide series starts with that ownership choice.

The document is visible in the controls. A tab can offer the engine's lock, reload, pin, pop-out, maximize and close actions (Workspace action contracts). The rail tab reveals a pinned pane from the edge, and the reveal's pin brings it home; a click into a field inside the reveal keeps its focus, a reveal open across a commit stays open, and one a pointer opens paints no focus ring. A keyboard detach command gives the window route a path when a platform refuses a pointer-born popup. A drag paints the region it would occupy before committing. Structural moves use FLIP motion, while the other transitions read shared tokens that collapse for reduced motion. Rails and splitters take their structural paint from the engine (#17524, #17531), with dock theming across every shipped theme.

The Workstation with System Metrics revealed from the right edge rail as an overlay

System Metrics, folded into the right edge rail, revealed from its rail tab over the workspace. A frame from the film tour.

A pane can become a real window

Without an explicit staging scheme, a popup opened through Main.windowOpen inherits the opener's color scheme instead of flashing the browser default before the app mounts. Releasing the pane commits one semantic move; cancelling it leaves the document untouched. A later journey handles the browser's native title bar, where the page cannot see pointerup: holding the popup over a target for 1.2 seconds fills its drop area and commits the transfer, then closes the popup. A whole stack can come home atomically; live-header preparation preserves its pane identities, and the Windows guide shows the route. A whole stack returned into a tabs node inside a split now settles: main's in-place projection had waited on itself, so the emptied window stayed open and main stopped projecting later commits. The title-bar dwell now returns a whole stack as well, refusing a partial stack or one from another Group. A blocked popup rolls the detach back. In the Workstation, a pane torn out again after a whole-stack return opens with its content instead of as an empty black popup, and a new popup shows its pane as a dock tab from the first frame, where it had been a dark window with a half-height card. From birth, that tab strip is the compact one the committed popup keeps, and the commit moves the pane's live DOM instead of rebuilding it. A popped-out Grid scrolls and virtualizes again: its dock host had grown to 1,885 px inside a 240 px window. A pane that a transfer lands in a popup is re-rastered once seated; Chrome had shown its card with only a kicker and an icon, its DOM complete, until another paint came along.

Two OS windows: the Workstation, whose source stack reads Moving pane to another window, and a new window holding the Metrics pane

A tab leaving the browser as its own OS window, mid-gesture: the source stack reads "Moving pane to another window…" while the new window already holds the pane. A frame from the film take on a real desktop.

A torn-out window over the Workstation's main window, with the drop region and indicators painted under it

Home again: a torn-out window held by its title bar over the main window, whose drop region and indicators paint under it. A frame from the same take.

🪟 War Story: green in emulation, broken by hand

On 2026-09-25 the operator tried the cross-window gesture by hand. A native popup dragged over another popup or the main window worked. A tab torn out of the dock and carried over another popup did nothing: no drop zones, no re-integration (#19225). The Workstation tour that drives the same gesture was green.

The tour ran under viewport emulation, where a popup has no window chrome; the hand ran on real windows, which have it. That one difference hid three defects:

  • Size. A newborn popup on real Chrome reports an outerWidth of 0 before it has a frame. The total-size resize read that value, so every torn-out vessel was born at Chrome's minimum width, 146×239 instead of the requested 320×240 (#19224).
  • Plane. The conversion sampled the vessel's content rect while the pointer rides its frame, so a title bar's height sat between the hand and the measured overlap (#19232).
  • Z-order. Once the conversion admitted, the operator's screen recording showed the last defect. The parked vessel stayed on top of the target and covered its drop zones, because focus() raises nothing during a real OS drag (#19289).

Each repair moved a measurement off the emulated plane:

Over the target, the operator ruled out a third proxy shape, a window-sized live pane covering the layout the hand aims at. The drag now shows the tab-header proxy it showed in its own window, with its label. Escape cancels a drag that crossed windows, and a tab that took focus gets it back at the drop.

The park went next. A vessel waiting in a corner was still a window the user had not asked to see, and the operator's rule is that a drag shows only the windows the user owns (#19539, ADR 0029). Over another window of the group, that window now carries the drag as its tab-header proxy, and the vessel riding the hand retires, as it does over main. Leaving for the desktop opens a fresh vessel under the held pointer; that needs the site's pop-up permission, and without it the drag stays in its window. Only a window dragged by its native title bar still parks behind its target once the OS releases it. The pointer path's conversion and park went with the rule (#19542, #19552), and the main-thread DragDrop addon shrank from 2,055 lines to 901.

The Metrics popup showing five drop choices around the held Audit tab's header proxy

The Audit tab's header proxy, held over a second window, the Metrics popup, gets that window's five drop choices. A frame from the scripted proof on real windows.

The operator's hand check before the cut found three more, all on real windows:

  • Size. A torn-out tab opened its window at the tab header's proxy size, so every vessel landed on Chrome's 320×240 floor. It now opens at the size the pane showed, measured when the drag arms; the window's outer size equals the pane's, so its content is smaller by the window chrome.
  • Header text. That size put a Grid header into a window under 600 px wide, where a narrow-window rule swapped the header's own size (13 px in neo-dark) for the 16 px button label. The header keeps its own size at every width.
  • Canvas. On a film take, rows a Grid recycled after its pane moved into a popup painted blank sparklines: that popup's main thread had never connected to its canvas worker. A canvas transfer now connects it first.

A fourth repair came from a local test run, not the hand. The size fix had left a header action's pop-out at the 480×360 fallback, and four Workstation Neural Link cases went red on dev while hosted CI stayed green, because hosted CI does not select that tier (#19597). A header pop-out opens at its pane's size again. Running that tier in hosted CI is open under D#19599. For 13.2 its evidence is a local run at 3ec6e0b945: 102 passed, 3 failed and 4 skipped. Two of the failures are known timing flakes that failed before these repairs as well, and the third was a test assumption, since repaired.

The last hand check ran in the installed Fleet Manager. A pane popped out through its header action and dragged by its title bar over the main window painted no drop zones. A window moved by its title bar reaches the page through no event, so the dock knows where it is only through the WindowPosition main-thread addon, and the dock had armed that addon only where an app listed it. The Fleet Manager listed none, and its popups run a child app with a config of their own. Now the dock loads the addon into each window it arms whenever its App Worker is a SharedWorker, so a tear-out app lists nothing for it.

The arrangement has a way back

The Workstation exposes the Group's shared Undo and Redo cursor across its main window and popup. Reset to default enables when the committed arrangement differs from the shipped one; during a live history it returns that arrangement in one undoable commit. The tour's Start buttons enter through the same reset, so a pane torn into its own window comes home before the tour begins, and a refused reset stops the tour with its errors. The current compact toolbar shows Reset, Undo and Redo with reactive history badges. The Workstation persists Group commits automatically in IndexedDB, so a cold boot restores the saved layout with an empty undo history. The engine makes a layout capturable, while an application chooses when to store it; the standalone example captures named arrangements and restores its collection at boot. State, Operations and Persistence shows the boundary.

Closing a popup is part of that history too. In the Workstation, closing a popup returns all its panes to main in one undoable Group transaction:

  • tabs whose original home still exists return to their positions;
  • Undo reopens the popup;
  • component, provider and store identities survive the round trip.

A popup that Undo or a restored perspective reopens closes too, once its stack is home. It had stayed open and empty, because the close looked for a tear-out record that only a torn-out window carries.

A reload stays distinct from a close. The return and its replay became engine seams any dock host can choose, and the Workstation deleted its own retry loop and replay override for them: 173 source lines out, 67 in. Closing a legacy dashboard popup returns its widget exactly once, visible at once.

The flagship's evidence is the film at the top of these notes. Its native take at f1fbfe5c08 ran the whole journey on real windows, 34 steps and 19 cues without a page or cue error: the 100k Matrix left the main window and grew, its grid took one inertial throw, and its recycled rows kept their painted sparklines (the take record). Its bounds stay beside it. A place-cycle restore once hid resident-card children transiently (#16498, still open); the recorded headed place-cycle at d75cc685 showed no drift. A pooled-grid gap in the complete Workstation tour (#17871) was closed as not reproduced: the production-order tour passes, and only a reduction that skips the tour's settle step showed it.


🧬 The repositories split: the Engine on its own line

On August 19, GitHub reported 5.21 GiB of storage for neomjs/neo (#17376). Today it reports 243 MiB (measured 2026-10-09), and a fresh clone downloads a 252 MiB pack (measured the same day). What arrives now is the Engine: its source, examples, apps, guides and tests. The Agent OS, the Fleet Manager, DevIndex and a mirror of every GitHub conversation used to come along with it. Between 13.1 and 13.2, each of them moved into a repository of its own.

Why one repository stopped working

The repository carried products with different readers and different speeds, and by August the cost was measurable:

  • Size. DevIndex rewrote one data file every hour. Across 1,767 versions, users.jsonl alone held 90.5% of all blob bytes in the history; with the rest of the apps/devindex/** and apps/devrank/** paths it was 95.6% (#17238). At 22.97 MiB it was also the largest file in the npm tarball, until #17253 stopped shipping DevIndex's data.
  • Onboarding. Days before the Agent OS cut, the Engine's root package.json held 117 scripts, 83 of them ai:* (audit, 2026-08-25). It now holds 38, and none of them is ai:*.
  • Release lines. npm install neo.mjs should deliver the Engine, while the Agent OS ships on its own cadence. The Introduction guide explains why the two have different homes and release lines (#18337).

How it ran

DevIndex went first. Its collection left the Engine's pipeline on August 19, and the app moved to neomjs/devindex a day later (#17429). Then came the history. #17376 purged the three generated DevIndex files with a partial rewrite, rehearsed on fresh mirrors first. Afterwards a fresh public mirror packed 231.57 MiB (measurement).

The Agent OS followed (#17500). The Engine had already learned to build with the Brain absent (#17506). On August 26 at 22:28 UTC, #17806 removed the received Brain implementation from this tree. It deleted 1,863 files, which are now at home in neo-agent-brain. A day later, #17810 deleted the Fleet Manager's duplicate, 303 files. The Fleet Manager lives in neo-agent-institution. The shared skills moved into neo-agent-skills, which the Engine, the Brain, the Fleet Manager and DevIndex install from npm.

Note

Thirty-one minutes. The Fleet Manager's repository did not come out of the fourteen preparation leaves behind the Agent OS cut. Its layout was written down as the target first, and the existing Fleet Manager source was ported into it. The first commit brought 134 files at 22:23 CEST on August 26, and the third ran the app at 22:54. Porting into a layout written in advance is easier than extracting 1,863 files with live consumers. Still, it is the lesson we kept from the split: when the target can be written, write it first, then port into it.

Green runs, frozen corpus

The cut also deleted 28 lines from buildScripts/dataSyncPipeline.mjs. Among them was the stage that generated the corpus of issues, pull requests and discussions; it called an ai/ script that had left with the Brain. Nothing failed. The corpus stopped moving on August 26, and two days later it crossed the data-sync watchdog's 48-hour age threshold (#17834). The watchdog's own updates kept reading "0 consecutive failures" while the corpus aged. It caught the freeze only because it measures the corpus's age and the runs on separate axes.

The answer was a home, not a patch. On September 8 the Engine stopped its data-sync schedules, because the corpus was leaving this repository (#18451). Generation moved to an hourly workflow in github-content-sync, created on September 19 (#17416). On September 29 the Engine dropped its 17,761-file mirror of GitHub (#19322), and the release notes moved to .github/RELEASE_NOTES/. Since #19547 that folder holds only the note being written; the published notes live in the corpus. The Portal's content views and build generators now read declared corpus and release-notes roots.

What you install now

You want You get it with
The Engine npm install neo.mjs. The package carries no Agent OS code, no corpus and no plane state (#17253).
The Agent OS A clone of neo-agent-brain plus npm install, which materializes the shared skills and starts no agent session.
The Fleet Manager A clone of neo-agent-institution plus npm install and npm run server-start. It pins the Engine and the Brain.
The skills neo-agent-skills from npm, materialized by the repository that installs it.

The README maps the repository homes, and the Portal's Agent OS boxes open the Brain's own docs.

flowchart TD
    fm["neo-agent-institution<br/>the Fleet Manager"]
    pages["pages<br/>neomjs.com"]
    brain["neo-agent-brain<br/>the Agent OS"]
    devindex["devindex<br/>DevIndex"]
    corpus["github-content-sync<br/>the GitHub corpus"]
    engine["neomjs/neo<br/>the Engine · npm neo.mjs"]
    fm -- "pins" --> brain
    fm -- "pins" --> engine
    brain -- "pins" --> engine
    brain -- "Knowledge Base ingests" --> corpus
    devindex -- "npm ^13.1" --> engine
    pages -- "npm, at a release" --> engine
    pages -- "a pinned revision" --> corpus

The split is still moving in places: #17416, the epic that gives content sync its own repository, is still open. Where the Agent OS and the Fleet Manager go next is the subject of What we build next, further down.


✏️ Grid cell editing: the edit follows the record

Double-click a cell, or press Enter or F2, and type. Scroll the Grid while the draft is open. When pooling takes the cell out of the DOM, the edit and its draft wait, suspended, in the App Worker, neither committed nor cancelled, and the render that shows the record again brings the editor back. A column whose editor holds state only its DOM has opts out with cancelEditOnProjectionLoss: its edit is cancelled instead, and the grid fires cellEditCancel with reason: 'projectionLoss', never silently. With a valid draft, Tab commits and moves to the next editable cell, and Shift+Tab moves back; at an edge, the edit ends on the Grid View. The public example's source is on dev, and the Grids guide teaches the editing contract alongside it.

The cell-editing example's grid with an open editor in the Firstname column

An open cell edit in the public example, holding a typed draft.

Four days, twenty-seven leaves

The old cell editor was still in the package, but a developer trying the public example could not use it. At dev@d688710175, the example rendered no rows; double-click, Enter and F2 mounted no editor, and double-click threw. The 2024 implementation was a 34-line subclass of Table editing, written before the Grid pooled rows and cells. Two repairs that kept that inheritance (#17937, #18222) were dropped. A Grid editor needed to belong to the Grid's own projection model.

D#17832, opened on 2026-08-28, set that model. The editing session is logical: {recordId, dataField} names what is being edited, while the Row places the editor in whichever pooled cell currently represents it. The epic that built it, #18762, opened on 2026-09-16 and closed on 2026-09-19 with 27 leaves.

The example changed the plan

The first implementation and its synthetic text fixture were green. Then a real pointer-and-keyboard pass through the public example failed six of eleven interactions. Picker focus, ComboBox ownership, Tab, locked bodies and accessibility attributes were not the same problem as placing one text editor in one cell. A return review over 120 columns and 400 rows found one more missed path, arrows while an edit was suspended (#18958). The repair grew from placing an editor to keeping a real user's edit intact through the Grid's changing view.

That evaluation has a precise limit. The aria-readonly tests inspect the rendered attribute; no screen-reader announcement was run (#18876). The IME witness uses synthetic Chromium composition events, not a real OS IME. Non-text editors in nested fields and tree columns were witnessed after the epic closed (#18991); the earlier closeout did not establish them.

Note

🧊 The tier that can see the cube

On 2026-09-19, an issue curated for new contributors (#19010) asked for a unit spec over Neo.layout.Cube, the CSS 3D layout behind the portal's cube easter egg. The operator, Tobi, corrected the tier, in words #18985 records:
"e2e and component-based testing beats unit testing for a long shot when it comes to visual items."

A unit assertion could only read back the transform the layout had just written, and it would pass while the cube rendered inside out. The issue moved to the component tier. @sloemo01's component spec now asks the browser which face elementFromPoint hits at the cube's center, for all six faces. The same limit runs through this release:

  • the Grid editor's synthetic fixture stayed green while a real pass failed six of eleven interactions;
  • the Workstation's emulated tour stayed green while a tab carried by hand over a real popup never converted;
  • the Buffered List's unit-level scroll calls missed a 3,780px runaway (below).

🧰 Also in 13.2

Buffered List: thousands of records, a small physical list

A long activity history should still behave like a list. Buffered List keeps the Store, ListModel and ul/li contracts while separating logical records from physical row slots: in its 5,000-record unit fixture, five visible rows and a two-row buffer on each side need nine mounted components, and the example runs a 16-row pool over the same 5,000 records. After the fixed-slot pass, a forward and reverse scroll that had made seven and two moveNode calls makes none.

The first real-wheel investigation found what unit-level scroll calls could not. A sixth 30px wheel delta crossed the mounted-range boundary at 180px, and within 1.5 seconds the list had run to 3,780px with no further gesture: browser scroll anchoring kept compensating for the growing top spacer, and each compensation changed the range again. One overflow-anchor: none rule stopped the same gesture at 180px (#17944). The investigation also retracted its own early green claim, whose checks had sampled one animation frame after input.

Menus, controls and composition

A menu assembled by independent modules can take one TreeStore of parent-linked commands. Hovering a parent previews its submenu without stealing focus, Right and Left walk levels, and closing a branch hands focus back to the opening item instead of dropping it on the page body. The pointer and keyboard paths have Chromium coverage; screen-reader and touch behavior have not been independently verified.

A TabContainer can carry header actions and keep overflow in its action rail without hiding the focused tab or collapsing the strip to its active tab after a reorder. Grid columns resize header and body together during the gesture, and a buffered Grid fires scrollEdge when its reader reaches the store's last rows, so nobody has to load everything to know they reached the end (the loading pattern). The Chip field is an array-valued multi-select, FileUpload accepts an app-supplied transport, and a Store supplied to a ComboBox stays unfiltered for its other consumers. Theme authors' palette overrides win across the pure Neo token sheets, and a shared motion vocabulary reaches all five themes. Collection#findBy and Store#forEach stopped copying every item on each iteration, which had made both quadratic, and Neo.merge skips prototype-chain keys in parsed input. A Grid keyed by its records' own ids (useInternalId: false) repaints a row when a snapshot replaces its record under the same key, where it had reused the old cells. Store#setData(data, {continuation: true}) assigns an already-held projection through the existing load-continuation contract; a plain assignment is unchanged.

Three issues curated for new contributors were fixed in their authors' first Engine PRs. A number field holding 0 shows its clear trigger (@GautamSahu028). The Paging toolbar treats an empty store as one empty page (@Parithosh-Varma), where next and last had stayed enabled and last went to page 0; @sloemo01's coverage spec had recorded that behavior as found. A form field keeps its own bottom margin whatever order the stylesheets load in (@Saadanjum0). A fourth first PR added coverage: @nightcityblade gave the Label component its first spec.

Runtime: a quiet window still has work to do

A worker-driven app must keep making progress while its window is not painting. The Workstation's hidden panes had stopped advancing: size changes waited on paint until ResizeObserver gained a paint-independent carrier, and the tour waited on a requestAnimationFrame a hidden pane never receives. Later, an App Worker hidden tick brought a size change, which a tab hidden for six minutes had missed for more than 90 seconds, to the worker in 0.71 seconds in a controlled Chrome run. That is a measured case, not a latency promise.

Failures became visible. In a SharedWorker development build, App Worker and other SharedWorker errors now reach the page console, and the Engine E2E error gate fails a test on one. A remote method that throws answers its caller with exactly one rejection; a synchronous throw used to send no reply, so the caller's promise never settled.

A render belongs to its own flight: a successful flight settles the update promises it claimed, and in a real Grid editor probe, 26 typed letters survived record updates every 4ms, where the previous behavior lost letters in three of three runs. And a second root keeps the first one painting. Two unrelated tabs of one app had shared an origin-wide Canvas SharedWorker, where one could stop the other's canvases (the witness). The Canvas group repair gives each unrelated root its own Canvas SharedWorker, and in the measured Chromium journey both roots keep painting. Firefox and WebKit have not been independently measured for that path.

Four VDOM repairs close the gaps around partial updates. A child that changed while its parent updated a sparse subtree could drop out of the batch, and a dense ancestor could lose a deeper child's silent update. A sparse ancestor sent a grandchild's change twice, extra work with a correct result. A first mount whose callback throws no longer strands the updates queued behind it.

A package a consumer can build

Build success has a stricter meaning. A dist/esm transform that exited 0 while its copied imports could not load now fails. The consumer-runtime check packs and installs the Engine, then compiles its App Worker against all 153 apps shipped in that measured package. It now proves the browser-bundle path as well, since the published package leaves those bundles to the consumer (the bundle decision). Two install defects were caught before publication:

  • a development-only materializer ran from postinstall, where a consumer lacked its binary. It moved to prepare, and a packed-tarball install that had exited 127 now exits 0;
  • the committed .npmignore trailed .gitignore by 16 rules, until prepack composed it at pack time.

In a workspace's development mode, a relative URL in a Fetch, Stream or Xhr connection resolves from the page. The string form of Fetch.request returns its URL in response.request; that metadata had referenced itself, so serializing it threw.

A clean checkout can prove itself. The contributor setup repair builds the git-ignored browser bundles a fresh clone lacks; without them, npm run test-unit had collected zero tests. The mounted-component tier entered CI after 41 specs had existed without a workflow, and the Engine E2E tier gained its own runner. A claim on a good first issue gets an automatic reply saying whether the claimant is first in line, and CONTRIBUTING.md explains what an assignment protects. The first live reply, on #19230, counted the claimant's draft in their own fork as a hold. #19414 repaired that rule, and through 2026-10-09 no live reply after the repair has been observed. A working packed build is not a claim that the new site has deployed.


▶️ What you can do with 13.2

Explore

You can open the current Calendar. The Portal's example catalog again lists DevIndex and keeps Calendar visible; both cards had been lost to separate catalog defects (#17437, #17487). With this release the catalog opens on the Dock Layouts card, which the live site's catalog keeps hidden until then. The DevIndex card opens the deployed app at neomjs.com/devindex/, which DevIndex now deploys itself, and its dist/development and dist/production tabs open DevIndex's own builds there. In 13.2, the month view draws its days even with no calendars or an event store that never loads; events still wait for their matching calendars. Its sidebar DateSelector no longer throws when the week's first day changes during a month slide (@sloemo01). The hosted example receives those repairs with the release.

Build

Start with the first Dock Layout tutorial: change the named panes and zones in its live previews, then use the Workstation's local run guide to try popup motion, Reset, Undo and Redo in an application.

The Buffered List example turns 5,000 records into something you can tune: change the mounted-row range, row height or viewport height and watch the list respond (#17941). It runs from a checkout: after dependencies and fresh theme assets are in place, npm run server-start -- --no-open serves examples/list/buffered/index.html on the printed local port. The styling guide gives the exact build-then-watch workflow before you change SCSS (#17902).

Extend

Once an example becomes your own app, two guides prevent a subtle class-system mistake: SetupClass explains generated is<Ntype> flags, and extending Neo classes shows why an inherited reactive config is overridden by its bare name, not a repeated underscore (#17905, #17959). Neo.manager.ClassHierarchy.isA() now recognizes ancestors above Neo.component.Base instead of stopping early. A component that needs to wait for a condition can use Neo.core.Base#waitFor() with bounded attempts; a pending wait rejects if the instance is destroyed. A custom canvas renderer can declare the context it draws with: contextType defaults to '2d' and contextAttributes pass through only when set, so a renderer in the canvas worker can draw with WebGL2, and a context the browser refuses is reported once while the renderer stays idle. The canvas host also forwards wheel, drag and modifier keys: mouse carries dx/dy, the held buttons and the modifiers, and a renderer overrides onMouseDown, onMouseUp or onWheel to handle a drag or a wheel gesture.

The engine now ships one such renderer. Neo.canvas.GraphScene draws a graph with WebGL2 on the canvas worker, with no external library:

  • the App Worker sends typed arrays;
  • an orbit camera frames the scene, and pick answers a node;
  • a frame is drawn only when something changed;
  • a lost context comes back with its scene uploaded again.

It was extracted from the Fleet Manager's Observatory, so that slice's follow-ups extend engine code.

A clustered scene draws through a foveated level of detail: far away, bundles stand in for the edges between clusters, and close up, the edges of the clusters nearest the camera's target appear. A level change uploads nothing. In a 100k-node, 1M-edge, 64-cluster scene, the far level draws 2,016 bundle lines in place of a million edges. That run used headless branded Chrome on one Apple M5 Max with the frame cap lifted. The median far level ran at 7.9× the full draw's rate (1,905 against 241 fps), and every level held 60 fps with the cap on. The GraphScene example runs that lap.

GraphScene also exposed an input-order defect. A press that followed a pointer jump within one frame reached the App Worker before the move, so a single click swung the camera from yaw 0.7 to 3.1. Every other mouse event now sends the waiting move first. After a resize, a SharedCanvas keeps the event's viewport origin for pointer input; the GraphScene example had picked every later click at the old offset. A SharedCanvas pauses its renderer while its window is hidden.

The Earthquakes map tutorial now asks you to supply your own Google Maps key instead of lending you one (#18830).

Ask, show, contribute

Built something with Dock Layouts, or hit a wall? Open an issue, start a Discussion, or find us on Discord and Slack. For a first pull request, take a good first issue: they stay reserved for first-time contributors (CONTRIBUTING.md).


🧭 What we build next: Agent Institution

The split gave the Fleet Manager a repository of its own and a product name. Agent Institution is the cockpit where an operator runs a standing team of AI agents:

  • a roster with each agent's provider and model family, health and lifecycle state;
  • the team's activity, tasks, memories, messages and wake state;
  • lifecycle controls that start and stop its agents.

It operates the Agent OS over its Fleet transport and copies none of the Brain's implementation.

The cockpit is also an application on this release: a Dock Layouts workspace with SharedWorkers on (neo-config.json). Its panes dock, pop out into their own windows and come home through the Engine's dock (cockpit/Container.mjs).

Agent Institution's fleet view with twelve agent cards and the live activity feed

The fleet view of the operator's installed Agent Institution: twelve agents with their state and lifecycle controls, and the team's live activity below.

The Observatory: the team's graph with the golden path highlighted

The Observatory in the same app: the team's graph, 123,017 nodes, with the golden path lit.

It is at version 0.1.0. The browser quickstart runs the cockpit from a clone without a Brain checkout. It ships no sample fleet, so the cockpit reads "not answered yet" until a Fleet transport answers; live state and lifecycle actions need a configured Brain. The first run for an operator outside our team is the work in front of us now. Watch or star the repository to follow it; its guides tour the cockpit, its data states and its optional desktop shell.


⬆️ Upgrading from 13.1

What stays the same: package.json's main, exports, bin, engines, type and dependencies are unchanged.

What changes is what the package ships: the 13.1 upgrade audit counted 6,261 files in 13.1.0; a pack of a clean checkout on 2026-10-11, after the release steps at dev@a2a7d5779b, counted 5,841.

If your code… In 13.2 Do this
imports src/dashboard/DockZoneModel.mjs, DockLayoutAdapter.mjs or DockSplitter.mjs removed, with no compatibility layer (#17841) port to Dock Layouts in src/dashboard/dock/ (see the Dock Layouts chapter and the guide series, #17540)
imports Neo.util.Env (src/util/Env.mjs) removed from the public surface (#17243). Nothing in the engine called it inline the parser you used
runs anything under node_modules/neo.mjs/ai/, or one of the 59 ai:* scripts the Agent OS left the package: 500 files, with its tests (#17806) run it from neomjs/neo-agent-brain
was scaffolded by npx neo-app the workspace's ai:* scripts point into ai/ delete them. npx neo-app stops writing them with its 13.2 release (neomjs/create-app#34)
builds optional browser bundles (bundle-browser-deps, bundle-mermaid, bundle-monaco) in an existing app npm reads overrides only in the root project; the app does not inherit the package's security floors (DOMPurify ≥3.4.16, KaTeX ≥0.18.2, #19636) merge the release's package.json.overrides into your app root, preserving app-owned rules, then reinstall before rebuilding; the 13.2 npx neo-app scaffold carries them (neomjs/create-app#34)
calls buildScripts/docs/jsdocx.mjs directly renamed to generateDocsJson.mjs (#15461) use npm run generate-docs-json, whose name is unchanged
uses src/ai/fleet/*, the devindex:* scripts or the agentos / devindex apps moved to the Fleet Manager and devindex repositories (#16687, #17810, #17429) use neomjs/neo-agent-institution or neomjs/devindex
links to examples/dashboard/index.html the dashboard root became a pure parent; its demo moved to examples/dashboard/base/ (#17520) update the link to examples/dashboard/base/index.html
runs the Google Maps example or the Earthquakes tutorial the committed Maps key is gone; both ship YOUR_GOOGLE_MAPS_API_KEY (#18830) supply your own Google Maps API key

Public behavior to check: Package entry points stayed stable, but code written against 13.1 can encounter these changes.

If your code… In 13.2 Do this
declares a class field with the same name as a config Neo.create throws before construct() (#18629); in 13.1 the field silently shadowed the config put the default in static config, or rename the field or config
sets a Grid selection model on a body the body's setter throws; the View owns the model (#18661) use viewConfig: {selectionModel}, or set grid.view.selectionModel at runtime
reads this in an inline function Grid column renderer whose effective rendererScope is unset this is the Grid Container; 13.1 used the column (#18773) read the renderer's column argument, or set rendererScope: 'this'
changes the items array a Store#forEach callback receives as its third argument the callbacks share one copy, refreshed when the store's count changes; 13.1 passed each call a fresh copy (#19193) copy it before changing it
expects a ComboBox given a shared Store to filter that Store for other consumers the field filters a view it owns; the supplied Store stays unfiltered (#18857) filter the shared Store yourself if other consumers need it
expects Neo.data.connection.WebSocket to retry after a clean code-1000 close it stays closed by default (#18984) set reconnectOnCleanClose: true
reads a main-thread Neo.worker.Manager.promiseMessage() reply envelope under SharedWorkers direct main/window-targeted replies now yield the payload, and remote rejection rejects (#18768) read the value directly and catch rejections
calls a Canvas remote without windowId, including scalar setTheme('dark') it works with one known Canvas group, but rejects with NEO_UNROUTABLE when unrelated roots create several groups (#19125) pass the caller's windowId; use {theme, windowId} for setTheme
awaits a remote method that throws the caller's promise rejects once; in 13.1 a synchronous throw sent no reply, so the promise never settled (#19291) catch the rejection
enumerates a child scope's provider data with Object.keys, spread or JSON.stringify keys from every provider on the chain are listed, closest first; 13.1 listed only the closest provider's keys (#19269) read the closest provider's own data if you relied on the old result
passes a relative ./ or ../ URL to a Fetch, Stream or Xhr connection in a workspace's development mode it resolves from the page's document base, not from the worker script inside the installed package (#19435) write the URL relative to the page, or use an absolute one
points rendererImportPath at a workspace-owned src/canvas/* module that prefix now loads from the installed Engine package rather than the workspace (#19165) move the module under apps/<app>/canvas/* and update rendererImportPath

The Canvas bridge also deprecates Neo.main.DomAccess#getOffscreenCanvas in favor of transferCanvasToWorker, and Neo.worker.Canvas#retrieveCanvas in favor of registerCanvas (#19125).

Gone from the package, with no code impact:

  • 102 learn/agentos guides, now in the Brain's repository;
  • the generated apps/portal/sitemap.xml and llms.txt (#17253), which a site build now regenerates.

📦 Full Changelog

2,195 pull requests merged in neomjs/neo between 13.1.0 and 13.2.0, and 2,576 issues closed (GitHub search, run 2026-10-11 15:29Z). 13.1.0...13.2.0 lists every commit.

By area, grouped by each pull request's title scope, and by keyword where a title has none (analyzeClosedSinceRelease.mjs over the corpus at 925a028b, which holds 2,194 of them; the method):

Area Merged PRs
Agent OS, Fleet Manager and DevIndex, before they moved 920
Dock Layouts, dashboards and the Workstation 468
Build, package, tests and CI 328
Runtime, data and Canvas 135
Components, forms, menus, lists and themes 106
Docs, guides, release and the Portal 100
Grid 72
No area matched 65

The 22 epics completed in the window:

  • Dock Layouts: the greenfield rebuild (#17836), consumers stopped re-deriving what only the engine knows (#18151), Workspace split by concern (#18304), multi-window transactions, topology and reload (#18303), declared arrangements (#18474), reactive perspectives (#18605), the Workstation as a compact consumer (#18456), Qt-parity docking polish (#13158) and the guide series (#17540).
  • Grid: cell editing over pooled rows and columns (#18762).
  • Components: a button's menu takes the arrow keys as soon as it opens (#6033).
  • The split and the release path: the Agent OS repository cut (#17500, #17786) and a 13.2 that can deploy (#19047).
  • The 13.1 tail: its release notes (#14483) and its documentation overhaul (#14310).
  • Agent OS and Fleet Manager work done here before it moved: #17018, #12456, #12740, #14564, #14581 and #14745.

Five more epics closed as not planned: #17539, #9492, #17260, #10757 and #13349.

Who built it: the AI maintainers Grace, Ada, Vega, Euclid, Emmy, Mnemosyne, Clio, Phoebe, Iris and Sophie, from the Claude, GPT and Kimi model families, and Eos, whose family is undisclosed by design, with Tobi (@tobiu) steering and merging.

Thank you to the 20 contributors from outside the team whose pull requests merged in this window: @sloemo01, @Bortlesboat, @wofiporia, @dchaudhari7177, @drakeo338, @GautamSahu028, @Harbor404, @msranjana, @nightcityblade, @novice-22, @Parithosh-Varma, @Saadanjum0, @sandeep780049, @Sharawey74, @stantheman0128, @terminalchai, @thomasschijf, @tomatotomata, @vaibhav8a and @vansh2408.

13.2 is a minor release by its number and close to a major by its scope: an Engine on its own line, and a workspace that can leave its window.

All changes delivered in 1 atomic commit: 14b1924