Changelog
All notable changes to LLui packages are documented here. LLui is a pre-1.0 project — every release may include breaking changes, though we try to call them out explicitly.
How to read this file: entries are anchored by release date. Inside each release, fixes are grouped by @llui/<package>@<version> sub-sections so you always know exactly which package and version a bullet applies to. Cross-cutting changes that affect every package (like build-output fixes) live under a shared "All packages" section. Breaking changes and migration notes sit at the top of each release block because they usually cut across multiple packages.
Versioning: packages version independently — only the ones a release actually changes get bumped, so version numbers across @llui/* have drifted apart and a shared number implies nothing. Larger releases often move a group together (@llui/dom, @llui/vite-plugin, @llui/test, @llui/router, @llui/transitions, @llui/components, @llui/vike have historically shared a version line), but that is a coincidence of scope, not a guarantee. Every package that consumes @llui/dom declares it as a peerDependency with a caret range, so a @llui/dom patch reaches consumers without its dependents being republished. Interaction consumers use the same peer pattern for @llui/interactions, preserving one document-level registry. (@llui/eslint-plugin was deprecated and removed — framework lint rules now live in @llui/compiler as compile-time errors.)
2026-08-26 — @llui/components@0.16.0, @llui/dom@0.13.1
Released: @llui/components@0.16.0; @llui/dom@0.13.1; @llui/a2ui@0.3.2; @llui/devmode-annotate@0.4.2; @llui/devmode-annotate-editor@0.1.3; @llui/markdown-editor@0.8.3; @llui/cli@0.1.0
The component registry reaches shadcn/ui parity — 85 items, every headless machine with a visual
surface skinned — and the rendering pass that got it there found eleven real defects in
@llui/components and @llui/dom. Almost all were silent: valid CSS naming an attribute nobody
emits, a part bag frozen at build time, an ARIA state that renders as no state at all.
@llui/components@0.15.0 and @llui/cli@0.1.0 were prepared on the same date but never reached
npm (publish auth failed). This release supersedes 0.15.0; @llui/cli@0.1.0 ships unchanged
alongside it.
Breaking
@llui/components@0.16.0— the menu highlight is now a baredata-highlightedattribute, notdata-state="highlighted". Every other highlightable list in the package (select,combobox,listbox,tags-input), the baseline stylesheet, and every registry menu recipe already used the bare flag, so the enum spelling matched nothing: dropdown, context-menu and menubar items had no hover or keyboard highlight at all.data-stateon this machine now means only open/closed.
Migration
- If you style menu items with
data-[state=highlighted]:, change it todata-[highlighted]:. If you use the baseline stylesheet or the registry recipes, nothing to do — they were already correct and are now reached.
@llui/dom@0.13.1
- Fixed
aria-*boolean values render as the strings"true"/"false". ARIA state attributes are enumerated, not HTML booleans:trueused to renderaria-checked=""(invalid — assistive tech reports no state) andfalseremoved the attribute entirely (arole="switch"with noaria-checkedhas no state at all). A part bag doing the naturalstate.map((s) => s.checked)hit exactly this across seven component families.null/undefinedstill remove the attribute — that is how a part bag spells "not applicable".
@llui/components@0.16.0
- Breaking the menu highlight is
data-highlighted. See top of release block. - Fixed
date-picker'sdayCellreturned a frozen snapshot. Every flag was a plain value read off theDayCellpassed in, andview()runs once — so selection, focus and range preview updated in state and never reached the DOM. The bag's flags are now Signals derived from live state. - Fixed
splitterannounced every separator the wrong way round.aria-orientationnames the axis of the SEPARATOR, which is the inverse of the split axis; the value was published straight through. - Fixed
menubarlost APG's cross-menu Left/Right. With a menu open, focus is on the content, so the arrows never reached the bar — a keyboard user could not get from File to Edit without closing File first. - Fixed
slider's range spanned lowest→highest value unconditionally. Correct for two thumbs, a zero-width band for one, so a single-thumb slider tracked its value perfectly and drew no fill. - Fixed
combobox's live region only announced onstatus: 'loaded', which only an async load sets — so a combobox given its items synchronously announced nothing, ever. - Added
select's trigger publishesdata-placeholderwhile nothing is selected, which is how shadcn greys that text. Without it the placeholder rendered at full weight and read as a real selection. - Added
patterns/searchable-select'soverlay()acceptspositionerClass. Everyoverlay()undercomponents/already took one; this pattern was missed, leaving a utility-styled consumer no way to give the popup a stacking context. - Fixed the baseline stylesheet ships
tokens.cssseparately fromtheme.css, so an app can take the tokens without the unlayered component rules that would otherwise override every utility class.
@llui/a2ui@0.3.2
- Fixed the calendar builder reads day flags off the
DayCellrather than spreading the part bag, which is required now that the bag is reactive: its signals are rooted in a2ui's own surface store and cannot be mounted inside aneachrow.
@llui/devmode-annotate@0.4.2, @llui/devmode-annotate-editor@0.1.3, @llui/markdown-editor@0.8.3
- Improved republished so their
@llui/componentspeer range admits 0.16.0. No source changes.
Registry (served from llui.dev, copied by llui add)
- Added 61 → 85 items. Every
@llui/componentsmachine with a visual surface now has a skin, plusdate-picker,data-tableanddrawer. Onlychart(#227) andform(#228) remain, both filed as decisions rather than ports. - Fixed five recipes styled attributes their machine never publishes — dead CSS that compiles
cleanly.
scroll-area's thumb rendered at zero pixels,carousel's active dot never highlighted, andcalendarhad no today marker, selection fill, range or focus ring on any day. - Fixed shadcn's default props, lost in a recipe-only port.
Switchput its entire geometry behinddata-[size=default]:, so without the attribute it rendered as a ~2px sliver with a zero-size thumb — toggling correctly, looking like nothing happened. - Added
scripts/test/registry-attrs.test.tscross-checks every attribute a recipe styles against what its machine publishes, and that everydata-[x=default]recipe supplies that attribute.
Docs
- Added Using the components — a walkthrough from an empty app to
a styled, state-wired screen, with a Gotchas section for the silent traps (machines that
deliberately do not track the pointer, parts that do not hide themselves, a live region whose
textis a child). - Fixed
/stylinghad a nav entry and a content file but no route — the link 404'd.
2026-08-26 — @llui/components@0.15.0, @llui/cli@0.1.0
Released: @llui/components@0.15.0; @llui/cli@0.1.0; @llui/a2ui@0.3.1; @llui/devmode-annotate@0.4.1; @llui/devmode-annotate-editor@0.1.2; @llui/markdown-editor@0.8.2
LLui adopts the shadcn/ui token contract and its distribution model: a component
registry you copy source out of, installed by a new llui add CLI. The layer it
replaces was shipped, untested against a real Tailwind build, and broken —
116 utility occurrences across 55 files compiled to no CSS at all.
Breaking
@llui/components@0.15.0— every theme token is renamed to the shadcn/ui contract.--color-surface→--background/--card/--popover,--color-text→--foreground,--color-text-muted→--muted-foreground,--color-border-focus→--ring, and the scales move to Tailwind's real namespaces (--duration-*→--transition-duration-*,--z-*→--z-index-*,--space-*→--spacing-*). Values are oklch. Any:rootoverride of a--color-*token stops applying.@llui/components@0.15.0—src/styles/classes/*and its 61./styles/<component>export entries are removed. The per-componentxClasses()helpers had no consumer, and 116 of the utility occurrences they emitted produced no CSS:duration-fast(89 uses) andz-dialog/z-popover/z-tooltip(18) were declared against--duration-*and--z-*, which are not Tailwind v4 namespaces. Every transition was instant and every overlay unstacked. Class recipes now ship as registry source you own, verified by a real Tailwind compile in CI.@llui/components@0.15.0— importingstyles/theme.cssalongside Tailwind utility classes on component parts no longer works as intended, and never did. Its[data-scope][data-part]rules are unlayered, and unlayered CSS beats@layer utilities, so every utility on a component part loses to it silently. Import the newstyles/tokens.cssinstead.
Migration
- Rename your token overrides to the shadcn/ui names, or paste a shadcn/ui theme
over the
:root/.darkblocks — the contract is identical. Overriding a base token is enough:--primary-hover,--accent-strong,--border-hoverand--primary-soft-foregroundarecolor-mix()expressions over it and follow automatically. Do not restate them in a dark block. - Replace
xClasses()imports withllui add <component>and edit the copied recipe, or write your own utilities. - Styling with utilities rather than the baseline stylesheet? Swap
@import '@llui/components/styles/theme.css'for@import '@llui/components/styles/tokens.css'(andtheme-dark.cssfortokens-dark.css). Keeptheme.cssonly if you want every component styled with no classes of your own. - Adding your own scale tokens? Use
--transition-duration-*,--z-index-*and--spacing-*. The wrong spelling still emits a valid custom property, sovar(--duration-fast)in plain CSS works while theduration-fastclass compiles to nothing — which is exactly how this shipped unnoticed.
@llui/cli@0.1.0
- Added
llui init/llui add <item...>/llui list— copies registry component source into your project, shadcn-style. The copied file is compiled by your@llui/vite-plugin, so it gets view lowering, the compile-time lint rules, and the agent metadata ($ms/$ss/__lluiVariants) that a precompiled library cannot provide. - Added
components.jsonconfig:registry,paths.ui/paths.lib, and an optionalaliases. Withoutaliases, imports are rewritten to relative specifiers computed from where each file landed — an alias your tsconfig does not declare would resolve nowhere, so relative is the default, not a fallback. - Added safety rules that are the point of the copy-in model:
addnever overwrites an existing file (the copy is your source and is expected to have been edited — pass--overwrite), registry file targets are validated against..and absolute paths at load rather than at write, and unknown registry keys are ignored so a registry ahead of your CLI still installs. - Added registry item JSON as a documented subset of shadcn's
registry-item.json, so a third-party registry is a supported thing to host.
@llui/components@0.15.0
- Added
styles/tokens.cssandstyles/tokens-dark.css— the design tokens with no component rules. This is the import for the registry path or any app styling with utilities.theme.cssnow imports them and adds the baseline stylesheet on top, so existing consumers are unchanged. - Added
positionerClassto everyoverlay()helper (dialog, alert-dialog, drawer, popover, tooltip, hover-card, menu, menubar, context-menu, combobox, select).createOverlaybuilds the floating wrapperdivitself, so it was the one node a consumer could not reach — and it is the element that carries the layer'sz-index. Omitting the option emits noclassattribute and reuses the same props object, so nothing changes for existing callers. - Improved the token contract is now shadcn/ui's, so any shadcn theme —
including the community theme generators' output — pastes in verbatim. Colours
are mapped through
@theme inline, which means a.darkoverride reachesbg-cardwith no regenerated CSS. - Improved dark mode activates three ways at once:
.dark(what shadcn tooling writes),[data-theme='dark'](whattheme-switchwrites), andprefers-color-scheme..light/[data-theme='light']opts a subtree out.
@llui/{a2ui,devmode-annotate,devmode-annotate-editor,markdown-editor}
- Improved version-only republish so their
@llui/componentsrange admits0.15.0. No code changes; without it, a consumer pulling@llui/componentsdirectly alongside any of these would install two physical copies.
Docs
- Added Styling & the component registry — the
token contract, the two mutually exclusive styling paths, the Tailwind
namespace traps, and what
overlay()does and does not give you. - Added the Registry Demo example:
every registry component on one page, rendered from source
llui addcopied into the app. Itssrc/components/ui/is checked in, so CI compiles and browser-boots the CLI's actual output.
2026-08-17 — @llui/markdown-editor@0.8.1
Released: @llui/markdown-editor@0.8.1
@llui/markdown-editor@0.8.1
- Fixed Markdown import now derives nested-list depth from the parent item's actual content column. Bullet, ordered, and task-list parents therefore accept CommonMark's valid 1–4-column marker padding, tabs, and multi-digit ordered markers without flattening children or misclassifying indented code. ([#226])
2026-08-17 — 0.13.0
Released: @llui/{agent,compiler,compiler-ssr,dom,markdown,test,vike}@0.13.0; @llui/{a2ui,effects}@0.3.0; @llui/components@0.14.0; @llui/{devmode-annotate,lexical-collab}@0.4.0; @llui/lexical@0.5.0; @llui/markdown-editor@0.8.0; @llui/mcp@0.15.0; @llui/{router,transitions}@0.12.0; @llui/{devmode-annotate-editor,interactions}@0.1.1; @llui/lexical-loro@0.1.2; @llui/{notes-format,security}@0.2.1; @llui/vite-plugin@0.12.1; llui-agent@0.11.1
This release introduces named typed routing, shared interaction primitives, optional annotation rich editing, headless TEA composition APIs, and extensive correctness improvements across the compiler, runtime, components, transitions, rich text, agents, and development tooling.
Breaking
-
@llui/router— routing now uses a named, bidirectional registry:createRouter({ home: route('/'), article: route('/articles/:slug') }). The former ordered route array,param()/rest()segments, mapping callbacks, and fabricatedfallbackroute are removed.match()returns{ name, params } | null;location/href/toPath, connected links, and navigation effects all take the route name plus its exact parameters. Typed path/query values use synchronous Standard SchemarouteCodec()/repeatedRouteCodec()pairs and must round-trip canonically as JSON-safe URL identity. (#156) -
@llui/components/@llui/a2ui/@llui/markdown-editor— interaction primitives moved to the new@llui/interactionspeer. Applications and libraries must resolve one shared instance of both@llui/domand@llui/interactions; duplicate interaction packages split focus-trap, dismissal, nested-layer, modal-isolation, and scroll-lock registries.@llui/components/utilsremains a compatibility re-export, while new custom interaction consumers import@llui/interactionsdirectly. (#49) -
@llui/devmode-annotate— the Lexical editor is now the optional@llui/devmode-annotate-editorpackage. Core keeps a functional Markdown textarea and no Lexical dependency. The Vite plugin auto-registers the rich editor when both packages are installed; manual/live-app mounts import the editor before activation. (#75) -
@llui/lexical—PluginContextnow includes the shared commit-hub seamsonCommitandwithFacts. Custom hosts/tests that constructed{ emit }by hand must usecreateCommitHub(editor, emit). The hub is now the single update-listener/read/selection-measurement authority for editor plugins. (#70, #74) -
@llui/markdown-editor—@lexical/tableis optional and the table plugin is no longer re-exported from the root barrel. ImporttablePluginfrom@llui/markdown-editor/plugins/table; editors without tables no longer install or bundle the table package. (#75) -
@llui/components/table—selectAllCheckbox(columnId)now requires the header column whose roving tab stop operates it. The header row participates in the grid roving sequence asTABLE_HEADER_ROW_INDEX; this replaces a keyboard-unreachable select-all control. (#122) -
@llui/compiler— the newempty-propsbuild error rejects provably redundant element calls such asdiv({}, [...]); use the children-only calldiv([...]). The diagnostic carries the mechanical fix. (#82) -
@llui/compiler/@llui/compiler-ssr— low-level compiler analysis now shares one filename-awareParsedModule. The former root analyzer exports (collectDeps,extractPaths, the accessor-resolver helpers, and related compatibility types) are replaced byparseModule,analyzeAccessor,analyzeSignalExpr, andcollectSignalDeps.transformUseClientSsrnow takes that parsed module instead of(source, filename), so compiler integrations that called these APIs directly must migrate. (#92, #93) -
@llui/interactions/@llui/components— nested-layer ownership now fails closed. A missing or unresolvedregisterNestedLayer(..., { owner })no longer exempts a portal (or descendants owned inside it) from every modal; scoped lookups exclude it and development builds warn once. Context menus capture the actual right-clicked region as runtime-only ownership, including when one parts instance is spread across multiple regions, and clear that ownership when the interaction ends so a later message/replay open fails closed. The shared overlay engine now declares placement, ownership, dismissal-ignore, and focus-return relationships independently; an unresolved placement relationship no longer suppresses the actionable ownership warning. (#215) -
@llui/router— guard redirects now chain to a fixed point.beforeEnteris re-asked about each target it returns until it accepts one, blocks one, or stops moving the URL (capped at 10 hops, then rests on the last hop with a console warning). It used to be asked exactly once per navigation, so a redirect target's own guard was never consulted. This is a semantic break at every call site —push(),replace(),navigate(),link()and the browser-driven listener — not an interface one: nothing fails to compile, and a chained guard that previously rested on its first hop now rests on the fixed point. AbeforeEnterwith side effects (analytics, an auth probe) is now called once per hop rather than once per navigation. See Redirect chains in the router API docs. (#161) -
@llui/router—RouterEnv.replaceLocationis removed. Its only caller, the hash-modereplace()effect, now writes withreplaceState, which carries the entry'shistory.stateinstead of dropping it and fires nohashchange. Under a<base href>the old mechanism was worse than untidy: a fragment-onlylocation.replace()performs a cross-document navigation that unloads the app (verified in Chrome), wherereplaceStatemoves the address bar and nothing else. (#164) -
@llui/components— a numeric bound in state is finite or absent, never±Infinityand neverNaN.NumberInputState.min/maxand the axes ofFloatingPanelState.maxSizeare now optional, so a consumer readingstate.minas anumberseesnumber | undefined. That is the type starting to tell the truth rather than a new hazard: unbounded used to be spelledmin: -Infinity, which JSON round-trips tonull, so the default state of anumber-inputalready rehydrated as a field declarednumberholdingnull. Bounds are normalised at every write byutils/number.ts:finiteBound. (#177)
Migration
- Replace ordered callback routes with a named route registry. Model the current
route as
RouteLocation<typeof routes>, branch onlocation.name, treatmatch()returningnullexplicitly, returnrouting.push/replace/navigateas effects, place...routing.listener(send)in the view, and feedrouting.handleEffectto the effect chain. - Add
@llui/interactionsbeside packages that declare it as a peer and dedupe it across the application. Move standalone imports from@llui/components/utilsto@llui/interactions; compatibility imports may remain temporarily, but must still resolve the same singleton. - If the annotation HUD needs rich editing, add
@llui/devmode-annotate-editor; manual mounts import its side-effect entry beforemountAnnotateHud(), while live-app activation dynamically imports it immediately before core activation. - Import the editor table plugin from
@llui/markdown-editor/plugins/table, and install@lexical/tableonly in apps that enable it. - Replace custom Lexical
{ emit }plugin contexts withcreateCommitHub(editor, emit)and subscribe selection-aware plugins throughctx.onCommit/ctx.withFactsinstead of adding another update listener. - Rewrite
tag({}, [...])totag([...])where the compiler reportsempty-props. - Parse each source once with
parseModule(source, filename), pass that module totransformUseClientSsr, and move low-level dependency analysis toanalyzeAccessor/analyzeSignalExpr/collectSignalDeps. - If you read
NumberInputState.min/maxorFloatingPanelState.maxSizeas a plainnumber, handle the absent case — absence is now spelled by omitting the key, sostate.min ?? -Infinityis the direct replacement for the old sentinel. Persisted snapshots are not migrated: one taken before this change keeps its old form (post-JSONnull, or a live pre-upgrade±Infinity) until the component is re-inited. Both were checked through every reducer of both changed shapes and behave identically or better, so this is not a regression — but nothing rewrites them. - If you implement your own
RouterEnv, delete thereplaceLocationmember. With the documented pattern — an object literal annotatedconst env: RouterEnv = {…}— leaving it in is a compile error. It is not an error behind anas RouterEnvcast or an unannotated factory; there the member is simply dead, so grep for it. - If your
beforeEnterreturns a route that the samebeforeEnterwould itself redirect, you no longer need to fold that chain by hand — but check that you want the fixed point, and that the guard is safe to call more than once per navigation. - If your guard's decision depends on a route field the URL cannot express, note
that the chain settles on
router.hrefequality: a hop that moves only such a field ends the chain, so that hop's verdict — including afalse— is never asked for. Decide on the fields yourhrefcarries. (#212)
@llui/dom@0.13.0
- Added
mapSend(parentSend, wrap)for type-safe parent/child message routing, and the supported view-lesscreateTeaDriver()with the mounted runtime's real synchronous reducer, effect-frame, reentrancy, batching, and disposal semantics.onTransitionobserves every reducer call;onStateChangeobserves settled state. (#46, #76) - Fixed portals remove their complete live region, lazy children mounted after cancellation dispose immediately, throwing subscribers are isolated from dispatch/effects, and reentrant commit scope is shared consistently. (#55, #56, #57, #59)
- Fixed
each/virtualEachreject malformed item seams without corrupting the tree, structural children keep reconciling across gated row updates, fresh child scopes are deferred to the next traversal boundary, and escaped framework/commit-token failures remain branded instead of being swallowed as a blank subtree. (#53, #60, #165, #216, #225)
@llui/compiler@0.13.0
- Added
tag-send-drift, JSON-form@example({...}), stricter annotation parsing, nullable/optional state-schema handling, and theempty-propsrule. The signal compiler emits final metadata keys directly and the debug/agent surfaces consume one shared collector. (#45, #82, #88, #89, #98, #118) - Fixed cross-file analysis is resolved per
component()call and parses each module once; generated helper imports avoid local-name collisions, and helpers are recognized by import provenance without losing expression self-bindings. (#90, #91, #92, #93, #213) - Fixed
tag-send-driftnow handles self-named handlers, returned closures/containers, method shorthands, accessors, decorators, and parameter defaults without inventing variants. (#118, #181, #182, #194) - Breaking low-level analysis now consumes a shared
ParsedModule; see the migration section above. TypeScript is a>=5 <7peer rather than a private dependency. (#64, #92, #93)
@llui/compiler-ssr@0.13.0
- Breaking
transformUseClientSsrnow consumes the shared filename-awareParsedModuleinstead of reparsing(source, filename), preserving the real script kind for TSX and sharing one parse with the signal compiler. TypeScript is a>=5 <7peer. See the migration section above. (#64, #93)
@llui/vite-plugin@0.12.1
- Fixed injected development relays shift source maps with their generated code, and compiler transforms share the filename-aware parsed module. (#87, #93)
- Improved the Vite integration is composed from named
compile/config/dev-server/HUD plugins. When
@llui/devmode-annotate-editoris installed, the development bootstrap resolves it from the consumer root and registers it before mounting the HUD. - Improved TypeScript is now a shared
>=5 <7peer rather than a private dependency. (#64)
@llui/interactions@0.1.1
- Added standalone floating, focus, dismissal/outside, modal isolation, scroll lock, direction, nested-layer, and roving-navigation primitives. (#49)
- Fixed overlay layers no longer lose Escape handling, timers, live-region cleanup, outside-interaction ordering, or focus restoration. Non-modal portals register scoped nested-layer ownership, and effectively hidden controls are excluded from focus enumeration. (#123, #155, #215, #219)
@llui/components@0.14.0
- Improved utility and formatter wildcard subpaths are granular and tree-shakeable, while compatibility exports retain one shared interaction registry. (#49)
- Fixed overlay layers preserve Escape handling, timers, live-region cleanup, outside-interaction ordering, and focus restoration; nested dialogs restore the immediately previous dialog's focus. (#123, #155, #209, #215)
- Fixed keyboard accessibility across table, tree-view, menubar, navigation-menu, tags-input, sortable headers, and other single-tab-stop widgets. Header cells participate in table roving focus, disabled items are skipped consistently, and engine-initiated focus moves no longer masquerade as outside interactions. (#121, #122, #126, #145)
- Fixed numeric/date/component state now stays JSON-safe at every init and reducer ingress. Non-finite positions/measurements are ignored atomically; finite bounds and positive divisors are validated; slider, pin-input, date-input, image-cropper, number-input, floating-panel, pagination, and related controls normalize consistently. (#119, #125, #152, #177, #214)
- Fixed carousel autoplay, file-handle cleanup, disabled agent-dispatch gates, menu direction, hover-timer teardown, and five isolated component correctness defects. Membership lookups and empty collections are shared so large lists avoid per-item/per-update allocation. (#120, #124, #127, #128)
@llui/a2ui@0.3.0
- Breaking headless catalog interactions now resolve through the shared
@llui/interactionspeer so focus, dismissal, modal, and nested-layer state cannot split across package instances. See the migration section above. (#49)
@llui/router@0.12.0
- Added named typed route locations, exact name-specific generation, Standard Schema codecs/refinements, defaults, optional/rest templates, deterministic precedence/ambiguity rejection, canonical round trips, and explicit unmatched locations. (#156)
- Fixed history/hash integrity: foreign state survives replacement, unstamped positions remain unknown, blocked traversals restore without destroying forward history, hash echoes are paired rather than guessed, and same-fragment traversals are observed. (#103, #108, #110, #150, #163, #164)
- Fixed route-definition choice is verified by round trip, duplicate path
slashes normalize internally, redirected browser navigation writes the final
URL, guard redirects settle to a fixed point, current-location navigation is
a no-op, and anchors with
downloador a non-self target remain native. (#104, #109, #111, #143, #161)
@llui/transitions@0.12.0
- Fixed
waitForEndfilters by transitioned property and consumestransitioncancel; interrupted enter/leave phases resume from live values; staggered deferred phases cancel per node. (#105, #106) - Fixed
flip()batches layout reads, cancels/supersedes the live animation, resumes from the visual position, composes the author's transform, measures layout rather than transformed drawing coordinates, and rebases origins through shared offset-parent chains without absorbing scroll. (#107, #144, #185, #217)
@llui/effects@0.3.0
- Fixed websocket retirement is idempotent, and aborting a debounce removes both its timer and abort listener. (#77, #83)
@llui/markdown@0.13.0
- Improved incremental definition collection avoids rescanning completed blocks; block fingerprints distinguish footnote references and remain injective and diffable. (#73, #84, #94)
@llui/lexical@0.5.0
- Added the shared commit hub, which is the single controlled-value echo, editor-read, selection-walk, and measurement authority across editor plugins. (#70, #71, #74)
@llui/markdown-editor@0.8.0
- Improved the shared commit hub coordinates overlay plugins and key commands carry their real payload types. (#70, #71, #74)
- Fixed task-list typing/import, adjacent-list marker boundaries, old stock
ListNodeupgrades, wikilink-search cancellation, and Escape release across dismissed typeahead surfaces. (#99, #100, #129, #130, #183)
@llui/lexical-collab@0.4.0
- Fixed collab undo and local history can no longer be enabled in conflict. (#72)
@llui/lexical-loro@0.1.2
- Fixed custom
TextNodesubclasses survive the JSON carrier, and accumulated-history stress coverage is separated from the ordinary PR lane. (#223)
@llui/vike@0.13.0
- Fixed hydration manifests validate version, layer chain, and server/client
seed provenance; dev builds verify deterministic JSON-safe
init()state across server and client; misalignedlluiLayoutDatais loud instead of seeding the wrong layer. (#112, #113)
@llui/devmode-annotate@0.4.0
- Fixed screenshot URLs are bounded and reclaimed, HUD mounting is reentrancy-safe, teardown errors remain visible, and the live-app install path is dynamically split. Rich editing lives in the new optional editor package. (#114, #115, #116)
@llui/devmode-annotate-editor@0.1.1
- Added the optional Lexical-powered rich Markdown surface for annotation HUDs, with explicit registration and side-effect registration entry points.
@llui/mcp@0.15.0
- Fixed concurrent runs have independent handshake state and ephemeral
ports; source searches use tracked files; test/dev child processes shut down
when their parent disappears unless deliberate daemonization sets
LLUI_MCP_NO_PARENT_WATCH=1. (#85, #86)
@llui/agent@0.13.0
- Fixed MCP endpoints are authenticated and bounded, closed-session buffers are reclaimed, dropped sessions can resume only through a valid pairing, revoked tokens cannot re-enter through resurrection, and durable deletion/reservation bookkeeping remains exact. (#101, #102, #190)
@llui/test@0.13.0
- Improved
testViewand property mounts forward real mount options and always dispose;assertEffectstreats expectedundefinedas an assertion;replayTracevalidates format version and component identity before reducing. (#66, #67, #68, #69)
llui-agent@0.11.1
- Fixed the package declares its CLI side effect so bundlers preserve the executable entry.
Docs
- Improved generated API references gate on the real package export graph and preserve JSDoc paragraphs and member kinds. Repository verification now type-checks and lints the site, checks generated documentation and package output, runs root-script and example smoke tests, serializes lint-staged across worktrees, kills orphan MCP processes, and reports load-normalized per-file test-duration regressions. Benchmark setup and execution are scripted, idempotent, containerized, and tied to the pinned Chrome/JFB environment.
2026-08-10 — @llui/markdown-editor@0.7.0
Released: @llui/markdown-editor@0.7.0; @llui/devmode-annotate@0.3.2
A render-time seam for image URLs — and the CommonMark correctness the seam needed to be worth having, since it turned out to be fed by a parser that mangled ordinary documents in silence.
Migration
- Nothing to change; the defaults are unchanged. If you worked around unresolved
image
srcvalues by rewriting the attribute behind Lexical's reconciler, or by writing absolute machine-local URLs into your markdown files, drop both and passimagePlugin({ resolveSrc })instead. - If you match image markdown yourself, note that
now yieldssrc: 'x.png'+title: 'Title'rather than asrcwith the title glued onto it. That was never a usable URL, so nothing that worked before stops working.
@llui/markdown-editor@0.7.0
- Added
imagePlugin({ resolveSrc })— map a stored imagesrcto the URL the<img>actually loads. It applies only when building the DOM: node data, the markdown the editor emits, and the URL the insert dialog shows all keep the stored value, so a document that stores a portable relative path (attachments/a.png) keeps storing it while the editor displays what the host can load (asset://localhost/…/attachments/a.png). Two properties worth knowing: the argument is the stored value after the image-src allowlist has run (an unsafe URL never reaches your resolver), and the result is deliberately not re-checked — returning an app-private scheme is the point. - Fixed image markdown is now parsed as CommonMark instead of by regex. The
old
\(([^)]+)\)capture swallowed a title into thesrc(→src: 'img.png "T"'), kept angle-bracket destinations verbatim (), and declined any filename containing parentheses (attachments/img(1).png) or any line with trailing whitespace, leaving them as literal text. All of it was silent: the export re-emitted whatever it captured, so a round-trip looked correct while the<img>never loaded. Image titles now round-trip as titles. - Fixed a document no longer loses a line when an image's URL scheme is
refused. Importing
deleted the line outright — the markdown importer truncates a matched line's text before the transformer runs, so refusing the image left an empty paragraph. Refused lines now survive as text. - Fixed the render path sanitizes the stored
srcfor itself.importJSON/updateFromJSONwrite node data raw — the collaboration path, undo, an editor-state swap, a pasted decorator node — so the ingress sanitizers cannot cover them. An unsafe value now renders<img data-blocked="true">with nosrcattribute: nothing is requested, the alt text shows, and the bundled stylesheet marks it as a deliberate placeholder. - Added
IMAGE_BRIDGE_TYPE,IMAGE_TRANSFORMER,ImageData,isImageData,parseImageLine, andformatImageLineto the public surface.formatImageLineis the exact inverse ofparseImageLine, for hosts that write image references themselves (a paste handler saving an attachment) and need the editor's own spelling.IMAGE_BRIDGE_TYPEnames the deeper escape hatch: a plugin listed afterimagePlugin()that contributes a decorator bridge of that type replaces the image rendering wholesale — now covered by a test, so it is a contract rather than an accident.
@llui/devmode-annotate@0.3.2
- Improved republished against
@llui/markdown-editor@0.7.0. No source change; its dependency range could not otherwise reach the new minor, which would have left an app using both with two copies of the editor.
2026-08-05 — @llui/transitions@0.11.1
Released: @llui/transitions@0.11.1
Two independent bugs with one symptom: an element left mounted, laid out, and permanently invisible — no error, clean console.
Migration
- Nothing to change. If you dropped
fromTransition(routeTransition(...))from yourcreateOnRenderClientconfig to work around blank pages after every navigation, put it back.
@llui/transitions@0.11.1
-
Fixed a completed
leaveno longer poisons a later phase on the same element.transition()— and every preset built on it (fade,slide,scale) — deliberately keeps its leave resting values (opacity: 0) on the element: undershow/branch/eachit is about to be detached, and blanking them would flash the outgoing content back for a frame. But it also DISCARDED the rollback that would undo them, which is only safe when the element really is removed.At the route seam it is not.
fromTransitionin@llui/vike/clientcallsleaveand thenenteron the SAME persistent element — the surviving layer's page slot, which is never removed. With the rollback gone,entersnapshotted the residue as though it were an author-set inline style, animated0 → 1correctly, and then restored that snapshot on cleanup — parking the page slot atopacity: 0one duration AFTER it had visibly faded in, on every client navigation. The DOM was correct the whole time, so it presented as a hydration or data-loading bug rather than a transition one.Runs now settle instead of ending: the rollback stays registered so a later phase on a reused element restores the pre-leave inline styles before it takes its own snapshot, while the run is marked no longer in-flight so a subsequent leave isn't mistaken for an interrupted enter. When the element really is removed, the entry is collected with it and the rollback never runs — the
show/branch/eachcontract is unchanged. -
Fixed
collapse()had the identical defect, leaving a reused element stuck atheight: 0/overflow: hidden. -
Fixed
stagger()can now cancel a phase it has deferred but not yet started. WithleaveOrder: 'sequential'or'reverse', a staggered leave still waiting out its delay would fire on a row thateach()had meanwhile RESURRECTED and re-entered — leaving it mounted, laid out and permanently invisible. Deferred phases are now tracked per node and cancelled by the opposite phase (per node, not per batch), and a cancelled phase still resolves its promise, which the runtime gates DOM removal on. The symmetric case is fixed too: a leave cancels a not-yet-started staggered enter that would otherwise animate a departing row back in.
2026-08-01 — @llui/dom@0.12.1
Released: @llui/dom@0.12.1
A reactivity fix: structural primitives inside an each row could silently stop
reconciling.
@llui/dom@0.12.1
-
Fixed A
show(),branch(), or nestedeach()inside aneachrow could silently stop swapping arms — deterministically, after an ODD number of row updates that didn't commit its binding. An ordinarytext()on the same signal kept updating, so the row visibly held the new value underneath a stale arm. (#52)A structural binding's accessor is the IDENTITY function — its commit reconciles arms/rows and needs the whole state to mount them against — so the reconciler's output-equality check was comparing the state BUFFER's identity rather than the binding's output.
eachrecycles two ctx buffers per row and rotates them on every row update, while the last-committed slot advances only on commit: one gated-out row update desynchronises the two, and from then on the accessor returns the very object already in that slot, suppressing every later reconcile regardless of the discriminant. Output equality is now understood as a VALUE-binding optimization; structural bindings are exempt (they are already gated by their deps and de-duplicate inside their own reconcile). No measurable cost to the update loop or to scope construction.The only reliable workaround was folding the discriminant into the
eachkey, which forced a DOM rebuild per arm change — that can now be reverted. -
Fixed
ArmController.switchToshort-circuited on the MOUNTED arm's key, which is absent for a key that mounts no arm (a falsyshowwith noorElse, a missingbrancharm). A redundant same-key reconcile then fell through and hard-detached an arm that was still animating out, cutting itsleavetransition short. It now short-circuits on the last RECONCILED key. Pre-existing, but the fix above makes redundant structural commits routine, so it surfaces far more easily.
Docs
- The structural-binding exemption is recorded in
Architecture as part of the update cycle, and as
an invariant in
CLAUDE.md.
2026-07-22 — @llui/markdown-editor@0.6.0, @llui/lexical-loro@0.1.1
Released: @llui/markdown-editor@0.6.0; @llui/lexical-loro@0.1.1
Editable links and document links, plus a Loro-persistence fix for custom inline nodes.
@llui/markdown-editor@0.6.0
- Added Document links are editable in place. Clicking a
[[link]]reopens the search/reference panel prefilled to REPOINT it — choosing a candidate rewrites the target by NodeKey (no re-insert); ⌘/Ctrl-click still navigates. In edit mode the panel carries its own query input, so the document is untouched until you pick a target. - Added Regular URL links are editable and followable. A plain click opens the
link dialog prefilled with the current URL — submitting repoints THAT link by
NodeKey (clearing the URL unlinks); ⌘/Ctrl-click follows the URL through a new
onFollowseam onlinkPlugin(default opens a new tab; a shell/desktop host wires it to the system browser). Links gain a hover affordance (underline + pointer cursor) matching document links. - Fixed The floating selection toolbar dismisses on editor blur, so it no longer lingers — visible but dead — over the link dialog's modal backdrop.
@llui/lexical-loro@0.1.1
- Fixed Custom
TextNodesubclasses (wikilink tokens, mentions, …) survive the Loro round-trip. All three child-grouping paths (to-loro,to-lexical,agent-write) merged every text node into a plain-text run — which stores only text + format marks — so a subclass's extra state (e.g. a wikilink's__target) was dropped and it came back as bare text on any save/sync. A subclass now rides the carrier path (exportJSON/importJSON), like a decorator leaf; the convergence/fuzz/constraints suite stays green.
2026-07-22 — @llui/markdown-editor@0.5.1
Released: @llui/markdown-editor@0.5.1
Follow-up fixes to the 0.5.0 editor surfaces.
@llui/markdown-editor@0.5.1
- Fixed the link dialog rendered under the floating selection toolbar. Its
backdrop/positioner were styled at
z-index: 50/51— below the 60–64OVERLAY_Zoverlay tier, so the toolbar (z 62) covered the modal whenever both were open (and, in a host that also styles[data-scope='dialog'], the generic rule could drag every dialog down with it). Raised to a modal tier (70/71), strictly above every caret/selection/element overlay, and documented the tier inOVERLAY_Z. - Added
DocCandidateto the package's public exports. It is part of the document-linksearchseam's contract (WikiLinkPluginOptions['search']resolves toDocCandidate[]), so a host implementing the seam can now name the type directly instead of deriving it.
2026-07-21 — @llui/markdown-editor@0.5.0 — block actions menu + document-link search panel
Released: @llui/markdown-editor@0.5.0
Two editor UX features: a block-actions menu on the drag handle, and a document-link search/reference panel for wikilinks. Both additive; one small keyboard behaviour change on the block grip (below).
Breaking
@llui/markdown-editor@0.5.0— the block drag grip's Enter/Space now opens the block-actions menu instead of toggling keyboard-grab. Keyboard-grab is preserved and moved toMod+Shift+D(and the "Move block" command), which now reveals-and-grabs in one step. Only affects direct keyboard interaction with the grip; pointer drag and every other affordance are unchanged.
Migration
- If you (or your docs) relied on focusing the block grip and pressing
Enter/Space to grab a block, use
Mod+Shift+Dinstead. Nothing else changes.
@llui/markdown-editor@0.5.0
- Added Block-actions menu on
blockDragPlugin. Clicking the block drag grip — or right-clicking a block — opens a menu: Turn into (Paragraph / H1–3 / Quote / Bulleted / Numbered / Task / Code, captured from the merged command items so any plugin's block type appears automatically), Duplicate, Delete, Move up/down. Keyboard-navigable (↑/↓, Escape); an outside pointer dismisses it. Styled underdata-scope="md-block-drag"data-part="menu"/menu-item/menu-label/menu-sep. - Added Document-link search + reference panel on
wikilinkPlugin. A new opt-insearch?: (query) => DocCandidate[] | Promise<…>seam turns typing[[queryinto a searchable, previewable document picker: a results list (title- snippet) beside a content-preview pane; ↑/↓ navigate, Enter/click inserts a
[[target]]link. Debounced, with a staleness guard that drops a response for a superseded/left query.DocCandidate = { target, title?, snippet?, preview? }. The command item is relabelled "Document link". Styled underdata-scope="md-wikilink"data-part="panel"/results/result/result-title/result-snippet/preview. Backward compatible — with nosearchseam the panel never opens and typing the closing]]still forms a wikilink.
- snippet) beside a content-preview pane; ↑/↓ navigate, Enter/click inserts a
- Improved
blockDragPlugin's "Move block" command /Mod+Shift+Dreveals AND grabs the block in one step (previously it only revealed the grip).
2026-07-20 — @llui/lexical-loro@0.1.0 (new) — Lexical editor stack: Loro CRDT binding, overlay + undo seams, markdown-editor plugins
Released: @llui/lexical-loro@0.1.0; @llui/lexical@0.4.0, @llui/markdown-editor@0.4.0; @llui/lexical-collab@0.3.1, @llui/devmode-annotate@0.3.1
The foundation for driving a collaborative Lexical editor over the Loro CRDT without switching to Yjs: a new binding package, the overlay and undo seams it needs in @llui/lexical, and four new @llui/markdown-editor plugins. Lexical is upgraded 0.46 → 0.48 across the workspace. Additive throughout — no breaking changes.
@llui/lexical-loro@0.1.0
- Added the package — a Loro CRDT collaborative-editing binding for Lexical that does not depend on
@lexical/yjs(which is hard-bound to Yjs types and cannot be parameterized over another CRDT). It reuses@lexical/yjs's architecture — a persistent mirror tree with stableNodeKeyidentity and surgical per-event inbound application — with a Loro backend, plugged intolexicalForeign's CRDT-agnostic seam (register+externalUndo+seedMode: 'deferred'). It is deliberately not a port ofloro-prosemirror, whose whole-document-replace-per-event model would remint everyNodeKeyand tear down every mounted decorator sub-app. - Added fractional-index child ordering (children are a
LoroMapof uuid-keyed carriers each holding apos; order issort by (pos, uuid); a same-parent move is one LWW write) instead ofLoroMovableList, which has two reproduced defects under concurrent move/delete in loro-crdt 1.13.7 — an uncatchable WASM panic and a silent convergence failure. Both are pinned withit.failsintest/loro-upstream.test.ts; a fixed loro release turns them red. - Added Loro-backed collaborative undo (
externalUndobuilt on Loro's local-scopedUndoManager) — local-only by construction, so undo never reverts a remote peer's edits. - Added
reconcileTargetIntoLoro— a history-preserving agent-write path for full-markdown rewrites. The naive$convertFromMarkdownStringapproach is wholesale (recreates every container, dropping concurrent edits from other windows); this matches existing carriers by content, so unchanged blocks keep theirContainerIDs and a concurrent edit in another window survives.@lexical/markdownis not a runtime dependency — the caller owns the markdown→target-tree parse with its own transformer set. - Known limits (documented + tested): cross-parent moves and two concurrent splits of one text run inherit the underlying CRDT's behaviour; byte-identical sibling blocks cannot be fully disambiguated on the agent-write path (mitigated by a content+position bias; residual pinned by a test).
@llui/lexical@0.4.0
- Added
nodeWidget— a seam for non-document computed overlay DOM attached to a Lexical node (the equivalent of a ProseMirrorDecoration.widget, which Lexical lacks). Built onEditorDOMRenderConfig($decorateDOM+$getDOMSlot), so an outer-tag change re-decorates synchronously in the same commit rather than flickering a frame later; all@experimental/@internalLexical imports are confined to one version-pinned module.lexicalForeigngains awidgetsoption (composed with each plugin'swidgets); with none registered,createEditoris called exactly as before. - Added
LexicalPlugin.nodes(andlexicalForeign'snodes) now acceptLexicalNodeConfig, so a plugin can register the{ replace, with, withKlass }node-replacement form (needed to subclass a built-in node, e.g. to reserve a DOM slot boundary). - Improved upgraded to Lexical
0.48.
@llui/markdown-editor@0.4.0
- Fixed (behavioural) fenced-code info strings are now read per CommonMark — the whole remainder of the fence line — instead of
@lexical/markdown's single([\w-]+)?token. Upstream'sCODEtransformer silently corrupted ordinary Markdown:```c++parsed as languagecwith++prepended to the code body, and```lance tableas languagelancewithtableprepended.GFM_TRANSFORMERSnow shipsCODE_INFO_TRANSFORMER(transformers/code.ts) in place of upstreamCODE, so every consumer gets the correct parse whether or not they enablecodeLanguagePlugin(), and plugin order can no longer reintroduce the corruption —corePlugin()andcodeLanguagePlugin()contribute the same transformer reference and the registry de-duplicates it. One caveat: the original fence width is not preserved across a round-trip (it is recomputed as the narrowest fence that safely encloses the body), so````tsaround a plain body normalizes to```ts. Content is never lost and the fence always widens when it must. - Added
wikilinkPlugin()—[[Target]]/[[Target|alias]]as an atomic token node, with aonNavigatehost-resolution seam routed through the ordinary plugin message/effect cycle. Coexists with MarkdownLINK; an alias is literal text, so[[a|**b**]]displays**b**rather than bolding. A wikilink now preserves surrounding text format across a round-trip (a formatted**[[Page]]**no longer loses its bold), and target/alias values containing|or]]are sanitized so a link cannot silently mutate on reload. - Added
frontmatterPlugin()— YAML frontmatter as an opaque block that round-trips byte-for-byte. Resolves the---collision withhrPluginstructurally: it is a multiline-element transformer, consulted ahead ofhr's element transformer regardless of plugin order and whetherhrPluginis loaded, and it claims the fence only at document start with a closing---— a genuine thematic break still becomes an<hr>. - Added
codeLanguagePlugin()— an editable badge showing a code block's info string while the caret is inside it. The language is an opaque label: stored, shown, re-emitted, never interpreted (the package depends on@lexical/code-core, not@lexical/code, so no highlighter is bundled). Its optionallanguagesoption only seeds<datalist>suggestions and never constrains input. - Added
blockDragPlugin()— a hover-gutter grip that reorders top-level blocks by mouse drag or keyboard (W3C APG drag-and-drop alternative: Enter/Space grabs, arrows move, Escape cancels, with anaria-liveannouncement). A reorder is a singleinsertBefore/insertAfterinside oneeditor.update, so it is one undo step and a genuine node move — correct for CRDT bindings that observe node moves rather than DOM mutations. Opt-in; shipsstyles/block-drag.css. - Added a collab binding may now supply
externalUndo(threaded through tolexicalForeign), so@llui/lexical-loro's CRDT-aware undo works through the editor; previously a collab editor sethistory: falsewith no external owner and had no undo at all. - Improved upgraded to Lexical
0.48.
@llui/lexical-collab@0.3.1
- Improved upgraded to Lexical /
@lexical/yjs0.48(patch; no API change).
@llui/devmode-annotate@0.3.1
- Improved upgraded to Lexical
0.48(patch; no API change).
2026-07-16 — @llui/dom@0.12.0 (audit remediation round 2 — correctness/security + release-pipeline hardening)
Released: @llui/dom@0.12.0, @llui/vite-plugin@0.12.0, @llui/test@0.12.0, @llui/router@0.11.0, @llui/transitions@0.11.0, @llui/components@0.13.0, @llui/vike@0.12.0; @llui/compiler@0.12.0, @llui/compiler-ssr@0.12.0, @llui/effects@0.2.0, @llui/mcp@0.14.0, @llui/agent@0.12.0, llui-agent@0.11.0; @llui/markdown@0.12.0, @llui/lexical@0.3.0, @llui/lexical-collab@0.3.0, @llui/markdown-editor@0.3.0, @llui/a2ui@0.2.0, @llui/devmode-annotate@0.3.0, @llui/notes-format@0.2.0, @llui/security@0.2.0
A second whole-repository audit and remediation, applied test-first. Verified correctness bugs fixed across the signal runtime, compiler, components, effects, router, the agent/MCP tooling, and the rich-text stack; security hardening of the dev-server task-spawn and untrusted-URL surfaces; and release-pipeline fixes (docs no longer deploy from a red main; tarballs are cleaned of stale build artifacts and the dormant __llui_deps.json manifest). All 21 packages are minor-bumped because a publish.sh change alters every published tarball.
Breaking
@llui/agent@0.12.0— the LAP wire protocol is now version 2. Frames in both directions are zod-validated and invalid frames are rejected;expiresAtis milliseconds on the wire (was seconds); a version-skewed client is terminated with an explicithello-ackreason instead of silently proceeding. A consumer app's shipped browser client and its server must be on matching LAP versions.@llui/components@0.13.0—selectandcomboboxstate is now value-identity based:highlightedIndex→highlightedValue: string | null, and thehighlightmessage carriesvalue(notindex). This fixes wrong-option highlight/selection under the documented value-keyedeach+index.peek()list pattern (reused rows no longer carry a stale index).@llui/router@0.11.0—createRouternow throws when nofallbackis configured and the first route has path params (it previously fabricated a wrong route with'1'-filled params for unmatched URLs).navigate/push/replacenow carry the full route object throughbeforeEnter/beforeLeaveguards and dispatch (non-URL fields are no longer dropped by anhref → matchround-trip).@llui/effects@0.2.0—ApiErrorgained a{ kind: 'parse' }member: a 2xx response whose body fails to parse is no longer reported as{ kind: 'network' }.defaultRetryOndoes not retryparse. An exhaustiveswitch (error.kind)needs a new case.@llui/markdown-editor@0.3.0— underline was removed from the editor (Lexical text-format transformers require a symmetric delimiter, so it could never round-trip to Markdown and was silently dropped on every serialize), andHIGHLIGHT(==…==, non-GFM) was removed from the default transformer set (available as an opt-inHIGHLIGHT_TRANSFORMER). The WYSIWYG surface and the serialization dialect now coincide.
Migration
- agent / LAP v2: redeploy the app's browser client (
@llui/agentclient runtime) and its server together — a v1 client paired with a v2 server (or vice versa) is now rejected rather than half-understood. If you readexpiresAtoff a mint/resume response, it is now milliseconds since epoch. - components select/combobox: replace reads of
state.highlightedIndexwithstate.highlightedValue, and changesend({ type: 'highlight', index })tosend({ type: 'highlight', value }). Derive a highlighted option by value, not by index. - router: pass an explicit
fallbacktocreateRouterwhen any route has params. If you relied onnavigatedropping extra route fields, note they now flow through to guards/dispatch. - effects: if you
switchonerror.kind, add aparsecase; if you want to retry parse failures, pass an explicitretryOn. - markdown-editor: if you used underline, it is gone (it never survived a round-trip); re-enable
==highlight==by registeringHIGHLIGHT_TRANSFORMERif you want the non-standard syntax.
@llui/dom@0.12.0
- Fixed
provide()now reaches everything a structural primitive builds —show/branch/each/virtualEach/lazyarms and rows. Because arms build lazily (afterprovide's synchronousrender()returned), the shared contexts map had already been restored, souseContextinside any arm/row silently returned the default. Contexts are now immutable-by-swap and snapshotted at each primitive's placement. - Fixed a keyed
eachrow removed then re-added while itsleaveanimation is in flight now re-evaluates state changes it missed while out of the reconcile set (previously it could render stale). - Fixed the post-mount replay commit runs under the reconcile
drainingguard, so a reentrantsendfrom a commit-firedblurcan no longer start a nested drain mid-reconcile; anddrain()stops reducing queued messages / dispatching effects once the component is disposed mid-drain. - Fixed SSR serializes form-control state set via IDL properties —
inputvalue/checked,textareavalue, and the selectedoption— so server-rendered forms no longer paint empty before hydration. - Added a dev-mode duplicate-key guard for
virtualEach(mirroringeach), and aneachDirectguard that rejects (dev) / rebuilds (prod) a hand-written row factory whose binding structure diverges from the first row instead of silently reusing the wrong masks.
@llui/vite-plugin@0.12.0
- Fixed the dev-server task-spawn now requires an unforgeable per-session capability token: a same-origin page script (XSS, malicious dep) can no longer POST a "trusted" task note that spawns a CLI agent in the repo root. The HUD echoes the token as
x-llui-task-capability. - Fixed note writes are atomic (tmp + rename) with exclusive (
wx) id allocation, so concurrent writers (dev server + MCP) can't mint duplicate ids or leave torn files;findNoteFiletreats multiple prefix matches as an integrity error; andlistNotessurfaces parse failures instead of silently dropping a corrupted note. - Fixed component files that import runtime helpers via an app barrel (not a literal
@llui/domspecifier) are now linted and transformed instead of silently skipping the compile-time safety layer.
@llui/compiler@0.12.0, @llui/compiler-ssr@0.12.0
- Fixed the dependency analyzer now evaluates destructuring/parameter default initializers and object-literal getter/method bodies — a missed dep there produced a mask that gated out a binding it actually reads (permanently stale UI).
- Fixed
.ts/.mts/.ctssources parse as TypeScript, not TSX — a generic arrow (const id = <T>(x: T) => x) no longer misparses as JSX, which had silently skipped the component's compilation and emitted a bogus non-bypassableoperator-on-signalerror. - Fixed a module-scope or method-parameter binding named
stateno longer false-triggersoperator-on-signal; schema metadata (__msgSchema/__schemaHash) is computed percomponent()call so a second component in one file gets its own Msg union. - Fixed (
compiler-ssr)export default function NAME/export default classare stubbed as real default exports (no phantom named stub), andexport enumis stubbed instead of dropped. - Improved the auto-batch transform keeps a
functionhandler a function (preservingthis/arguments) instead of rewriting it to an arrow.
@llui/components@0.13.0
- Breaking value-based
select/comboboxidentity — see the top of this release block. - Fixed pressing Escape in an open submenu now unwinds one level (the dismissable layer was claiming the key and closing the whole menu — the "unwind one level" branch was dead code).
- Fixed a deselectable
tabswidget keeps a rovingtabindex=0when nothing is selected, so the tablist stays reachable by keyboard. - Fixed overlay presence (dialog/popover/menu/toast) advances only on the element's own animation/transition end, so a child animation finishing during close no longer unmounts the overlay early.
- Fixed lazy-loading a checked tree-view branch cascades the check onto the freshly-loaded descendants instead of erasing the user's check.
- Fixed
formFieldasync validation is request-sequenced — a slow earlier validation can no longer overwrite a newer result.
@llui/transitions@0.11.0
- Fixed an interrupted enter→leave (fast toggle) now starts the leave from the element's current value/size instead of snapping to the fully-visible resting state first (spring, collapse, and CSS presets).
@llui/effects@0.2.0
- Breaking new
parseApiErrorkind — see the top of this release block. - Fixed
sequenceadvances to the next step on a step's completion, not its first emitted message, so a nestedsequenceruns strictly in order. - Fixed the
asOnEffectfallback allocates a freshAbortControllerper mount (keyed offsend), so disposing one mount no longer aborts a concurrent mount's in-flight effects.
@llui/router@0.11.0
- Breaking
createRouterfallback requirement + full-route-object guards — see the top of this release block. - Fixed literal path segments are compared decoded, so a route with a non-ASCII literal (
['café']) matches the percent-encoded URL a browser produces.
@llui/test@0.12.0
- Fixed
replayTracematches effects that carry callbacks (http/storage/websocket) by comparing the JSON projection, instead of diverging unconditionally on the function-valued keys. - Fixed the harness runs effect cleanups in FIFO (registration) order to match the runtime's real dispose order, and pure-mode
testComponentopens one effects window perbatch(matchingwithEffectsmode).
@llui/a2ui@0.2.0
- Improved a single
updateComponentsnow reconciles per component id instead of rebuilding the entire surface tree — one changed node no longer disposes every DOM node, binding, and component-state machine (and no longer resets focus/scroll); a WebSocket array frame is applied as one batched reconcile. - Fixed
applyPointerrefuses a non-index token that targets an array (returns the node unchanged with a warn) instead of silently replacing the whole collection with an object. - Fixed a number
TextFieldwrites anumberto the data model;ChoicePickeris keyed by option value (duplicate labels no longer collapse); Intl formatters are memoized instead of reconstructed per reactive evaluation.
@llui/markdown@0.12.0, @llui/lexical@0.3.0, @llui/lexical-collab@0.3.0, @llui/markdown-editor@0.3.0
- Fixed (
markdown) the incremental parser no longer reuses an EOF-terminated block (an unclosed ``` fence or HTML flow block) as sealed when new content is appended — streaming a code block chunk-by-chunk now produces the correct document. Default HTML handling keeps the visible text between inline tags (a <em>word</em> bno longer drops "word"). - Fixed (
lexical) keyboard chords for digits/letters match the physicalevent.codewhen Shift/Alt is in the chord, so the shippedMod-Shift-7/8/9(list) andMod-Alt-1/2/3(heading) shortcuts actually fire;lexicalForeigndelivers the final debounce window of edits to the consumer on unmount instead of dropping it through the dying update loop. - Breaking (
markdown-editor) underline removed +HIGHLIGHToff by default — see the top of this release block. Fixed the table transformer preserves inline formatting and column alignment on round-trip and accepts GFM rows without outer pipes. - Improved (
lexical-collab) republished for the peer-range bump.
@llui/agent@0.12.0, @llui/mcp@0.14.0, @llui/devmode-annotate@0.3.0, @llui/notes-format@0.2.0, llui-agent@0.11.0
- Breaking (
agent) LAP v2 — see the top of this release block. - Fixed (
agent)revoketears down the live pairing (frame + unregister + socket close) instead of only flipping the token; a reconnect migrates in-flight RPCs to the new socket instead of orphaning them to a full timeout; the client keeps a level-triggered confirm outbox flushed on every WSopen, so a user approval during a socket gap is no longer lost; and the LAP body-size cap covers chunked requests. - Fixed (
mcp) the loopbackHost-header check routes through@llui/security'sisLoopbackAuthorityinstead of a hand-rolled copy that diverged. - Added (
devmode-annotate) the HUD forwards the vite-plugin's task capability token as thex-llui-task-capabilityheader so legitimate in-HUD task submissions are trusted. - Improved (
notes-format)listNotescan report per-note parse failures (ListNotesError).
@llui/security@0.2.0
- Fixed
sanitizeUrlnow gates protocol-relative URLs (//host/x) against the allowlist instead of waving them through as safe-relative — untrusted Markdown can no longer smuggle a live cross-origin image/link request past the sanitizer.
@llui/vike@0.12.0
- Improved republished for the peer-range bump and the cleaned tarball (no source changes).
All packages — build output
- Improved published tarballs no longer ship the dormant
__llui_deps.jsonlibrary-ABI manifest (~191 KB across the stack, for a consumer path that is evidence-closed and unreadable) and are cleaned of stale artifacts from deleted sources before packing, so no zombie modules ship.scripts/emit-deps.mjsstays in-repo as dormant forward-compat.
Docs / CI
- Fixed the docs site (
llui.dev) now deploys only after CI succeeds onmain(workflow_run, pinned to the validated commit) instead of an independent push, so a redmaincan no longer redeploy a broken site.
2026-07-13 — @llui/dom@0.11.8 (whole-repo audit remediation + review follow-ups)
Released: @llui/dom@0.11.8, @llui/vite-plugin@0.11.7, @llui/test@0.11.9, @llui/router@0.10.8, @llui/transitions@0.10.8, @llui/components@0.12.4, @llui/vike@0.11.8; @llui/compiler@0.11.5, @llui/compiler-ssr@0.11.4, @llui/effects@0.1.4, @llui/mcp@0.13.4, @llui/agent@0.11.3, llui-agent@0.10.5; @llui/markdown@0.11.3, @llui/lexical@0.2.10, @llui/lexical-collab@0.2.8, @llui/markdown-editor@0.2.14, @llui/a2ui@0.1.3, @llui/devmode-annotate@0.2.12, @llui/notes-format@0.1.1; new: @llui/security@0.1.0
A second whole-repository audit and remediation: verified correctness bugs across the runtime, compiler, components, agent, and rich-text stacks; security hardening of the dev-server and untrusted-input surfaces; documentation brought back in line with the signal runtime; and running-cost reductions. Plus the code-review follow-ups: a new shared @llui/security leaf package that de-duplicates the URL/loopback sanitization, removal of unsafe casts, and a derivedHandle memoization that runs a shared .map projection once per update instead of once per consumer.
Breaking
@llui/lexical@0.2.10—decoratorBridgenow takes a reactive view builder(data: Signal<Data>, api) => Renderable(was a plain mount callback), and itsmountreturns{ dispose, update }. A node's data commit now updates the mounted sub-view in place instead of tearing it down and remounting — focus and transient state survive an edit. Consumers building custom decorator bridges must adopt the new signature.@llui/vite-plugin@0.11.7— the notes attention router is now opt-in (was on by default whenever theclaudeCLI was on PATH). Passrouter: 'claude'(or a config object) to enable auto-solving.--dangerously-skip-permissionsis no longer in the default preset — it now requires an explicitdangerouslySkipPermissions: true.0.0.0.0is no longer treated as a loopback authority.@llui/dom@0.11.8— the root barrel no longer value-exportsinstallSignalDebug/startRelay; import them from@llui/dom/devtools(types are still re-exported from the root). Disposing a container-target mount now removes the nodes it inserted (previously it left them attached with dead listeners), matching anchor-target dispose.@llui/transitions@0.10.8— animations now respectprefers-reduced-motion: reduceby default (enter/leave resolve to their final state instantly). PassrespectReducedMotion: falseon a specific transition to opt out for an essential animation.@llui/effects@0.1.4—retry()no longer retries everyApiError: by default it retries only network / timeout / 5xx / rate-limit errors and honors aRetry-Afterheader. PassretryOn: (error, attempt) => booleanto customize. The builtin effect union is now generic (BuiltinEffect<M>) — source-compatible via a default ofunknown.@llui/components@0.12.4—number-inputno longer commits (clamp/snap) on every keystroke, so decimals and non-step-aligned intermediate values are now typeable; it commits on blur/Enter as before.
Migration
- Lexical decorator bridges: update custom
decoratorBridgebuilders to the(data: Signal<Data>, api) => Renderablesignature and read from thedatasignal reactively; the sub-view is no longer rebuilt per data change. - Notes attention router: if you relied on the dev HUD auto-spawning a coding agent, set
router: 'claude'in the vite-plugin options; adddangerouslySkipPermissions: trueonly if you knowingly want the unattended, permission-skipping mode. - dom devtools: change
import { installSignalDebug } from '@llui/dom'toimport { installSignalDebug } from '@llui/dom/devtools'. - Reduced motion: no action needed unless an animation is essential (e.g. conveys state) — in that case pass
respectReducedMotion: false. - retry: if you depended on
retry()re-issuing 4xx/validation errors, pass an explicitretryOnpredicate to restore that.
New package
@llui/security@0.1.0— a tiny, dependency-free, browser-safe leaf package that owns the framework's security-sensitive primitives in exactly one place:sanitizeUrl/defaultAllowedProtocols(the URL scheme allow-list shared by@llui/markdown,@llui/markdown-editor, and@llui/a2ui) and the loopback-authority/origin recognizers (isLoopbackHost/isLoopbackAuthority/isLoopbackOrigin, shared by@llui/mcpand@llui/vite-pluginfor CSRF/CSWSH gating). This replaces byte-for-byte copies that had begun to diverge.
@llui/dom@0.11.8
- Fixed a leaving
show/brancharm mid-transition is now frozen — it no longer re-evaluates its bindings against subsequent state, so toggling an arm off because the data it read becameundefinedcan no longer throw. - Fixed the devtools
setState(time-travel/restore) commit now runs under the same reentrancy guard as normal commits, preventing scope-tree corruption when the commit triggers a reentrantsend. - Fixed
virtualEachnested inside aneachrow now roots a row-local items source against the row (likeeach), instead of resolving it against component state. - Improved
state.map/derivedhandles memoize on resolved-input identity, so N bindings (or rows) sharing one projection run it once per update instead of once each — proven equivalent to the un-memoized path by a property-test oracle.
@llui/compiler@0.11.5
- Fixed a compiled component view's
each(items, { key, render, transition })no longer silently drops thetransition(or a shorthandkey) — such calls now route through the runtime helper that honors them, so compiled and view-helper forms behave identically. - Fixed
svg(...)is no longer lowered to a non-namespaced element (it rendered nothing); it routes through the namespaced runtime helper. RealignedELEMENT_HELPERSwith the runtime (addsblockquote,hr,br,optgroup,dl/dt/dd,caption,time), which also closes the lint false-negatives for those tags. - Fixed the
controlled-inputlint rule now also flags a reactivecheckedwith noonChange/onInput;summary/labelare no longer a11y false-positives. - Improved
__msgAnnotationsmetadata is emitted sparsely (default-valued fields omitted), shrinking agent-enabled production bundles.
@llui/vite-plugin@0.11.7
- Breaking attention router is opt-in and no longer passes
--dangerously-skip-permissionsby default;0.0.0.0removed from loopback hosts. See top of release block. - Fixed the notes
POST /notes/:id/statusendpoint validates the target status and the transition graph (rejecting illegal edges with 409), so a forged request can no longer drive the destructiveaccepted/rejectedfile operations from an arbitrary state. - Fixed request bodies are size-capped (default 32 MB → 413) and bundle imports cap per-entry and total decompressed size, closing an OOM / zip-bomb vector on the dev server.
- Fixed the build-integrity "did a signal component compile" flag is now derived from the transform result rather than a loose regex.
@llui/components@0.12.4
- Breaking
number-inputno longer commits live while typing. See top of release block. - Fixed
table,toolbar,tags-input, andrating-groupnow move real DOM focus after keyboard navigation (they previously only updated rovingtabindexstate), so keyboard and screen-reader users are no longer stranded. - Fixed menu submenus are keyboard-navigable — the root content handler dispatches to the deepest open level and ArrowRight/ArrowLeft open/close submenus;
aria-activedescendantreflects the active level. - Fixed
meter/progressdefaultaria-valuetextnow honorsmin((value − min)/(max − min)), matching the rendered bar. - Added
tailwindcssis declared as an optional peer dependency, documenting thatstyles/theme.cssrequires a Tailwind 4 build.
@llui/router@0.10.8
- Fixed
stripBaseno longer corrupts URLs of the formbase + '?query'/base + '#hash'(the delimiter is preserved);createRouter([])with no fallback now throws a clear error instead of aTypeError.
@llui/transitions@0.10.8
- Breaking honors
prefers-reduced-motionby default. See top of release block. - Fixed an interrupted
spring()no longer runs two competing loops that fight over the element; the superseded loop is cancelled without its final snap. - Fixed
transition()cleanup restores an element's pre-existing inline styles instead of blanking them, so a bound inlineopacity/transformsurvives a preset.
@llui/test@0.11.9
- Fixed
fire()now constructs the event type matching the name (KeyboardEvent/MouseEvent/PointerEvent/InputEvent), sofire(sel, 'keydown', { key: 'Enter' })actually deliversevent.key. - Fixed
emulateBlurOnRemovalalso patchesRange.deleteContents/extractContentsandreplaceChildren, so clearing a list containing the focused input fires the emulated blur (matching the runtime's bulk-removal paths).
@llui/vike@0.11.8
- Fixed a failed layout-chain suffix mount now rolls back the layers it already mounted (innermost-first) instead of leaving a half-committed chain; a second hydration render throws instead of silently duplicating the chain.
@llui/markdown@0.11.3
- Fixed streaming re-keys only the changed tail — per-block content hashes are memoized on node identity, turning per-chunk work from O(document) to O(tail).
- Fixed incremental prefix reuse is disabled (full re-parse) when custom
extensions/mdastExtensionsare present, unless explicitly declared seal-safe, closing a stale-prefix hazard for consumer syntax extensions.
@llui/markdown-editor@0.2.14
- Fixed link/image/paste ingress now enforce a URL scheme allow-list (via
@llui/security), so pasted or typedjavascript:URLs no longer become live links. - Fixed the live editor reference is per-mount, so mounting the same editor definition twice no longer cross-wires their effects, and a disposed editor is no longer retained by the definition closure.
- Fixed the table transformer splits cells on unescaped
|only, so a cell containing an escaped pipe round-trips.
@llui/a2ui@0.1.3
- Fixed catalog component/function lookups use null-prototype records and own-property + callable guards, so a server-supplied
__proto__/toString/constructorcan no longer crash the render or invoke a prototype member as a builder. - Fixed
renderByIdguards against cyclic component graphs; JSON-Pointer array writes are bounded (append + absolute cap) so one envelope can't balloon a sparse array into an OOM; reserved pointer tokens are rejected. - Fixed
openUrlactions, mediasrcbindings, theregexcheck, and theme values are all validated/bounded (scheme allow-list +noopener, pattern/input caps, CSS value sanitization); the slider pointer handler is torn down onpointercancel.
@llui/effects@0.1.4
- Breaking
retry()is selective and honorsRetry-After; builtin effect union is generic. See top of release block. - Fixed
sequenceno longer deadlocks on a custom/plugin step that completes without dispatching; SSRresolveEffectsunwraps composite effects (sequence/race/retry/cancel/debounce) to pre-resolve their innerhttp.
@llui/mcp@0.13.4
- Fixed message-size caps on the transports; the loopback/token helpers are de-duplicated (loopback recognition now comes from
@llui/security).
@llui/agent@0.11.3 / llui-agent@0.10.5
- Fixed confirm resolution is level-triggered (buffered by
confirmId), so an approval arriving in the poll gap is no longer lost while the action reports as forever-pending. - Fixed
would_dispatchapplies the samehuman-onlygate assend_message; the default audit sink is runtime-neutral (no longer throws on Cloudflare/Deno); the Cloudflare target gained an internal/__resolveendpoint and a shared-TokenStoreseam so a sharded deployment can resolve tokens.
Docs
- Rewrote the
@llui/dom,@llui/transitions,@llui/components,@llui/agent,@llui/vike, and@llui/effectsREADMEs (which ship to npm and feed the API pages) to the signal runtime; fixed theonEffectwiring examples to useasOnEffect; regenerated the MCP tool and lint catalogs to match reality; corrected the published batch speed-up figure to ~2.4× (per the committed ticker baseline); and restored the React column in the generated benchmarks page.
All packages — build output
- Fixed removed
inlineSourcesfrom every package's build, so.js.mapfiles no longer duplicate the source that already ships insrc/— smaller tarballs on every install. - Improved in-repo
@llui/*peer ranges are nowworkspace:^, so they are rewritten to the concrete version at pack time and no longer need a hand-bump each release.
2026-07-12 — @llui/dom@0.11.7 (repo-wide audit remediation)
Released: @llui/dom@0.11.7, @llui/vite-plugin@0.11.6, @llui/test@0.11.8, @llui/router@0.10.7, @llui/transitions@0.10.7, @llui/components@0.12.3, @llui/vike@0.11.7; @llui/compiler@0.11.4, @llui/compiler-ssr@0.11.3, @llui/effects@0.1.3, @llui/mcp@0.13.3, @llui/agent@0.11.2, llui-agent@0.10.4; @llui/markdown@0.11.2, @llui/lexical@0.2.9, @llui/lexical-collab@0.2.7, @llui/markdown-editor@0.2.13, @llui/a2ui@0.1.2, @llui/devmode-annotate@0.2.11; new: @llui/notes-format@0.1.0
A whole-repository audit and remediation touching every package: correctness and security fixes, accessibility, running-cost reductions, large behavior-preserving refactors, and the previously-dead element transitions now wired end-to-end. New leaf package @llui/notes-format removes the Lexical editor stack (~18 MB) from every @llui/vite-plugin / @llui/mcp consumer install.
Breaking
@llui/vite-plugin@0.11.6— the dev HUD (@llui/devmode-annotate) is no longer a hard dependency; the plugin resolves it from your project when installed. Removed the unusedcrossFileandverboseplugin options.@llui/components@0.12.3— part-bag changes:menu/context-menugroup()now takes an opaque group id (was the label text, which produced invalid ids for multi-word labels); thecomboboxroot part no longer carriesrole="combobox"/aria-expanded/aria-haspopup/aria-controls(these live solely on the input per ARIA 1.2);tree-viewandlistboxroots no longer emitaria-owns(items are DOM descendants);alert-dialogdrops the never-consumedcancelLabel/confirmLabeloptions; the overlaytransitionoption is back and now actually animates.@llui/vike@0.11.7— the hydration envelope is now versioned ({ v: 2 }) and the legacy flat shape is dropped; init effects now run on hydration by default (runInitEffectsOnHydratedefaults totrue, since the server never runs them);vikeis now a declared peer dependency.@llui/effects@0.1.3—upload()routes non-2xx responses throughonError(matchinghttp()) instead ofonSuccess;retry()is typed toHttpEffect; plugins registered via.use()now run before built-in runners (so a plugin can intercepthttp).@llui/agent@0.11.2/llui-agent@0.10.4— a confirm that times out now returns{ status: 'pending-confirmation', confirmId }instead of a spurious rejection (pollget_confirm_result); LAP frames carry alapVersion.
Migration
- Dev HUD: if you relied on
@llui/vite-pluginauto-injecting the annotate HUD, add it to your dev deps —pnpm add -D @llui/devmode-annotate. The plugin resolves it from your project in dev builds. - Smaller markdown bundle: import from
@llui/markdown/commonmarkto skip the GFM machinery when you don't need tables/footnotes/strikethrough; the default@llui/markdownentry still bundles GFM. - Menu groups: change
group('My Label')togroup('my-group-id')and render the label yourself; the id must be a valid HTML id. - Vike hydration: if your
init()effects were already satisfied by a+datahook, passrunInitEffectsOnHydrate: falseto keep them from re-running on hydrate.
New package
@llui/notes-format@0.1.0— a zero-dependency leaf package holding the pure notebook note modules (note-types,note-format,note-serialize), extracted from@llui/devmode-annotate.@llui/vite-pluginand@llui/mcpnow depend on it instead of the full HUD package, removinglexical+ 8@lexical/*+html-to-image+ the editor stack (~18 MB) from every consumer's install closure.
@llui/dom@0.11.7
- Fixed
virtualEachrows that read component state now update on state-only changes (they previously had no whole-state dependency and went stale). - Fixed
send()/batch()afterdispose()are now no-ops, and an effect cleanup returned after dispose still runs (was a subscription leak); a component-state field literally namedstate/item/indexnow resolves correctly insideeachrows; duplicateeachkeys are detected in dev. - Fixed the SSR serializer no longer HTML-escapes
<style>/<script>contents, and anonymous<head>dedup keys are deterministic per render (no more duplicate tags accumulating across SSR requests). - Added a per-mount
signal: AbortSignalon theEffectApibag; exportednormalizeUpdateResult;virtualEachauthoring accepts a per-itemitemHeightfunction. - Added structural transition hooks:
show/branch/eachaccept an optionaltransition(enter/leave/onTransition), SSR-gated and interruption-safe; the no-transition path is byte-identical. - Improved SSR renders reuse one cached JSDOM window per process (per-request documents) instead of constructing a full window each time.
@llui/vite-plugin@0.11.6
- Fixed (security) the
/_llui/*notes middleware now enforces a loopback Host + same-origin/absent-Origin gate and rejects non-JSON bodies; note-suppliedproposedDiffpaths are contained within the project root (was an arbitrary-file-delete vector); a task note only spawns a CLI agent when its provenance is trusted (not on the on-diskintentfield alone);sessionIdtraversal is guarded. - Fixed the signal transform now emits real sourcemaps; editing a sibling
Msg/Statetype re-transforms the importing component (watch invalidation); the integrity check drives the real pipeline. - Breaking removed
crossFile/verbose; the HUD dependency is now consumer-provided (see top of block).
@llui/components@0.12.3
- Fixed (a11y) roving-tabindex components (tabs, tree-view, accordion, radio-group, toggle-group, pin-input) now move DOM focus on arrow keys; menus expose
aria-activedescendant; selecting an option restores focus to the trigger; seven overlays are now SSR-safe; shadow-DOM outside-detection uses the composed path so overlays inside a shadow root don't self-dismiss. - Fixed hover-intent submenu timers no longer fire into closed/unmounted state; tooltip content is now pointer-interactive (WCAG hover persistence).
- Improved one unified
createOverlayengine replaces 12 copy-pasted overlay bodies;context-menude-duplicated againstmenuvia a shared machine; pointer-move highlight is now a no-op when unchanged (removes a per-tick reconcile storm). - Breaking part-bag/aria changes and the newly-functional
transitionoption — see top of block.
@llui/effects@0.1.3
- Fixed per-mount effect lifecycle —
handleEffects/asOnEffectkey their AbortControllers off the runtime's per-mount signal, so unmounting one instance of a component no longer aborts a concurrent instance's in-flight effects/sockets/timers;runHttpno longer rebrands a reducer/onSuccessexception as a network error; abort is re-checked before dispatch;debounce+cancel, the websocket-replacement race, and SSRresolveEffectsheader/error parity are fixed. - Improved runners split into modules with a Map-based dispatch and an opt-in
handleEffectsWith(runners)for tree-shaking; the batteries-includedhandleEffects()is unchanged. - Breaking
upload()/retry()/.use()contract changes — see top of block.
@llui/router@0.10.7
- Fixed
link()in history mode now runsbeforeLeave/beforeEnterguards (was bypassing them) and updatescurrentRoute; hash mode no longer double-dispatches; a guard-blocked popstate no longer corrupts the history stack; malformed percent-encoding no longer throws; query parsing usesURLSearchParams. - Added
basepath support (RouterConfig.base). - Improved route→URL formatting is now a direct O(segments) format via tagged route defs (no per-call
build({})heuristic).
@llui/markdown@0.11.2
- Fixed
transformLinknow runs for reference-style links/images (was bypassed); reference definitions that arrive later in a stream now resolve in earlier blocks; split inline raw HTML is sanitized as one run; the content hash is 64-bit. - Added
@llui/markdown/commonmarkentry that never bundles GFM; incremental tail re-parse for reactivemarkdown()— reuses parsed prefix blocks and re-parses only the changed tail (~41× faster on long streaming appends), with setext/lazy-continuation/definition hazards handled and a dev self-heal assertion.
@llui/lexical@0.2.9, @llui/lexical-collab@0.2.7, @llui/markdown-editor@0.2.13
- Fixed
lexicalForeigncallseditor.setRootElement(null)on dispose (was leaking the editor + document listener on every unmount); the outbound debounce flushes on dispose and no longer emits programmatic writes as user edits; explicitCtrl-shortcut chords work off-mac. - Fixed collab bootstrap fires against an already-synced provider; an internally-created
YDocis destroyed on teardown; themarkdownEditorlive-editor reference is per-instance (two editors no longer cross-wire); decorator round-trips (callout/math/image) preserve content.
@llui/agent@0.11.2, llui-agent@0.10.4
- Fixed a stale WebSocket's close no longer tears down a live replacement pairing;
/resume/claimpersists the rotated token; the hello frame isn't lost in the accept race; app callbacks receive un-wire-encoded state; path-scopedwait_for_changeworks. - Fixed (security/cost)
/agent/mintis rate-limited and token records are evicted; full-state snapshots are no longer shipped over the socket on every commit (subscription-driven watch). - Improved a shared
withLapGatescombinator and a unified MCP executor across the server and bridge surfaces; the bridge invalidates its describe cache onschemaHashchange and times out forwarded calls;computeStateDiffis exported from@llui/agent/protocol.
@llui/mcp@0.13.3
- Fixed (security) the browser-bridge WebSocket upgrade now validates Origin + a per-launch bearer token;
sessionIdtraversal is guarded; synchronousexecFile/eslint calls are async (no more event-loop stalls); the CDP session manager no longer leaks headless Chromium. - Breaking removed ~25 advertised debug tools that no runtime could serve (they failed at call time);
llui_lintnow runs the real@llui/compilersignal rules; removed the deprecated numeric-port constructor.
@llui/compiler@0.11.4, @llui/compiler-ssr@0.11.3
- Fixed six miscompiles: non-identifier
.at()keys, helper-inline operator precedence, a nestedcomponent()producing unparseable output, duplicate-importSyntaxError, a stack overflow on recursiveStatetypes, andderived()leaking signal handles into the produced value. - Fixed two non-bypassable lint false-positives (a valid reducer using
statein an operator;readonly/disabledcontrolled inputs) and emitted real sourcemaps. - Improved framework-call recognition is now import-binding based (resolves
@llui/domaliases, honors lexical shadowing) — a user's own function namedtext/each/divis no longer miscompiled.
@llui/transitions@0.10.7
- Fixed interruption-safe enter/leave (superseded runs roll back their transient styles); completion resolves on
transitionend/animationend(no stall in throttled tabs;springleave no longer deadlocks in a hidden tab);flip()no longer strongly retains detached rows. - Improved element-level structural transitions (
show/each/branch, per-rowstagger, FLIP viaonTransition) are now wired through the@llui/domseam and documented as working.
@llui/a2ui@0.1.2
- Fixed (security) the
openUrlaction validates the URL scheme and passesnoopener; theme/style strings are no longer concatenated from untrusted server input;__proto__/array-index writes in the pointer store are guarded. - Improved streamed
updateComponentsreconciles changed subtrees instead of rebuilding the whole surface; unreachable components/UI-state are garbage-collected; a render-cycle guard prevents stack overflow on a cyclic component graph.
@llui/devmode-annotate@0.2.11
- Fixed the element picker no longer leaves the host app click-dead after
destroy(); screenshot annotations are correct on scrolled/retina pages and exclude the HUD's own chrome; the IndexedDB note-id race is fixed; shadow-DOM isolate mode portals its overlays correctly; the repro trace is no longer lost on a failed submit. - Improved the 2,022-line entry closure split into lifecycle-owning modules with a disposer registry; ~1.3k lines of dead prototype code removed.
@llui/test@0.11.8
- Added
testComponentgains an opt-inwithEffectsmode that faithfully replicates the runtime drain (effect cascades,batch), with a parity test against a real mount;propertyTestis seeded (deterministic replay) and reports the minimal failing payloads. - Fixed shared JSON comparators (
partialMatchnow length-checks nested arrays);normalizeUpdateResultis imported from@llui/domrather than re-implemented.
All packages — build output
- Fixed
sideEffectson packages that export CSS is an allow-list (bundlers no longer tree-shake the stylesheet away); exports maps listtypesfirst (publint clean); every publishable package ships a LICENSE and README; five packages now extend the roottsconfig(fixing latent strictness gaps). - Fixed
publish.shderives its package list (topo-sorted, with a completeness assertion) and skips dependents of a failed publish;turbodeclaresglobalDependencies+buildinputs so config changes bust stale caches; CI caches.turboand gates on generated-doc drift.
Docs
- Fixed a large accuracy sweep across
site/contentanddocs/proposals: corrected samples (child()→subApp(),handleEffects().else()+asOnEffect), the lint-rule inventory (the fictionalview-bag-importrule that inverted reality is gone), removed the deletedsigningKeyagent option, and un-stale proposal statuses. Site headings now get anchor ids; the API reference regenerates from a TypeChecker walk with a CI drift gate.
2026-07-12 — @llui/dom@0.11.6 + compiler/runtime cleanup
Released: @llui/dom@0.11.6, @llui/compiler@0.11.3, @llui/effects@0.1.2, @llui/test@0.11.7, @llui/a2ui@0.1.1, @llui/vite-plugin@0.11.5, @llui/mcp@0.13.2
Reactive-runtime correctness fixes (each / lazy), a real propertyTest shrinker, effect-sequencing fixes, and removal of the dead v2c compiler module-registry / cross-file-walker surface (including two now-unused packages). All changes are backward compatible except the compiler API removal below.
Breaking
@llui/compiler@0.11.3— removed the dead v2c module system from the public API:ModuleRegistry,CompilerModule,registerIntrospectionFactory/registerDevtoolsFactory/getIntrospectionFactory/getDevtoolsFactory, and the cross-file walker (walkProgram,crossFileAccessorPaths). None were wired into the live signal transform (which emits introspection metadata inline). The@llui/compiler-introspectionand@llui/compiler-devtoolspackages are removed entirely for the same reason.
Migration
- If you imported any removed compiler symbol or the
@llui/compiler-{introspection,devtools}packages, delete those imports — they registered factories the shipping compiler never invoked, so removal is behavior-preserving. Agent/devtools metadata (__msgSchema,__msgAnnotations,__stateSchema,__schemaHash,__componentMeta) is still emitted by@llui/vite-pluginexactly as before.
@llui/dom@0.11.6
- Fixed
eachderives its per-row reactivity probe (rebase / reads-component-state / fan-out gating) per row instead of latching it from the first row. A data-conditionalrenderwhere the first row reads no component state but a later row does now renders and updates that later row correctly — previously it rendered empty and went stale on state changes. Homogeneous rows are unaffected, and the streaming (ticker) fast path is untouched. - Fixed
lazy()'s error arm mounts against the live component state and stays reactive to it (via a state getter threaded through the mount path), instead of mounting withnull. An error arm that reads component state — a localized message, a retry button — now renders and updates correctly. - Added
virtualEachsupports variable row heights:itemHeightaccepts a per-item function(item, index) => number(cumulative-offset windowing via prefix sums) in addition to a uniformnumber. Fully backward compatible; measured/auto heights remain unsupported.
@llui/compiler@0.11.3
- Breaking removed the dead v2c module-registry / cross-file-walker public API and the
@llui/compiler-{introspection,devtools}packages. See the top of this release block.
@llui/effects@0.1.2
- Fixed
sequenceadvances exactly once per step — on the step's first emitted message — so a step that emits several messages no longer fast-forwards the rest of the chain, and side-effect-only steps (log,storageSet,storageRemove,broadcast,wsSend) auto-advance instead of stalling the chain forever. - Added
httpStatusToApiErroris now exported (previously a private helper duplicated in the SSR resolver, now the single source of truth for both paths).
@llui/test@0.11.7
- Improved
propertyTestperforms real delta-debugging shrinking: on failure it replays recorded messages, drops the steps not needed to reproduce the same failure, and reports the minimal sequence. Previously the shrink step was a no-op that reported the full sequence.
@llui/a2ui@0.1.1
- Added
webSocketTransportaccepts anonErroroption (defaultconsole.warn) plus a frame-shape guard — unparseable or non-envelope frames are reported instead of dropped silently. NewWebSocketTransportOptionstype.
@llui/vite-plugin@0.11.5
- Improved dropped the dead registration of the v2c introspection/devtools compiler-module factories (removed with
@llui/compiler@0.11.3). Metadata emission is unchanged — it was always inline in the signal transform.
@llui/mcp@0.13.2
- Improved dropped the same dead compiler-module factory registration in the static-compiler tool.
Docs
- Removed dead references to the pre-signal
docs/designs/spec files across README, CLAUDE.md, and package docs — the authoritative docs live at llui.dev. Documented theObject.isoutput-equality perf cliff (composition patterns), the@routeGated/@validatesnew FunctionCSP caveat (agents), and the current status of the precompiled-library dependency manifest.
2026-07-05 — @llui/a2ui@0.1.0
Released: @llui/a2ui@0.1.0
New package: a renderer for Google's A2UI protocol v0.9 on the LLui signal runtime, reusing @llui/components for interactive controls. See docs/proposals/a2ui-renderer.md.
@llui/a2ui@0.1.0
- Added
mountA2ui(container, opts)renders A2UI v0.9 server→client envelopes (createSurface,updateComponents,updateDataModel,deleteSurface) into live LLui DOM. Structure reacts to the component map, data to the data model, so a streamingupdateDataModelre-commits only bound values without rebuilding the tree. - Added literal,
{ path }(JSON-Pointer), and nested{ call }bindings; templates over arrays and objects with spec-correct item-scoped paths; two-way binding; actions delivered viaonAction. - Added client-defined functions (
formatString,formatNumber/Currency/Date,pluralize,required/regex/length/numeric/email,and/or/not) and validationchecks(input error messages, disabled buttons). - Added full Basic catalog (18 components): display/layout as semantic HTML;
CheckBox/Tabs/Modal/Slider/ChoicePicker/DateTimeInputreuse@llui/components(checkbox, tabs, dialog with focus-trap + scroll-lock, slider with drag+keyboard, combobox with typeahead+chips, date-picker calendar). Open catalog registry viadefineCatalog;catalogIdselects the catalog with fallback to Basic. - Added
sendDataModelclient→server sync (data model rides on the action event), best-effort version negotiation (SUPPORTED_VERSIONS),handle.capabilities(), and a WebSocket transport adapter (connectA2ui+webSocketTransport). - Conformance-tested against the real
google/A2UIv0.9 sample payloads.
2026-07-01 — @llui/markdown-editor@0.2.12 (Lexical 0.46)
Released: @llui/lexical@0.2.8, @llui/lexical-collab@0.2.6, @llui/markdown-editor@0.2.12, @llui/devmode-annotate@0.2.10
Migrates the Lexical peer stack from 0.45.0 to 0.46.0, which fixes the markdown-editor typing loop reported in #44. The published packages now require lexical@^0.46.0 (and matching @lexical/*).
Breaking
@llui/lexical@0.2.8,@llui/lexical-collab@0.2.6,@llui/markdown-editor@0.2.12,@llui/devmode-annotate@0.2.10— thelexical/@lexical/*peer ranges move from^0.45.0to^0.46.0. Consumers must upgrade their Lexical install to0.46.x.
Migration
- Bump
lexicaland every@lexical/*package in your app to^0.46.0(they release in lockstep). No changes to your editor code are required.
@llui/markdown-editor@0.2.12
- Fixed typing ~160 characters of continuous prose no longer throws Lexical's "update listeners are endlessly enqueueing more updates" error (#44). The root cause was upstream:
@lexical/markdown'sregisterMarkdownShortcutsenqueues a follow-upeditor.update()on every keystroke that is a no-op on ordinary prose (no markdown trigger character). Lexical 0.45 skipped committing no-op updates, so the$triggerEnqueuedUpdatescascade-counter reset never ran and the counter leaked +1 per keystroke until it tripped the guard (~100 qualifying keys). Reproduced deterministically in bare Lexical with no LLui involved, confirming the LLui↔Lexical bridge was not at fault. Lexical 0.46 fixes it by scheduling a macrotask cascade reset that runs once control returns to the event loop between keystrokes (bounded typing resets the budget; genuine synchronous recursion still trips), and a tripped guard now clears the queue and routes through_onWarninstead of a fatal uncaught throw. A regression test types ~480 characters, yielding to the event loop between keys, and asserts no error with a bounded cascade counter. - Breaking
@llui/lexical/lexicalpeer ranges move to^0.2.8/^0.46.0. See top of release block.
@llui/lexical@0.2.8
- Breaking
lexical/@lexical/*peer ranges move to^0.46.0. See top of release block. No code changes.
@llui/lexical-collab@0.2.6
- Breaking
@llui/lexicalpeer range moves to^0.2.8andlexical/@lexical/*(incl.@lexical/yjs) to^0.46.0. See top of release block. No code changes.
@llui/devmode-annotate@0.2.10
- Improved version-only cascade bump so its bundled rich-text note editor picks up the
@llui/markdown-editor@0.2.12/@llui/lexical@0.2.8fix. No code changes.
2026-06-21 — @llui/compiler@0.11.2
Released: @llui/compiler@0.11.2
A single @llui/compiler change: the peek-in-slot lint error now teaches the sanctioned one-shot-read idiom.
@llui/compiler@0.11.2
- Improved the
peek-in-sloterror message now names the sanctioned escape for a deliberate one-shot read — snapshot the value in a block-body renderconst(render: (item) => { const v = state.peek(); return [...] }) and pass the plain value into helpers — instead of only suggesting.at()/.map(). Authors who genuinely wanted a snapshot previously had to rediscover a fragile laundering wrapper (a function whose param isn't namedstate) to dodge the rule; naming the real path keeps the non-bypassable error honest. No rule-logic or API change.
2026-06-15 — 0.11.5
Released: @llui/dom@0.11.5, @llui/test@0.11.6, @llui/router@0.10.6, @llui/transitions@0.10.6, @llui/components@0.12.2, @llui/markdown@0.11.1, @llui/mcp@0.13.1, @llui/agent@0.11.1, @llui/vike@0.11.6; @llui/lexical@0.2.7, @llui/lexical-collab@0.2.5, @llui/markdown-editor@0.2.11, @llui/devmode-annotate@0.2.9
A single @llui/dom SSR fix; every package carrying a @llui/dom peer is republished to pick it up.
@llui/dom@0.11.5
- Fixed
onMountcallbacks no longer fire during a DOM-less server render (renderNodes/renderToString). The mount lifecycle is a client-DOM concern; under SSR anonMountbody touching a browser global (window,querySelector, …) would run and throw, 500-ing the render (thedicerunbranch-arm repro). Anssrflag now threads through the build context — seeded byrenderSignalTreeand inherited by every nested build (eachrows,show/brancharms) — so under SSRonMountskips registering its callback andsignalSubAppskips mounting its isolated child, while both still emit their marker/anchor comment so the serialized tree stays stable. The client mount/hydrate pass runs the callback and mounts the child as before, so no per-callback browser-global guard is needed.
Cascade republish
- Improved
@llui/agent@0.11.1,@llui/components@0.12.2,@llui/devmode-annotate@0.2.9,@llui/lexical@0.2.7,@llui/lexical-collab@0.2.5,@llui/markdown@0.11.1,@llui/markdown-editor@0.2.11,@llui/mcp@0.13.1,@llui/router@0.10.6,@llui/test@0.11.6,@llui/transitions@0.10.6, and@llui/vike@0.11.6are version-only bumps that advance their@llui/dompeer range to^0.11.5(and the@llui/lexical/@llui/componentspeers onlexical-collab/markdown-editor). No code changes.
2026-06-14 — security hardening
Released: @llui/agent@0.11.0, @llui/mcp@0.13.0, @llui/markdown@0.11.0; @llui/lexical@0.2.6, @llui/devmode-annotate@0.2.8, @llui/markdown-editor@0.2.10; @llui/lexical-collab@0.2.4, @llui/vite-plugin@0.11.4, llui-agent@0.10.3
A security-hardening pass across the agent/MCP, markdown, lexical, and devmode surfaces, from a whole-repo audit. Three packages carry breaking or behavioral changes — read Breaking/Migration first.
Breaking
@llui/markdown@0.11.0— theallowDangerousHtml: booleanoption is removed. Raw HTML is dropped by default; to render it, supply asanitizeHtml: (html: string) => stringhook whose result is injected. There is intentionally no unsanitized passthrough.@llui/agent@0.11.0—/agent/mintnow fails closed when the identity resolver yields nouid(the default). Deployments that relied on anonymous mint must either provide anidentityResolveror setallowAnonymous: true.@llui/mcp@0.13.0— thellui-mcp --httptransport now requires a per-launch bearer token (written to a 0600 file) plus a loopbackHostand same-originOrigin; unauthenticated/cross-origin requests are rejected. The arbitrary-JSllui_evaltool is no longer registered unlessLLUI_MCP_ENABLE_EVAL=1.
Migration
- markdown — replace
allowDangerousHtml: truewithsanitizeHtml: (html) => /* sanitized */ html(e.g. wrap DOMPurify). Without it, raw HTML drops. - agent — if you depended on anonymous mint, pass
allowAnonymous: true(or wire anidentityResolver). SetcorsOriginsif browsers connect from a non-same-origin host. - mcp —
--httpclients must sendAuthorization: Bearer <token>(read it from the marker file) and a loopbackHost. Opt intollui_evalwithLLUI_MCP_ENABLE_EVAL=1only if you need it.
@llui/agent@0.11.0
- Breaking anonymous
/agent/mintfails closed; see top. - Added
allowAnonymous,corsOrigins(CSWSH origin allowlist, now enforced on every WebSocket upgrade across the Node, Cloudflare, and Deno adapters),slidingTtlMs(inactivity expiry, refreshed on every authenticated call including the/waitand/confirm-resultlong polls), andredactState(a source-side hook applied to every state snapshot leaving the app —get_state/observe/query_state, the per-change broadcast, confirm snapshots, and the hello-frame affordances sample — so a redacted field never reaches the server or the model). - Fixed WebSocket upgrades validate
Origin(CSWSH defense), honoringx-forwarded-proto/x-forwarded-hostbehind a TLS proxy; the Node WS path now routes through the sharedacceptConnectionso revoke / sliding-TTL / pending-resume grace are enforced in one place; the codec wire-decoder is hardened against__proto__/__codecinjection.
@llui/mcp@0.13.0
- Breaking
--httprequires a bearer token + loopback Host/Origin;llui_evalis opt-in. See top. - Fixed command injection in
llui_lint/llui_run_test/llui_find_msg_producers(nowexecFileSyncwith an argv array — no shell); path-traversal in the source tools (every caller-supplied path/rootDir, includingllui_compiler_diagnostics, is constrained to the workspace root); a literalOrigin: nullis no longer auto-trusted.
@llui/markdown@0.11.0
- Breaking
allowDangerousHtmlremoved in favor of thesanitizeHtmlhook; see top. - Fixed the URL sanitizer strips tabs/newlines and leading control characters before resolving the scheme, closing a
java\tscript:-style bypass.
@llui/lexical@0.2.6
- Added an
externalUndoseam option onlexicalForeign— when set, it owns the undo stack and forces the built-in@lexical/historyoff, so a CRDT/collab undo owner can't double-apply; combining it withhistory: trueis reported as a misconfiguration.
@llui/devmode-annotate@0.2.8
- Added
defaultSecretRedactor— an opt-in state-channel redactor that masks common secret shapes (Bearer tokens,sk-/ghp_keys, JWTs, emails). - Fixed the repro recorder captures input values only when a field opts in via
data-llui-capture-value, and no longer records keystrokes (including insidedata-llui-privateregions); replay is gated behind a confirmation, an origin-path match, and app-root containment; screenshots are redacted before annotation labels are baked in, on both the human and LLM-driven capture paths.
@llui/markdown-editor@0.2.10
- Improved the math / mermaid
rendercallback may now return a DOMNode(mounted directly, no sanitization needed) in addition to a trusted HTML string, removing the forced string→unsafeHtmlsink. Peer@llui/lexical→^0.2.6.
@llui/lexical-collab@0.2.4
- Improved peer
@llui/lexical→^0.2.6(cascade).
@llui/vite-plugin@0.11.4
- Improved rebuilt against
@llui/devmode-annotate@0.2.8(cascade).
llui-agent@0.10.3
- Improved rebuilt against
@llui/agent@0.11.0(cascade).
2026-06-14 — @llui/markdown-editor@0.2.9
Released: @llui/markdown-editor@0.2.9
@llui/markdown-editor@0.2.9
- Added Markdown is now converted to rich content on paste by default. Plain-text pastes are parsed as Markdown and inserted at the caret (
# Heading,- item,**bold**,`code`, links, task lists, etc. become real formatting); pastes that also carrytext/htmlstill defer to Lexical's richer HTML import, so copy-from-the-web keeps its formatting. Disable per-editor withmarkdownEditor({ pasteMarkdown: false }). New exports:registerMarkdownPasteand$insertMarkdownAtSelection. - Fixed The paste handler no longer swallows a paste it can't convert (e.g. over a selected decorator/
NodeSelection): it now only claims the event when there is a range selection to insert into, otherwise falling through to Lexical's default paste handler. Also adds Lexical's decorator-input guard so native inputs inside decorator nodes paste themselves.
2026-06-12 — 0.11.4
Released: @llui/dom@0.11.4; @llui/components@0.12.1; @llui/markdown@0.10.5; @llui/router@0.10.5; @llui/transitions@0.10.5; @llui/agent@0.10.5; @llui/test@0.11.5; @llui/vike@0.11.5; @llui/mcp@0.12.6; @llui/lexical@0.2.5; @llui/lexical-collab@0.2.3; @llui/markdown-editor@0.2.8; @llui/devmode-annotate@0.2.7
Fixes a high-severity reactivity bug in @llui/dom: a send() issued from an onMount callback (or a synchronous effect it kicks off) updated state but never reconciled the view, so a "compute on mount" pane stayed frozen on its loading placeholder until an unrelated later dispatch. Every other package is an unchanged cascade re-release so consumers pinning ^0.11.4 resolve the fixed runtime.
@llui/dom@0.11.4
- Fixed a
send()dispatched from anonMountcallback now reconciles the view on first paint.onMountcallbacks run synchronously inside the initialmount = mountSignal(...)call — beforemountis assigned — so a state-changing send during that window committed against a still-nullmountand was silently dropped, leavingshow/each/brancharms frozen until the next user-driven dispatch. The commit is now deferred whilemountis null and replayed once it is live (sync scheduler;rafmode already schedules a post-assignment frame). App-side workarounds that deferred the mount dispatch tosetTimeout(0)/queueMicrotaskare no longer needed and can be reverted.
All packages — cascade re-release
- Fixed
@llui/components,@llui/markdown,@llui/router,@llui/transitions,@llui/agent,@llui/test,@llui/vike,@llui/mcp,@llui/lexical,@llui/lexical-collab,@llui/markdown-editor, and@llui/devmode-annotateare republished with their@llui/dompeer range bumped to^0.11.4. No behavior change in these packages — the bump exists so consumers pick up the runtime fix above.
2026-06-12 — @llui/components@0.12.0, @llui/markdown-editor@0.2.7
Released: @llui/components@0.12.0; @llui/markdown-editor@0.2.7
Fixes a high-severity interaction-layering bug: an interactive overlay portaled to a body-level sibling (e.g. a @llui/markdown-editor floating toolbar) opened from inside a dialog.overlay() was mis-classified as "outside", so clicking it dismissed the host dialog. Adds a shared nested-layer registry to @llui/components so a logically-nested portal counts as part of the active layer. Split release — @llui/dom and the other lockstep packages are unchanged (peer range stays ^0.11.3).
@llui/components@0.12.0
- Added
registerNestedLayer(el | () => Element[])(plusgetNestedLayers/isInNestedLayer), exported from@llui/componentsand@llui/components/utils. A portaled, interactive overlay that is logically nested inside an active dismissable / focus-trap / aria-hidden layer but physically rendered as a body-level sibling can register its root so the three "outside-aware" utilities treat it as inside:watchInteractOutsidewon't dismiss on interaction within it,setAriaHiddenOutsidewon'tinertit, andpushFocusTrapincludes it as an additional focusable container. Mirrors thegetPersistentElementspattern (Zag/Ark) — a flat global registry; the resolver form is re-read live so an overlay registers once and surfaces its live root only while open. - Fixed a
dialog.overlay()(or any dismissable layer) wrapped around content that itself opens a portaled overlay — a markdown-editor floating toolbar / typeahead / context menu, or any future combobox/menu/tooltip used inside a dialog — no longer dismisses when you interact with that overlay. The dialog's capture-phasepointerdownwatcher previously saw the sibling portal as "outside" and (becausesendis synchronous) closed the dialog before the click's own handler ran, so e.g. an inline-format button never applied. The three interaction utilities now honor the nested-layer registry.@llui/components' own portaled overlays (select/menu/popover) were already safe via the dismissable stack; this covers overlays that don't push a dismissable layer.
@llui/markdown-editor@0.2.7
- Fixed the rich Markdown editor is now usable inside a
@llui/componentsdialog:overlayRoot(the shared primitive behind the floating toolbar, slash/mention typeaheads, context menu, and table tools) registers its portal root as a nested layer on mount, so a host dialog no longer dismisses,inerts, or focus-excludes those overlays. The link dialog already worked (it's a real nesteddialog.overlay()). Peer range on@llui/componentsbumped to^0.12.0.
2026-06-12 — @llui/vike@0.11.4
Released: @llui/vike@0.11.4
Adds a first-class navigation-pending signal to @llui/vike, plus documentation fixes for the page-transition timing model and the pageSlot() sibling contract. Single-package release; @llui/dom and the other lockstep packages are unchanged (peer range stays ^0.11.3).
@llui/vike@0.11.4
- Added
createNavigationProgress()(exported from@llui/vikeand@llui/vike/client) — a first-class way to show a loader while a client navigation is in flight. It wraps Vike's nativeonPageTransitionStart/onPageTransitionEndhooks (which fire on the click, before the+dataround-trip) into aLiveSignal<boolean>pendinghandle the layout binds withpeek()/bind(), plus the two hook functions to re-export from the+onPageTransition*.tsconvention files. An optional{ delay }debounce suppresses the indicator for navigations that resolve faster than the threshold (e.g. served from a hover prefetch). Replaces the module-singleton + layout-handle capture + hand-rollednav/pendingmessage + reducer boilerplate every app would otherwise re-derive. None ofcreateOnRenderClient's existing hooks expose this moment: Vike only invokesonRenderClientafter it has fetched the new page's data, soonLeave/onEnterbracket the DOM swap, not the network wait. - Improved
onLeave/fromTransitionjsdoc and the README now state explicitly that these fire after Vike has fetched the new page's+data— they bracket the DOM swap, not the network wait — and point atcreateNavigationProgressfor a during-fetch loader. - Improved
pageSlot()jsdoc and README now document that consumer siblings placed before or after the slot in the same parent survive SSR and every client navigation (the slot owns only the region between its anchor and end sentinel). Thedisplay: contentswrapper older guidance used to isolate the slot is no longer needed and has been retired. Backed by a new regression test asserting siblings keep node identity across a full page-to-page navigation.
2026-06-11 — 0.11.3
Released: @llui/dom@0.11.3; @llui/components@0.11.7; @llui/vike@0.11.3; @llui/test@0.11.4; @llui/router@0.10.4; @llui/transitions@0.10.4; @llui/markdown@0.10.4; @llui/lexical@0.2.4; @llui/lexical-collab@0.2.2; @llui/markdown-editor@0.2.6; @llui/devmode-annotate@0.2.6; @llui/agent@0.10.4; llui-agent@0.10.2; @llui/mcp@0.12.5
A large @llui/components batch — many new components and patterns, an accessibility sweep, RTL support, and overlay exit animations — plus a @llui/dom type fix that lets recursive state shapes work with .at(). The dom change cascades a peer-range bump (^0.11.3) to every dom consumer.
Breaking
@llui/components@0.11.7— themenu/menubaritem model is now an object tree (MenuItem[]—{ value, kind, group?, children?, disabled? }) rather thanstring[], enabling submenus andmenuitemcheckbox/menuitemradioitems.selectandcomboboxkeep accepting flatstring[]. TypeScript consumers passingstring[]tomenu/menubar(incl.setItems) must wrap each entry as{ value, kind: 'action' }.
Migration
- If you build a
menu/menubarfrom astring[], map it to items:items.map((value) => ({ value, kind: 'action' as const })).
@llui/dom@0.11.3
- Fixed
ValidPath<T>(the.at()path type) now terminates on recursive state shapes — e.g. a tree/menu node whose field references the node type — instead of expanding forever (TS2615/TS2589). A generous depth budget (8 path segments) bounds the union; deeper paths still fall back to.map(s => s.deep.path). Type-only change; runtime behaviour is unchanged.
@llui/components@0.11.7
- Added new components:
field,fieldset,form,meter,search-field,breadcrumbs,menubar,toolbar,table(sortable/selectable data grid),sortable,in-view,theme-switch. - Added new patterns:
command-menu(⌘K palette +watchHotkey),data-table,form-field,searchable-select,wizard. - Added RTL support across direction-sensitive components — an optional
dirinit/state field +setDir, a sharedflipArrow(horizontal arrows flip underrtl; vertical + Home/End never do), and RTL-aware floating placement. - Added presence lifecycle / exit animations for overlays — a
status('closed' | 'opening' | 'open' | 'closing') layered onopen, with reactivedata-stateand anisPresentsignal; synchronous no-animation default (skipAnimations) so close never hangs waiting on ananimationendthat won't fire. - Improved accessibility sweep: roving focus for
toggle-group/pagination/tabs(Home/End), tooltip hover-grace + escape-from-content (WCAG 1.4.13),menu/context-menusubmenus + checkbox/radio items + groups,select/comboboxoption groups, asynccomboboxloading (stale-request guard,aria-busy, live region) + creatable mode,date-pickerrange selection,toasttick-driven auto-dismiss +aria-liveby type, 2Dcolor-picker(saturation/value area, alpha, presets). - Fixed
searchable-selectwas unusable — the overlay self-dismissed on open because the dismiss boundary was the listbox, not the whole popup, so focusing the filter input counted as an outside interaction. The dismiss boundary is now the floating popup. - Fixed
combobox/searchable-selectarrow-key navigation no longer sticks at the first option: theopenreducer reset the highlight on every keypress (the input sendsopenthenhighlightNext); it now seeds the highlight only on the closed→open transition.setItemsalso clampshighlightedIndexto the new list so a shrinking list can't danglearia-activedescendant. - Fixed the
searchable-selectoption highlight stays correct after filtering (it tracks the live position / highlighted value rather than a frozen build-time index). - Fixed
menu/context-menuinteraction teardown (floating, dismissable, focus) now runs at close-request instead of animation-end, so listeners don't linger during the closing phase.
@llui/vike@0.11.3
- Added typed Vike route fields on the Layout-resolver
pageContext(LayoutResolverContext/ServerLayoutResolverContext).
@llui/devmode-annotate@0.2.6
- Fixed migrated the HUD solve-menu from
string[]to the menu component'sMenuItem[]API so it builds against the newer@llui/componentsmenu (see Breaking).
@llui/markdown-editor@0.2.6
- Fixed the image/link plugins'
dialogOpenswitches are now exhaustive (handle the dialog'sanimationEnd/transitionEndmessages), so they build against the newer@llui/componentsdialog.
Cascade — peer-range bump only
- Improved
@llui/test@0.11.4,@llui/router@0.10.4,@llui/transitions@0.10.4,@llui/markdown@0.10.4,@llui/lexical@0.2.4,@llui/lexical-collab@0.2.2,@llui/agent@0.10.4,llui-agent@0.10.2,@llui/mcp@0.12.5— republished withpeerDependencies["@llui/dom"]advanced to^0.11.3(and@llui/lexical/@llui/componentspeers bumped on the lexical / markdown-editor packages). No runtime changes.
2026-06-10 — @llui/components@0.11.6, @llui/markdown-editor@0.2.5
Released: @llui/components@0.11.6; @llui/markdown-editor@0.2.5
Note:
@llui/components@0.11.4and@llui/components@0.11.5were incomplete publishes — the newutils/rovingmodule was missing from the packeddist/. Root cause:pnpm publishruns no build (the packages carried noprepack/preparehook), so it packed a staledist/left on disk from an earlier checkout.0.11.6is the corrected publish, and every publishable package now carries aprepack: "pnpm run build"hook sopnpm publish/pnpm packalways build fresh from source (and fail loud rather than ship stale). Use0.11.6; do not use0.11.4or0.11.5.
@llui/components@0.11.6
- Added
utils/roving— a DOM-agnostic headless roving-tablist primitive.resolveRovingMove(key, current, items, opts)maps a keypress + current value to a move (arrows/Home/End →{ type: 'focus', value }, Enter/Space →{ type: 'activate' }), honouring orientation, RTL (viaflipArrow), looping, and disabled-skip;firstEnabled/lastEnabled/nextEnabledare the pure nav core;focusRovingTab(container, value)moves DOM focus via the shared[role="tab"][data-value]contract. This lets a consumer keep its own tablist markup (custom classes/ids — nodata-scope/data-partrequirement) while delegating WAI-ARIA keyboard behaviour to the library.tabsnow sources its nav core from the same primitive — behaviour unchanged, covered by the existing reducer/connect suite.
@llui/markdown-editor@0.2.5
- Improved Cascade release — peer range for
@llui/componentsbumped to^0.11.4. No functional change.
2026-06-10 — 0.11.2
Released: @llui/dom@0.11.2; @llui/compiler@0.11.1; @llui/vite-plugin@0.11.3; @llui/compiler-{introspection,devtools,ssr}@0.11.2; @llui/components@0.11.3; @llui/test@0.11.3; @llui/vike@0.11.2; @llui/router@0.10.3; @llui/transitions@0.10.3; @llui/markdown@0.10.3; @llui/agent@0.10.3; @llui/mcp@0.12.4; @llui/lexical@0.2.3; @llui/lexical-collab@0.2.1; @llui/markdown-editor@0.2.4; @llui/devmode-annotate@0.2.5
An opt-in frame-scheduled commit mode for @llui/dom, a latent each reactivity
fix, broader each lowering across the compiler/plugin (helper-only files now
compile; mid-tier eachArm codegen), and a batch of per-row / per-send allocation
cuts. No breaking changes.
@llui/dom@0.11.2
- Added opt-in frame-scheduled commits:
mountApp(container, def, { scheduler: 'raf' }). Reducers and effects still run synchronously persend— state andgetState()advance immediately, so the data contract is untouched — but the DOM commit + subscriber notification coalesce to one reconcile per animation frame. It's the streaming/burst fast path for consumers that accept DOM-lags-state-by-a-frame (game loops, high-frequency feeds).handle.flush()forces the pending commit synchronously (tests, agent protocol, read-after-write); microtask fallback where rAF is absent (SSR, plain jsdom, headless agent);disposecancels the pending frame. Measured at hand-written-vanilla parity on the ticker batch-1k op (5.9ms coalesced vs 14.1ms per-send). NOT a default — the synchronous contract holds unless opted in. - Fixed
eachstate-fanout staleness: the authoringeach's structural binding fired only on the items handle's deps, so a row-nested arm reading an unrelated state path (ashowonstate.at('flag'),connectparts) was silently frozen out of state-only changes — breaking the documented "a row reacts to its item AND component state" contract. Structural specs now takeextraDeps; the authoringeachpasses[''](rows can read state through code invisible at runtime), compiledeachDirectpasses the precise collectedstateDeps, and legacy 3-arg emissions degrade conservative-correct. jfb/ticker paths are fully compiled with precise deps — bench unaffected. - Improved per-row machinery: ~8 fewer allocations/row (the Row is now the
live-ctx box — the separate
holderwrapper and its per-update pointer write are gone;sparescratch ctx allocates lazily on first update;buildDirectRowinlined away;SignalScopeImpltakes specs as-is with a parallelmasksarray instead of per-binding wrappers). Same Row slimming applied tovirtualEach's window rows. Send path: lazy effects buffer, closure-free commit when no binding-error handler is installed, empty-subscriber guard, and a same-structure fast path that indexes rows viarowsInOrderinstead of a per-rowMap.get. Isolated jsdom A/B: burst-1k @200rows 4.2→3.2ms (−24% JS), create-10k −2.5%, heap −0.69MB/10k rows; real-Chromium same-env: every jfb op improved (Update −9%, Swap −9%, Clear −10%).
@llui/compiler@0.11.1
- Added mid-tier
eachArmcodegen for helpereaches whose rows have structural children (the corpus-dominant shapesignalEachnever reached): producers read the row ctx directly, un-lowerable children stay verbatim inside the arm, and handler.peek()reads rewrite to livegetCtx()reads. Plus a leaked-handle prelude — a row param leaking into a verbatim helper call no longer bails; the arm binds it to a real runtime handle. Block-body arms (decls + return) lower too, foreach,show, andbranch. - Added three
inlineHelperRenderlifts for the corpus-dominant delegation shapes (bare-call renders, helpers returning the documentedRenderablearray, leading render-side decls) — lifting helper-compositioneachsites onto the direct row fast path. - Added lowering telemetry:
SignalTransformOptions.onLowerBail(with theLowerBailtype) reports a stable reason token + position for every lowering attempt that gives up, andonPerfDiagnosticemits onellui/each-verbatimwarning pereachsite that ends fully verbatim, naming deduped bail reasons with actionable hints. - Fixed
peek-in-slotno longer flags the documented render-once row-local idiom (const isDir = item.peek().type === 'dir'in a block-body render) that the factory itself compiles as per-row wire decls: block-body render declarations now allowpeek; peeks in returned-array slots stay errors. - Improved the per-row factory hoists row-invariant
depsarrays andproduceclosures to per-each-site consts next to the cached skeleton (deduped by source); only the node-capturingcommitstays per-row. Isolated jsdom A/B: create-10k 71.6→67.9ms (~−5% JS).
@llui/vite-plugin@0.11.3
- Fixed helper-only modules now enter the transform: the pre-check required
component(in a file, so helper-only files — where real apps keep most of theireachsites (29/50 each-bearing files in one measured app) — never reached pass-2 helper-eachlowering. The pre-check now also routes dom-importing files containingeach(. Measured: +21 each sites compile to the directcloneNodefactory across three real apps (hasComponentCallstill solely arms the build-integrity scan). - Added surfacing of the compiler's
llui/each-verbatimperf diagnostics viathis.warn, behind aperfDiagnosticsoption (default on in dev, off in build, advisory only).
@llui/compiler-introspection@0.11.2, @llui/compiler-devtools@0.11.2, @llui/compiler-ssr@0.11.2
- Improved rebuilt against
@llui/compiler@0.11.1(cascade — no functional change).
@llui/components@0.11.3, @llui/test@0.11.3, @llui/vike@0.11.2, @llui/router@0.10.3, @llui/transitions@0.10.3, @llui/markdown@0.10.3, @llui/agent@0.10.3, @llui/mcp@0.12.4, @llui/lexical@0.2.3, @llui/lexical-collab@0.2.1, @llui/markdown-editor@0.2.4, @llui/devmode-annotate@0.2.5
- Improved peer range bumped to
@llui/dom@^0.11.2(cascade — no functional change).@llui/markdown-editoralso tracks@llui/lexical@^0.2.3and@llui/components@^0.11.3.
2026-06-09 — @llui/effects@0.1.1
Released: @llui/effects@0.1.1
@llui/effects@0.1.1
- Fixed
asOnEffectnow gives each mount a freshAbortControllerinstead of reusing one held in a definition-scope closure. BecauseasOnEffect(chain)is evaluated once at the component literal, the returnedonEffect(and its controller) was a per-definition singleton; after an unmount's cleanup aborted it, the=== nullguard never recreated it, so every re-mount of the same definition — e.g.@llui/vikedisposing + re-mounting a page on client-side navigation — inherited the already-aborted signal. Any async effect that guards onsignal.abortedbefore itssendthen silently dropped its result, leaving state stuck mid-transition with no error. The controller is now recreated once aborted (effects within a single mount still share it), and each dispatch's cleanup captures its own controller so a stale unmount can't abort a successor mount's live signal. Lazy creation is preserved (Cloudflare Workers global-scope safety).
2026-06-08 — devmode-annotate live apps
Released: @llui/devmode-annotate@0.2.4; @llui/vite-plugin@0.11.2; @llui/markdown-editor@0.2.3
The dev-mode HUD can now ship to production as a capture-only triage client; the Markdown editor gains a single-block (inline-only) mode.
@llui/devmode-annotate@0.2.4
- Added capture-only support for live (production) apps behind one pluggable
NotesStoreport. Three adapters:devServerStore(the existing dev path, now on a shared HTTP core),indexedDbStore(browser-local; screenshots as blobs), andhttpStore(host backend + injected auth headers, never baked). - Added
installAnnotateHud(new@llui/devmode-annotate/installsubpath) — a tiny lazy installer that code-splits the HUD behind an activation trigger, so an app that wires but never opens it ships no HUD chunk. Production mounting is opt-in viaallowProduction. - Added
exportBundle— exports the notebook as a standard.zip(canonical.md+.png+status.jsonllayout) with a content-hashedbundle.jsonmanifest; surfaced ashandle.exportBundle()and a store-gated download button in the browse toolbar. New@llui/devmode-annotate/note-format+/note-serializesubpaths expose the shared on-disk-format helpers. - Added open-shadow-root style isolation (opt-in
isolate, on by default underinstallAnnotateHud) via constructableadoptedStyleSheets(CSP-clean) with a shadow-<style>fallback. - Added per-channel redaction hooks (
redact.state/.repro/.screenshot) and prod-safe capture defaults — the debug-telemetry body and interaction recording default OFF in production. - Improved
EventSourcesubscription no longer escalates benign auto-reconnect errors toonError.
@llui/vite-plugin@0.11.2
- Added
POST /_llui/import— ingest an exported bundle into~/.llui/notes(content-addressed, idempotent, atomic, path-traversal-safe); the existing router/MCP solve flow then picks it up unchanged. Closes the capture → export → import → solve loop. - Improved the on-disk note format (filename/slug/session-name/status-replay +
.mdserialization) is now sourced from@llui/devmode-annotate/note-format+/note-serialize; the server delegates so browser and dev server share one contract (no drift).
@llui/markdown-editor@0.2.3
- Added
singleBlockPlugin()— constrains the editor to a single paragraph with inline formatting only (bold/italic/strikethrough/code, optional links); no block markdown, enforced against typing, the markdown seed, and paste. Options:formats,allowLineBreaks,link.
2026-06-07 — 0.11.1
Released: @llui/dom@0.11.1; @llui/lexical@0.2.2; @llui/markdown-editor@0.2.2; @llui/lexical-collab@0.2.0 (new); @llui/{agent@0.10.2,components@0.11.2,devmode-annotate@0.2.3,markdown@0.10.2,mcp@0.12.3,router@0.10.2,test@0.11.2,transitions@0.10.2,vike@0.11.1}
Ships a @llui/dom head-management fix plus opt-in real-time collaborative editing for the Lexical binding (new @llui/lexical-collab package). No breaking changes.
Migration
- dice.run / SSR head consumers may revert the "never gate head with
show()" workaround — head primitives gated byshow()/branch()now release correctly on client navigation.
@llui/dom@0.11.1
- Fixed head primitives (
title/meta/link/script/…) placed insideshow()/branch()/each()(or asubApppage) now release from<head>when their arm unmounts on a client-side navigation. Previously, an entry that was server-rendered and re-adopted on hydration was restored to its SSR content instead of removed, leaking stale<head>tags (wrong canonical / JSON-LD / meta) onto the destination route. The sink now distinguishes LLui-owned elements (client-created, or re-adopted SSR tags markeddata-llui-head→ removed when no writer remains) from genuinely foreign pre-existing elements (an unmarked static<title>→ restored). Surfaced by dice.run.
@llui/lexical@0.2.2
- Added two boot options on
lexicalForeign:history?: boolean(defaulttrue; setfalseto skip the built-in@lexical/historystack) andseedMode?: 'auto' | 'deferred'(defer the boot-time seed to an external owner). Both must be off for collaborative editing — a local undo stack crosses peers and per-client seeding duplicates content.
@llui/lexical-collab@0.2.0 (new)
- Added real-time collaborative editing via Yjs.
yjsCollab(config)composes@lexical/yjsinto oneregister(editor): CRDT sync, a local-origin-scopedUndoManager, a sync-gated bootstrap (one peer seeds while the shared doc is empty), presence cursors, andTOGGLE_CONNECT. The network provider is injected (y-websocket/y-webrtc/ Hocuspocus), keeping Yjs out of non-collab bundles.
@llui/markdown-editor@0.2.2
- Added a dependency-free collab seam: the editor injects a markdown seed (
defaultValue+ its transformers) and status sinks into a newstate.collabslice; throws oncollab+valuetogether (the CRDT owns the content). - Added a live presence indicator in the built-in toolbar — a connection dot (offline/connecting/synced) and live peer count ("Only you" / "N editing") via an optional
SignalonToolbarOptions; inert when collab is off.
Cascade peer bumps (no code change)
@llui/dom's bump rolls thepeerDependencies["@llui/dom"]range forward to^0.11.1for:@llui/agent@0.10.2,@llui/components@0.11.2,@llui/devmode-annotate@0.2.3,@llui/markdown@0.10.2,@llui/mcp@0.12.3,@llui/router@0.10.2,@llui/test@0.11.2,@llui/transitions@0.10.2,@llui/vike@0.11.1.
2026-06-07 — packaging: caret-pin sibling @llui/* deps
Released: @llui/vite-plugin@0.11.1; @llui/mcp@0.12.2; @llui/{compiler-introspection,compiler-devtools,compiler-ssr}@0.11.1; @llui/devmode-annotate@0.2.2; llui-agent@0.10.1
Publish-metadata fix only — no code changes. Sibling @llui/* entries under dependencies used workspace:*, which pnpm publish rewrites to an exact version pin. A single sibling minor bump then stranded dependents a version behind: published @llui/vite-plugin@0.11.0 exact-pinned @llui/devmode-annotate@0.2.0, dragging a dormant duplicate pre-0.11 head-model tree (devmode-annotate/markdown-editor/lexical/components) into consumer lockfiles, each declaring an unmet @llui/dom ^0.10.0 peer on @llui/dom@0.11. Surfaced by dice.run. Switching the specs to workspace:^ makes published tarballs carry caret ranges, so a future minor bump of a sibling no longer leaves the dev-tooling chain a version behind.
@llui/vite-plugin@0.11.1
- Fixed sibling
@llui/*dependencies(compiler,compiler-devtools,compiler-introspection,compiler-ssr,devmode-annotate) now publish as caret ranges instead of exact pins. Republished so consumers on@llui/dom@0.11stop resolving the stale@llui/devmode-annotate@0.2.0chain.
@llui/devmode-annotate@0.2.2
- Fixed sibling
@llui/{components,lexical,markdown-editor}dependenciespublish as caret ranges instead of exact pins.
@llui/{compiler-introspection,compiler-devtools,compiler-ssr}@0.11.1
- Fixed
@llui/compiler(and inter-module)dependenciespublish as caret ranges. ABI skew stays guarded by the separatecompilerVersionsemver check (@llui/compilermodule.ts+manifest-io.ts), so caret is safe.
@llui/mcp@0.12.2
- Fixed sibling
@llui/{compiler,compiler-devtools,compiler-introspection,vite-plugin}dependenciespublish as caret ranges instead of exact pins.
llui-agent@0.10.1
- Fixed
@llui/agentdependency publishes as a caret range instead of an exact pin.
2026-06-07 — 0.11.0 (head / metadata management)
Released: @llui/{dom,vike}@0.11.0; @llui/{agent,markdown,router,transitions}@0.10.1; @llui/{components,test}@0.11.1; @llui/mcp@0.12.1; @llui/{lexical,markdown-editor,devmode-annotate}@0.2.1
Helmet-style head/metadata management for the signal runtime: set the document title, <meta>/<link> tags, and <html>/<body> attributes reactively from page content, with full SSR collection and hydration adoption.
Breaking
@llui/vike@0.11.0—DocumentContext(the argument to a customdocumenttemplate increateOnRenderHtml) gains two required fields,htmlAttrsandbodyAttrs, and the default template now interpolates them as<html${htmlAttrs}>/<body${bodyAttrs}>. Existing custom templates keep working (they consume a subset of the context), but to surfacehtmlAttr(...)/bodyAttr(...)in SSR output you must interpolate the two new fields into your<html>/<body>tags.
Migration
- If you pass a custom
documenttemplate tocreateOnRenderHtmland usehtmlAttr(...)/bodyAttr(...), add${htmlAttrs}/${bodyAttrs}to your<html>/<body>tags. Otherwise no action needed.
@llui/dom@0.11.0
- Added head/metadata primitives:
title,titleTemplate,meta,link,htmlAttr,bodyAttr,base,style,script,noscript. Each accepts a plain value or aSignal(HeadValue<T>), commits through the existing chunked-mask reconciler (reactive, mask-gated,batch()-coalesced), and dedups via a per-key last-writer-wins stack — a nested page'stitle/metaoverrides its layout's and is restored on unmount.titleTemplate('%s · Site')composes withtitle('Page')→Page · Site. - Added the
HeadSinkcoordination layer:domHeadSink(writes to a livedocument.head, adopting server-rendereddata-llui-headtags on hydration so nothing duplicates or flashes),collectHeadSink(SSR collector → escaped head / html-attrs / body-attrs strings via the shared serializer),mergeStaticHead, and theHEAD_SINKcontext. Sink resolution is per-document (no cross-document global). - Added public extension points
registerBinding,onTeardown,currentDoc, plusSignalDoc.head/documentElement— for sibling modules that build bindings with non-element commit targets.
@llui/vike@0.11.0
- Added SSR head collection:
createOnRenderHtmlthreads one collector through the whole layout→page chain, merges collected component head with the static+Head.tshead (component entries override colliding<title>/<meta>, so the document never carries two<title>s), and injects collected<html>/<body>attribute strings. See Breaking for theDocumentContextchange.
Peer-range refresh
- Improved
@llui/{agent,markdown,router,transitions}@0.10.1,@llui/{components,test}@0.11.1,@llui/mcp@0.12.1,@llui/{lexical,markdown-editor,devmode-annotate}@0.2.1— re-released withpeerDependencies["@llui/dom"]widened to^0.11.0. No code changes.
2026-06-07 — 0.10.0 (HTML-native attribute spelling + compiler autofix)
Released: @llui/dom@0.10.0; @llui/compiler@0.11.0; @llui/{vite-plugin,components,test,compiler-introspection,compiler-devtools,compiler-ssr}@0.11.0; @llui/{router,transitions,vike,agent,markdown}@0.10.0; llui-agent@0.10.0; @llui/{lexical,markdown-editor,devmode-annotate}@0.2.0; @llui/mcp@0.12.0
LLui now authors HTML attributes in their native lowercase form (tabindex, readonly, class/for — matching aria-*/data-*), enforced by a new compiler lint layer that ships deterministic, auto-applied fixes. Event handlers stay camelCase (onClick). Plus a runtime fix for <select> value resolution.
Breaking
@llui/compiler@0.11.0— three new always-on lint rules.conventionflags a camelCase DOM attribute (tabIndex,readOnly,maxLength, …) and rewrites it to the HTML-native lowercase spelling.attr-nameis a hard error on React-isms that silently never applied in LLui (className → class,htmlFor → for).event-handler-casingis a hard error on a miscased handler the runtime never binds (onclick → onClick).@llui/components@0.11.0— part-bag and option fields that mirror HTML attributes were renamed to their lowercase spelling:tabIndex → tabindex,readOnly → readonly,spellCheck → spellcheck,inputMode → inputmode,autoComplete → autocomplete,maxLength → maxlength. Reading these off a part bag (e.g.parts.root.tabIndex) or passing them asinitoptions now uses the lowercase name.@llui/lexical@0.2.0— thelexicalForeignoptions fieldreadOnlywas renamed toreadonly.@llui/markdown-editor@0.2.0— the editor init option and state fieldreadOnlywere renamed toreadonly.@llui/dom@0.10.0/@llui/mcp@0.12.0— thegetRenderedHtmldebug-API / MCP-tool parametermaxLengthwas renamed tomaxlength.
Migration
- Rename camelCase HTML attributes in your views to lowercase (
tabIndex → tabindex, etc.).@llui/vite-pluginauto-applies theconventionfix and warns, so most call sites need no manual edit — run a build to see what it rewrites. - Replace
className → classandhtmlFor → for(these were dead attributes in LLui — they never applied at runtime). - Fix any miscased event handler the build flags (
onclick → onClick); event handlers stay camelCase. - If you read attribute-shaped fields off a
@llui/componentspart bag, or passreadOnlyto@llui/lexical'slexicalForeignor@llui/markdown-editor, switch to the lowercase name.
@llui/dom@0.10.0
- Fixed form-control selection now resolves after the subtree is assembled, so a
<select>'s reactivevaluebinds the correct<option>even when options are appended after the select element.
@llui/compiler@0.11.0
- Added the
convention,event-handler-casing, andattr-namelint rules (see Breaking), plus a deterministic-fix layer: diagnostics now carry aLintFix, andapplyLintFixes()+ a shared offset-safe edit applier are exported for tooling.
@llui/vite-plugin@0.11.0
- Added auto-applies
conventionfixes to the emitted code with a warning, so the dev loop never blocks on a pure casing nit; correctness rules still halt the build but now carry their fix data.
@llui/mcp@0.12.0
- Added
llui_compiler_diagnosticsnow emits afix(title + edits with old/new text) for rename-style rules, so an agent can apply the correction deterministically.
All packages — version cascade
- Changed the
@llui/domand@llui/compilerbumps cascade to their peer-holders and opt-in modules, which bump in lockstep to pick up the new peer ranges. The purely-cascaded packages have no source changes:@llui/{router,transitions,vike,agent,markdown,test,devmode-annotate,compiler-introspection,compiler-devtools,compiler-ssr}andllui-agent.
2026-06-06 — 0.9.0 (Markdown editor: @llui/lexical + @llui/markdown-editor)
Released: new @llui/lexical@0.1.0, @llui/markdown-editor@0.1.0, @llui/markdown@0.9.0; @llui/dom@0.9.0; @llui/compiler@0.10.0; @llui/{vite-plugin,test,components,compiler-introspection,compiler-devtools,compiler-ssr}@0.10.0; @llui/{router,transitions,vike,agent}@0.9.0; llui-agent@0.9.0; @llui/mcp@0.11.0; @llui/effects@0.1.0; @llui/devmode-annotate@0.1.0
Headline: a WYSIWYG Markdown editor built on Lexical, shipped as two new packages. Plus per-row reconcile perf in the runtime, broader compiler each-row lowering, and batch() send coalescing.
Breaking
@llui/compiler@0.10.0— the signal lint set is broader and stricter (it runs as non-bypassable build errors). Code that was previously accepted may now fail the build if it trips a newly-covered rule (e.g. node construction in a body, an operator applied to a signal). The compiler error names the rule and the fix.
Migration
- If a build newly fails a signal lint rule after upgrading
@llui/compiler/@llui/vite-plugin, fix the flagged expression as the error message describes — there is no opt-out (by design: LLMs ignore warnings, so framework rules are hard errors).
@llui/lexical@0.1.0 (new)
- Added Low-level Lexical ↔ signal-runtime binding: the
lexicalForeignseam (markdown-agnostic, injected serialize/deserialize), theLexicalPlugincontract, keyboard-shortcut wiring, and aDecoratorNode↔ LLui sub-view bridge so a Lexical node can host an independent LLui component.
@llui/markdown-editor@0.1.0 (new)
- Added
markdownEditor(config)— a WYSIWYG editor that hides Markdown behind a rich editing surface (Lexical owns the live document; Markdown is the serialization boundary). Scales from a bare keyboard-formatting field to a Google-Docs-class surface. - Added 14 composable plugins on three seams (plugin-UI / decorator bridge / transformer registry): GFM core, link dialog, image, horizontal rule, emoji, editable callout decorator, math, mermaid, GFM tables with classic cell-editing tools, slash command menu,
@-mentions, right-click context menu, and a floating selection toolbar. - Added A shared anchored-overlay primitive backing all positioned surfaces, a transformer registry governing round-trip fidelity, a ready-made toolbar surface, and a shipped theme (
@llui/markdown-editor/styles/editor.css). Demo inexamples/markdown-editor.
@llui/markdown@0.9.0 (new)
- Added
@llui/markdown— reactive Markdown rendering (render Markdown to live LLui nodes) with a showcase demo.
@llui/dom@0.9.0
- Improved Per-row reconcile cost:
eachrows clone a template and each row gets a lean, class-based signal scope (1 allocation per row instead of 5). - Added
batch(fn)— coalesce a burst ofsends into a single reconcile (drain a websocket frame of N ticks as one re-render); available on the handle and in the view /onEffectbag. The compiler also auto-wraps provably-safe straight-line multi-sendhandlers. - Added Dev tooling can opt out of the global
__lluiComponentsregistry viamountSignalComponent(el, def, { devtools: false })— so an embedded tool's own components don't pollute the host app's introspection. - Improved Error-message clarity, an
.at()-after-.map()guard, variadicderived, and typed handler signatures.
@llui/compiler@0.10.0
- Added Broader
each-row lowering tosignalEachDirect— block-body rows, rows inside view-helper functions, item-handler list rows, and same-file row-helper inlining (cross-function phases 1–2). - Added
batch(...)auto-wrap for straight-line multi-sendhandlers. - Improved Expanded signal lint coverage (see Breaking).
- Fixed Double-lowering (pass-1 edit ranges are now captured after pass 1 runs) and a cross-file leak of ambient lowering state on throw.
@llui/compiler-introspection@0.10.0 · @llui/compiler-devtools@0.10.0 · @llui/compiler-ssr@0.10.0
- Improved Rebuilt against
@llui/compiler@0.10.0.
@llui/effects@0.1.0
- Added A
logeffect builder (LogEffect) and adelayalias fortimeout.
@llui/agent@0.9.0
- Added
@routeGatedactions are surfaced to the agent as unavailable with a reason (Tier 4.2). - Improved Validation errors carry a valid example message (Tier 4.1).
@llui/vite-plugin@0.10.0
- Improved Notes frontmatter parsing and on-disk store; rebuilt against the new runtime + compiler.
@llui/mcp@0.11.0
- Improved CDP and relay transport robustness.
@llui/router@0.9.0 · @llui/vike@0.9.0 · @llui/test@0.10.0
- Fixed Code-review findings across
connect/guards/navigation (router),onRenderHtml(vike), andpropertyTest(test); all rebuilt against@llui/dom@0.9.0.
@llui/components@0.10.0 · @llui/transitions@0.9.0 · llui-agent@0.9.0
- Improved Rebuilt against
@llui/dom@0.9.0.
@llui/devmode-annotate@0.1.0
- Added Now published. The compose field is a real embedded
markdownEditor()(replacing the raw textarea + hand-rolled Markdown toolbar); new publicAnnotateHudHandle.setProse(text)to set the draft programmatically. - Improved Migrated the HUD to idiomatic LLui and adopted
@llui/components(select, etc.); tightened the compose layout and fixed host@llui/componentsCSS leaking into the HUD.
Docs
- Regenerated API references; recorded the measured conclusions of the create/reorder perf investigation and the remaining perf opportunities; published the example mini-apps as live embedded demos.
2026-06-05 — @llui/dom@0.8.0 (signalEachDirect + reconcile perf)
Released: @llui/dom@0.8.0; @llui/compiler@0.9.0; @llui/{vite-plugin,compiler-devtools,compiler-introspection,compiler-ssr,components,test}@0.9.0; @llui/{router,transitions,vike,agent}@0.8.0; llui-agent@0.8.0; @llui/mcp@0.10.0
Restores LLui to the leading benchmark pack after the signals migration: the create-construction regression is closed via a compiler direct-row path, and fine-grained streaming updates (a "ticker" suite) now lead the framework field. No breaking changes — all additive.
@llui/dom@0.8.0
- Added
signalEachDirect/ authoringeachDirect(items, key, rowFactory)— a direct-construction keyed list: the same keyed reconcile aseach, but each row is built by aRowFactory(direct DOM ops + binding specs wired by node reference) instead of running authoring helpers per row. New typesDirectRow/RowFactory;BindingSpecis now exported as the factory contract. This is the compiler's fast-path target (see@llui/compiler) and is also usable hand-written. - Improved Each reconcile, two fine-grained-update wins: a same-structure fast path skips the O(n) keyed scan for in-place value updates (streaming ticks), and state-fanout gating skips the all-row re-evaluation sweep when the component-state paths a row reads didn't change. On the ticker suite: burst-1k 31.7 → 15.9 ms, tick-100 6.4 → 4.8 ms — LLui now beats every other framework on every ticker op.
- Improved Lower per-node cost: class-based
Mountable(one allocation, monomorphicmount()),.at(path)pre-splits its path once (no per-readString.split), and the binding mount/update loops drop the per-iterationtry/catchon the common (no error-hook) path. Run-1k memory −11% vs the prior baseline.
@llui/compiler@0.9.0
- Added Static-skeleton
eachrows (elements with static OR reactive attributes + static/signaltext) lower tosignalEachDirectwith a generatedRowFactory— direct DOM construction + bindings by node reference, no per-row authoring/populate/pathHandleoverhead. Falls back tosignalEach(unchanged behavior) foron*handlers, structural children, spreads, IDL/style props, dynamic args, orindexreads.
@llui/vite-plugin@0.9.0, @llui/compiler-devtools@0.9.0, @llui/compiler-introspection@0.9.0, @llui/compiler-ssr@0.9.0
- Improved Rebuilt against
@llui/compiler@0.9.0(the direct-row codegen).
@llui/components@0.9.0, @llui/test@0.9.0, @llui/router@0.8.0, @llui/transitions@0.8.0, @llui/vike@0.8.0, @llui/agent@0.8.0, llui-agent@0.8.0
- Improved Rebuilt against
@llui/dom@0.8.0; peer@llui/domrange bumped to^0.8.0.
@llui/mcp@0.10.0
- Improved Rebuilt against
@llui/dom@0.8.0+@llui/compiler@0.9.0.
Docs
- Added API reference regenerated for the new
signalEachDirect/eachDirect/DirectRow/RowFactory/BindingSpecexports; README performance tables refreshed from an authoritative 5-run (standard + ticker) baseline; thecompiled-row-constructionproposal documents the full investigation.
2026-06-03 — 0.7.0
Released: @llui/dom@0.7.0; @llui/{router,transitions,vike,agent}@0.7.0; llui-agent@0.7.0; @llui/{compiler,compiler-devtools,compiler-introspection,compiler-ssr,components,test,vite-plugin}@0.8.0; @llui/mcp@0.9.0
Completes the lazy-Mountable model: every authoring helper (elements, text, structural primitives) is now a lazy description materialized at placement — capture-and-reuse is correct by construction and the silent double-placement footgun is gone. Plus an accurate API-doc rewrite and removal of the abandoned two-word-bitmask compiler code.
Breaking
@llui/dom@0.7.0—el/elNS/text/staticText/onMountand the element helpers (div/span/…) now return a lazyMountable(joiningeach/show/branch/…), so they are only realized when placed (as an element child or in a view/arm/row return). The child typeChildNodecollapses toMountable | string | number— a bareNodeis no longer accepted; wrap a raw DOM node with the newly-exportedmountable(() => node). A helper called for its side effect (notablyonMount) is inert unless its returned marker is placed in the view array.@llui/compiler@0.8.0—collectDeps(source)returns{ paths: string[], opaque, … }instead of the two-word{ lo, hi }bitmask maps. The legacycollectAccessorPathSetsexport is removed.
Migration
- Annotate view-helper functions
Renderable(a list,readonly Mountable[]) orMountable(a single element) — notNode/Node[]. The compiler's cross-file walker recognizes both. - Anywhere you called
onMount(...)for its side effect and discarded the result, place the returned marker instead (return [onMount(...), …]). Same for any helper whose return you dropped — discarded helpers now do nothing. - Pass raw DOM nodes as children via
mountable(() => node)rather than the node directly. - Consumers of
@llui/compiler'scollectDepsread.pathsinstead of.lo/.hi.
@llui/dom@0.7.0
- Breaking all authoring helpers return
Mountable;ChildNodedropsNode. See top of release block. - Improved placing the same captured
Mountabletwice now builds two independent live instances; discarded helpers are fully inert (no orphaned bindings). - Added
mountable(build)(build a placeable from a closure / raw node) andisMountable, exported alongside theMountable/Renderable/ChildNodetypes.
@llui/compiler@0.8.0
- Breaking
collectDepsreturns{ paths };collectAccessorPathSetsremoved. See top of release block. - Fixed the cross-file view-helper walker now recognizes
Mountable/Renderablereturn types (previously every helper on the new convention was misclassified as opaque, degrading dependency analysis); diagnostics point at the new types. - Improved retired the abandoned two-word-bitmask machinery (the chunked-mask runtime never used it) and cleaned the stale
__dirty/__mask/bitmask-overflowJSDoc that surfaced in the API reference.
@llui/vite-plugin@0.8.0
- Improved re-pins
@llui/compiler@0.8.0;verboselog wording updated to the signal model.
@llui/components@0.8.0
- Fixed every
overlay()placed its focus-trap/dismissable/scroll-lockonMountmarker — under the lazy model a discardedonMountwas inert, so outside-click / Esc dismissal silently stopped working. Requires@llui/dom@^0.7.0.
@llui/router@0.7.0
- Fixed
listener()places itsonMountmarker (was discarded → URL listener never registered);link()returns aMountable. Requires@llui/dom@^0.7.0.
@llui/vike@0.7.0
- Changed
pageSlot()returns aMountable;AnyLayer.viewreturnsRenderable. Requires@llui/dom@^0.7.0.
@llui/test@0.8.0
- Changed
defineTestComponent'sviewreturnsRenderable. Requires@llui/dom@^0.7.0.
@llui/mcp@0.9.0
- Changed
llui_static_collect_pathsreports dependency paths + anopaqueflag (dropped the legacy per-bit / 62-path-budget output). Requires@llui/dom@^0.7.0and re-pins@llui/compiler@0.8.0.
@llui/transitions@0.7.0, @llui/agent@0.7.0, llui-agent@0.7.0
- Changed require
@llui/dom@^0.7.0(transitions/agent) / re-pin@llui/agent@0.7.0(llui-agent); no source change.
@llui/compiler-{devtools,introspection,ssr}@0.8.0
- Changed lockstep re-pin of
@llui/compiler@0.8.0(internalworkspace:*); no source change.
Docs
- Rewrote the
@llui/dom,@llui/compiler,@llui/vite-plugin, and@llui/componentsAPI references off the legacy pre-signals API onto the signal +Mountablemodel, and fixed the auto-API generator (it silently emitted nothing fordom/transitions, so those references had frozen).
2026-06-03 — 0.6.0
Released: @llui/dom@0.6.0; @llui/{router,transitions,vike,agent}@0.6.0; @llui/{components,test}@0.7.0; @llui/mcp@0.8.0; llui-agent@0.6.0
Structural primitives become lazy descriptions, fixing a silent blank-render bug when a captured each/show/branch was reused across a remount.
Breaking
@llui/dom@0.6.0—each,show,branch,unsafeHtml,lazy,virtualEach,foreign,portal, andprovidenow return aMountable(a lazy recipe materialized at placement) instead of aNode. Inline usage as element children (div([show(...)])) is unchanged. Code that returned one of these at the top level of a view-helper array, or treated the return value as a real DOM node, must adapt: annotate the helper's return typeRenderable(newly exported) instead ofNode[], and don't callNodemethods on the return value before it is placed.@llui/components@0.7.0— everyoverlay()(dialog, popover, select, menu, tooltip, hover-card, drawer, context-menu, combobox, alert-dialog) andconfirmDialog.view()now returnMountable/Renderableinstead ofNode/Node[], since they wrapshow(). Consumers that place them as children are unaffected.
Migration
- If you worked around the blank-render bug by passing a builder thunk (
() => [each(...)]) and calling it inside ashow/branchcallback, you can revert to capturing the primitive once (const slot = [each(...)]) — reuse across remounts is now correct. - For view-helper functions that return a structural primitive at the top of their array, change the return annotation from
Node[]/readonly Node[]to the exportedRenderable. - Bump
@llui/domto^0.6.0wherever you pin it; the cascade packages now require it.
@llui/dom@0.6.0
- Fixed a structural primitive built once and captured, then reused inside a
show/brancharm that toggled, rendered nothing on the second mount — the returned fragment was drained on first insertion and its scope disposed on hide, so re-inserting the same nodes was a silent no-op. - Improved structural primitives are now lazy
Mountables materialized at the point they are placed (bypopulatefor element children andrunBuildfor a build's returned array), always under a live build context. The reconcile spec lands in the placing scope, so a captured Mountable rebuilds fresh on every remount, and placing one twice yields two independent live instances. - Added exported
Mountable,Renderable,ChildNodetypes and theisMountableguard.
@llui/components@0.7.0
- Breaking overlay/view helpers return
Mountable/Renderable. See top of release block. - Improved recompiled against the new
@llui/domstructural-primitive API; requires@llui/dom@^0.6.0.
@llui/test@0.7.0
- Changed requires
@llui/dom@^0.6.0(no source change; recompiled against the new structural-primitive API).
@llui/router@0.6.0
- Changed requires
@llui/dom@^0.6.0(no source change).
@llui/transitions@0.6.0
- Changed requires
@llui/dom@^0.6.0(no source change;transition()still receives mounted arm nodes, unaffected by the lazy-Mountable change).
@llui/vike@0.6.0
- Changed requires
@llui/dom@^0.6.0(no source change; SSR materializes Mountables through the same build seams).
@llui/mcp@0.8.0
- Changed requires
@llui/dom@^0.6.0(no source change).
@llui/agent@0.6.0
- Changed requires
@llui/dom@^0.6.0(type-only consumer; peer range bumped).
llui-agent@0.6.0
- Changed re-pins
@llui/agent@0.6.0(internalworkspace:*, substituted at pack time).
2026-06-03 — @llui/compiler-introspection@0.7.0, @llui/compiler-devtools@0.7.0, @llui/compiler-ssr@0.7.0, @llui/mcp@0.7.0
Released: @llui/compiler-introspection@0.7.0, @llui/compiler-devtools@0.7.0, @llui/compiler-ssr@0.7.0, @llui/mcp@0.7.0
Lockstep bump of the rest of the compiler family to 0.7.0 so their published tarballs re-pin @llui/compiler@0.7.0 (internal deps are workspace:*, substituted at pack time). No source/API change in any of these packages — they don't use the new cross-package ABI, but this keeps the family on one compiler version and avoids a consumer installing two @llui/compiler majors side by side.
2026-06-03 — @llui/components@0.6.0
Released: @llui/components@0.6.0
Now ships a dist/__llui_deps.json dependency manifest (the @llui/compiler@0.7.0 library ABI). No source/API change — the win is for consumers: an app calling a components derive helper in a reactive position (text((s) => itemFill(s, i)), showScrollbars/thumbPosition/thumbSize) now narrows the binding to the exact state paths the helper reads instead of coarsening to the whole slice at the npm boundary. Generated at publish time by scripts/emit-deps.mjs (wired into scripts/publish.sh); requires consumers on @llui/vite-plugin@0.7.0+. connect/overlay parts-helpers compose via runtime Signal handles and are unaffected (correctly opaque). See docs/publishing-a-precompiled-library.md.
2026-06-03 — @llui/compiler@0.7.0, @llui/vite-plugin@0.7.0
Released: @llui/compiler@0.7.0, @llui/vite-plugin@0.7.0
Ships the cross-package library ABI — the last deferred piece of the v2-compiler vision. A consumer app can now narrow reactive bindings through a precompiled package's helpers (text((s) => itemFill(s, i)) from @llui/components) instead of coarsening to the whole slice at the npm boundary.
@llui/compiler@0.7.0
- Feature (manifest schema v2): added the
state-valueParamSpec/FieldSpecshape for helpers called ashelper(s)— the state value passed directly, thestate.map(s => helper(s))pattern — which v1's accessor-function shapes could not express.substituteHelperCallcomposes the value-argument's path prefix onto the declared reads. (manifest.ts.) - Feature (producer):
buildManifest(program, { srcRoot })walks a package's source and emits aHelperEntryper narrowable helper, reusing the canonicalextractPaths/detectOpaqueStateFlow(now exported fromcollect-deps.ts) so producer and consumer agree exactly. Any parameter that leaks (passed whole, element access, delegated to an opaque call) is emittedopaque→ the consumer coarsens, never mis-narrows. (build-manifest.ts.) - Feature (IO + resolution):
serializeManifest(stable sorted keys) /parseManifest(version + structural validation; reasoned failure, never throws) (manifest-io.ts); symbol → owning package →dist/__llui_deps.jsonlookup, memoized per package (manifest-resolve.ts). - Feature (consume): the cross-file walker, at a body-less
.d.tscall it cannot follow, consults the package's manifest and substitutes the helper's reads before coarsening. Soundness floor: a missing / version-incompatible / malformed / helper-absent manifest all coarsen to opaque. (cross-file-walker.ts; testscross-package-narrowing.test.ts,build-manifest.test.ts,manifest-io.test.ts, and astate-valueworked example inmanifest-roundtrip.test.ts.) - Tooling:
scripts/emit-deps.mjsgenerates a package'sdist/__llui_deps.json; wired intoscripts/publish.shso a normal publish emits a fresh manifest. Seedocs/publishing-a-precompiled-library.md.
@llui/vite-plugin@0.7.0
- Dependency: re-pins
@llui/compiler@0.7.0so apps consuming the plugin pick up cross-package narrowing. No API change.
2026-06-02 — @llui/test@0.6.0
Released: @llui/test@0.6.0
Follow-up to @llui/dom@0.5.10: the reentrancy + removeBetween fixes were real, but the most reentrancy-prone view pattern (inline-edit-commit) could not be tested on its genuine path. jsdom omits the HTML "removing steps" focus fixup — removing a focused node fires no blur/focusout — so the literal focus→commit→blur sequence was unreachable, and the removeBetween snapshot defense had no direct coverage at all.
@llui/test@0.6.0
- Feature: new
emulateBlurOnRemoval()/withBlurOnRemoval(fn)helpers. They patchremoveChild/remove/replaceChildso that removing the focused element (or an ancestor) dispatchesblurthenfocusoutsynchronously, in browser order — matching the focus fixup jsdom skips.emulateBlurOnRemoval()returns an uninstall function (use inafterEach);withBlurOnRemoval(fn)scopes the patch aroundfn. This unblocks faithful inline-edit-commit testing for any LLui app. (packages/test/src/blur-on-removal.ts; teststest/blur-on-removal.test.ts,test/inline-edit-commit.test.ts.) - Tests: added the genuine focus→commit→blur regression for the
@llui/dom@0.5.10send-queue fix (asserts effect ORDER as the queue-vs-nest discriminator) and the first direct test ofremoveBetweensurviving a reentrant sibling detach mid-walk. Both are mutation-verified — reverting the runtime fix turns them red.
2026-06-02 — @llui/dom@0.5.10
Released: @llui/dom@0.5.10
Surfaced finishing the dicerun signals migration (Studio's inline rename): committing a rename removed the focused <input> as a structural branch arm swapped — which fires blur synchronously, re-entering the update cycle mid-reconcile and crashing with a NotFoundError, which in turn aborted the message's effects (the PATCH never fired).
@llui/dom@0.5.10
- Fix (reentrancy):
sendis now reentrancy-safe. A message dispatched WHILE another is being processed (the canonical trigger: removing a focused node during a structural arm swap firesblur→ its handler callssend) is queued and drained by the active call, never run as a nested reducer + reconcile. Nesting mutated the scope tree / DOM mid-reconcile and silently skipped the outer message's effects. From a top-level callersendstays synchronous (the queue fully drains before it returns). (component.ts; regression testtest/signals/send-reentrancy.test.ts.) - Fix (defense-in-depth):
removeBetween(structural arm / sub-app teardown) now snapshots the doomed node list BEFORE removing any, so ablur-driven reentrant mutation during removal can't corrupt the livenextSiblingwalk. (dom.ts.)
2026-06-02 — @llui/dom@0.5.9
Released: @llui/dom@0.5.9
Another dicerun-migration find: buttons in the packs / my-rolls sidebar were permanently disabled (disabled="[object Object]"), so every interaction that went through them timed out.
@llui/dom@0.5.9
- Fix:
el(...)now binds a raw signal-handle prop value reactively instead of stringifying it into the attribute. The compiler lowers INLINEstate.map(...)props toreact(...), but a signal kept in a variable (const off = state.map(...); button({ disabled: off }), a spread, or a helper's return) is opaque to it and reacheselverbatim.elpreviously treated any non-react, non-handler value as a static attribute — so the handle serialized to"[object Object]"(a truthy, permanently-stuckdisabled). It now detects a signal handle and binds it, matching the authoring element helpers. (populateinpackages/dom/src/signals/dom.ts; testtest/signals/el-signal-prop.test.ts.)
2026-06-02 — @llui/dom@0.5.8
Released: @llui/dom@0.5.8
Surfaced finishing the dicerun signals migration — the inline-roll / dice-result and stats widgets rendered nothing after a roll, with no error and correct state. The cluster (homepage /d/<expr>, /explore inline rolls, /docs re-rolls, studio stats, export-stats, compare-overlay) all share one shape: an outer keyed each (per epoch / per row) whose row renders an inner each whose items derive from the ROW ITEM — each(item.map(i => …), …).
@llui/dom@0.5.8
- Fix: a nested
eachwhose items source reads the enclosing row'sitem(e.g.each(item.map(i => …))) rendered zero rows. AninRoweach's structural binding reconciled againstctx.state(the component state), so the items source'sitempath resolved toundefined→ empty list, silently. The reconcile now derives the items-source state from the source's deps — a row-local source (item.map/item.at, all-row-local deps) reads the combined row ctx soitem/indexresolve, while a component-state source (state.map) still readsctx.state; rows always mount with the component state regardless. Previously only inner eaches over component state (each(state.map(s => s.list))) worked; inner eaches over the row item silently produced nothing. (signalEach,packages/dom/src/signals/dom.ts; regression test intest/signals/each-nested-structural.test.ts.)
2026-06-02 — @llui/devmode-annotate@0.0.4
Released: @llui/devmode-annotate@0.0.4
Surfaced finishing the dicerun signals migration: the whole Playwright e2e suite hung on page.waitForLoadState('networkidle') and the failures looked like render bugs (30s timeouts across /explore, /my-rolls, /docs, /packs, …). The app actually rendered fine — the network simply never went idle.
@llui/devmode-annotate@0.0.4
- Fix: the in-app HUD's persistent SSE subscription to
/_llui/events?role=hudnow defaults off under an automation-controlled browser (navigator.webdriver === true). The stream never closes, so in any consumer app that mounts the HUD in dev it permanently blockedwaitForLoadState('networkidle')— hanging every Playwright / WebDriver e2e suite. There is no human to drive the HUD and no LLM capture session is expected under automation, so suppressing it is always correct. An explicitsubscribeEvents(trueorfalse) still wins, so a suite that specifically exercises the SSE path can force it on. (mountAnnotateHud,packages/devmode-annotate/src/index.ts.)
2026-06-01 — @llui/dom@0.5.6, @llui/components@0.5.3, @llui/effects@0.0.11
Released: @llui/dom@0.5.6, @llui/components@0.5.3, @llui/effects@0.0.11
Surfaced migrating a real app (dicerun — a dice-notation studio with Monaco, SSR, a persistent layout, and an agent panel) from the pre-signals 0.4.x runtime to signals. Four gaps in the signal surface, all caught only at runtime (SSR / e2e) after the static gates were green — a reminder that the build is not the last gate.
@llui/dom@0.5.6
- Added
unsafeHtml(value: Reactive<string>)— renders a raw HTML string as live DOM nodes inline (between anchor comments, no wrapper element), reactive on aSignal<string>; a plain string renders once. Parses via the envparseHtmlFragment(SSR) or a<template>fallback (raw clientDocument). The escape hatch for pre-rendered markup (markdown, syntax highlighting); the caller owns trust/sanitization. The legacy runtime had this; the signal runtime had dropped it. - Added
subApp({ reason, def, initialState?, contexts?, onHandle? })at the@llui/dom/escape-hatchsubpath — mounts an ISOLATED component instance (own update loop + mask scope + DOM region) at an anchor; not registered as a child scope, disposed with the host. For genuine isolation only (third-party UI, a long-lived loop with no reactive props, a 60fps layer) — everyday decomposition uses view-helper functions overSignal<T>slices. Thereasonfield documents why isolation is warranted. Per the signals design doc, which always specifiedsubAppas the one isolation valve. - Internal
SignalDocgains an optionalparseHtmlFragment(present on the serverDomEnv+browserEnv; absent on a raw clientDocument, whereunsafeHtmlfalls back to a<template>parse).
@llui/components@0.5.3
- Fixed SSR crash in
dialog/combobox/draweroverlay(): the portal host was resolved eagerly atoverlay()build time viadocument.querySelector(target) ?? document.body, but the view build runs on the server too — so this threwReferenceError: document is not definedand 500'd every SSR render that mounts one of these overlays (e.g. a persistent layout that always renders an auth dialog). NewresolvePortalTarget()(inutils/) guardsdocument: on the server it returnsundefinedandportal()falls back to the envdoc.body(overlays areshow(state.open)-gated, so a closed overlay never mounts its portal server-side anyway). Themenu/popover/select/context-menu/hover-card/tooltipgroup already resolved the host inside theshowarm and were unaffected. - Changed
toastparts.toastnow takes the row'sSignal<Toast>instead of aToastvalue. The value API forced consumers to.peek()theeach-row item signal in a reactive slot, which the signal compiler rejects (peek-in-slot). It reads the value once internally — a toast is immutable for itsid's lifetime (created → dismissed, never mutated), so the keyedeachrebuilds the row ifidchanges. Migration:parts.toast(item.peek())→parts.toast(item).
@llui/effects@0.0.11
- Added
asOnEffect(chain)— adapts ahandleEffects().….else(…)chain (a(ctx: {effect, send, signal}) => void) to the signal-runtimeonEffectshape ((effect, { send }) => cleanup). The signal component'sonEffectno longer receives an ambientAbortSignal, so the adapter owns one component-lifetimeAbortController: every effect dispatches through the chain with itssignal, and the returned cleanup aborts it — in-flight http / debounce / interval / websocket effects tear down on unmount. Usage:onEffect: asOnEffect(handleEffects<E, M>().http(…).else(…)).
2026-05-31 — @llui/dom@0.5.5, @llui/compiler@0.6.4
Released: @llui/dom@0.5.5; @llui/{compiler,compiler-devtools,compiler-introspection,compiler-ssr,vite-plugin,mcp}@0.6.4
Surfaced migrating a real app (a canvas/SVG landscape planner, full of per-row state.someId === row.id conditions and bare-string children) to the signal runtime. The dom-family peers (router/transitions/components/vike/test/agent) were not republished — their ^0.5.0 peer range already accepts @llui/dom@0.5.5.
@llui/dom@0.5.5
- Fixed a
derived([state, item], …)— or any signal value mixing component state and a row item/index — now resolves each input against the correct root inside aneachrow in the authoring path: component-state inputs readctx.state, item/index inputs read the combined row ctx. Previously the item input was handed the wrong root, so a per-row condition likestate.someId === item.id(used as a prop binding or ashow/branchcondition) threwCannot read properties of undefinedor silently failed to react in uncompiled/tested views. (The compiled path already handled this.) - Fixed element children may now be bare strings or numbers —
el/elNScoerce them to text nodes — sodiv(['hi', 42])works without an explicittext(...), matching every mainstream framework. Reactive text still usestext(state.map(…))/text(item.at(…)). Scoped to the client runtime; SSR serialization is unchanged.
@llui/compiler@0.6.4
- Added
prefer-at-over-maplint (compile-time error): a single-field projectionsignal.map((p) => p.field)must be writtensignal.at('field')— a path-narrowed signal that depends only on that field, not the whole source (finer-grained reactivity). Computed bodies (String(s.n), templates, ternaries, method calls), nested access (s.a.b), opaque accessors (state.map(b.foo)), and plainArray.mapare not flagged. Migration: rewrite flaggedsig.map(p => p.x)reads assig.at('x').
@llui/{compiler-devtools,compiler-introspection,compiler-ssr}@0.6.4, @llui/vite-plugin@0.6.4, @llui/mcp@0.6.4
- Improved republished to pick up the
@llui/compiler@0.6.4lint (@llui/mcpalso picks up@llui/dom@0.5.5).
2026-05-30 — @llui/dom@0.5.4, @llui/compiler@0.6.3
Released: @llui/dom@0.5.4; @llui/{compiler,compiler-devtools,compiler-introspection,compiler-ssr,vite-plugin,mcp}@0.6.3
Two gaps surfaced migrating a real app (a personal-health SPA, full of <form> controls and static <option> lists) to the signal runtime. The dom-family peers (router/transitions/components/vike/test/agent) were not republished — their ^0.5.0 peer range already accepts @llui/dom@0.5.4.
@llui/dom@0.5.4
- Fixed form-control "live value" props (
value,checked,selected,indeterminate) are now applied as DOM IDL properties, not content attributes.<textarea>/<select>have novaluecontent attribute, and a control'schecked/selectedattribute is its default (not current state), so the old attribute-only path silently failed to update them — a reactive<textarea value={…}>or<select value={…}>never moved, and a bound checkbox never toggled. Routed via a curated allowlist gated byname in node; SVG and every reflected attribute (disabled,aria-*,data-*, …) stay on the attribute path. (Scoped to the client runtime; SSR serialization is handled by the separate SSR renderer.)
@llui/compiler@0.6.3
- Fixed the
no-node-construction-in-bodylint no longer fires for a plainArray.mapthat builds nodes (e.g. a static<option>list spread into a<select>) — that runs once at build time and is legitimate. Only a signal-rooted.mapis treated as a reactive derive body. DOM built inside a signal.mapbody is still caught (via the full derive-body walk), so the correctness guarantee is unchanged.
@llui/{compiler-devtools,compiler-introspection,compiler-ssr}@0.6.3, @llui/vite-plugin@0.6.3, @llui/mcp@0.6.3
- Improved republished to pick up the
@llui/compiler@0.6.3lint fix (@llui/mcpalso picks up@llui/dom@0.5.4).
2026-05-30 — @llui/dom@0.5.3, @llui/compiler@0.6.2
Released: @llui/dom@0.5.3; @llui/{compiler,compiler-devtools,compiler-introspection,compiler-ssr,vite-plugin,mcp}@0.6.2; @llui/{agent,components,router,test,transitions,vike}@0.5.1
Fixes a class of bugs where a structural condition (show/branch) that reads the row item — or mixes component state and the row item via derived([state, item], …) — was broken inside an each row, in both the compiled and authoring paths. Surfaced migrating a real desktop app (a file-tree sidebar: each → folder/file show → nested inline-rename show), where it would have crashed at runtime.
@llui/dom@0.5.3
- Fixed a
show/branchwhose condition reads the row item (or a mixed state+itemderived) inside aneachrow evaluated the condition againstctx.state(the component state) rather than the row's combined ctx — so an item-rooted or compiler-emitted (ctx-rooted) condition read the wrong root and threw. The condition is now rooted per its deps (mirroring value-spec rebasing): all-row-local deps read the full combined ctx; an enclosing-view component-state handle readsctx.state. - Fixed a row containing a structural child (
show/branch/each) is now treated as reading component state — its arms are built lazily and may read state from inside (e.g. a folder/fileshowwhose file arm nests astate-driven renameshow). Such rows now re-evaluate on the relevant state change and propagate the update down to their arm scopes; previously they were skipped and never reacted. - Fixed a bare
show/branch/eachas a keyedeachrow's top-level node returns aDocumentFragmentthat empties on insertion, corrupting reorder/removal (NotFoundError).eachnow throws a clear authoring error directing you to wrap the conditional body in an element (the row's stable boundary). - Dom-family consumers need no republish for the range bump —
^0.5.0already accepts0.5.3.
@llui/compiler@0.6.2
- Fixed the view transform did not collect a nested structural primitive's condition/discriminant (and arm) component-state deps into the enclosing
each's reconcile deps — so a parenteachnever learned whichstate.*paths its rows read through a nestedshow/branch/each, and rows never re-evaluated when that state changed.show/branch/eachlowering now feed those deps into the row's dependency collector.
@llui/{compiler-devtools,compiler-introspection,compiler-ssr}@0.6.2, @llui/vite-plugin@0.6.2, @llui/mcp@0.6.2
- Improved republished to pick up the
@llui/compiler@0.6.2transform fix (@llui/mcpalso picks up@llui/dom@0.5.3).
@llui/{agent,components,router,test,transitions,vike}@0.5.1
- Improved republished against
@llui/dom@0.5.3(peer range^0.5.0unchanged).
2026-05-30 — @llui/dom@0.5.2
Released: @llui/dom@0.5.2
@llui/dom@0.5.2
- Fixed tearing down a
show/brancharm (on swap or dispose) removed only the arm's own nodes, orphaning the content a nestedshow/branch/eachhad mounted between its own anchors (a sibling, not captured) — so swapping an outer arm left inner content behind in the DOM. Arm teardown now clears everything between the primitive'sstart/endanchors, at any nesting depth (the inner primitive's teardown still runs first, forforeign/onMountcleanups). Dom-family consumers need no republish —^0.5.0already accepts0.5.2.
2026-05-30 — @llui/dom@0.5.1
Released: @llui/dom@0.5.1
@llui/dom@0.5.1
- Fixed a component-state read (
state.map/state.at) inside ashow/brancharm — or a nestedeach/virtualEach— within aneachrow resolved against the combined row ctx ({ item, state, index }) instead of the component state, throwingCannot read properties of undefinedon the first update after mount. Theeachrebased only its row's direct specs; arm and nested-scope specs (built lazily) escaped it. Structural primitives are now row-aware at every depth:show/branchevaluate their condition againstctx.stateand rebase their arm's value reads, whileeach/virtualEachread the component state from the row ctx for their reconcile.virtualEachrows are now rebased at all (they never were). Dom-family consumers need no republish — their@llui/dompeer range (^0.5.0) already accepts0.5.1.
2026-05-30 — @llui/compiler@0.6.1
Released: @llui/{compiler,compiler-devtools,compiler-introspection,compiler-ssr,vite-plugin,mcp}@0.6.1
A follow-up to the signal release, surfaced migrating a real app to the signal API.
@llui/compiler@0.6.1
- Removed the
whole-state-to-calllint rule. It flagged wholestatepassed to any call and false-positived on the canonical top-levelview: ({ state }) => [shell(state)](handing whole state to a view-helper that narrows internally is fine) and on a plainstatereducer param shadowing the view root. The rule was also redundant: rendering a whole-state object is already a type error (text/AttrValueacceptReactive<string | number>), and aSignalcoerced into a template/operator is caught byoperator-on-signal— which, unlike the removed rule, catches the type-invisible cases and isn't circumventable by rewritingfmt(state)asstate.map(fmt). Coarse-but-correct whole-state derivations are bounded by output-equality and are no longer a build error. (Surfaced migrating a real app to the signal API.)
@llui/{compiler-devtools,compiler-introspection,compiler-ssr,vite-plugin,mcp}@0.6.1
- Improved cascade republish so each re-pins the fixed
@llui/compiler(workspace deps resolve to exact versions at pack time).
2026-05-30 — 0.5.0 (the signal runtime) · @llui/compiler@0.6.0
Released: @llui/{dom,components,router,transitions,vike,test,agent}@0.5.0; llui-agent@0.5.0; @llui/{compiler,compiler-devtools,compiler-introspection,compiler-ssr,vite-plugin}@0.6.0; @llui/mcp@0.6.0
The signal runtime ships. LLui's rendering core moves off the legacy mask-binding runtime and the 3-pass compiler onto the signal model: view() runs once at mount, building real DOM nodes with reactive bindings; a chunked-mask reconciler commits only the values that actually change. The legacy runtime and the legacy 3-pass transform are deleted. @llui/dom is the single import surface.
Breaking
@llui/dom@0.5.0— complete runtime rewrite. Components author againststate: Signal<State>(.at(path)/.map(fn)/.peek()); element + structural helpers (div,each,show,branch, …) are module imports, not bag members; there is no virtual DOM. Legacy-runtime code must be rewritten — seedocs/designs/01 Architecture.mdanddocs/designs/13 Migration from v0.0.x.md.@llui/dom— the@llui/dom/signalssubpath is removed. Import everything from@llui/dom.@llui/compiler@0.6.0— the legacy 3-pass transform is gone; the signal transform is the only compilation route. Framework lint rules are non-bypassable compile-time errors (never ESLint).
Migration
- Repoint any
@llui/dom/signalsimports to@llui/dom. - Rewrite components to the signal
view({ state, send })shape; read state viastate.at(...)/.map(...)/.peek()instead of a plain value. - No codemod is shipped (Franco controls all consumers); follow
docs/designs/13 Migration from v0.0.x.md.
@llui/dom@0.5.0
- Added the signal runtime:
component/mountApp,text+ the element-helper set, structuraleach/show/branch,provide/useContext,portal,onMount,foreign,lazy,virtualEach, and signal SSR (renderToString+hydrateSignalApp). - Added
derived([a, b, …], fn)runtime implementation — combine N independent signals into one. It was previously a type-surface-only stub that threw at runtime; the view-helper path (e.g. form-validation'sfield()) now works. - Fixed keyed
eachreconcile is minimal-move (longest-increasing-subsequence): a 2-row swap is 2 DOM moves and a single removal is 0, where the old cursor walk degraded to O(n) (swap/remove had regressed to ~6×/4× slower than peer frameworks; both are now faster than solid/svelte). - Improved pure in-place list updates skip the reorder bookkeeping; rows whose
item/indexare unchanged skipscope.updatewhen the row template reads no component state; contexts are copy-on-write per build; a full clear removes rows in oneRangeop. - Fixed structural primitives tear down mounted content on host dispose;
useContextreturns its default outside a build.
@llui/compiler@0.6.0
- Added the signal view transform (lowers reactive text/element slots and
each/show/branchto runtime helpers), cross-file Msg/State/Effect resolution, and introspection metadata emission. - Fixed
each/show/branchstay verbatim when a render leaks its row or narrowed param into a helper call or event handler — lowering it emitted a free variable that threw at mount. - Fixed block-body views lower correctly, and dynamic children passed to an element helper are no longer dropped.
@llui/vite-plugin@0.6.0
- Added single signal-transform wiring; signal lint rules enforced as build errors; fails loudly when a direct view can't be lowered.
- Fixed the dev HUD is no longer injected into the HTML under Vike — Vike owns the document pipeline and rejected the injected
<script>with a "Wrong Usage" error, blanking every page.
@llui/{components,router,transitions,vike,test,agent}@0.5.0 · llui-agent@0.5.0
- Changed ported to the signal runtime —
connect()/overlay()return Signal-handle part bags, SSR runs through the signal renderer, andpeerDependencies["@llui/dom"]moves to^0.5.0.
@llui/mcp@0.6.0
- Changed ported to the signal debug API, with graceful degradation for the binding-introspection methods the signal runtime omits.
@llui/{compiler-devtools,compiler-introspection,compiler-ssr}@0.6.0
- Changed republished against the signal
@llui/compiler.
2026-05-27 — @llui/compiler@0.5.13
Released: @llui/{compiler,compiler-devtools,compiler-introspection,compiler-ssr}@0.5.13; @llui/vite-plugin@0.5.14; @llui/mcp@0.5.16
After fixing the file-local and cross-file walkers' shadow-blindness in 0.5.12, audited the rest of the compiler for the same bug shape (walker matches identifiers by name against an outer paramName without respecting nested-function parameter bindings that shadow that name). Five more walkers had it; all fixed via the shared shadowsStateParam predicate.
@llui/compiler@0.5.13
- Fixed
transform.ts:computeAccessorMask— the central mask classifier consumed by element-rewrite, structural-mask, each-memo, and text-mask. Pre-fix, a binding accessor(s) => { ... }that contained any nested arrow re-usingsas a parameter mis-attributed the inner reads to the outer state, producing over-conservative FULL_MASK fallbacks or spurious opaque-flow flags. Affects mask precision across every binding emit; perf cliff, not a correctness bug at the runtime layer (the runtime stays correct via the sentinel). - Fixed
modules/opaque-state-flow.ts:findFirstLeakInAccessor— the strict rule (severityerror, fails the build). Same shadow blind spot meant an inner arrow re-usingscould cause a spurious build-failing diagnostic against the outer accessor. Pre-fix users hit this as a hard error; the fix removes that false-positive surface entirely. - Fixed
modules/static-on.ts:readsParam— lint rule classifying staticon:accessors. Pre-fix(s) => (s) => s.xwas treated as reading the outerseven when only the inner is touched, missing the static-on opportunity (over-pessimistic). - Fixed
modules/static-items.ts:readsParam— same shape asstatic-on; identical fix. - Fixed
modules/row-factory.ts:rewriteStmt— code-gen rewriter that substitutes template-variable references withr. Pre-fix, a user-named template (e.g.const tpl = elTemplate(...)) paired with a render-body callback re-usingtplas a parameter name would have the inner reference incorrectly rewritten. Low likelihood in practice (templates are usually__tpl{N}-style auto-generated), but a real code-gen correctness bug under the right user code. - Added
shadowsStateParamexported fromcollect-deps.ts. Single canonical predicate consumed by all eight shadow-aware walkers in the compiler now: the three already in collect-deps + cross-file-walker, plus the five fixed in this release.
@llui/{compiler-devtools,compiler-introspection,compiler-ssr}@0.5.13 · @llui/vite-plugin@0.5.14 · @llui/mcp@0.5.16
- Improved Cascade republish for the bumped
@llui/compilerworkspace dep.
2026-05-27 — @llui/compiler@0.5.12
Released: @llui/{compiler,compiler-devtools,compiler-introspection,compiler-ssr}@0.5.12; @llui/vite-plugin@0.5.13; @llui/mcp@0.5.15
dicerun 2026-05-27 follow-up: the track({ deps: (s) => [...] }) suppression introduced in 0.5.11 worked when the outer accessor was () => { ... } but NOT when it was (s: PS) => { ... }. Root cause was a missing scope check in the file-local and cross-file walkers — both matched by IDENTIFIER NAME against the outer accessor's stateParam without respecting lexical shadowing. A nested track({ deps: (s) => [opts.X(s)] }) whose inner parameter also named s had its inner reads mis-attributed to the outer state.
@llui/compiler@0.5.12
- Fixed
detectOpaqueStateFlow,extractPaths(both incollect-deps.ts), andwalkAccessorBody(incross-file-walker.ts) now skip descent into nested function expressions / arrows / function declarations whose parameter binding shadows the outerstateParam. The shadowing was previously invisible to all three walkers; an inneropts.X(s)flowed up to flag the outer state as if it were the outer'ss. Net effect: thetrackrecipe in cookbook.md no longer requires renaming the inner parameter for correctness — though renaming remains a sensible defensive style for readability. HelpershadowsStateParam(file-local walker) handles identifier params and simple destructured/array patterns; the cross-file walker has the equivalent check inlined.
@llui/{compiler-devtools,compiler-introspection,compiler-ssr}@0.5.12 · @llui/vite-plugin@0.5.13 · @llui/mcp@0.5.15
- Improved Cascade republish for the bumped
@llui/compilerworkspace dep.
2026-05-27 — 0.4.10 / 0.5.11
Released: @llui/{dom,components,router,transitions,vike,test,agent}@0.4.10; llui-agent@0.4.10; @llui/{compiler,compiler-devtools,compiler-introspection,compiler-ssr}@0.5.11; @llui/vite-plugin@0.5.12; @llui/mcp@0.5.14
Two fixes addressing dicerun follow-up reports against 0.4.9 / 0.5.10:
The llui/opaque-accessor-file-wide-mask diagnostic recommended track({ deps: (s) => [...] }) as the escape hatch but the suppression only honoured the stricter llui/opaque-state-flow rule — adding track() didn't silence the warning, it just shifted it to the track() line. The documented escape hatch now actually works.
New h.getState(): S on View<S, M> closes the gap that motivated the "latest-ref" workaround pattern (a sentinel class binding writing state into a module-local ref so event handlers can read it). AppHandle.getState() already existed at the handle layer; the view bag now exposes the same primitive, typed by the component's S. Event handlers and async callbacks read current state with a one-liner; the side-effect-in-accessor pattern goes away.
@llui/compiler@0.5.11
- Fixed
track({ deps })suppressesllui/opaque-accessor-file-wide-mask(both[file-local]and[cross-file]variants), matchingtrack's docstring and the diagnostic's own remediation hint. The suppression now extends to: the file-local opaque flag incollect-deps.ts(driving the perf warning), the cross-file walker's opaque detection incross-file-walker.ts, AND the delegation-following pass that previously flagged unresolvable helpers insidetrack.deps. Directs.Xreads insidetrack.depsstill get extracted as paths — the user's explicit declaration of dependencies. - Added
track-utils.tsexportingisInsideTrackDeps. Extracted frommodules/opaque-state-flow.tsso the three call sites (the strict rule, file-local walker, cross-file walker) share one canonical predicate.
@llui/dom@0.4.10
- Added
h.getState(): SonView<S, M>. Sanctioned escape hatch for "read current state from an event handler / async callback / post-mount adapter code." MirrorsAppHandle.getState(). CallinggetState()inside a reactive accessor is unnecessary — the accessor argument is already the live state; use it via(s) => ….slice()lifts the parent bag'sgetStatethrough the projection so sliced views see their sub-state.
@llui/vite-plugin@0.5.12 · @llui/mcp@0.5.14 · @llui/{compiler-devtools,compiler-introspection,compiler-ssr}@0.5.11 · @llui/{components,router,transitions,test,vike,agent}@0.4.10 · llui-agent@0.4.10
- Improved Cascade republish for the bumped
@llui/compilerworkspace dep and@llui/dompeer (^0.4.9→^0.4.10).
Docs
- Cookbook — "Reading current state from event handlers with
h.getState()" recipe + anti-pattern callout for the latest-ref dance. "track({ deps })— declaring opaque accessor dependencies" recipe now that the suppression works as documented.
2026-05-27 — @llui/compiler@0.5.10, @llui/vite-plugin@0.5.11
Released: @llui/{compiler,compiler-devtools,compiler-introspection,compiler-ssr}@0.5.10; @llui/vite-plugin@0.5.11; @llui/mcp@0.5.13
Diagnostic UX fix in response to a consumer follow-up on 0.4.9: cross-file llui/opaque-accessor-file-wide-mask warnings emitted at line 0 with identical message bodies, so N offending files produced N indistinguishable lines in vite build stdout. And even when loc was set, Vite/Rolldown's build reporter drops it in most configurations — users were left with a bare message and no actionable location.
Two-part fix. The cross-file walker now captures the FIRST focal-file accessor whose body triggers the opacity flip and returns it as opaqueNode. The vite-plugin forwards it to transformLlui, which emits the cross-file diagnostic with the node's precise range instead of falling back to line 0 — IDEs can jump to it, and Rollup's (code, file, line) dedup distinguishes multiple offenders correctly. On the formatter side, the vite-plugin now embeds <relfile>:<line>: directly in the message body so the location survives every reporter, regardless of how it treats loc.
@llui/compiler@0.5.10
- Fixed
crossFileAccessorPathsnow returns{ paths, opaque, opaqueNode? }. The walker captures the focal-file accessor at the visit-level so the offending location lives in the file the user can act on (not in a recursed-into helper file). - Added
transformLluiaccepts an optionalcrossFileOpaqueNode?: ts.Nodeparameter. When provided, the cross-fileopaque-accessor-file-wide-maskdiagnostic emits with the node's range; when omitted (back-compat), it falls back to the file-levelline: 0range.
@llui/vite-plugin@0.5.11
- Improved Diagnostic formatter now embeds
<relfile>:<line>:(or<relfile>:when no line is available) in the message body. The format survives Vite/Rolldown's build reporter droppingloc. Path computed relative toconfig.root; absolute path retained when the file lives outside the project root. - Fixed Forwards
opaqueNodefromcrossFileAccessorPathstotransformLlui. Combined with the compiler change, cross-file warnings now carry actionable line numbers.
@llui/{compiler-devtools,compiler-introspection,compiler-ssr}@0.5.10 · @llui/mcp@0.5.13
- Improved Cascade republish for the bumped
@llui/compilerworkspace dep.
2026-05-26 — 0.4.9 / 0.5.9
Released: @llui/{dom,components,router,transitions,vike,test,agent}@0.4.9; llui-agent@0.4.9; @llui/{compiler,compiler-devtools,compiler-introspection,compiler-ssr}@0.5.9; @llui/vite-plugin@0.5.10; @llui/mcp@0.5.12
Closes an entire bug class — gate-asymmetry between low-word and high-word dirty bits — where reactive accessors driving a structural primitive or binding could silently fail to re-evaluate when their driving state field lived at prefix index ≥ 31. The Phase 1 / Phase 2 gate (mask & dirty) | (maskHi & dirtyHi) evaluated to 0 when mask: FULL_MASK was paired with the default maskHi: 0 and only the high word was dirty. Originally surfaced as a happy-dom-specific h.show() regression in a consumer app; the actual bug reproduces under jsdom and real browsers too — the env was a red herring.
Same shape as the __bindUncertain fix in 0.4.7 (53512ad) but carried in ten more places: the four runtime structural primitives, the two runtime decoders that explicitly forced maskHi: 0, the createBinding and memo defaults, and two inline literal blocks. The compiler likewise emitted only the low-word mask from three modules (structural-mask, each-memo, text-mask) and the catch-all "can't analyze" branches of computeAccessorMask returned an asymmetric default. The fix unifies the contract everywhere: when no precise mask was emitted, BOTH words default to FULL_MASK; when one word is precise, the other defaults to 0.
Two latent diagnostic gaps closed in the same audit. llui/opaque-accessor-file-wide-mask now fires for cross-file opacity (was file-local only), with messages tagged [file-local] / [cross-file] for triage. And the file-local walker now recognizes string-literal-keyed accessors ('data-foo', 'aria-label') identically to identifier-keyed ones — previously their bodies were never analyzed, so any opaque flow inside them was silent and any field uniquely read through them stayed out of __prefixes.
@llui/dom@0.4.9
- Fixed
branch(),each(),show(),scope(),unsafe-html(),virtual-each()structural blocks now defaultmaskHi: FULL_MASKwhenmask: FULL_MASKand no explicitmaskHiwas provided. Pre-fix the Phase 1 gate silently dropped high-word dirty bits for these blocks.show()andscope()now correctly forward__maskHito their underlyingbranch()call. - Fixed
createBinding()derivesmaskHifrommaskwhen omitted —FULL_MASK → FULL_MASK, otherwise0. The asymmetric default was the root footgun for every uncompiled-fallback binding (inelements.ts,svg-elements.ts,mathml-elements.ts,foreign.ts,selector.ts,text.ts) that wrotemask: FULL_MASKwithout an explicitmaskHi. - Fixed
memo()applies the same mask-mirroring default.memo(fn, FULL_MASK)now correctly invalidates on high-word state changes; pre-fix the cache stayed warm forever for those updates. - Fixed
el-splitandel-templateno longer inject an explicitmaskHi: 0when the compiler emits a 4-tuple / 5-arg__bind. Both passundefinedsocreateBinding's default takes over — closes the same bug for the destructured-param accessor catch-all. - Added
BranchOptionsBase.__maskHi: type-level companion to__mask, documenting the high-word contract.
@llui/compiler@0.5.9
- Fixed
computeAccessorMaskreturns{ mask: FULL_MASK, maskHi: FULL_MASK }for every "can't statically analyze" path: zero-arg accessor, destructured first param, missing body, dynamic key access, opaque-state-flow, and the catch-all "reads state but no resolvable path." Pre-fix four of these returnedmaskHi: 0— the source of the gate-asymmetry that leaked downstream into bindings, structural blocks, and memo wrappers. - Fixed
structural-maskmodule passesfieldBitsHito the analyzer and emits__maskHialongside__maskwhen the driver accessor reads a high-word field.each-memoandtext-maskmodules likewise emit a 3-arg form (memo(fn, mask, maskHi),text(accessor, mask, maskHi)) when the analyzed accessor reads high-word fields. - Fixed
isReactiveAccessorincollect-deps.tsnow walks string-literal-keyed accessors ('data-foo','aria-label') identically to identifier-keyed ones. Pre-fix the gate at line 479 requiredts.isIdentifier(key), so paths read uniquely through string-literal-keyed accessors never entered__prefixes(forced sentinel fallback at runtime) and opaque flow inside them was never diagnosed. - Added
llui/opaque-accessor-file-wide-maskdiagnostic now fires forcrossFileOpaquetoo (was file-local only). Messages prefixed with[file-local]or[cross-file]so consumers can identify which walker bailed.
@llui/vite-plugin@0.5.10
- Improved Cascade republish for the bumped
@llui/compilerdep. The plugin's transform-hook routing of compiler diagnostics throughthis.warn(...)/this.error(...)is now end-to-end tested (test/opaque-warning-surface.test.ts) — the consumer-reported "warning doesn't surface invite buildstdout" path is now locked.
@llui/mcp@0.5.12
- Improved Cascade republish for the bumped
@llui/dompeer and@llui/compiler/@llui/vite-pluginworkspace deps.
@llui/{components,router,transitions,test,vike,agent}@0.4.9 · @llui/{compiler-devtools,compiler-introspection,compiler-ssr}@0.5.9 · llui-agent@0.4.9
- Improved Cascade-only bumps. Republished to pick up the bumped
@llui/dompeer (^0.4.8→^0.4.9) or the bumped@llui/compiler/@llui/agentworkspace:*dep.
2026-05-26 — 0.4.8 / 0.5.8
Released: @llui/{dom,components,router,transitions,vike,test,agent}@0.4.8; llui-agent@0.4.8; @llui/{compiler,compiler-devtools,compiler-introspection,compiler-ssr}@0.5.8; @llui/vite-plugin@0.5.9; @llui/mcp@0.5.11; @llui/devmode-annotate@0.0.3
Headline change is @llui/devmode-annotate: the in-app HUD now auto-mounts via @llui/vite-plugin in dev (no per-app wiring), routes "Solve" through a configurable LLM CLI with streaming progress + per-chain --resume continuity, ships a notes browser with screenshot/diff/timeline rows, and pivots to a direct-edit architecture — the LLM writes the file changes itself; the HUD just records the resulting git diff for the Accept/Reject toast. Six pre-listed limitations were closed in the same cycle: annotation-placeholder cleanup (rect+element only), MCP writes routed through middleware for format consistency, per-chain serialization for safe concurrency > 1, repro recorder with ▶ Replay support, and .chain-state.json persistence so chains survive dev-server restarts.
Compiler/runtime side: a latent isReactiveAccessor misclassification was forcing whole-file FULL_MASK fallback on virtually every file with object-shorthand property assignments; the fix restores precise per-case masks across the codebase. injectViewBag now correctly emits __view for shorthand and identifier-ref view forms (was silently producing components that crashed at mount with "missing __view despite being compiled"), with a build-time integrity check that fails closed on future regressions.
@llui/devmode-annotate@0.0.3
- Added Auto-mount via
@llui/vite-pluginin dev. Consumers do not add@llui/devmode-annotateto their own dependencies and do not callmountAnnotateHud(); the plugin injects a virtual module throughtransformIndexHtml. Production builds tree-shake the HUD entirely. - Added Configurable LLM router with
claude/codex/geminipresets plus a fully custom invocation.router: falsedisables both the router and the HUD's Solve button.claudedefaults to--model sonnetand streams progress via--output-format stream-json, surfacing live token counters (14k ctx (13.8k cached) · 380 out) with a defensive 1s local ticker so the status display never looks frozen. - Added Per-chain
--resumecontinuity. The router maintainssessionByChain: Map<chainName, sessionId>, persisted to<notesRoot>/.chain-state.jsonso chains survive dev-server restart.drain()serializes per-chain via anactiveChainsset:concurrency > 1now parallelizes across distinct chains but never races within one chain. - Added Direct-edit architecture. Replaces the previous patch-apply flow. The router captures a git baseline before spawn, prompts the LLM to edit files directly with full write access, then computes
git diff HEAD -- <files>as theproposedDiffshown in the reply note. Accept = no-op (already on disk); Reject =git checkout HEAD -- <files>(+rmfor new files). Eliminates the entire "corrupt patch at line N" failure class that bedeviled earlier iterations. - Added Notes browser view (toggled from the heading row): session dropdown, filter row (kind/author/status/search), bulk actions, expandable rows showing prose + screenshot lightbox + diff viewer + status timeline + Edit/Delete/Re-solve. Live-updates via the existing SSE feed — no refresh button.
- Added Element-picker tool (
⌖ Add region/⌖ Pick element): hover-highlight DOM picker with separate dim/outline/label elements layered below the modal so the modal stays clickable. Captures{ selector, bbox }into the note'sannotations[]. - Added Repro recorder:
● Recordtoggle in the compose view capturesclick/input/keydown/popstateevents intobody.repro(ring-buffered at 200 events; password fields and[data-llui-private]ancestors are skipped).replayReproEvents()dispatches the captured events back to the live DOM; browse rows show▶ Replay (N)on notes that carry areproarray. - Added Auto-capture on uncaught error: optional
autoCaptureOnError: true(default) installswindow.onerror+unhandledrejectionlisteners that open the HUD with a pre-filled error capture, so the user can ship the note without losing context. - Added HUD state persistence: open/closed state, in-flight tracked tasks, chain histories, and Accept-toast queue are all persisted to
localStorage(200ms debounced) and rehydrated on mount. Reloading the page mid-solve no longer loses the dialog state or the proposed-state toasts. - Added Reply notes from the router include a
proposedDiffplus a▾ Resume previous / ○ Start freshsplit-button menu populated with LLM-summary labels per chain. First-task chains hide the caret entirely (nothing to resume yet). - Added Reject button on proposed-state toasts (was Accept-only). Toasts now ship a
ToastAction[]array withvariant: 'primary' | 'secondary' | 'ghost'; the proposed-state toast carries[Reject, Accept]. - Added Configurable notes location and format:
notesDir,format.formatSessionFolder,format.deriveSlug. MCP-side writes (@llui/mcp'screateNotetool) now route throughPOST /_llui/notesso they honour the same format overrides as HUD writes. - Improved HUD UX overhaul: text wordmark replacing the lasso icon, dark mode via CSS custom properties scoped to both
#llui-devmode-annotate-rootand#llui-devmode-annotate-toasts(so document-body-mounted toasts inherit the theme), context subhead replacing the unhelpful "New note" title, inline⌖ Add regionpill, markdown toolbar, More-options expander, footer keyboard hints. - Improved Screenshot capture is more robust:
html-to-imageruns withimagePlaceholder(1×1 transparent PNG) +onImageErrorHandlerso a single broken<img>no longer rejects the whole capture;describeCaptureError(err)extractstarget.src/target.tagNamefrom Event-shaped errors instead of displaying[object Event]. - Improved Error toasts are sticky — failures don't auto-close. Failure reasons surface concretely (the spawn stderr, the git checkout error) rather than the previous generic "❌ failed".
- Improved Floating button edge-anchored position now tracks window resize: a button in the right or bottom half follows its edge instead of clipping off-screen.
- Removed Annotation placeholders (
lasso,pin,arrow,highlight).Annotationis nowrect | elementonly;NoteKindisrect | element | text | capture | reply. The placeholders added LOC without measurable value for LLM consumption.
@llui/compiler@0.5.8
- Fixed
injectViewBagonly matchedview:when its initializer was an arrow or function expression. Shorthandview,and identifier-refview: viewFnsilently fell through with no__viewemitted, whilecompilerStampModulestill stamped__compilerVersionon the same call. The runtime then threw"missing __view despite being compiled"at mount. Detect the view property in all three forms; for non-inspectable initializers, emit__view: ($send) => createView($send). - Fixed
isReactiveAccessorwas misclassifyingPropertyAssignmentkey Identifiers (e.g. thetitleindiv({ title: arrow })) as reactive accessors, silently flippinghasOpaqueAccessor = trueand forcing the whole-file FULL_MASK fallback for almost every file in the codebase. Skip property-assignment keys. Restores precise dirty masks repo-wide. - Fixed
item-dedupnow skipscurrentfield accesses; loweringitem.currenttoacc(r => r.current)was producingentry.current.current→ undefined for any row without a literalcurrentfield. The runtimeProxyalready handles.currentcorrectly. - Fixed
no-repeated-item-currentlint escalated to warn on any chaineditem.current().Xaccess, not just two-or-more. A single chained access already opaques the bitmask analyzer (FULL_MASK fallback); the 2+ reconcile-race risk is additive. - Added
llui/opaque-accessor-file-wide-maskwarning. Names the accessor + line that flippedhasOpaqueAccessor=trueand forced the whole-file FULL_MASK fallback. Catches the method-call-with-state shape (host.fn(s, …)) that the existing strictllui/opaque-state-flowrule deliberately tolerates.
@llui/vite-plugin@0.5.9
- Added Auto-injects the devmode-annotate HUD in dev via a virtual ES module (
virtual:llui-devmode-annotate-init) resolved throughimport.meta.resolve()against the plugin's own location, so consumers don't need to install@llui/devmode-annotatethemselves. - Added
devmodeAnnotateplugin option (false | DevmodeAnnotateConfig). Default = on in dev. SupportsnotesDir,captureTimeoutMs,format,router,hud. The plugin computessolveEnabledand threads it into the HUD bootstrap so the Solve button only renders when a router CLI is actually onPATH. - Added Notes router (
packages/vite-plugin/src/notes/): CLI spawner with preset table, stream-json parser, git baseline capture / diff computation, per-chain serialization,.chain-state.jsonpersistence, and asafeAppendStatuswrapper that tolerates ENOENT during test teardown. - Added Notes middleware HTTP endpoints (
/_llui/notes,/_llui/sessions,/_llui/eventsSSE,/_llui/capture-request, etc.) withrevertProposedChangesfor the Reject path.note-createdis broadcast from router-side replies too. - Added Build-time integrity check: every
__lluiCompilerEmittedoccurrence in the bundle must be paired with a__view:marker. Pre-fix, shorthandview,and identifier-refview: fngot stamped without a__viewfactory and crashed at mount. The check fails closed so any future regression of the same shape is caught before reaching users.
@llui/dom@0.4.8
- Fixed
__bindUncertainnow setsmaskHi: FULL_MASKso its documented "fire on any state change" fallback actually fires for components with ≥32 reactive prefixes. The gate is(mask & dirty) | (maskHi & dirtyHi);maskHiwas defaulting to0, silently dropping every high-word change for opaque-bound primitives in wide-state components. - Improved Dev-only serializability check is now extracted into a shared
findNonSerializablehelper, runs once-per-instance after each reducer update, and the warning concretely names the impact (HMR / devtools / SSR / replay) with mount vs. update phase distinguished.
@llui/mcp@0.5.11
- Fixed
llui_list_sessionstool andllui://sessionsresource now projectSessionListEntry[]back tostring[]at the MCP boundary.listSessionswas changed to return the richer shape for the HUD browse view; the MCP surface still advertises the documented{ sessions: string[] }contract, so the projection happens at the wrapper. - Added
createNoteViaServerOrDirecthelper: whenctx.devServerUrlis set, MCP-sidecreateNoterequests route throughPOST /_llui/notesso they honour the dev server'sformatoverrides. Falls back to direct on-disk write when offline. Both paths produce the same filename layout now. - Fixed Playwright e2e harness: probe each candidate bridge port with a synchronous
net.createServertest before handing it toLluiMcpServer(avoids EADDRINUSE races from the previous random-port-in-range strategy). Switchedpage.gototowaitUntil: 'load'so the auto-injected HUD's long-lived/_llui/eventsSSE no longer preventsnetworkidlefrom resolving.
@llui/{components,router,transitions,test,vike,agent}@0.4.8 · @llui/{compiler-devtools,compiler-introspection,compiler-ssr}@0.5.8 · llui-agent@0.4.8
- Improved Cascade-only bumps. Republished to pick up the bumped
@llui/dompeer (^0.4.7→^0.4.8) or the bumped@llui/compiler/@llui/agentworkspace:*dep, whichpnpm publishrewrites to the concrete version at pack time. No source-level changes in these packages this release.
Docs
- Added
docs/02 Compiler.mdcross-file resolution table — which identifier/export shapes the analyzer follows versus treats as opaque. - Updated
docs/proposals/devmode-annotate/current-state.mdis now the authoritative contract for the HUD surface. Old proposal docs (01…05) remain for design rationale; treat anything they say as superseded by what's incurrent-state.md.
2026-05-26 — 0.4.7 / 0.5.7
Released: @llui/{dom,components,router,transitions,vike,test,agent}@0.4.7; llui-agent@0.4.7; @llui/{compiler,compiler-devtools,compiler-introspection,compiler-ssr}@0.5.7; @llui/vite-plugin@0.5.8; @llui/mcp@0.5.10
Layered dirty-mask precision: the compiler now emits leaf-path-precise dirty masks for nested object literals, and the runtime recovers leaf-path precision at commit time for opaque patterns the compiler can't analyze statically. Headline impact on high-frequency partial updates (ticker bench, median-of-medians across 3 headless Chrome runs):
| Op | Before | After | Δ |
|---|---|---|---|
narrow-100 |
2.1ms | 1.6ms | -24% |
burst-1k |
18.4ms | 15.0ms | -19% |
tick-100 |
5.5ms | 5.1ms | -7% |
wide-toggle |
3.3ms | 3.4ms | noise |
LLui now beats Solid on burst-1k by 32% and on narrow-100 by 6%; React by 3.3× on narrow-100 and 3.6× on burst-1k. The compiler change is opt-in only in that it activates whenever the case body is a nested { ...state, foo: { ...state.foo, bar: 1 } } literal — older patterns continue to use the conservative top-level mask plus the runtime walk. Both layers are sound — no behavior change for code that already worked.
@llui/dom@0.4.7
- Improved
_handleMsgnow walks the compiler-emitted__prefixesarray post-update()to recover leaf-path-precise dirty bits when the case mask covers more than 4 paths (popcount-gated, since narrow cases gain nothing from the walk). Recovers ~95% of wasted Phase 2 binding fires on patterns where the compiler had to fall back to top-level mask precision —{ ...state, dashboard: { ...state.dashboard, ...patch } }with an opaque inner spread is the prototypical case. Implemented as a pure runtime change: no compiler edits, no API surface. Bounded overhead: ≤62 closure invocations + identity compares per commit when the gate fires; zero when it doesn't. - Improved Per-instance prefix-value memoization halves accessor invocations under steady-state commit flow. Each prefix is normally called twice per commit (once on
prev, once onnext); caching thenextvalues onComponentInstance._prefixCachemeans the following commit reuses them as itsprevbaseline. On accessor throw the cache is invalidated via try-finally so correctness is preserved if a prefix is non-pure._forceStatealso invalidates. Drove a further -11% onnarrow-100past the walk alone. - Improved
each()andbranch()no longer callreattachDriftedEntrieson their first reconcile —lastParent === nullat that point, so no drift can possibly have occurred. The Pattern-4 self-heal from 0.4.5 (commit5a1b8fd) still fires correctly on subsequent reconciles after an ancestor arm-swap. Saves two forward+backward entry scans on everyeach()block's mount; bounded gain (~20ns per first-reconcile × each-block count) but eliminates a verifiably wasted call on a known hot path. - Improved Bench harness
benchmarks/run-jfb.tsnow deduplicates--frameworkargs sopnpm bench --framework lluidoesn't queuekeyed/lluitwice. Harness-only; not part of the published runtime.
@llui/compiler@0.5.7
- Improved
analyzeModifiedFieldsnow descends into nested object literals when a matching...state.<path>spread preserves unwritten siblings. A case body{ ...state, foo: { ...state.foo, bar: 1 } }now emits a dirty mask containing onlyfoo.bar, instead of collapsing tofoo.*.tryBuildHandlerslooks up bits at leaf granularity via bidirectional prefix-overlap (W === P || P startsWith W. || W startsWith P.), so a binding reading the parent observes the leaf write through prefix-reactivity. Concrete impact on the ticker bench:wide-togglecase mask 2147483647 → 402653184 (onlydisplayMode+tickCountbits set),churn2147483647 → 268435456. Patterns that still bail to top-level (correct, by design): opaque inner spreads ({ ...state.foo, ...patch }), computed keys ({ ...state.values, [msg.field]: msg.value }). 8 new compiler tests cover the new behavior. - Fixed Tightened
analyzeNestedField'skeyNamebinding fromstring | nulltostring— every branch that doesn'treturn nullassigns it before use; the nullable annotation was a leftover. No runtime change.
@llui/{components,router,transitions,vike,test,agent}@0.4.7 / llui-agent@0.4.7 / @llui/mcp@0.5.10 / @llui/{compiler-devtools,compiler-introspection,compiler-ssr}@0.5.7 / @llui/vite-plugin@0.5.8
- Improved Cascade republish — peer
@llui/dompinned to^0.4.7;workspace:*deps on@llui/compilerand@llui/agentresolve to the new versions at pack time. No source changes.
2026-05-25 — 0.4.6 / 0.5.9
Released: @llui/{dom,components,router,transitions,vike,test,agent}@0.4.6; llui-agent@0.4.6; @llui/mcp@0.5.9
Same-day follow-up to 0.4.5: the __view fallback DEV-gate from the 2026-05-24 perf size-cut (commit 7ecd83e) broke downstream consumers running vitest. Vitest doesn't transform node_modules by default, so the published @llui/dom dist evaluated import.meta.env?.DEV as undefined at runtime and threw on every mountApp call with a hand-rolled ComponentDef. Surfaced when dicerun2 bumped to 0.4.5 and saw 66 unit-test failures (every testView(Component, …) call). The fix re-gates the fallback on __compilerVersion semantics instead of DEV mode. All packages republish to pick up the new @llui/dom peer range.
@llui/dom@0.4.6
- Fixed
getInstanceViewBagno longer throws "missing__view— recompile with @llui/vite-plugin" for hand-rolledComponentDefliterals running outside Vite's transform pipeline (vitest fixtures, esbuild-bundled e2e harnesses, ad-hoc node scripts). The DEV-gate from 0.4.5 was the wrong signal — vitest doesn't transformnode_modules, soimport.meta.env?.DEVevaluated asundefinedand the throw fired even when the def was clearly hand-rolled. The new gate uses__compilerVersion: if a real version is present and__viewis missing, the build pipeline is genuinely broken and a focused error fires; if__compilerVersionis undefined or__test__, fall back tocreateView. Bundle impact: regains ~400–800 bytes ofcreateViewand the structural primitives it imports (show,branch,scope,memo,unsafeHtml,useContext,clientOnly); apps that don't reference those primitives via their compiled__viewstill tree-shake the unused ones. Verified by re-running dicerun2'spnpm testagainst the rebuilt dist — 900/900 web tests pass.
@llui/{components,router,transitions,vike,test,agent}@0.4.6 / llui-agent@0.4.6 / @llui/mcp@0.5.9
- Improved Cascade republish — peer
@llui/dompinned to^0.4.6. No source changes.
2026-05-25 — 0.4.5 / 0.5.8
Released: @llui/{dom,components,router,transitions,vike,test,agent}@0.4.5; llui-agent@0.4.5; @llui/mcp@0.5.8
@llui/dom ships a correctness fix for "Pattern-4 stale Node[] capture" — a class of bug where an each() / branch() constructed at an outer view site and threaded through a helper view (per the documented composition pattern) silently lost track of its current entries when an ancestor show() / branch() rebuilt its wrapper element. Reported on dicerun2's /studio Roller tab: clicking Roll a second time left the result-hero containing only the boundary comments. The remaining tier-1 packages republish to pick up the new @llui/dom peer range. @llui/agent ships a small Cloudflare Workers compatibility fix.
@llui/dom@0.4.5
- Fixed
each()/branch()now self-heal when an ancestor structural primitive (show()/ outerbranch()) rebuilds its wrapper element from a stale user-passed Node[]. Pre-fix: when a parent view constructedeach()at its view site and threaded the returned Node[] through a helper that placed the array inside ashow()'s arm builder (the documented Pattern 4), only the boundary comments (<!--each-->/<!--each-end-->) moved into the new wrapper after a show false→true swap — the entries built by reconciles between outer-view time and now stayed orphaned in the old detached wrapper. Surfaced as dicerun2's "second Roll click shows an empty result hero" symptom. The fix: everyeach()/branch()tracks itsanchor.parentNodeacross reconciles; when an arm-swap fires the runtime drains a one-shot post-Phase-1 fixup pass that calls a newrebindParent(state)hook on each structural block. Drifted entries are re-attached as a contiguousRange.extractContents()move (which carries along nested-primitive content too). Regression coverage:each-rekey-inside-show-loses-dom.test.ts,branch-pattern4-stale-capture.test.ts. - Improved Removed an unused
eslint-disable-next-line no-consoledirective indev-trace.ts— nono-consolerule was ever configured ineslint.config.ts, so the suppression was lint noise.
@llui/agent@0.4.5
- Fixed
server/lap/narrate.tsno longer importsrandomUUIDfrom the legacy unprefixed'crypto'module. That import is Node-only and would break the@llui/agent/server/cloudflareentry on Workers. Now uses the globalcrypto.randomUUID(), matching the three other server files (mcp/router.ts,http/mint.ts,ws/rpc.ts).
@llui/{components,router,transitions,vike,test}@0.4.5 / llui-agent@0.4.5 / @llui/mcp@0.5.8
- Improved Cascade republish — peer
@llui/dompinned to^0.4.5. No source changes.
2026-05-24 — 0.4.4 / 0.5.7
Released: @llui/{dom,components,router,transitions,vike,test,agent}@0.4.4; llui-agent@0.4.4; @llui/{vite-plugin,mcp}@0.5.7; @llui/devmode-annotate@0.0.2
@llui/dom ships a real correctness fix for a cross-instance render-context leak that silently froze nested-each bindings in apps using subApp. The remaining tier-1 packages republish to pick up the new @llui/dom peer range. @llui/devmode-annotate lands the full proposal: notes-on-disk, lasso/HUD overlay, MCP capture surface, and the auto-Solve attention router — wired through @llui/vite-plugin's new notes middleware and @llui/mcp's notes resources/tools.
@llui/dom@0.4.4
- Fixed Nested structural primitives (
each,virtualEach,branch,lazy,unsafeHtml) now snapshot the live render context at construction via the newcaptureRenderContext(). Pre-fix, primitives nested inside an outereach.rendercaptured the sharedbuildCtxsingleton; an intervening sub-app'sbuildEntrywould reassign itsstructuralBlocks/allBindingsto the sub-app's arrays, and the nested primitive's later reconciles would register their new blocks/bindings into the wrong component instance — silently freezing the owning component's view until a re-key happened in a window where the singleton coincidentally pointed at the right instance. Surfaced as a slider/number-input pair where state updated correctly but the bound DOM stayed stale; same class would have hit anysubAppconsumer with nested control flow. Regression coverage:nested-each-cross-instance-blocks.test.ts,nested-branch-cross-instance.test.ts,nested-each-trailing-binding.test.ts. - Added
autocapitalizeonBaseAttributesandautocorrectonInputAttributes/TextareaAttributes. Both are real HTML attributes regularly needed on free-form text inputs whose content shouldn't be munged by the mobile keyboard (dice expressions, code-shaped identifiers). Workarounds in consumers that calledsetAttributefrom anonMountcan revert. - Added Dev-only
window.__lluiTracering buffer with__lluiTraceDump()/__lluiTraceClear()/__lluiTraceEnable()globals. Captures every dispatch (msgType, dirty mask, block list) and every structural-block reconcile (block id, mask, items length before/after, key set diff). Production builds dead-code the entire module viaimport.meta.env?.DEV. The instrumentation is what found the singleton-leak bug above — keeping it in tree so the next investigation has the same lever.
@llui/{components,router,transitions,vike,test,agent}@0.4.4 / llui-agent@0.4.4
- Improved Cascade republish — peer
@llui/dompinned to^0.4.4. No source changes.
@llui/vite-plugin@0.5.7
- Added Notes middleware: dev-server routes for reading/writing/listing on-disk notes under
<example>/.llui/notes/, plus a session-aware capture registry and event bus. Wires@llui/devmode-annotate's on-disk format into the running dev server. - Improved Cascade republish —
workspace:*dependency on@llui/dompinned to the new version via@llui/devmode-annotate's consumption.
@llui/mcp@0.5.7
- Added Notes MCP surface:
notes/*resources (list/read/by-status) plus tools (llui_note_propose,llui_note_resolve,llui_note_skip,llui_capture).llui_capturehas a Playwright fallback when the dev-server can't reach the page directly. Pairs with@llui/devmode-annotate's solver loop. - Improved Cascade republish — peer
@llui/dompinned to^0.4.4.
@llui/devmode-annotate@0.0.2
- Added Full implementation of the proposal (P1–P6): lasso/rect overlay HUD, draggable/anchored modal, screenshot bake, frontmatter-tagged on-disk notes per session under
<repo>/examples/<x>/.llui/notes/session-<id>/, debug-collector that pulls live component telemetry into note bodies, attention router that auto-routes "Solve" actions to a headless Claude Code process and streams status back into the HUD, queue counter and per-task toasts, parseable stdout reply block for the orchestrator to consume. - Improved Defaults to ON in dev mode (opt out with
devmodeAnnotate({ enabled: false })) so example apps light up the HUD without per-example config.
2026-05-23 — 0.4.3 / 0.5.6
Released: @llui/{dom,components,router,transitions,vike,test,agent}@0.4.3; llui-agent@0.4.3; @llui/{compiler,vite-plugin,compiler-devtools,compiler-introspection,compiler-ssr,mcp}@0.5.6
Tiny follow-up to 0.5.5: llui/opaque-state-flow's diagnostic hint and track()'s JSDoc now reference the composition-patterns guide via an absolute GitHub URL so the link is clickable from an IDE error overlay. The relative path shipped in 0.5.5 only worked when the reader had the LLui repo checked out — a real consumer-side UX gap. No behavior change.
@llui/compiler@0.5.6
- Improved
llui/opaque-state-flow's function-parameter-callee hint links tohttps://github.com/fponticelli/llui/blob/main/docs/composition-patterns.mdinstead of the relativedocs/composition-patterns.md. Consumers reading the diagnostic in their IDE can now click through to the four-pattern catalogue without needing the LLui repo on disk.
@llui/dom@0.4.3
- Improved
track()JSDoc carries the absolute GitHub URL for the composition-patterns guide, matching the diagnostic hint. The JSDoc ships in the published.d.ts, so editor tooltips show the same clickable link.
@llui/{vite-plugin,compiler-devtools,compiler-introspection,compiler-ssr,mcp}@0.5.6
- Improved Cascade republish —
workspace:*dependency on@llui/compilerpinned to the new version.@llui/mcpalso picks up the new@llui/dompeer range. No source changes.
@llui/{components,router,transitions,vike,test,agent}@0.4.3 / llui-agent@0.4.3
- Improved Cascade republish —
peerDependencies["@llui/dom"]pinned to the new version. No source changes.
Docs
- Added The composition-patterns guide is now exposed on the documentation site at
/composition-patterns. New nav entry between Cookbook and Architecture. Thesite/content/composition-patterns.mdis symlinked to the canonicaldocs/composition-patterns.mdso there's a single source of truth (mirrors the CHANGELOG.md ↔ site/content/changelog.md pattern).
2026-05-22 — 0.4.2 / 0.5.5
Released: @llui/{dom,components,router,transitions,vike,test,agent}@0.4.2; llui-agent@0.4.2; @llui/{compiler,vite-plugin,compiler-devtools,compiler-introspection,compiler-ssr,mcp}@0.5.5
Documentation + diagnostic-hint follow-up to 0.5.4. The new docs/composition-patterns.md is the spec for the four migration shapes the llui/opaque-state-flow rule's recommended remediations refer to; the rule's hint string and track()'s docstring both now link to it. No runtime behavior change in 0.5.5 — pure docs / diagnostic-text update so consumers on 0.5.4 see the improved hint without checking the GitHub docs.
@llui/compiler@0.5.5
- Improved
llui/opaque-state-flow's function-parameter-callee hint now points atdocs/composition-patterns.mdand explicitly names the non-iterating patterns (accessor passthrough, pre-built Nodes, Node[] slots). Previously the hint only named the items-bag answer, leaving non-iterating cases without a pointer — a real consumer feedback point after the 0.5.4 convergence test.
@llui/dom@0.4.2
- Improved
track()JSDoc explicitly calls out that the primitive is NOT a workaround for function-parameter callback diagnostics. The opaque-flow suppression insidetrack.deps(0.5.4) makes the escape hatch usable, but the perf trade-off is unchanged: opaque deps collapse to FULL_MASK + sentinel. Links todocs/composition-patterns.mdfor the canonical migration shapes.
@llui/{vite-plugin,compiler-devtools,compiler-introspection,compiler-ssr,mcp}@0.5.5
- Improved Cascade republish —
workspace:*dependency on@llui/compilerpinned to the new version.@llui/mcpalso picks up the new@llui/dompeer range. No source changes.
@llui/{components,router,transitions,vike,test,agent}@0.4.2 / llui-agent@0.4.2
- Improved Cascade republish —
peerDependencies["@llui/dom"]pinned to the new version. No source changes.
Docs
- Added
docs/composition-patterns.md— the four-pattern catalogue for generic UI helpers, with worked before/after examples for each shape. Pattern 4 (items-bag lift) primary; Patterns 1–3 (accessor passthrough, pre-built Nodes, Node[] slots) for non-iterating cases. Anti-pattern callout for function-parameter callbacks. Bitmask-budget mitigations under items-bag lift. TheparamControlsViewreference-case section is a placeholder pending a real consumer's worked diffs.
2026-05-22 — 0.4.1 / 0.5.4
Released: @llui/{dom,components,router,transitions,vike,test,agent}@0.4.1; llui-agent@0.4.1; @llui/{compiler,vite-plugin,compiler-devtools,compiler-introspection,compiler-ssr,mcp}@0.5.4
Follow-up to 0.5.3 + 0.4.0 unblocking a real consumer's migration. Six distinct false-positive shapes in llui/opaque-state-flow and llui/map-on-state-array, plus a type-level bug in ItemAccessor<T> for primitive T and a docstring honesty pass on track().
@llui/dom@0.4.1
- Fixed
ItemAccessor<T>exposescurrent()and the call signature<R>(selector) => () => Rcleanly whenTis a primitive (string,number, …). Previously the field-map branch[K in keyof T]-?: () => T[K]expandedkeyof stringover every intrinsic string method (toString,charAt,slice, …), structurally colliding withcurrent()so it was unreachable onItemAccessor<string>. Gated the field-map onT extends object. Consumers using the documentedprovider.current()escape hatch no longer need anas unknown as { current(): T }cast. - Improved
track()docstring is honest about opaque deps:track({deps: (s) => [getError(s)]})wheregetErroris itself opaque collapses to FULL_MASK + sentinel — same behavior as not usingtrack. The primitive is a localized escape for statically-pinnable reads, not a universal fix for callback-parameter composition patterns. The underlying pattern (helpers taking(s) => …callbacks) is the real lift.
@llui/compiler@0.5.4
- Fixed
isReactiveAccessorno longer defaults totruefor bare-Identifier callees at arg[0]. Previously every user mutator with an arrow argument (change((c) => …),dispatch((s) => …),setTimeout(fn, ms), subscribe callbacks, …) was misclassified as a reactive accessor, with two downstream consequences: the path collector polluted__prefixeswith the closure's parameter shape, andllui/opaque-state-flowwalked the body and flagged perfectly legitimate updater patterns. Narrowed to an explicit allow-list of@llui/domprimitives that take a reactive arrow at arg[0]:text,memo,unsafeHtml,selector. - Fixed Destructure-rename alias resolution.
view: ({text: t}) => [t(s => …)]correctly resolvest→textvia the parameter's binding pattern, so the narrowed predicate still recognizes the View-bag destructure idiom. Symmetric in the PropertyAccessExpression branch (h.text(arrow)/h.memo(arrow)/h.unsafeHtml(arrow)/h.selector(arrow)). - Fixed Const-rebinding alias resolution.
const t = text; t((s) => …)follows the const-initializer identifier chain (visited-set guarded against cycles), so the rebinding is recognized as the primitive. Local shadowing is detected: afunction text(x) {…}declaration OR a non-Identifier-initializerconst text = …in scope correctly STOPS the resolver — the local binding wins over a primitive of the same name. - Fixed
llui/map-on-state-arrayno longer fires inside an enclosingeach({items: (s) => s.foo.map(…)})accessor. The diagnostic told the author to useeach— which they were already inside. Suppress in both the bare form (each(…)) and the View-bag form (h.each(…)). - Fixed
llui/opaque-state-flowis suppressed insidetrack({deps}). The primitive is the user's explicit declaration of "trust my declaration"; firing a perf lint inside it moved the diagnostic from the call site to insidetrack, leaving authors with no recovery path. The mask/path classifier still extracts what it can; only the lint is silenced. - Improved
llui/opaque-state-flowhint variant for function-parameter callees (existing 0.5.3 behavior preserved): when the unresolvable callee is itself a function parameter (the helper-takes-(s) => …-callback composition pattern), the diagnostic redirects authors at theeachitems-bag rewrite rather than the generic "inline or refactor" advice — interprocedural narrowing isn't going to save closure-callback composition. - Added 10+ regression tests covering: the 4 new opaque-flow shapes (state-updater, dispatch, ItemAccessor identity-projection, shorthand callback param), const-alias resolution, local-shadowing detection, selector primitive, View-bag destructure, track.deps suppression, and 2
map-on-state-arraycases.
@llui/vite-plugin@0.5.4
- Improved Cascade republish —
workspace:*dependency on@llui/compilerpinned to the new version. No source changes beyond the version bump.
@llui/{compiler-devtools,compiler-introspection,compiler-ssr,mcp}@0.5.4
- Improved Cascade republish —
workspace:*dependency on@llui/compilerpinned to the new version.@llui/mcpalso picks up the new@llui/dompeer range.
@llui/{components,router,transitions,vike,test,agent}@0.4.1 / llui-agent@0.4.1
- Improved Cascade republish —
peerDependencies["@llui/dom"]pinned to the new version. No source changes.
Known migration friction
A few things worth knowing if you're bumping from 0.5.3 (or earlier):
- Vite optimized-deps cache wedge. Every plugin bump invalidates Vite's
node_modules/.vite/deps_*cache asymmetrically — dev may throwfile does not exist in .vite/depsafter the upgrade. Workaround:rm -rf node_modules/.viteafterpnpm install. Unfixable from our side (it's Vite's cache invalidation); the call-out exists so you don't lose 20 minutes diagnosing it. - Non-iterating helper composition. The
llui/opaque-state-flowdiagnostic's "useeachitems-bag" hint is the right answer when the helper iterates. For non-iterating helpers (single-value renderers, form rows, layout chrome), the answer is: pass reactive accessors as direct function references ({value: props.value}, not{value: (s) => props.value(s)}— though that form was already tracked since 0.5.3's method-callee fix and isn't flagged either). The full pattern catalogue with worked examples lives indocs/composition-patterns.md— four shapes (items-bag lift, accessor passthrough, pre-built Nodes, Node[] slots) covering both iterating and non-iterating cases. If you're hittingfunction-parameter calleediagnostics on a non-iterating helper, the migration is to remove thegetX: (s) => Xcallback parameter and accept the value or aNodeat the call site instead. track({deps})does not silence opaque flow in its own body. If you triedtrack({deps: (s) => [opaque(s)]})on 0.5.3 as a workaround for a flagged accessor and saw the diagnostic move intotrackwithout going away — that was correct behavior on 0.5.3 and is now suppressed in 0.5.4. But the suppression doesn't meantrackdoes anything useful in that case: an opaque deps body collapses to FULL_MASK + sentinel just like no track at all. The primitive only helps when the deps body itself is statically extractable (literal property-access chains). Seetrack.ts's updated docstring.
2026-05-22 — 0.5.3
Released: @llui/{compiler,vite-plugin,compiler-devtools,compiler-introspection,compiler-ssr,mcp}@0.5.3
Follow-up to 0.5.2 unblocking three classes of false-positive in the new llui/opaque-state-flow lint that were blocking real consumers from upgrading. The rule was over-strict in shapes the runtime already handled correctly: imported helpers called at arg0, state passed at arg1+ to any call, and View-bag callbacks (branch.default, branch.cases.<k>, show.render, show.fallback, scope.render) whose single parameter is a View<S, M> bag, not state. Each case got a targeted fix without weakening the diagnostic for actual leaks.
@llui/compiler@0.5.3
- Fixed cross-file resolution in
resolveAccessorBody. The resolver now accepts an optionalts.TypeCheckerand follows alias chains across files viagetAliasedSymbol, mirroring the cross-file walker.import { matrixOrEmpty } from '../state'followed by(s) => matrixOrEmpty(s).fieldno longer tripsllui/opaque-state-flow— the walker descends into the imported helper and the call is tracked. Without a Program (test-onlytransformLlui), behavior falls back to file-local lookup as before. - Fixed
opaque-state-flowno longer flags state passed at arg1+ of a call (helper(opts, s)). The header comment documented arg1+ as intentionally not flagged — the runtime sentinel keeps the binding correct — but the implementation only matchedarguments[0], so any other position fell through to the default "outside a tracked container" leak. - Improved when the unresolvable callee is a function parameter (the helper-takes-
(s) => ...-callback composition pattern), the diagnostic's hint now redirects the author at theeachitems-bag rewrite — the framework's intended answer to per-row dynamic state — instead of the generic "inline or refactor" advice. The closure passed at the call site is opaque to per-binding analysis no matter how it's wrapped, so suggesting same-module hoisting is misleading. - Fixed
isReactiveAccessorno longer treats View-bag callbacks as reactive accessors. Property keysdefault,render,fallbackon structural primitives (branch,show,scope,each) and case-arm values inside a nestedcases:object receive aView<S, M>bag, not state. The walker used to enter these as(stateParam) => ...and chase the bag's identifier through the body as if every reference were a state leak — including legitimatesubView(item, kind, h)calls insidesamplecallbacks where the bag was forwarded to a sub-view. - Added
AnalysisContext.programexposes the cross-filets.Program(when supplied by the host adapter) so modules can resolve identifiers in Program-bound nodes. The locally-reparsed source file the registry hands modules is not part of any Program; modules that need symbol resolution must walkprogram.getSourceFile(sourceFile.fileName)to get nodes the checker can bind. Documented inline on the field. - Added 5 regression tests in
opaque-state-flow-rule.test.tscovering: cross-file imported helper (with Program), cross-file fallback (without Program, still correctly flagged), function-parameter hint variant pointing at items-bag, arg1+ accepted as tracked, and View-bagdefaultcallback accepted as non-reactive.
@llui/vite-plugin@0.5.3
- Improved
transformLluiaccepts a newcrossFileProgramparameter; the plugin's existingcrossFileProgram(built once per project undercrossFile: 'silent'/true) now flows through to the compiler, where the lint pipeline derives aTypeCheckerand threads both intoAnalysisContextfor cross-file symbol resolution.
@llui/{compiler-devtools,compiler-introspection,compiler-ssr,mcp}@0.5.3
- Improved cascade republish —
workspace:*dependency on@llui/compilerpinned to the new version. No other source changes.
2026-05-22 — 0.5.2
Released: @llui/{compiler,vite-plugin,compiler-devtools,compiler-introspection,compiler-ssr,mcp}@0.5.2
Fix for a silent-skip class in the compiler's per-binding mask. Reactive accessors that flowed the state identifier into an expression the walker couldn't trace — helper(s) where helper is a function parameter / import / destructured binding, obj.helper(s), new Wrapper(s), spread {...s}, conditional branch cond ? s : other, dynamic key s[key], etc. — emitted a narrow precise mask covering only the direct reads it could see. A reducer that narrowly touched only the field reached through the opaque expression produced dirty bits the binding's mask didn't intersect, so the binding silently never re-evaluated. Symptom: an <input type="range"> stayed at its initial value while sibling text bindings updated; bisects through reducers landed on "narrow update" vs "wide update" with no clear cause. Surfaced by a user bug report against @llui/vite-plugin@0.5.1.
Two-layer fix: (1) the per-binding mask now bails to FULL_MASK in both words when the classifier detects opaque state flow, and (2) the synthesis pipeline appends a (s) => s whole-state sentinel to __prefixes whenever any accessor in the file (or any imported view-helper, via the cross-file walker) flows state opaquely. FULL_MASK bindings intersect the sentinel bit on every update; precise narrow bindings don't include it. The cross-file walker now also reports opaque flow from imported view-helpers and ambient declare function callees.
A new llui/opaque-state-flow lint rule surfaces the leak at compile time so authors can rewrite the read as a direct property access or declare deps via track({ deps: (s) => [...] }). The rule does NOT flag the obj.helper(s) shape — that's the documented headless-components composition idiom (e.g. pr.valueText(s) from progress.connect()); refactoring it would defeat the API surface. The runtime sentinel keeps such bindings correct at the cost of per-update re-evaluation.
@llui/compiler@0.5.2
- Fixed
computeAccessorMasknow runs a classifier over every appearance of the state identifiers. The use is tracked only when it's the parameter binding, the root of a PAE chain, the root of an element-access with a literal key, or arg0 of an Identifier-callee call that resolves to a local declaration. Any other context (NewExpression arg, TaggedTemplate span, spread, const alias, ternary branch, method-call arg, dynamic key, arg1+ of a call, …) forces FULL_MASK in BOTH the low and high words so the binding catches the sentinel bit regardless of which prefix word it lands in. Without the dual-word bail, components past 31 paths would miss the sentinel when it lands in the hi word. - Fixed
collect-depsmirrors the same classifier per accessor and surfaces anopaque: booleanflag throughcollectStatePathsFromSourceandcollectDeps. When set,core-synthesis.buildPrefixesPropappends(s) => sto the prefixes array. Immutable reducers always return a fresh state identity, so the sentinel's prefix bit dirties on every update — FULL_MASK bindings re-evaluate even when the changed field has no other prefix entry. Precise narrow bindings don't include the sentinel bit, so their gating stays precise. - Added
llui/opaque-state-flowlint rule (severity: error,category: perf). Detected shapes: unresolvable identifier-callee call (function parameter / import / destructured), NewExpression with state arg, TaggedTemplate spans, spread, const alias, ternary branch, dynamic element access, type assertion wrapping state. Each diagnostic names the leak shape and gives a concrete fix hint — inline as a PAE chain, refactor into a same-module helper, or declare reads viatrack({ deps: (s) => [...] }). Method-call callees (obj.helper(s)) are NOT flagged — that's the documented headless-components composition idiom; the runtime sentinel keeps them correct. - Added comprehensive regression coverage: 10 new cases in
transform.test.ts(one per leak shape + no-regression for resolvablehelper(s)still producing precise masks), 3 new cases incross-file-paths.test.ts(lift-arrow opaque, state-passes-through helper opaque, negative), 8 new cases inopaque-state-flow-rule.test.tscovering positive shapes plus no-false-positive checks for precise PAE accessors, resolvable helpers, and event handlers.
@llui/vite-plugin@0.5.2
- Improved
transformLluiaccepts a newcrossFileOpaqueparameter. The plugin's call tocrossFileAccessorPathsnow destructures{ paths, opaque }and forwards both fields, so a host file picks up the sentinel when any imported view-helper has opaque flow that the cross-file walker can see.
@llui/{compiler-devtools,compiler-introspection,compiler-ssr,mcp}@0.5.2
- Improved cascade republish —
workspace:*dependency on@llui/compilerpinned to the new version. No other source changes.
Docs
docs/designs/02 Compiler.mdgains two new subsections under Pass 2 — "Opaque-flow classifier" and "Whole-state sentinel in__prefixes" — documenting the tracked-container rules, the leak shapes, the dual-FULL_MASK requirement, the cross-file integration, and the runtime contract for the sentinel arrow.docs/designs/14 Compile-Time Rules.mdregenerated; rule count rises from 41 → 44 (the prior 41 in CLAUDE.md was already stale; corrected in this release).
2026-05-22 — 0.5.1
Released: @llui/{compiler,vite-plugin,compiler-devtools,compiler-introspection,compiler-ssr,mcp}@0.5.1
Fix for a silent path-collection bug in the compiler's reactive-accessor recognizer. Paths read only through h.scope({on}) / h.show({when}) / h.branch({on}) / h.each({items}) were dropped from __prefixes, so when an update touched only those fields the runtime dirty mask stayed 0 and no structural block reconciled. Symptom: scope subtrees silently stuck on the old key after a state-only navigation, while sibling h.text bindings still updated. Surfaced by dungeonlogs' route-keyed scope.
@llui/compiler@0.5.1
- Fixed
isReactiveAccessornow recognizesh.<name>({...})method calls forscope/show/branch/each/ other structural primitives, symmetric to the existingh.text/h.memohandling. Paths read insideon:/when:/items:accessors ofh.<structural>calls now enter__prefixes; the destructured form (scope({on})) always worked. Five regression tests incollect-deps.test.tscover the four primitives + a parity assertion that both call shapes collect identical paths.
@llui/{vite-plugin,compiler-devtools,compiler-introspection,compiler-ssr,mcp}@0.5.1
- Improved cascade republish —
workspace:*dependency on@llui/compilerpinned to the new version. No other source changes.
2026-05-21 — 0.4.0 / 0.5.0
Released: @llui/{dom,test,router,transitions,components,vike,agent}@0.4.0; llui-agent@0.4.0; @llui/{compiler,compiler-ssr,compiler-introspection,compiler-devtools,mcp,vite-plugin}@0.5.0
Resolution release for the dungeonlogs 2026-05-20 issue report. Four layers of fixes for the same underlying class of bug: an accessor (or disposer, onEffect handler, onMount callback) threw mid-commit, the framework silently swallowed the throw, the UI froze with text bindings still updating but branch/show swaps stopped committing, and the developer spent an hour bisecting through silent symptoms. After this release: errors are loud + named in dev with a hard panic on the next commit; the disposer-throw root cause that retained stale bindings is contained; two compile-time rules catch the highest-leverage traps at build time; element helpers carry per-tag prop autocomplete.
Breaking
@llui/compiler@0.5.0— newllui/no-sample-in-event-handlerrule fires at error severity. Any existing code withsample()/h.sample()inside anon*handler (onClick,onInput, etc.) will fail to build. The runtime error has always thrown on this; the rule moves the diagnosis from "click button → console error" to "build fails with line + suggested fix." Affected codepaths are uncommon — most TEA users hit this once during onboarding and migrate away.
Migration
- For each
sample()/h.sample()call inside an event handler: lift it out of the handler. Capture at render time and close over the captured value:
Or use the mount handle for the "I need current state right now" case:// before — now errors at build button({ onClick: () => send({ type: 'pick', id: h.sample((s) => s.id) }) }) // after const id = h.sample((s) => s.id) button({ onClick: () => send({ type: 'pick', id }) })const handle = mountApp(container, App) button({ onClick: () => send({ type: 'pick', id: handle.getState().id }) })
@llui/dom@0.4.0
- Added dev-mode panic on the next commit when a reconcile or binding accessor throws AND no
_onBindingErrorhook is installed. The console error includes the source-mapped stack + active accessor label (e.g.branch().on,each().key); the next user interaction surfaces a hard panic with full context instead of the silent-degraded UI that previously masked the cause. Production behavior unchanged. Install_onBindingErrorto route errors elsewhere and disable the panic. - Fixed disposers in
disposeLifetime/disposeLifetimesBulkare wrapped — a throw in one disposer no longer aborts the cleanup loop OR the binding-dead-marking pass. This is almost certainly the actual mechanism behind the dungeonlogs Issue 1 ("fact-row bindings stayed alive after navigate, threw during reconcile"). The remaining disposers and the dead-marking always run. - Fixed
dispatchEffect'sonEffectinvocation +mount.ts's initial-effects dispatch +flushMountQueue'sonMountcallbacks are all wrapped. One throwing handler can no longer derail the rest of the commit / init / mount sequence. - Added per-element prop types.
input({now autocompletesvalue,checked,type,placeholder, etc.; event handlers infer the correctEventsubtype. 25+ element-specific interfaces (a, button, input, form, label, select, option, textarea, img, video, audio, iframe, script, details, dialog, meter, progress, output, time, td, th, col, fieldset, blockquote, source, optgroup). Other tags fall through toCommonHTMLProps. Template-literal index sigs fordata-*/aria-*/style.<prop>keep custom attrs accessible without unsafe casts. - Added public exports:
ElementPropsFor,CommonHTMLProps,Reactive,EventHandler. Existing code is unchanged — the types are strict enough to surface typos in new code but permissive enough that no example, no test, no in-repo consumer required edits. - Improved reconcile/binding error reporting in dev now uses
console.error(notwarn), includes the full stack, and names the active accessor. Was previously a single-line warn that hid behind dev-console noise.
@llui/compiler@0.5.0
- Breaking new
llui/no-sample-in-event-handlerrule at error severity. See top of release block. - Added
llui/no-repeated-item-currentrule at warning severity. Flags 2+ chaineditem.current().Xreads in the sameeach.renderaccessor. The pattern hides the read from the static analyzer (FULL_MASK fallback) and can throw under reconcile races. Suggests destructure-once or project-to-row-type. Single calls pass through (sometimes necessary for guards).
@llui/test@0.4.0
- Added
propertyTest(def, { mount: { assertDom } })mount mode. Constructs a real DOM container, mounts the component, dispatches the random message sequence throughhandle.send+handle.flush, capturesconsole.error, and runs the user'sassertDom(state, container)callback after each commit. Catches reconcile races, disposer throws, and binding-accessor errors that the previous reducer-onlypropertyTestmissed. Failures formatted with the failing sequence prefix.
@llui/{components,router,transitions,vike,agent}@0.4.0, llui-agent@0.4.0, @llui/{compiler-introspection,compiler-devtools,compiler-ssr,vite-plugin,mcp}@0.5.0
- Improved cascade republish — runtime peer range bumped to
@llui/dom@^0.4.0and transitive@llui/compiler/@llui/agentranges updated viaworkspace:*. No other source changes.
Tests
packages/dom/test/dev-reconcile-panic.test.ts— asserts the new console.error + next-commit panic + hook-absorbs path.packages/dom/test/each-mutation-fuzz.test.ts— outershowgates an innereachover a per-row array; 80 random mutations × 12 reproducible seeds (add, remove, swap, touch, replace-same-key, clear, show/hide) assert DOM ↔ state per commit. Defensive coverage for the issue-3 reconcile-race class.packages/compiler/test/dungeonlogs-rules.test.ts— 7 tests covering both new compiler rules including the negative cases (destructured-once, single.current(), non-handler props starting with "on").
Docs
packages/dom/README.md"Common patterns" section: reading state in event handlers, iterating a normalized record with nested per-item fields, forcing a remount on identity change (h.scopekeyed pattern), global keyboard shortcuts viaonEffect+signal, error-boundary install via_onBindingError.site/content/cookbook.md"Normalized entity store + route-keyed scope" — longer worked example of the dungeonlogs shape.
2026-05-20 — 0.3.0 / 0.4.0
Released: @llui/{dom,test,router,transitions,components,vike,agent}@0.3.0; llui-agent@0.3.0; @llui/{compiler,compiler-ssr,compiler-introspection,compiler-devtools,mcp,vite-plugin}@0.4.0
Engineering-excellent fix for the dicerun2 Vike SSR MISSING_EXPORT regression. Six compiler-emitted runtime helpers move from @llui/dom's root barrel to the @llui/dom/internal subpath; the vite-plugin's post-bundle rename pass is wired to a single source of truth in @llui/compiler with a type-level disjointness assertion that prevents the same class of bug from re-occurring at compile time. New Vike SSR smoke fixture + static-import check in scripts/smoke-examples.ts ensure the externalized-server build path stays exercised in CI.
Breaking
@llui/dom@0.3.0— six__-prefixed runtime helpers are no longer re-exported from the root@llui/dombarrel. They live on@llui/dom/internalnow:__bindUncertain,__cloneStaticTemplate,__runPhase2,__handleMsg,__registerScopeVariants,__clientOnlyStub. The compiler emits the new import path automatically; only hand-written imports of these names from@llui/dom(uncommon — the underscore prefix is the framework's "private API" signal) need updating.
Migration
- If your code imports any of the six helpers above, change
import { __X } from '@llui/dom'toimport { __X } from '@llui/dom/internal'. The subpath's stability contract is explicit: not semver, may change without a major bump. Anyone reaching into it should pin a concrete patch and re-verify on each upgrade. - No other changes are required. View-bag primitives (
text,show,each,branch, …), element helpers (div,button, …),component/mountApp/createView, etc. stay on the root barrel exactly as before.
@llui/dom@0.3.0
- Breaking six compiler-emitted runtime helpers moved off the root barrel. See top of release block.
- Improved
@llui/dom/internal's docstring expanded to cover both framework-adapter primitives (the existing tenant —getRenderContext,createLifetime, etc.) and the newly-relocated compiler-emitted helpers. The subpath now has a single coherent purpose: "not semver-stable; the compiler emits imports here, framework adapters reach in here."
@llui/compiler@0.4.0
- Added
COMPILER_RENAMEABLE_KEYSandCOMPILER_DOM_INTERNAL_IMPORTSconstants in@llui/compiler— the single source of truth for "which names does the compiler synthesize, and where do they live." A type-levelExtract<...>assertion at the declaration site forces the two sets to remain disjoint;tsc --noEmitfails if a future contributor accidentally adds a name to both. Adapters (vite-plugin, ssr) consume the constants instead of maintaining parallel lists. - Improved
cleanupImportsnow emits two separate import declarations: public-surface names continue onfrom '@llui/dom', and the six compiler-internal helpers (__bindUncertain,__cloneStaticTemplate,__runPhase2,__handleMsg,__registerScopeVariants) ride onfrom '@llui/dom/internal'. The internal import is inserted as a text-level edit (not a new AST statement) so the per-statement origin↔transformed index pairing the diff loop relies on stays 1:1 — the previous attempt that inserted an AST statement dropped every trailing original statement from the edit list.
@llui/compiler-ssr@0.4.0
- Improved the
'use client'SSR transform now emitsimport { __clientOnlyStub } from '@llui/dom/internal'instead offrom '@llui/dom'. Same module-boundary safety as the cleanupImports change above — rename-pass-immune.
@llui/vite-plugin@0.4.0
- Improved the post-bundle property-rename pass's
RENAME_TARGETSset is nownew Set(COMPILER_RENAMEABLE_KEYS)imported from@llui/compiler. The hand-coded list with its four problematic dom-export names (__bindUncertain,__cloneStaticTemplate,__runPhase2,__handleMsg) is gone — the type-level disjointness assertion in@llui/compilermakes it impossible to accidentally re-add them. - Fixed closes the
MISSING_EXPORTrolldown error observed in dicerun2's Vike SSR production build. The rename pass operates on raw chunk text via regex and would rewrite the four runtime helpers even when they appeared inside an externalizedimport { … } from "@llui/dom"specifier; the renamed$a/$b/… didn't exist on the package's public export surface, and rolldown failed the build. With the helpers now on a subpath whose name the rename regex doesn't touch (it only matches__-prefixed identifiers; subpath specifier text containsdom/internalwhich doesn't), the cross-module rename path is structurally closed.
@llui/{components,router,test,transitions,vike,agent}@0.3.0, llui-agent@0.3.0, @llui/{mcp,compiler-devtools,compiler-introspection}@0.4.0
- Improved cascade republish — runtime peer range bumped to
@llui/dom@^0.3.0(or transitive@llui/compiler/@llui/agentpickup viaworkspace:*). No other changes.
CI
- Added
examples/vike-ssr-smoke/— minimal Vike + LLui app withprerender: falseandssr.external: ['@llui/dom', '@llui/dom/internal']. Forces the externalized-server build path that surfaces theMISSING_EXPORTclass of bug; without it, vike's defaults inline workspace dependencies and hide the regression behind a single-chunk bundle where the rename pass shortens both ends in lockstep. - Added
scripts/smoke-examples.tsstatic-import check. Walks every example'sdist/server/**/*.{m,c,}jsand asserts everyimport { ... } from '@llui/dom'/from '@llui/dom/internal'specifier name resolves against the real package exports. Catches the rename-into-import-specifier bug deterministically before rolldown's laterMISSING_EXPORTpass would surface it — the smoke harness now reports both browser-boot failures (the issue-#5 class) and externalized-import mismatches (this release's class).
2026-05-20 — 0.2.2 / 0.3.2
Released: @llui/{dom,test,router,transitions,components,vike,agent}@0.2.2; llui-agent@0.2.2; @llui/{compiler,compiler-introspection,compiler-devtools,compiler-ssr,mcp,vite-plugin}@0.3.2
Fixes #5 — a prod-only crash for components with identifier-style view: (h) => …, plus a silent-stale-render bug from cross-file walker accuracy. All three root causes addressed; no public API changes. The Vite plugin now opts into cross-file path resolution by default (silent mode).
@llui/compiler@0.3.2
- Fixed
__viewis now synthesized for identifier-style and zero-arg view params (view: (h) => …,view: () => …,view: (send) => …), not just destructured (view: ({...}) => …). Pre-fix, the compiler silently skipped these and the prod runtime fell back to a{ send }-only bag that crashed on the firsth.show(...)/h.each(...)/h.branch(...)call. The emitted factory is__view: ($send) => createView($send); destructured-style views continue to get the tree-shaken bag. - Fixed cross-file accessor walker no longer descends into every 1-param arrow in the focal file. Callbacks like
onEffect: (bag) => bag.send(...)andhandleEffects((eff) => eff.path)were polluting__prefixeswith phantom paths (send,effect,signal,path,message). Walker now gates entry onisReactiveAccessor, plus arg-0 of a §2.1 view-helper call. - Fixed cross-file walker now descends into non-Node-returning helpers (
(s: State) => s.route.kind === 'a'etc.) when the focal accessor's state param is passed through. Pre-fix the walker only followed §2.1 view-helpers, so predicate-style helpers silently dropped their reads from__prefixesand dependent bindings stopped firing on update — no error to point at it, just stale renders.
@llui/dom@0.2.2
- Fixed the
__prefixes-undefined fallback toFULL_MASKis no longer gated onMODE !== 'production'. Components without compiler-emitted__prefixes(legitimate whenfieldBitsis empty; or hand-rolled defs) crashed in prod withCannot read properties of undefined (reading 'length'). Fallback now fires in every mode. ~30 bytes gz added; previous behavior was a guaranteed crash for any matching component. - Fixed the
getInstanceViewBagfallback now always returnscreateView(send)when no__viewis present — the prod path previously returned a{ send }-only bag, banking on the (no longer true) invariant that the compiler emits__viewfor every component.
@llui/vite-plugin@0.3.2
- Improved
crossFiledefault flipped fromfalseto'silent'. With the walker false-positive bug fixed in@llui/compiler@0.3.2, the historical reason to keep it opt-in is gone.'silent'folds cross-file paths into__prefixesautomatically without polluting dev logs withllui/opaque-view-calldiagnostics. SetcrossFile: trueto surface them, orcrossFile: falseto skip the Program build entirely (saves startup cost on very large repos). - Fixed the cross-file
ts.Programbuild is now deferred untilconfigResolvedsets the project root. Direct-transformcallers (unit tests, alt-host adapters) fall back to per-file path collection silently instead of scanningprocess.cwd()'s tsconfig.
@llui/{components,router,test,transitions,vike,agent}@0.2.2, llui-agent@0.2.2, @llui/{mcp,compiler-introspection,compiler-devtools,compiler-ssr}@0.3.2
- Improved cascade republish — runtime peer range bumped to
@llui/dom@^0.2.2(or transitive@llui/compiler/@llui/agentpickup viaworkspace:*). No other changes.
CI
- Added
scripts/smoke-examples.ts— Playwright-driven harness that boots every built example in headless Chromium and fails onconsole.error/pageerror/requestfailed. Covers all 9 examples (8 SPA + 1 Vike-prerendered). Wired into theverifyworkflow afterturbo test. Closes the CI gap that let issue #5 ship:pnpm turbo buildran but nobody loaded the result. - Fixed
deploy-docs.ymlno longer tries to build the deleted@llui/eslint-pluginpackage.
Docs
- Removed stale references to
@llui/eslint-plugin(the package was deprecated on npm; rules now live in@llui/compiler) from README, site index, debugging guide, cookbook, andsite/content/api/eslint-plugin-llui.md. - Added site pages for
@llui/compiler,@llui/compiler-introspection,@llui/compiler-devtools,@llui/compiler-ssr— the four packages that split out of@llui/vite-pluginin 0.3.1 but weren't yet documented. Wired intogenerate-api.tsand the site nav. - Deprecated
@llui/lint-idiomaticon npm (already gone from the repo since the lint→compiler migration; sibling@llui/eslint-pluginwas deprecated previously). Both now carry the same "Lint rules moved to @llui/compiler as compile-time errors" deprecation message.
2026-05-20 — 0.2.1 / 0.3.1
Released: @llui/{dom,test,router,transitions,components,vike,agent}@0.2.1; llui-agent@0.2.1; @llui/{compiler,compiler-introspection,compiler-devtools,compiler-ssr,mcp,vite-plugin}@0.3.1
Bundle-size release. The js-framework-benchmark LLui bundle dropped from 8.9 kB gz to 7.0 kB gz (-21 %) through a series of dev-only DCE gates, build-flag gates, a property-rename pass in the vite-plugin, and one bench-config fix. No public API changes; no perf regressions in median-of-3 measurements (Select / Update 10th / Swap all faster or unchanged).
@llui/dom@0.2.1
- Improved dev-only error enrichment (
enhanceBindingError,dispatchEffectDev), dev-only field writes (disposalCause,Lifetime._kind), and long-form dev error messages (getRenderContext,applyBinding,sample(),useContext,AppHandle.getState()-after-dispose) now gated behindimport.meta.env?.DEVso production builds DCE them out. Combined contributions ~1.5 kB gz on the bench. - Added
__LLUI_TRANSITIONS__build flag gateseach()'senter/leave/onTransitioncallback handling. Apps that don't animate drop ~0.22 kB gz; apps using@llui/transitionsor custom transition callbacks must opt in via the vite plugin. - Improved per-env
WeakMap<DomEnv, Map<...>>template cache replaced with a singletonMap<string, HTMLTemplateElement>. SSR adapters needing a fresh cache between renders call the exported_resetTemplateCache(). - Improved mount-time HMR and devtools-install checks (
hmrModule,devToolsInstallconsultations acrossmount.ts's 13 entry points) gated behindimport.meta.env?.DEV.
@llui/vite-plugin@0.3.1
- Added
transitions?: booleanplugin option (default false). Sets the__LLUI_TRANSITIONS__build flag the runtime checks. - Improved
generateBundlehook strips_lluiCompilerEmitted: 1integrity-check markers from production chunks after verification, and renames LLui-internal__view/__prefixes/__handlers/__compilerVersion/ etc. compiler-emit properties to short$a/$b/$cforms. Allow-list approach — only LLui-emitted names are renamed; Vite's__vite__mapDeps, Vike's__VIKE__NOT_SERIALIZABLE__, user-defined__LLUI_STATE__containers and other framework-internal__-prefixed identifiers pass through unmolested.
@llui/compiler@0.3.1
- Improved static-template HTML emission drops unneeded attribute quotes for safe values (
class=col-md-6instead ofclass="col-md-6"). Reduces emitted HTML size onelTemplate/__cloneStaticTemplatecall sites by 2 bytes per simple-value attribute.
@llui/test@0.2.1
- Added
defineTestComponentandstampTestVersionre-exported from@llui/dom/internaland@llui/test's public surface. Test fixtures opt into the optimized runtime path via a single canonical helper. - Improved
testViewnow stamps__compilerVersion: '__test__'on rawComponentDefliterals it receives, silencingwarnUncompiledOncewithout forcing callers to usedefineTestComponent.
@llui/{compiler-devtools,compiler-introspection,compiler-ssr}@0.3.1
- Cascade bump — picks up the new
@llui/compiler@0.3.1baseline.
@llui/mcp@0.3.1 and @llui/{agent,components,router,transitions,vike}@0.2.1, llui-agent@0.2.1
- Cascade bumps —
peerDependencies["@llui/dom"]rolled to^0.2.1for the dom-peer packages;llui-agentrolls to track@llui/agent.
Bench
- Improved the
js-framework-benchmarkLLui app dropped Vite'sbuild.libmode for a standard app build. Lib mode preserves whitespace +//#regionsource-map markers for downstream re-bundling, but the bench bundle is served directly to Chrome — ~1.3 kB gz of formatting overhead was dead weight.
Cumulative
| Pre-release | Post-release | |
|---|---|---|
| jfb bundle (gzipped) | ~8.9 kB | 7.0 kB |
| jfb Select (median-of-3) | 4.3 ms | 3.9 ms |
| jfb Update 10th | 15.9 ms | 14.4 ms |
| jfb Swap 1↔998 | 11.5 ms | 10.9 ms |
| vs Solid (bundle gz multiple) | ~2.0× | ~1.55× |
2026-05-17 — 0.2.0
Released: @llui/{dom,vite-plugin,components,vike,transitions,router,test,mcp,agent,eslint-plugin}@0.2.0; llui-agent@0.2.0
Cleanup pass over what 0.1.0's unified composition model rendered redundant: the public composition surface drops the child* naming (the primitive it referenced no longer exists), the compiler drops two layers of dead code, and three ESLint rules stop false-positiving on generic slice/view helpers.
Breaking
@llui/dom@0.2.0—childHandlersrenamed tocomposeModules;ChildState<T>renamed toModulesState<T>;ChildMsg<T>renamed toModulesMsg<T>. The runtime behavior is identical — these names alluded to the removed-in-0.1.0child()primitive and were misleading. TheChildOptions<S, ChildM>interface (also a 0.1.0 leftover that was exported but never referenced) is removed.
Migration
- Replace
import { childHandlers }withimport { composeModules }. Same signature, same runtime behavior — pure rename. - Replace
import type { ChildState, ChildMsg }withimport type { ModulesState, ModulesMsg }. - Delete any import of
ChildOptions— the type was unused as of 0.1.0. - If you applied
/* eslint-disable llui/static-items, llui/static-on, llui/each-closure-violation */to suppress false positives on generic slice/view helpers, the underlying rules are fixed in 0.2.0 — those disables can come off.
@llui/dom@0.2.0
- Breaking
child*→module*rename on the composition surface. See top of release block. - Fixed five stale
child()references in error messages and docstrings (render-context.ts,sample.ts,types.ts×2,update-loop.ts).
@llui/vite-plugin@0.2.0
- Improved dropped ~85 lines of voided
__dirtyemission machinery fromtryInjectDirty. 0.1.0 stopped attaching__dirtyto the emittedComponentDefbut left the function-building code in place behind twovoid-statements. ThetopLevelBitsaggregation thattryBuildHandlersand__maskLegendneed stays. - Improved removed the
computePhase2Maskstub that always returnedFULL_MASK. The per-binding gate in Phase 2 already does this work bit-by-bit; the function was a placeholder for a not-shipped aggregate analysis.buildUpdateBodysignature trimmed.
@llui/eslint-plugin@0.2.0
- Fixed
static-itemsandstatic-onno longer fire false positives when the accessor reads state through a call argument.(s) => opts.getProps(s).itemsand(s) => derive(s, ctx)are now recognized as reactive reads; previously the rule only matched direct member access (s.field). - Fixed
each-closure-violationno longer flags captures inside event handler properties. Properties matching/^on[A-Z]/(except the three structural namesonMsg/onSuccess/onError) are treated as event handler contexts where captures ofsend, dispatch helpers, and parent-helper plumbing (opts,wrapMsg, …) are standard. Reactive-binding captures (text(() => ...),class: () => ...) still fire the rule.
@llui/{components,router,transitions,vike,test,mcp,agent}@0.2.0
- Cascade peer dependency on
@llui/dombumped from^0.1.0to^0.2.0. No source changes.
llui-agent@0.2.0
- Cascade dependency on
@llui/agentbumped to track. No source changes.
Tests / dev infra
- Added regression test (
packages/dom/test/nested-mountapp.test.ts) covering child-app reactivity whenmountApp(ChildDef)is invoked from inside another app's view tick (the classicforeign({ mount })+ deferredonMountpattern). Locks in the compiled fast paths' contract:__update,__handlers, and__prefixeswork correctly under nested mounts; the fields take instance state as parameters and do not capture binding lists at definition time. - Added pre-commit hook (
simple-git-hooks+lint-staged) that runsprettier --writeon staged files. Auto-installs via thepreparescript afterpnpm install. No release impact; collaborator-facing only.
2026-05-16 — 0.1.0 (unified composition model)
Released: @llui/{dom,vite-plugin,components,vike,transitions,router,test,mcp,agent,eslint-plugin}@0.1.0; llui-agent@0.1.0
First minor bump of the 0.0.x line. Removes the two-tier component()/child() composition model in favor of a single primitive: view functions, with combine() for reducer composition and subApp as a lint-enforced escape hatch. Path-keyed reactivity (__prefixes) replaces the per-top-level-field __dirty bitmask and now supports up to 62 reactive paths per component (was 31). Every package re-lockstepped to 0.1.0 so the boundary between the old and new model is unambiguous.
See Migration from v0.0.x (docs/designs/13 Migration from v0.0.x.md) for the full migration recipe.
Breaking
@llui/dom@0.1.0— removed:child(),addressOf,setAddressedDispatcher,propsMsg/receivesonComponentDef, and theAddressedEffectruntime registry. Migrate everychild({ def, props, onMsg })call to a view function that the parent invokes directly with the parent owning the child's state slice. Migrate addressed-effect cross-component coordination to shared parent state withcombine()-routed slices; for adapter-layer push (vike persistent layouts), use the newonLayerDataChangecallback.@llui/dom@0.1.0— user-authored__dirtyonComponentDefis now rejected atcreateComponentInstancewith a hard throw. The compiler emits__prefixes(path-keyed reactivity) automatically; hand-written__dirtyis no longer accepted at the type level or at runtime.@llui/vike@0.1.0—def.propsMsgis no longer honored as a persistent-layout-chain prop pusher. Use the newonLayerDataChangeoption onRenderClientOptionsto dispatch state-update messages through the framework-suppliedAppHandlewhen a layer'slluiLayoutData[i]slice changes across navigations.@llui/eslint-plugin@0.1.0—unnecessary-childandchild-static-propsrules removed (their target primitive no longer exists). Newsubapp-requires-reasonrule enforces a non-emptyreasonfield on everysubApp({ reason, ... })call.bitmask-overflowthreshold raised 31 → 62 paths.
Migration
- Replace every
child({ def, props, onMsg })call with a view function: the child module exportsupdate(slice, msg)andview(props, send)instead of aComponentDef; the parent owns the slice and namespaces messages as{ type: 'child-name', msg: ChildMsg }. Seedocs/designs/13 Migration from v0.0.x.mdfor the worked example. - Replace mechanical "route by message-type prefix to a sub-reducer" parent reducers with
combine({ slice: reducer, ... }). Messages must use{ type: '${slice}/${action}', ... }shape. - Replace addressed effects (
toToastManager.show(...)) with shared parent state slices ({ type: 'toasts/add', ... }). - In
@llui/vikeapps, replace eachLayoutdef'spropsMsgwith anonLayerDataChangecallback oncreateOnRenderClient({ onLayerDataChange: ... }). - Delete any hand-written
__dirtyfromComponentDefliterals. The compiler emits__prefixesautomatically — for components built without@llui/vite-plugin, the runtime falls back toFULL_MASK(re-evaluate every binding every cycle). - Embed genuinely isolated TEA loops via
subApp({ reason, def, ... })with a non-emptyreasonstring explaining why state-lifetime isolation is required. Don't usesubAppto "isolate a complex component" — extract a view function instead.
@llui/dom@0.1.0
- Added
combine<S, M, E>({ slice: reducer, ... }, top?)— reducer composition by${slice}/${action}message-prefix routing. Preserves top-level state reference equality when slices return unchanged. - Added
subApp({ reason, def, data?, onHandle? })at@llui/dom/escape-hatch— embed an independent TEA loop with its own state lifetime. Thereasonfield is required and surfaces in the rendered DOM asdata-llui-sub-app-reason. - Added path-keyed reactivity runtime:
__prefixesis the supported compiler-emitted dirty-detection mechanism. Each entry is a hoisted closure(s: S) => unknown; the runtime reference-comparesprefix(prev) !== prefix(next)per entry. - Added two-word mask architecture:
Binding.maskHi,StructuralBlock.maskHi, two-wordcomputeDirtyFromPrefixesreturn type[lo, hi]. Supports up to 62 reactive paths per component before falling back to FULL_MASK. Gates emit as(mask & d) | (maskHi & dHi); for ≤31-prefix components the high-word branch collapses on V8's inline cache. - Breaking —
child(),addressOf,setAddressedDispatcher, addressed-effect registry removed. See top of release block. - Breaking —
propsMsg/receivesremoved fromComponentDef,AnyComponentDef,LazyDef.AppHandle.sendis the imperative external-dispatch surface. - Breaking — user-authored
__dirtyrejected atcreateComponentInstance. See top of release block. - Improved
_runPhase2and_handleMsgwidened with optionaldirtyHiparameter (defaults to 0 — backward compat for stale compiled bundles).__updatearrow gains trailingdHi = 0parameter; runtime passescombinedDirtyHias the 6th positional arg.
@llui/vite-plugin@0.1.0
- Added
__prefixesemission for every component with reactive accessors. Replaces__dirty(which is no longer emitted at all). Path positions 0..30 land in the low-word mask, 31..61 in the high word. - Added per-binding
maskHiliteral emission via a 5th tuple slot onelSplitbinding tuples and an optional 6th positional arg onelTemplate's__bindcallback. Emitted only when the binding reads a high-word prefix — the common ≤31-prefix case stays byte-identical to the pre-multi-word baseline. - Added two-word Phase 1 block gate emission in compiler-emitted
__update:!((bk.mask & d) | (bk.maskHi & dHi)).block.reconcileand__runPhase2calls threaddHithrough. - Added two-word
__handlerscase-handler emission:_handleMsg(inst, msg, caseDirty, method, caseDirtyHi)when (and only when) the case touches a high-word top-level field. - Improved
collectDepsreturns{ lo, hi }maps;computeAccessorMaskreturns{ mask, maskHi, readsState }. - Breaking —
__dirtyPropertyAssignment emission removed. Any tooling that grep'd compiled output for__dirty:should look for__prefixes:instead.
@llui/components@0.1.0
- Fixed dialog-child-form-submit test cleanup after the
child()primitive was removed. - Improved peer @llui/dom pinned to
^0.1.0.
@llui/vike@0.1.0
- Added
onLayerDataChange?: (ctx: { def, handle, newData, prevData }) => voidoption onRenderClientOptions. Fires for each surviving layout layer whoselluiLayoutData[i]slice changed across a navigation; the user dispatches a state-update message through the suppliedAppHandle. - Breaking —
def.propsMsgis no longer consulted for persistent-layout chain prop pushes. See top of release block.
@llui/test@0.1.0
- Improved peer @llui/dom pinned to
^0.1.0. No user-visible API surface changes — internal helpers re-checked against the new ComponentDef shape.
@llui/router@0.1.0
- Improved peer @llui/dom pinned to
^0.1.0. No user-visible API surface changes.
@llui/transitions@0.1.0
- Improved peer @llui/dom pinned to
^0.1.0. No user-visible API surface changes.
@llui/eslint-plugin@0.1.0
- Added
subapp-requires-reasonrule — enforces a non-emptyreasonstring on everysubApp({ reason, ... })call. The reason surfaces in the rendered DOM asdata-llui-sub-app-reasonfor code-review visibility. - Improved
bitmask-overflowthreshold raised 31 → 62 paths to match the new two-word mask capacity. Message text rewritten to recommend state restructuring or view-function extraction rather thanchild()extraction. - Breaking —
unnecessary-childrule removed. Its target call shape (child(...)) no longer exists. - Breaking —
child-static-propsrule removed. Its target call shape no longer exists.
@llui/mcp@0.1.0
- Improved peer @llui/dom pinned to
^0.1.0. No user-visible API surface changes; internal recompile against the new ComponentDef shape.
@llui/agent@0.1.0
- Improved peer @llui/dom pinned to
^0.1.0. No user-visible API surface changes.
llui-agent@0.1.0
- Improved transitive bump on
@llui/agentpeer change.
Docs
- Added
docs/designs/13 Migration from v0.0.x.md— step-by-step migration guide covering all seven concrete migrations downstream apps need (child → view function, propsMsg → onLayerDataChange / slice ownership, addressed effects → shared state, mergeHandlers/sliceHandler → combine, user__dirtyremoval, subApp for genuine isolation, plus a mechanical sweep checklist). - Rewrote
docs/designs/01 Architecture.mdanddocs/designs/07 LLM Friendliness.mdaround the unified composition model. - Site content (
getting-started.md,cookbook.md,architecture.md,api/dom.md) swept for stale references to removed primitives. - See also
docs/proposals/unified-composition-model.md(original design),unified-composition-model-spike-result.md(benchmark validation),unified-composition-model-status.md(branch status).
2026-05-16 — 0.0.40
Released: @llui/{dom,components,transitions}@0.0.40; @llui/{test,router}@0.0.41; @llui/vike@0.0.42; @llui/mcp@0.0.37; @llui/agent@0.0.58; llui-agent@0.0.22
Fix child({ onMsg }) silently no-opping when mounted inside an each() (or virtualEach()) row.
@llui/dom@0.0.40
- Fixed
each()andvirtualEach()reuse a module-scopedbuildCtxper row to avoid allocation, andbuildEntrymutated only a fixed set of fields per row — droppingsendandcontainer.child({ def, onMsg })readsparentCtx.sendto forwardonMsgoutput to the parent reducer; withsend === undefined, the bubble silently no-opped. Controlled inputs paired with each() lost every user interaction: the child fired its message, the parent never saw it, and the parent's reactivevalue:accessor re-evaluated against unchanged state on the next render — racing the user's drag and resetting the DOM. Surface symptom looked like an input race; root cause was three layers deep (each row → child mount → onMsg microtask → undefined parentSend). Both primitives now copy every non-rootLifetime/non-statefield from the surrounding context, matching the comment's stated intent. The missingcontainerfield separately affectedonMountcalls from inside each rows (fell back todocument.bodyinstead of the parent component's container).
@llui/{components,transitions}@0.0.40, @llui/{test,router}@0.0.41, @llui/vike@0.0.42, @llui/mcp@0.0.37, @llui/agent@0.0.58
- Fixed Cascade bump for
@llui/dom@0.0.40. Peer range updated from^0.0.39to^0.0.40. No behaviour change in these packages themselves.
llui-agent@0.0.22
- Fixed Cascade bump for
@llui/agent@0.0.58. No behaviour change in the bridge itself.
2026-05-14 — 0.0.39
Released: @llui/{dom,components,transitions}@0.0.39; @llui/{test,router}@0.0.40; @llui/vike@0.0.41; @llui/mcp@0.0.36; @llui/agent@0.0.57; llui-agent@0.0.21
Fix nested mountApp failing past the first instance, plus the same class of bug latent across mountAtAnchor / hydrateApp / hydrateAtAnchor.
@llui/dom@0.0.39
- Fixed The HMR fast path in
mountAppmatched ondef.namealone, so a second call into a different container firedreplaceComponenton the existing entry instead of mounting a new instance. The docs-page idiom of iterating placeholder spans and callingmountApp(span, InlineRollChip, …)for each only rendered the first chip — every subsequent call silently re-rendered chip #1 with new state and left the new span empty. Fix scopes the fast path by container identity (newreplaceComponentForContainer); independent mounts of the same-named component into distinct containers now each produce their own instance. - Fixed The same class of bug was latent (as a leak, not as wrong output) in the other three mount paths.
mountAtAnchor,hydrateApp, andhydrateAtAnchorhad no fast path at all, so a repeated call into the same root created a new instance while leaving the prior one orphaned — itsrootLifetimewas never disposed, its HMR entry stayed in the registry, itsactiveInstancesentry stuck, and its bindings kept running on detached DOM. All three now check an identity-keyed fast path (replaceComponentForContainer/ newreplaceComponentForAnchor) before doing any work, matchingmountApp's shape. Re-execution of the user's mount/hydrate call (typical of HMR module re-run, page navigation in vike persistent layouts) hot-swaps cleanly instead of leaking. - Improved Broadcast
replaceComponent(name, def)— the variant the vite plugin'simport.meta.hot.acceptcallback fires — is factored through a sharedswapEntryhelper with the new identity-scoped variants. Behaviour for the HMR-accept path is unchanged.
@llui/{components,transitions}@0.0.39, @llui/{test,router}@0.0.40, @llui/vike@0.0.41, @llui/mcp@0.0.36, @llui/agent@0.0.57
- Fixed Cascade bump for
@llui/dom@0.0.39. Peer range updated from^0.0.38to^0.0.39. No behaviour change in these packages themselves.
llui-agent@0.0.21
- Fixed Cascade bump for
@llui/agent@0.0.57. No behaviour change in the bridge itself.
2026-05-12 — 0.0.38
Released: @llui/{dom,components,transitions}@0.0.38; @llui/{test,router}@0.0.39; @llui/vike@0.0.40; @llui/mcp@0.0.35; @llui/agent@0.0.56; llui-agent@0.0.20
Fix silent freezing of memo'd structural accessors on the single-message fast path.
@llui/dom@0.0.38
- Fixed The per-msg fast path (
__handlers→_handleMsg) reconciled structural blocks without updatingcurrentDirtyMask, so the compiler-emittedmemo(fn, mask)wrappers that the Vite plugin auto-applies to multi-field structural accessors (each.items,branch.on,show.when) short-circuited on the stale mask left over from the previous cycle and returned cached output. The structural block was correctly invoked but reconciled against frozen input — lists didn't filter, branches didn't switch, conditional content didn't update. Bug surfaced only when an accessor read 2+ state fields (single-field accessors don't get auto-memo'd) and was masked by the fact that per-attribute bindings (text(...),el({class: ...})) gate on adirtyparameter, so attribute-level reactivity continued working on the same update — making the asymmetry hard to spot. Reported with a fully-worked repro against a multi-fieldeach.itemsin a downstream consumer.
@llui/{components,transitions}@0.0.38, @llui/{test,router}@0.0.39, @llui/vike@0.0.40, @llui/mcp@0.0.35, @llui/agent@0.0.56
- Fixed Cascade bump for
@llui/dom@0.0.38. Peer range updated from^0.0.37to^0.0.38. No behaviour change in these packages themselves.
llui-agent@0.0.20
- Fixed Cascade bump for
@llui/agent@0.0.56. No behaviour change in the bridge itself.
Docs
routeToAgentDO's API reference now documents themcpPath?: stringoption (shipped in@llui/agent@0.0.55).
2026-05-04 — @llui/agent@0.0.55, llui-agent@0.0.19
Released: @llui/agent@0.0.55; llui-agent@0.0.19
Fix routeToAgentDO so Claude Code can reach the MCP endpoint without a bearer token.
@llui/agent@0.0.55
- Fixed
routeToAgentDOnow routes/agent/mcp(and any custommcpPath) to the root DO without requiring aBearertoken. Previously the function only exempted the hardcoded management endpoints (/agent/mint,/agent/revoke,/agent/resume/*,/agent/sessions) from the token gate, so every MCP initialization request from Claude Code received401 Unauthorizedandmcp__<server>__connect_sessionnever appeared in the tool list. MCP auth happens inside the protocol viaconnect_session({token}), not at the HTTP layer. AddedmcpPath?: stringoption torouteToAgentDO(default'/agent/mcp') for deployments that customize the path.
llui-agent@0.0.19
- Fixed Cascade bump for
@llui/agent@0.0.55. No behaviour change in the bridge itself.
2026-05-04 — @llui/agent@0.0.54, llui-agent@0.0.18
Released: @llui/agent@0.0.54; llui-agent@0.0.18
Token prefix changed from llui-agent_ to agt_ — LLM clients no longer pattern-match the bearer token to the bridge MCP tool.
Breaking
@llui/agent@0.0.54— Theagt_prefix replacesllui-agent_. All previously-minted tokens are invalid; users must generate a new token after upgrading. The token is user-visible (pasted into the LLM chat) so existing sessions end naturally on the next reconnect.
Migration
- If you store or validate the token format (e.g. regex, length checks), update to expect
agt_prefix and length 47 (was 54). - Re-generate any in-flight tokens after deploying the update — old
llui-agent_…tokens will fail authentication withunknown.
@llui/agent@0.0.54
- Fixed Token prefix changed from
llui-agent_toagt_. Claude Code and Claude Desktop were pattern-matching the old prefix tomcp__llui__connect_session(the bridge tool, schema{url, token}) and asking for a URL even when connected to the server-side MCP endpoint whoseconnect_sessiononly needs{token}. The neutralagt_prefix carries no MCP namespace hint — the auth model is opaque: only the server-side hash lookup determines validity.
llui-agent@0.0.18
- Fixed Cascade bump for the
agt_token prefix change in@llui/agent@0.0.54. No behaviour change in the bridge itself — the bridge's ownconnect_session({url, token})is unchanged.
2026-05-04 — @llui/agent@0.0.53, llui-agent@0.0.17, @llui/eslint-plugin@0.0.24
Released: @llui/agent@0.0.53; llui-agent@0.0.17; @llui/eslint-plugin@0.0.24; @llui/mcp@0.0.34
Server-side MCP endpoint for @llui/agent (no bridge required), plus tree-shake-friendly import linting across the wider @llui/* namespace.
@llui/agent@0.0.53
- Added Server-side MCP endpoint at
/agent/mcp(opt-in viamcp?: boolean | McpRouterOptionsinServerOptions/DurableObjectOptions). Enables Claude Desktop and Claude Code to connect directly to an app backend without installing thellui-agentbridge — the user pastes a per-session token in-chat,connect_session({token})binds the session, and all 14 forwarded tools work exactly as they do through the bridge. - Added
@llui/agent/mcp/toolssub-path export — single source of truth for the shared tool catalogue (14 forwarded tools +disconnect_session).connect_sessionis intentionally absent: the bridge needs{url, token}, the server surface needs only{token}. - Added
createMcpRouter— WHATWG-compatible MCP router usingWebStandardStreamableHTTPServerTransport. Integrates intocreateLluiAgentServervia the newmcpoption; also available standalone. - Added
mcp?: boolean | McpRouterOptionstoAgentPairingDurableObject(@llui/agent/server/cloudflare) — enabling MCP inside a Cloudflare Workers Durable Object is nownew AgentPairingDurableObject({ mcp: true }).
llui-agent@0.0.17
- Improved
tools.tsnow imports shared descriptors from@llui/agent/mcp/tools(new single source of truth) rather than duplicating them. The bridge's ownconnect_session({url, token})is retained — its surface differs from the server-sideconnect_session({token}). Type aliasesForwardedToolDescriptor,MetaToolDescriptor,ToolDescriptorare re-exported for back-compat.
@llui/eslint-plugin@0.0.24
- Improved
llui/namespace-importnow covers@llui/dom,@llui/components,@llui/router,@llui/transitions,@llui/effects, and@llui/agent(was justdom+components). Autofix uses scope analysis to enumerate every namespace member access, builds a sorted-deduped named-import list, and rewrites both the import statement and every call site. Bails without a fix when any reference is non-static. - Added
llui/no-barrel-import-when-subpath-exists— reads the target package'sexportsfield at lint init (cached) and for each named specifier matching an existing./<name>sub-path export, splits the barrel import. Targets@llui/componentstoday. Ships inrecommendedaterror.
@llui/mcp@0.0.34
- Cascade only — picks up
@llui/eslint-plugin@0.0.24. No behavior change.
2026-05-03 — @llui/vite-plugin@0.0.42
Released: @llui/vite-plugin@0.0.42
Follow-up to 0.0.41 — the deeper collect-deps walk that follows named identifier references at reactive positions stopped at the first call to another local helper, so accessors of the shape (s) => helper(s) extracted only the outer body's reads and missed everything helper read transitively. The result was a precise mask that under-counted: a sibling reactive accessor reading only the helper-internal fields could drive a non-zero dirty that AND'd with the narrow each.__mask was zero, silently skipping the reconcile. This release recurses through helper delegations.
@llui/vite-plugin@0.0.42
- Fixed
collect-deps.tsextractAccessorPathsandtransform.tscomputeAccessorMasknow recurse throughhelper(s)delegation calls — when an accessor body calls another local function and passes the state param verbatim (helper(s)wheresmatches our state param name), the helper is resolved viaaccessor-resolver.tsand its body is walked too. A visited-set breaks cycles on mutually-recursive helpers. Both walkers gate the recursion behind: top-level only (don't descend into nested function bodies whose params shadow ours), skip framework helpers (memo/text/unsafeHtml/sample/item), and only follow when arg0 is the state param verbatim — neverhelper(s.foo)orhelper(otherVar). - Fixed
computeAccessorMask's chain-prefix matcher now handles "we read deeper than fieldBits tracks" symmetrically. A chain like'items.filter'froms.items.filter(...)now masks in the'items'bit when fieldBits has'items'(depth 1), so calling builtin array methods on a tracked path correctly contributes to the per-element mask.
2026-05-03 — @llui/vite-plugin@0.0.41, @llui/eslint-plugin@0.0.23, @llui/mcp@0.0.33
Released: @llui/vite-plugin@0.0.41; @llui/eslint-plugin@0.0.23; @llui/mcp@0.0.33
Compiler fix for reactive prop values that aren't an inline arrow — named-function references, memo() results, hoisted function declarations, imported helpers — which were silently miscompiled at element-helper call sites. Plus a new lint rule covering the remaining let-as-accessor footgun.
@llui/vite-plugin@0.0.41
- Fixed Reactive prop values that resolved to anything other than an inline arrow or a const-bound arrow no longer silently degrade. The buggy paths —
__cloneStaticTemplate("<button></button>")(prop dropped entirely when the element had no other reactive binding) and__e.disabled = isGated(function reference written to a boolean DOM property when it had a sibling) — are gone. Function declarations,memo()results, and other recognised callable shapes now emit binding tuples; unresolvable identifiers (imports, parameters) bail the element to the runtime helper, which classifiestypeof v === 'function'correctly. A newclassifyReactiveValuehelper is the single source of truth, and Pass 2 mask injection fortext()/show()/branch()/each()now uses the same shape contract. - Improved
collect-deps.tsfollows identifier references at reactive positions to their local declarations and extracts state-path reads from the resolved bodies. Refactoring an inline arrow to a named helper (function isGated(s) { return s.gated }orconst isGated = (s) => s.gated) now keeps the precise-mask optimization — previously, files whose every accessor was a named reference produced emptyfieldBitsand the bitmask gating was a no-op. - Improved Resolver helpers (
resolveLocalConstInitializer,resolveAccessorBody,isMemoCallWithArrowArg) are extracted into a sharedaccessor-resolver.tsmodule sotransform.tsandcollect-deps.tsuse one definition of "what counts as a callable accessor in this file."
@llui/eslint-plugin@0.0.23
- Added
llui/no-let-reactive-accessor— flagslet/varbindings used at reactive-accessor positions. The compiler's resolver only followsconst(reassignment would invalidate the analysis), solet isGated = (s) => s.gated; button({ disabled: isGated })silently falls back to FULL_MASK — runtime correct but every binding fires on every state change. Autofixeslet→constwhen the binding is never reassigned; reports without a fix when there's at least one write. Ships inrecommendedaterror.
@llui/mcp@0.0.33
- Cascade only — picks up the new
@llui/eslint-plugin@0.0.23. No behavior change.
2026-05-02 — @llui/agent@0.0.52, llui-agent@0.0.16
Released: @llui/agent@0.0.52; llui-agent@0.0.16
Removes the in-app chat composer surface introduced in 0.0.50 (agentChat slice + wait_for_user_input LAP method + bridge tool + UserInputStorage adapter). The visibility primitives — agentLog / agentAttention / narrate — stay intact and unchanged.
Breaking
@llui/agent@0.0.52—agentChatnamespace,agentChat.AgentChatState/AgentChatMsg/connect(),LapWaitForUserInputRequest/LapWaitForUserInputResponse,UserInputSubmittedFrame,LogKind: 'user-input',WsClient.submitUserInput,AgentChatSendInputeffect,EffectHandlerHost.wrapAgentChat/getWsClient,CreateAgentClientOpts.slices.wrapChatMsg,UserInputStorageinterface,CoreOptions.userInputStorageare all removed. The/lap/v1/wait-for-user-inputendpoint is gone; its handler file is deleted. The pairing registry's per-tid user-input buffer + parked-waiter queue are gone.llui-agent@0.0.16— thewait_for_user_inputMCP tool is removed. Thellui-connectMCP prompt no longer mentions it.@llui/agent/server/cloudflare—makeDurableObjectUserInputStorageandDurableObjectStorageLikeexports are removed (they were only useful for the deletedUserInputStorageadapter).
Migration
- Hosts that wired the chat composer must drop the slice from state, the reducer case, the
wrapChatMsgfactory option, and any panel UI that rendered an input. One mechanical commit per host — seedecisive.space-2@d084466for a worked diff. - The connect snippet auto-regenerates on the next mint and no longer mentions
wait_for_user_input. Existing pending snippets (sessions inpending-claudeat upgrade time) keep their old text but the missing MCP tool is harmless — Claude just won't find it on tool lookup. - CF DO hosts that wired
makeDurableObjectUserInputStorageshould drop the import + theuserInputStorageopt; the registry no longer accepts the constructor option.
Why
The chat composer crossed from a "visibility surface" into a "half-conversation" without delivering true conversational continuity (which would require an embedded LLM, deliberately rejected for cost / cross-app-context reasons). The result was two competing input surfaces (LLM client window vs. in-app composer) and an "always listening" long-poll model that felt uncanny. Visibility (agentLog + agentAttention + narrate) is the part that earns its keep — the agent's actions become perceptible inside the app — without trying to make the app itself the conversation surface. See docs/designs/10 Agent Protocol.md §5b for the rewritten architectural rationale.
@llui/agent@0.0.52
- Removed see Breaking. Net: ~600 lines of source + ~700 lines of tests deleted, 14 source files touched. The connect snippet shrinks to
connect_session+narrate+ namespacing edge case. - Improved
narratebecomes the canonical "LLM surfaces intent in the app" primitive. The connect snippet now nudges the LLM to call it during multi-step tasks; the bridge prompt mirrors the same nudge.
llui-agent@0.0.16
- Removed
wait_for_user_inputMCP tool descriptor (mirrors the LAP method removal in@llui/agent). - Improved
narratetool description loses the "pair withwait_for_user_input" sentence —narratenow stands alone as a one-way LLM → user signal.
Docs
docs/designs/10 Agent Protocol.md§5b retitled "In-app Visibility Surface" (was "Conversational Surface"). Strips theagentChat/wait_for_user_inputsubsections; rewrites the framing to "operate-and-narrate, conversation lives in the LLM client". Composition contract collapses from five slices to four. A historical note at the end explains why the chat surface was removed for any future reader.site/content/cookbook.md"Agent Conversational Surface" recipe retitled "Agent Visibility Surface". Wiring example drops the chat slice; rendering example drops the composer; tool table dropswait_for_user_input.examples/github-explorer/src/views/agent-panel.tsworked example loses its chat composer block.
2026-05-01 — @llui/dom@0.0.37
Released: @llui/{dom,components,transitions}@0.0.37; @llui/{router,test}@0.0.38; @llui/vike@0.0.39; @llui/agent@0.0.51; @llui/mcp@0.0.32; llui-agent@0.0.15
Two same-day fixes: nested-each DOM mutations no longer break parent reconcile, and the new chat composer's event handlers actually fire (the camelCase requirement bit me in the agentChat bag types).
@llui/dom@0.0.37
- Fixed Nested-each reconcile no longer throws
InvalidNodeTypeErrorfromRange#setEndAfterwhen an inner structural primitive replaces its territory between an outer render snapshot and the next outer reconcile. The bulk-remove paths (reconcileClear, fast-path-1 clear, fast-path-5 full-replace) used to anchorrange.setEndAfter(lastEntry.lastNode)against the most recent entry's tail node — but a nestedeach/branch/showcould detach that node out from under the outer block, leavingsetEndAfterto throw on a parent-less node. The fix adds a stableeach-endcomment-anchor owned by eacheach()block and threads it throughreconcileEntriesso bulk Range ops always span the two outer comments regardless of inner-each / show / branch mutations in between.
@llui/agent@0.0.51
- Fixed
agentChat.connect()'s prop bag declaredoninputandonkeydown(lowercase). LLui's element runtime only attaches keys matching/^on[A-Z]/as DOM event listeners (packages/dom/src/elements.ts:72); lowercase silently degrades to a string attribute. The visible symptom: typing into the in-app chat composer never firedSetInput,pendingInputstayed empty, and the submit button stayed disabled forever. Renamed the bag's keys + types + handlers toonInput/onKeyDown, updated the doc comments with a sharp warning, and adjusted the 19agentChattests. No behaviour change for any other slice;agentConnect/agentConfirm/agentLogwere already camelCase.
@llui/components@0.0.37, @llui/transitions@0.0.37, @llui/router@0.0.38, @llui/test@0.0.38, @llui/vike@0.0.39, @llui/mcp@0.0.32, llui-agent@0.0.15
- Cascade release picking up the new
@llui/dom@0.0.37peer range. No package-level source changes.
2026-05-01 — @llui/agent@0.0.50, llui-agent@0.0.14
Released: @llui/agent@0.0.50; llui-agent@0.0.14
In-app conversational surface for the agent: chat composer (the user's voice), visual attention layer (the framework directs the user's eye to changed regions), narrate() LAP method (the agent's prose), and the plumbing that turns the activity log into a real chronological timeline. The user's LLM stays external (BYOL, cross-app context, no per-app cost); the conversation now lives inside the host app's window.
@llui/agent@0.0.50
- Added
agentChatnamespace — Level 1 slice owning the in-app chat composer's editor state.init/update/Msg/connect()shape matching the existingagentConnect/agentConfirm/agentLogsiblings. The reducer guards double-submit and whitespace-only sends;connect()returns a static prop bag with reactive accessors (input value, disabled state, Enter/Shift+Enter handling, submit button props,canSubmitpredicate). Submit firesAgentChatSendInput { text, at }which the framework's effect handler chains throughWsClient.submitUserInput(one upstreamuser-input-submittedWS frame + one synthesizedLogEntry { kind: 'user-input', detail: text }mirrored locally so the activity feed renders the user's reply inline with agent actions) followed bySubmitCompleteto re-enable the input. - Added
agentAttentionnamespace — visual attention layer. Listens for the sameAppend { entry }payloadagentLogaccepts (factory fans a singlelog-appendto both slices), extracts top-level paths fromLogEntry.stateDiff(with'/'collapsing to wildcard'*'), recordslatestDispatch: { entryId, paths, variant, intent, at }, firesAgentAttentionFlashTimeoutfor race-tolerant auto-clear (theClear { entryId }Msg returned by the timer no-ops if a fresher dispatch already replaced the spotlight).connect()exposesflashing(path),flashClass(path, className?),regionAction(path), andlatestDispatchaccessors. - Added
entryDiff(id)accessor onagentLog.connect()'sConnectBag<S>— memoized reactive accessor returning the entry's JSON-PatchstateDiff(ornullfor entries without one or unknown ids). Memoized per-id, looks up against the raw entries (not the visibility filter) so a diff sidecar over a hidden entry still resolves. - Added
LogEntry.detailauto-narration inws-client— schema-freek=vsummary ofsend_messagepayloads (first 3 non-typefields, objects rendered as keysets, arrays as length, strings JSON-quoted, all values truncated at 30 chars). Populates the existing-but-previously-unuseddetailfield; surfaces a glanceable second line underintenteven for variants without@intent. - Added
narrate()LAP method — the agent pushes prose into the activity feed without inventing fake@agentOnlyMsgs. Server handler synthesizesLogEntry { kind: 'narrate', detail: text, intent }and pushes a newlog-pushserver-frame to the paired runtime; ws-client mirrors it viaonLogEntrylocally and echoes alog-appendupstream so the recent-log buffer + audit sink see it through the existing browser → server channel — single audit pathway, no double-record. - Added
LogKind: 'user-input' | 'narrate'— distinct chips/styles in the activity feed for the user's typed replies and the agent's commentary. - Added
summarizeDiff/groupDiff/describeOpexports from@llui/agent/client— pure renderers that turn JSON-Patch into one-line headlines ("3 changes in cart"/"2 items added across 3 regions"), per-region structured breakdowns, or short verb + dotted path strings ("changed cart.total"). Schema-free; the host renders however it likes. - Added
@llui/agent/styles/agent-panel.css— opt-in default stylesheet shipping a.agent-flashkeyframe withprefers-reduced-motionfallback, per-LogKindcolour hints (via[data-scope='agent-log'] [data-part='entry'][data-kind='…']selectors), tunable CSS custom properties (--llui-agent-flash-color,--llui-agent-flash-duration, etc.), and a chat-composer disabled-state style. Hosts that want a panel that works on first paint import this; production apps override the custom properties or write their own keyframes. - Added
UserInputStorageadapter interface +coreoption — optional persistence for the chat composer's user-input buffer across runtime restarts. Cloudflare Durable Object hosts get a ready-made adapter from@llui/agent/server/cloudflare's newmakeDurableObjectUserInputStorage(state.storage); pass the result tonew AgentPairingDurableObject({ userInputStorage: … })and buffered messages survive DO eviction. ParkedwaitForUserInputwaiters can't be persisted (they're JS Promise resolvers); the LAP client retries naturally and the restored buffer drains on the retry. Calls are best-effort: storage outages cause lost messages on eviction but never wedge a live conversation. - Improved Factory wiring: a single inbound
log-appendnow fans out to BOTHwrapLogMsgandwrapAttentionMsgwhen both are wired (the host's reducer sees the same entry through different sub channels and routes to the right slice). New optional slices:wrapAttentionMsg,wrapChatMsg. NewgetWsClienthost hook so the chat-send effect handler can find the active client lazily across the connection lifecycle (graceful no-op pre-open / post-close — the input field re-enables either way). - Improved
LogPushFrame(server → browser) added toServerFrame; ws-client handlest: 'log-push'by mirroring throughonLogEntryand echoinglog-appendupstream so the existing audit pathway captures server-originated entries.
llui-agent@0.0.14
- Added
wait_for_user_inputMCP tool — long-poll the in-app chat composer's submission queue. Returns{ status: 'submitted', text, at }on receipt of auser-input-submittedWS frame, or{ status: 'timeout' }aftertimeoutMs(default 30s). Submissions buffer briefly when no waiter is parked (8-message FIFO with drop-oldest on overflow) so a user typing before the agent reaches the tool call still gets through. - Added
narrateMCP tool — push a one-line prose update into the activity feed without dispatching a Msg. Use before long-running actions, to surface inferred reasoning, or to acknowledge user input before acting. Returns{ ok: true }once the host runtime has the entry.
Docs
docs/designs/10 Agent Protocol.mdgains "5b. In-app Conversational Surface" — explains the architectural reframe (protocol-mediated external LLM stays right; the missing piece was an in-app surface for the LLM's presence and the user's voice) and the composition contract for the five slices.- Cookbook adds an "Agent Conversational Surface" recipe walking through host wiring, panel rendering, the visual attention layer, the helper utilities, and a tool-selection table for
send_message/narrate/wait_for_user_input/wait_for_change. examples/github-explorer/src/views/agent-panel.tsextended end-to-end with all five slices composed (connect + confirm + log + attention + chat). Activity rows now show payloaddetail+ diff summary; chat composer wires up viaagentChat.connect()'s prop bag with no extra event-handling code in the host.
2026-05-01 — @llui/dom@0.0.36
Released: @llui/{dom,components,transitions}@0.0.36; @llui/{router,test}@0.0.37; @llui/vite-plugin@0.0.40; @llui/vike@0.0.38; @llui/agent@0.0.49; @llui/mcp@0.0.31; llui-agent@0.0.13
Hotfix: a component whose update case modifies multiple state fields and incidentally resets one to [] (e.g. { ...state, open: true, name: '', tags: [] }) used to go structurally inert after mount — propsMsg and update fired, the new state landed, but every show / branch / scope block in the view stopped reacting because their when / on accessors never re-evaluated. The compiler's per-message handler routed the case through an each-only reconcile method that no-ops on non-each blocks. Fixed at both the compiler and the runtime.
@llui/dom@0.0.36
- Fixed Phase 1 reconcile in
_handleMsgfalls back toblock.reconcile(s, dirty)when the specialized method (reconcileItems/reconcileClear/reconcileRemove/reconcileChanged) is undefined on a selected block. Previously, a compiler-emitted handler withmethod=2(clear) would invokeblock.reconcileClear?.()on every block whose mask intersected the case's dirty bits — but those specialized methods only exist oneachblocks.show/branch/scopeblocks silently no-opped, leaving theirwhen/onaccessors stuck at the mount-time evaluation. The defense-in-depth fallback ensures non-each blocks still reconcile correctly even if a compile-time miss slips through.
@llui/vite-plugin@0.0.40
- Fixed
detectArrayOponly emits'clear'/'mutate'/'remove'/'strided'when the case modifies exactly one field AND that field is the one with the array op. A multi-field case like{ ...state, open: true, name: '', tags: [] }previously matched on the firsttags: []it walked and routed the entire case tomethod=2, bypassingblock.reconcilefor show/branch blocks gated onopenorname. Multi-field cases now fall through to'general'(method=0). Sister of themethod=-1fix inshow-helper-reconcile.test.ts— same architectural rule: optimizations that route aroundblock.reconcilemust hold for every primitive, because the compiler can't see helpers and library overlays (e.g.dialog.overlayfrom@llui/components, which uses show internally).
@llui/components@0.0.36, @llui/transitions@0.0.36, @llui/router@0.0.37, @llui/test@0.0.37, @llui/vike@0.0.38, @llui/agent@0.0.49, @llui/mcp@0.0.31, llui-agent@0.0.13
- Cascade release picking up the new
@llui/dom@0.0.36peer range. No package-level source changes.
2026-04-30 — @llui/dom@0.0.35
Released: @llui/{dom,components,transitions}@0.0.35; @llui/{router,test}@0.0.36; @llui/vike@0.0.37; @llui/agent@0.0.48; @llui/mcp@0.0.30; @llui/eslint-plugin@0.0.22; llui-agent@0.0.12
Enforce the accessor-purity contract end-to-end. sample() (and h.sample()) called from inside any structural-primitive accessor (each.{items,key}, branch.on, show.when, scope.on, child.props, foreign.props) or a binding accessor (text(s => …), unsafeHtml(s => …)) now throws a targeted runtime error at the first invocation — typically initial mount, before the bug can ship. A new ESLint rule catches the same antipattern at edit time. Phase 1 reconcile gains the same try/catch defense Phase 2 already had: a thrown accessor surfaces via _onBindingError instead of dying silently in a microtask.
Breaking
@llui/dom@0.0.35—sample()/h.sample()calls from inside an accessor now throw at the first invocation with a targeted error. The previous behaviour was undefined: the accessor's mask analysis silently dropped the hidden dep (so the block was mask-gated out when the sampled state changed), and on the reconcile paths where the accessor did run,sample()threw a generic "outside render context" error mid-flush that escaped to an unhandled microtask. Apps that depended on the prior accidental behaviour need to lift the dep into the accessor's parameter — bake outer state intoitems, return a wider object fromprops, etc. The lint rulellui/no-sample-in-accessorflags every site at edit time.
Migration
- For
each().keyreading sibling state viasample(), replace with the items-map pattern. Before:
After:each({ items: (s) => s.rows, key: (it) => `${it.id}|${sample((s) => s.rev)}` })
The same shape applies to the other accessors: lift the dep into the parameter so the compiler's mask analysis can see it. The lint rule's error message points at the corresponding workaround.each({ items: (s) => s.rows.map((it) => ({ it, rev: s.rev })), key: (r) => `${r.it.id}|${r.rev}`, })
@llui/dom@0.0.35
- Fixed
sample()inside an accessor used to fail silently. The compiler's mask analysis only walksparam.Xreads on the accessor's parameter, so a read viasample(s2 => s2.X)was invisible — the dep silently dropped from the structural block's mask. When the hidden dep changed, the block was gated out and reconcile never fired; when reconcile did fire (e.g. on a sibling change), the accessor threw atsample()because there's no render context during the update phase, and the throw escaped the update loop into a swallowed microtask. The new runtime accessor stack (inrender-context.ts) letssample()detect every accessor call site by name (each().key,each().items,branch().on,show().when,child().props,foreign().props,a binding accessor) and throw a targeted error pointing at the lift-into-parameter workaround. The fail-fast happens at initial mount, so the bug can't ship. - Improved Phase 1 structural reconcile errors now flow through
_onBindingError(parallel to Phase 2 binding errors) instead of escaping the update loop. A throwing accessor no longer kills the rest of the update or vanishes into an unhandled microtask rejection — the dev/agent integration surfaces the error in the same channel as binding accessor throws, withkind: 'reconcile'to distinguish it.
@llui/eslint-plugin@0.0.22
- Added
llui/no-sample-in-accessorrule (recommended aterror). Flagssample()/h.sample()calls insideeach.{items,key},branch.on,show.when,scope.on,child.props,foreign.props, and the binding helperstext/unsafeHtml. Catches the runtime-throw antipattern at edit time with zero runtime cost. The walker intentionally does not descend into nested function bodies, so asample()inside an event handler attached during render (a non-accessor closure) is not flagged. Sister rule ofno-sample-in-reactive-position, which catches the adjacent "sample's result in a reactive position" antipattern.
@llui/components@0.0.35, @llui/transitions@0.0.35, @llui/router@0.0.36, @llui/test@0.0.36, @llui/vike@0.0.37, @llui/agent@0.0.48, @llui/mcp@0.0.30, llui-agent@0.0.12
- Cascade release picking up the new
@llui/dom@0.0.35peer range. No package-level source changes.
Docs
docs/designs/03 Runtime DOM.md— added an "accessor purity contract" paragraph in theeach()section spelling out the construction-vs-update phase split, the mask-gating implication, and the runtime + lint enforcement.docs/designs/09 API Reference.md—sample()description updated with the accessor restriction and pointer to the lint rule.
2026-04-29 — @llui/agent@0.0.47, llui-agent@0.0.11
Released: @llui/agent@0.0.47; llui-agent@0.0.11
Fix: panel correctly transitions to "Connected" after WS re-pair (page refresh / brief drop).
@llui/agent@0.0.47
- Fixed
acceptConnectionnow sends the'active'frame to the new WS on the re-pair branch. The grace-window re-pair path callsmarkActivedirectly to skip the awaiting-claude → active transition, but the matching browser notification was missing — leaving the page stuck onpending-claude("Waiting for AI to claim") indefinitely after a refresh, even though the session was fully alive.ensureActivecouldn't help on subsequent LAP calls because the record was alreadyactiveby then.
llui-agent@0.0.11
- Cascade release for
@llui/agent@0.0.47. No bridge-level changes.
2026-04-29 — @llui/agent@0.0.46, llui-agent@0.0.10
Released: @llui/agent@0.0.46; llui-agent@0.0.10
Fix: the panel's connect status now correctly flips from "Waiting for AI to claim" → "Connected" when the AI binds via /observe, not just /describe.
@llui/agent@0.0.46
- Fixed
markActive+'active'browser frame fire from any LAP call, not just/describe. Thellui-agentMCP bridge connects via/observe(the unified bootstrap endpoint), so the previous describe-only path left the panel stuck onpending-claudeindefinitely — even though LAP calls worked. Centralised the transition inlap/active.ts:ensureActive; every LAP handler runs it after auth+paired (observe, message, confirm-result, wait, describe, recent-actions, every forward handler). The transition is no-op when the record isn'tawaiting-claude, so it's cheap and idempotent on every call.
llui-agent@0.0.10
- Cascade release for
@llui/agent@0.0.46. No bridge-level changes.
2026-04-29 — @llui/agent@0.0.45, llui-agent@0.0.9
Released: @llui/agent@0.0.45; llui-agent@0.0.9
Robust session lifecycle. Brief network drops, server restarts, page reloads, and explicit user disconnects now all behave correctly without putting the LLM in a confusing state. Session = (token, tid) is the durable identity; the WS is just the realtime delivery channel and can churn freely.
Breaking
@llui/agent@0.0.45—AgentConnectStatusadds'reconnecting'and'failed'variants;AgentConnectStateaddsreconnectAttemptandreconnectElapsedMsfields;AgentConnectPendingTokenaddswsUrl. UI code that switches over the status union or destructures the state shape needs updating to match. New MsgsDisconnect,ReconnectAttempt, andReconnectGaveUpare also part of the union; exhaustive Msg matchers must add cases (or fall through). The default behaviour forWsClosedwhile apendingTokenis set is now "schedule auto-reconnect" instead of "zero state to idle" — apps that explicitly want the old behaviour should dispatchDisconnectfrom the same site.
Migration
- Hosts that wired the legacy
AgentSessionPersist/AgentSessionCleareffects tosessionStoragethemselves can keep doing so — the framework's auto-handler co-exists. To migrate cleanly, passsessionStorage: nulltocreateAgentClientif you want only your handler to run, or remove your handler and let the framework own it (default key:'llui-agent:session'). - Any app that surfaced the connect status in its UI should add a render path for
'reconnecting'(compact "reconnecting…" pill is the canonical UX) and'failed'(the loop gave up; offer a manual retry). - An explicit "Disconnect" button in the agent panel should dispatch the new
DisconnectMsg instead ofRevoke— same revoke behaviour PLUS clears persisted credentials and short-circuits the reconnect loop.
@llui/agent@0.0.45
- Added WS-close grace window.
createLluiAgentCore({ pendingResumeGraceMs })(default 60s) controls how long a token's record stays inpending-resumeafter the WS closes. During the window, a reconnect with the same bearer re-pairs without rotating —acceptConnectiondetects the pending-resume record and callsmarkActivedirectly, so the agent's existing token stays valid the whole time. Wires up dead code that was sitting in the protocol/storage layer (markPendingResume,pending-resumestatus,resume/listfiltering) — all become live for the first time. Set0to opt out (legacy behaviour: WS close immediately drops the record, reconnect must rotate via/resume/claim). - Added
Retry-AfterandX-LLui-Reconnect: pending|revoked|expired|unknownheaders on every 503pausedresponse (centralized inbuildPausedResponse). The MCP bridge can distinguish "WS bouncing, will be back" from "session is dead, paste a new snippet" instead of guessing from a bare 503. - Added Browser auto-reconnect with exponential backoff (1s → 2s → 4s → 8s → 16s → 30s cap, 5-minute cumulative ceiling, then
'failed'). Uses the cachedwsUrl/tokendirectly — no mint round-trip, no/resume/claimrotation. The reducer schedulesAgentReconnectScheduleeffects; the handler is a thinsetTimeoutwrapper. UserDisconnectshort-circuits the loop via the reducer's status guard (no cancel handles needed). - Added Framework-owned session persistence.
AgentSessionStorageadapter passed tocreateAgentClient(default =defaultSessionStorage()readingwindow.sessionStorageunder'llui-agent:session'). Onstart()the framework reads the blob and auto-dispatchesRestoreSessionif a non-expired session is present;MintSucceededwrites;Revoke/Disconnectclear. Hosts can passsessionStorage: nullto opt out (legacy host-handledAgentSessionPersist/AgentSessionCleareffects still flow through). - Added
DisconnectMsg — explicit user-initiated revoke. Clears credentials, blocks any in-flight reconnect timer, dispatchesAgentRevoke+AgentSessionClear+AgentCloseWS. Distinct fromRevoke(per-tid revoke from the sessions list — keeps reconnect-loop semantics for non-active tids). - Fixed
markPendingResumeno longer liftsrevoked/expiredrecords into a fresh grace window. The transition is guarded toactive/awaiting-claudeonly, so a stale WS-close after a deliberateRevokecan't accidentally resurrect the session.
llui-agent@0.0.9
- Cascade release for
@llui/agent@0.0.45. No bridge-level changes.
2026-04-29 — @llui/vite-plugin@0.0.39
Released: @llui/vite-plugin@0.0.39
@should / @validates JSDoc annotations are now read from every nested-field site, not just top-level Msg variant fields. Apps that put guidance on domain types (e.g. interface Alternative.image) finally see it surface in the agent's payloadHint and fieldHints.
@llui/vite-plugin@0.0.39
- Fixed Schema extractor reads
@shouldand@validatesJSDoc on every nested-field call site: interface members, inline-object members, and discriminated-union variant fields. Previously only top-level Msg variant fields (the ones that flow throughbuildFieldDescriptor) honored these annotations; nested fields silently dropped the JSDoc, so domain types annotatinginterface Alternative.imageorQuantity.formatgot nothing in the agent's surface. NowMatrix/AddAlternatives's synthesized example showsimage: ""with the hint pointing at Wikimedia, and an annotatedformatonQuantitysurfaces alongside the discriminant-kind hint. JSDoc source is recovered frommember.getSourceFile().textso cross-file domain types (annotations in@decisive/domainreferenced fromapps/web) carry their hints transparently. Centralized field resolution into aresolveMember()helper used by all three call sites — sameT | undefinedpeel rules, same JSDoc rules.
2026-04-28 — @llui/agent@0.0.44, llui-agent@0.0.8
Released: @llui/agent@0.0.44; llui-agent@0.0.8
Page-refresh now preserves an active agent session — the AI's existing token stays valid because the browser reattaches a new WS without going through the rotate-on-resume path. Connect-snippet prefix is now the concrete mcp__llui__connect_session instead of the placeholder mcp__<server>__connect_session.
@llui/agent@0.0.44
- Added
RestoreSessionMsg +AgentSessionPersist/AgentSessionCleareffects.MintSucceedednow also emitsAgentSessionPersistalongsideAgentOpenWSso the host can write the credentials tosessionStorage. On boot, the host reads them back and dispatchesRestoreSession— the reducer re-enterspending-claudeand re-opens the WS without minting. The agent's existing token stays valid because we don't go through/resume/claim(which rotates by design).Revokeof the active tid emitsAgentSessionClearso the persisted blob doesn't outlive the server-side session. Hosts that don't implement the persist/restore loop can ignore both effects — the rest of the connect lifecycle still works (the page falls back to "mint a new session" after refresh, same as before this effect existed). The existing/resume/claimflow stays for the browser-closed-and-reopened case wheresessionStorageis gone buttidsinlocalStoragemay still be alive on the server; that path must rotate the token because the previous bearer might be leaked. - Improved Connect-snippet now uses the concrete prefix
mcp__llui__connect_session(matches theclaude mcp add --transport stdio llui ...install command in the docs) instead of the placeholdermcp__<server>__connect_session. Some LLMs were copying the placeholder name literally; the concrete form removes the ambiguity for the common case. Users who renamed the server in their MCP config can substitute their name.
llui-agent@0.0.8
- Cascade release for
@llui/agent@0.0.44. No bridge-level changes.
2026-04-28 — @llui/vite-plugin@0.0.38, @llui/agent@0.0.43, llui-agent@0.0.7
Released: @llui/vite-plugin@0.0.38; @llui/agent@0.0.43; llui-agent@0.0.7
Eliminate two compounding sources of bogus 'unknown' schema fields and stop the synthesizer from emitting null for the rest. A fresh-LLM dogfood failed when Matrix/AddCriteria's payloadHint contained clamp: null, bound: null, ease: null — the agent copied them verbatim, the validator passed (it exempts unknowns), and the renderer crashed reading .kind off null.
@llui/vite-plugin@0.0.38
- Improved Schema-extractor depth budget now decrements only on named-type lookups, not on inline structural moves (array element, inline object literal, inline discriminated-union variants). Cyclic types still terminate via the named-lookup decrement; deeply-nested but finite type trees fully resolve. Concrete:
Matrix/AddCriteria.criteria[].type(quantity).clampresolves to its full discriminated-union shape instead of collapsing one hop short. Deepens what the agent sees without growing the budget constant or the bundle size. - Added Detect
T | undefined(andundefined | T,T1 | T2 | undefined) as optional T at every property-resolver site (top-level Msg variants, inline object literals, interface bodies, DU variant fields). Decisive-stylefield: T | undefinedno longer extracts as required+unknown; the agent can omit the field instead of having to spell outfield: undefinedliterally.
@llui/agent@0.0.43
- Fixed
list_actionssynthesizer omitsunknown-typed fields frompayloadHintinstead of emittingnull. Emittingnullmisled agents into copying it verbatim — the validator let it through (it exempts unknowns), the value landed in state, and consumer code crashed onnull.kind/null.length. The agent should now consultdescription.messagesfor the field's actual shape when the example doesn't mention it. Empty array[]replaces[null]for arrays whose element schema isunknown.
llui-agent@0.0.7
- Cascade release for
@llui/agent@0.0.43. No bridge-level changes.
2026-04-28 — @llui/router@0.0.35, @llui/agent@0.0.42, llui-agent@0.0.6
Released: @llui/router@0.0.35; @llui/agent@0.0.42; llui-agent@0.0.6
Two dogfood gaps closed against decisive: route-changing effects from arbitrary reducers now keep state.route in sync with the URL, and the agent payload validator stops rejecting required-but-unknown-typed fields when missing.
@llui/router@0.0.35
- Added
connectedRouter.navigate(route)effect — pushState plus dispatch the listener-captured navigate message in one operation. Resolves the asymmetry wherelink()did push+send (it has send/factory in scope at click time) whilepush()did pushState only, leaving apps with desyncedstate.routewhenever a non-Router/Navigatereducer programmatically navigated. Existingpush()/replace()keep their URL-only semantics for the inline-RouteChanged pattern; switch tonavigate()when you want the framework to handle the round-trip. Ifnavigate()runs beforeconnectedRouter.listener()mounts, the URL still updates and aconsole.warnsurfaces the gap.
@llui/agent@0.0.42
- Fixed Payload validator no longer rejects required fields whose schema is
unknownwhen they're missing from the payload. The schema extractor emitsfield: string | undefinedas required+unknown(the union isn't a branded primitive), but the validator's stated philosophy is "treat 'unknown' as any goes" — agents were forced to spell outdetails: undefined,url: undefined, … on every authored object, defeating the payload hints. Strict-mode unknown-field warnings are unaffected; this only relaxes the missing-field branch.
llui-agent@0.0.6
- Cascade release for
@llui/agent@0.0.42. No bridge-level changes.
2026-04-28 — 0.0.34 + @llui/agent@0.0.41, @llui/vite-plugin@0.0.37, @llui/test@0.0.35, @llui/vike@0.0.36, @llui/mcp@0.0.29
Released: @llui/{dom,components,router,transitions}@0.0.34; @llui/test@0.0.35; @llui/vike@0.0.36; @llui/mcp@0.0.29; @llui/agent@0.0.41; @llui/vite-plugin@0.0.37
Four post-dogfood improvements: @agentOnly respects agentAffordances, per-binding throw isolation in the runtime, current URL on describe_visible_content, and a new @routeGated JSDoc tag for compile-time affordance gating.
Breaking
@llui/dom@0.0.34—AppHandleaddssetOnBindingError(hook | null): void. Custom AppHandle implementations (test fakes, mocks, adapter layers) need to provide it;setOnBindingError: () => {}is a fine no-op for callers that don't need the hook.@llui/agent@0.0.41—DescribeVisibleResultaddsurl: string | null.MessageAnnotationsaddsrouteGate?: string | null. Both shapes are additive; exhaustive type assertions over the result need updating.
@llui/dom@0.0.34
- Added Per-binding throw isolation in the Phase-2 update loop. A single accessor that throws (e.g. scoring fails on a malformed criterion) now leaves its binding's
lastValueunchanged — DOM stays at the previous value rather than going blank — and continues with sibling bindings on the same commit. Reverses the previous "one bad accessor freezes the entire view" UX. - Added
inst._onBindingErrorruntime hook (internal field) and the publicAppHandle.setOnBindingError(hook)accessor. The agent factory wires it into the dispatch envelope'sdrain.errors, so the LLM sees that a binding crashed without the dispatch reporting transport failure. Without a hook, throws fall back toconsole.error(dev mode, with the existing rich wrapped error message — component name, kind, node descriptor, accessor source) orconsole.warn(prod).
@llui/agent@0.0.41
- Added
@agentOnlyschema-source variants now respectagentAffordances(state). When the app provides an affordances hook,@agentOnlyMsgs surface only when the hook returns them — bulk-edit Msgs likeMatrix/AddCriteriastop surfacing on routes where they don't apply. Apps withoutagentAffordanceskeep the previous permissive default ("everything tagged@agentOnlyis always available"). - Added
describe_visible_contentreturns the user's current URL (url: string | null, read fromwindow.location.href). The agent uses this to verify "did my dispatch actually navigate the user?" — apps that bundle navigation into a Msg's effect chain update the URL on commit; the agent reads it back here to confirm the user's view tracked the state change. - Added
@routeGated("predicate")annotation evaluated at affordance time. The compiler captures the predicate verbatim; the runtime evaluates withstatebound and gates the variant fromlist_actionswhen the predicate returns falsy. Compile-time alternative to runtimeagentAffordances(state) => Msg[]for the common "this Msg is reachable when state.X looks like Y" case. - Improved Agent factory wires
setOnBindingErrorto push entries intodrain.errors. A binding crash during a dispatch lands in the dispatched envelope (status: 'dispatched'with the error reported) — sibling bindings update normally, so the page survives.
@llui/vite-plugin@0.0.37
- Added
@routeGated("predicate")JSDoc tag captured intoMessageAnnotations.routeGateand emitted into the runtime annotations object. Mirrors@validates's grammar but withstateas the bound variable instead ofv(the predicate sees the whole app state, not a single field value).
Cascade releases
@llui/{components,router,transitions}@0.0.34,@llui/test@0.0.35,@llui/vike@0.0.36,@llui/mcp@0.0.29— peer-dependency cascade for@llui/dom@0.0.34. No package-level changes.
2026-04-28 — @llui/agent@0.0.40, @llui/eslint-plugin@0.0.21
Released: @llui/agent@0.0.40; @llui/eslint-plugin@0.0.21
Six follow-up improvements pulled from a real dogfood session against decisive.space-2. The previous batch closed the schema-fidelity gap; this batch closes the agent-experience gaps that surface once an LLM is actually driving an app.
Breaking
@llui/agent@0.0.40—would_dispatchadds a new result status'reducer-threw'(withmessageand optionalstack) for the case where the candidate Msg's reducer throws during prediction.LapDrainMetaadds an optionalwarnings?: Array<{path, code, message}>field. Both shapes are strictly additive; existing handlers that exhaustively switch overstatusneed to add cases or fall through.
@llui/agent@0.0.40
- Added
list_actionsfilters bindings by Msg-schema membership. Library-internal Msgs leaking throughtagSend(the sortable component'smove/drop/cancel/start/toggleGrab/moveByetc.) no longer show up in the agent's affordance list. Schema absence (older builds) keeps the previous permissive behavior so this is safe to ship. - Added
would_dispatchcatches reducer throws as{status: 'reducer-threw', message, stack?}— same Phase-5 contractsend_messagegot last release. The agent's safety net no longer crashes alongside the candidate dispatch. - Added
@validates(...)predicate text surfaces as afieldHint("validates: v >= 0 && v <= 100") at affordance time. The agent reads the constraint when shaping its first attempt, not as an after-the-fact rejection. - Added Validator warnings propagate from the strict-mode validator to the dispatch envelope as
drain.warnings. New optionalgetDispatchPolicy()host accessor lets a server opt into strict; default stays lenient and omits the field entirely. - Added Framework-tracked
LastDispatchOutcome. The WS layer captures everysend_messageoutcome (dispatched/rejected/reducer-threw);describe_contextprepends a syntheticLAST DISPATCH …hint when the most recent outcome had errors or warnings. Apps no longer need to maintain their ownlastDispatchErrorstate field — the framework owns it.
@llui/eslint-plugin@0.0.21
- Added
agent-tagsend-translator-missingrule. Flags*.connect(get, send, ...)calls where the second argument is the raw componentsendrather than a translator ((libMsg) => send({type: 'X', msg: libMsg})). The bare-sendpattern is exactly what leaks library Msgs into the binding registry, polluting the agent's affordance list. The rule's message includes the wrap suggestion inline. Ships inrecommendedandagentconfigs at error severity.
2026-04-28 — @llui/agent@0.0.39, @llui/vite-plugin@0.0.36
Released: @llui/agent@0.0.39; @llui/vite-plugin@0.0.36
Five-phase upgrade to agent-boundary validation. The framework now generates a runtime validator from the Msg union's TS types (cross-file shape transitivity, branded primitives, discriminated unions, optional @validates predicates), runs it for agent-driven dispatches, and catches downstream throws so a partial-failure dispatch reports as dispatched with errors in drain.errors rather than masquerading as HTTP 500. Reducers can now trust their inputs are well-formed.
Breaking
@llui/vite-plugin@0.0.36—MsgFieldTypeadds a new richer-descriptor fieldvalidates?: string(the captured@validates(...)predicate). The compiler emits it alongsideoptional/priority/hint. Code that read__msgSchemadirectly and exhaustively typed the rich-descriptor shape needs to widen for the new field. Apps that didn't poke at the schema directly are unaffected.@llui/agent@0.0.39—validatePayloadadds new error codes'unexpected-field'(strict mode catches typos / hallucinated keys) and'validates-failed'(predicate rejection). Existing handlers that exhaustively switch oncodeneed to add cases or fall through. New optional 3rd argument tovalidatePayload:{ policy: 'strict' | 'lenient' }. Default stays lenient.
Migration
- For most apps: no migration needed. The schema gets richer automatically; the validator gets stricter only when you opt in via
policy: 'strict'. - If you had reducers with hand-written semantic guards (e.g. "this criterion's
easefield must be a{kind: ...}object, not a string"), those guards are now redundant for agent-driven dispatches — the cross-file resolver fully resolves the shape and the validator rejects malformed payloads upstream of the reducer. - For domain invariants the type system can't express (numeric ranges, format predicates, length bounds), tag the field with
@validates("predicate-expression"). The predicate hasvbound to the field value at runtime.
@llui/vite-plugin@0.0.36
- Added transitive cross-file shape resolution. The
buildEnrichedTypeIndexwalk now follows imports recursively — whenCriterionis imported fromdomain.tsandCriterionitself referencesEaseFunction(imported bydomain.tsfromease.ts), the resolver pullsease.ts's declarations into the index too. Previously the inner types collapsed to'unknown'; now the full discriminated-union descriptor lands in the schema. Closes the schema gap that produced theease: 'linear'agent-side guess in dogfood testing. - Added branded-primitive resolution.
string & {__brand: 'UID'},number & {readonly __brand: 'Cents'}, etc. emit as their underlying primitive ('string','number') so the validator's typeof check passes for any primitive value rather than rejecting against'unknown'. Intersections that mix in real (non-__-prefixed) fields are left alone — those aren't brands. - Added
@validates("predicate")JSDoc tag captured into the rich field descriptor. Examples:@validates("v >= 0 && v <= 100")for a numeric range;@validates("/^[a-z0-9-]+$/.test(v)")for a slug format;@validates("v.length > 0")for a non-empty string. Predicates run at the agent boundary only — TypeScript validates the call site for human dispatches. - Improved transitive walk silently skips imports that fail to resolve (bare specifiers like
'fs', vite-externalized modules) rather than throwing. Consequence: the schema extractor is robust to non-type-relevant imports anywhere in the transitive closure.
@llui/agent@0.0.39
- Added
validatePayload(msg, schema, opts?)accepts a newpolicyoption.'strict'rejects fields not in the schema (typos, hallucinated keys) withcode: 'unexpected-field'and emits warnings for'unknown'-typed fields the agent provided values for (code: 'untyped-field').'lenient'(default) accepts extras silently;'unknown'is a passthrough. - Added
@validates("...")predicate execution. The compiler emits the predicate string inMsgSchemaField.validates; the validator compiles it lazily withnew Function('v', 'return (' + src + ')')and caches. Predicate failures emitcode: 'validates-failed'with the predicate source in the message. Predicates run only after structural validation passes — a wrong-type field doesn't double-report. Malformed predicates degrade to no-op rather than breaking dispatch; predicates that throw at evaluation are treated as fail-closed. - Added Phase 5 catch-and-report at the dispatch boundary. A throw inside
host.send/host.flushduring asend_message(reducer crash, binding-evaluation crash, persist-effect crash) now lands indrain.errorsand the dispatch returns{status: 'dispatched', stateDiff, drain: {errors: [...]}}— instead of HTTP 500 /{status: 'rejected'}. The agent gets a structured "dispatch landed AND something errored downstream" signal and can self-correct or back off rather than retrying the same payload. - Improved
MsgSchemaFieldrich-descriptor type extended withvalidates?: string. The validator unwraps it via the existingfieldType()accessor pattern.
2026-04-27 — @llui/eslint-plugin@0.0.20
Released: @llui/eslint-plugin@0.0.20
Two more silent-staleness lints in the same family as the previous batch — both ship in recommended at error severity.
@llui/eslint-plugin@0.0.20
- Added
static-items— symmetric withstatic-on, applied toeach({items}). Flags factories that don't read state (items: () => [literal],items: (s) => CONST). When items doesn't read state the list builds once at mount and theeachnever reconciles — adds/removes/updates never appear in the DOM. - Added
no-sample-in-reactive-position— generalizesno-list-render-in-sample. Flagstext(sample(…))andunsafeHtml(sample(…))— passing sample's string return value to a reactive primitive typechecks (string is a valid static accessor) but the cell never updates.sampleis an opt-out of reactivity; the rule explains that and points at the right form (text((s) => …)ortext(item.field)).
2026-04-27 — 0.0.33 + @llui/agent@0.0.38, @llui/vike@0.0.35, @llui/mcp@0.0.28, @llui/eslint-plugin@0.0.19
Released: @llui/{dom,router,transitions,components}@0.0.33; @llui/test@0.0.34; @llui/vike@0.0.35; @llui/mcp@0.0.28; @llui/agent@0.0.38; @llui/eslint-plugin@0.0.19
Reactive ItemAccessor reads at the obvious call site (text(item.title), show({when: () => item.banned()}), branch({on: () => item.kind()})), a cookbook recipe + sample doc warning for variable-length lists, and two ESLint rules to catch the silent-staleness footgun before it ships.
Breaking
@llui/eslint-plugin@0.0.19—static-onrule loosens for zero-arg accessors that read from item / memo / closure sources (on: () => item.kind()is now valid). Bare-literal zero-arg bodies (on: () => 'tab') still fire. Apps using the new pattern stop false-positiving; apps with literal-bodied accessors see the same error as before.
@llui/dom@0.0.33
- Added
textandunsafeHtmlon the View bag and primitives accept() => Valongside(s: S) => V. The runtime already detected zero-arg accessors and routed them through the per-item updater path; the type widening letstext(item.title)typecheck. Eliminates thetext(_ => item.title())papercut that made the static-vs-reactive distinction easy to misread inside aneach.rendercallback. - Added
show.when,branch.on,scope.onaccept() => Vsimilarly. Same runtime path; same ergonomic win. - Improved
sample()docstring spells out the variable-length-list footgun and redirects to the cookbook recipe. The pattern (sample((s) => s.list.items.map(rowFn))) looks idiomatic but silently captures rows in closure; cells go stale on in-place updates.
@llui/eslint-plugin@0.0.19
- Added
no-eager-item-accessorflagstext(item.X())/unsafeHtml(item.X())— eager invocation captures the value at view-construction; the cell never updates when row state changes. Fix is to drop the():text(item.X)reads reactively. Ships inrecommendedat error severity. - Added
no-list-render-in-sampleflags.map()over a state-derived array inside asample()callback — exactly the antipattern that produces stale rendered rows. Useeach+ItemAccessorfor variable-length lists. Ships inrecommendedat error severity. - Improved
static-onaccepts zero-arg accessors whose body contains a CallExpression or MemberExpression (item accessors, memo readers, closure-captured selectors). Bare-literal bodies still fire.
Cascade releases
@llui/{router,transitions,components}@0.0.33,@llui/test@0.0.34,@llui/vike@0.0.35,@llui/mcp@0.0.28,@llui/agent@0.0.38— peer-dependency cascade for@llui/dom@0.0.33. No package-level changes.
Docs
- New cookbook recipe "List of editable rows — reactive cells over
each" walks through the correcteach+ItemAccessor+ reactive bindings shape, including the explicit anti-pattern note on wrapping a list insample().
2026-04-27 — @llui/agent@0.0.37, @llui/vite-plugin@0.0.35
Released: @llui/agent@0.0.37; @llui/vite-plugin@0.0.35
The schema the compiler emits for Msg payloads now describes discriminated unions and number / boolean literal unions, and would_dispatch / send_message validate every payload against it before the reducer runs. Together this collapses the agent's "guess a shape, dispatch, read prose error, guess again" loop into one round trip — the LLM sees the legal shapes upfront and gets path-keyed structured errors when it gets one wrong.
Breaking
@llui/vite-plugin@0.0.35—MsgFieldType(compiler) andMsgSchemaBareType(agent) gain adiscriminated-unionshape andenumwidens fromstring[]toReadonlyArray<string | number | boolean>. Code that read__msgSchemadirectly and assumedenum: string[]will need to widen its type. Apps that didn't poke at the schema directly are unaffected.@llui/agent@0.0.37—would_dispatchadds a third rejection variant:{status: 'rejected', reason: 'schema-mismatch', errors: ValidationError[]}. Existing handlers that matched onreason: 'invalid' | 'unsupported'need to also handle the new variant or fall through.send_messagecontinues to usereason: 'invalid'for the same failures, but thedetailstring is now a compactpath: message; path: messagelist rather than free-form English.
Migration
- Code reading
__msgSchema.variants[*].field: widen the type to accept the newdiscriminated-unionshape and the broadenedenumvalue type. The runtime check is oneif (t.kind === 'discriminated-union')arm. - Agents that called
would_dispatchand only handledreason: 'invalid' | 'unsupported': add a case forreason: 'schema-mismatch'. Theerrorsarray is structured ({path, code, message}) and is the recommended source —detailis no longer set on this rejection variant.
@llui/vite-plugin@0.0.35
- Added discriminated-union extraction. A field typed
A | B | Cwhose members are object literals sharing one literal-string discriminant property emits as{kind: 'discriminated-union', discriminant, variants}. Symmetric with how the top-level Msg union itself is encoded — same shape, recursed. - Added number-literal and boolean-literal unions emit as enum types.
1 | 2 | 3→{enum: [1, 2, 3]};true | false→{enum: [true, false]}. Mixed-type literal unions ('a' | 1) stay'unknown'rather than emit a misleading enum. - Added standalone literal types emit as single-element enums.
flag: true→{enum: [true]};value: 5→{enum: [5]}. - Improved
MAX_FIELD_DEPTH3 → 5. Realistic payloads (e.g.Matrix/AddCriteria.criteria[].format.kindat depth 4) now resolve fully instead of collapsing to'unknown'at depth 3.
@llui/agent@0.0.37
- Added
validate-payload.ts— schema-driven structured validator. Walks the compiled schema against a candidate Msg and returns path-keyedValidationError[]on mismatch. Error codes:unknown-variant,missing,wrong-type,not-in-enum,not-array,not-object,missing-discriminant,unknown-discriminant-value. Discriminated-union branches carry a disambiguating(discriminant=value)segment in the path so the LLM can see which branch the error applies to. - Added
would_dispatchruns the validator before the reducer; mismatches return{status: 'rejected', reason: 'schema-mismatch', errors}without firing reducer side-effects.WouldDispatchHostgains an optionalgetMsgSchema()accessor. - Improved
send_messagedelegates to the shared validator. The bespoke top-level-only validator (~80 LOC insidesend-message.ts) is gone. - Improved
list_actionssynthesizer emits the first branch of a discriminated-union field as the canonicalpayloadHintexample, andfieldHintscarry a syntheticDiscriminated union — set \` to one of: ...` summary at the union path so agents don't have to walk the schema for the simple case.
Docs
- Design doc 11 §2.3 adds a field-type coverage table mapping TypeScript shapes to schema emit. New §2.3a explains schema-driven validation with example errors.
2026-04-27 — @llui/agent@0.0.36
Released: @llui/agent@0.0.36
list_actions now derives the agent's affordance surface from the live binding graph only — what the user can click right now. Apps that exposed the full @intent-tagged Msg union to the LLM no longer cause "UI mysteriously rearranges when the agent dispatches" because hidden Msgs simply aren't listed.
Breaking
@llui/agent@0.0.36—list_actionsno longer surfaces'shared'Msg variants from the schema fallback. The new default is "what's affordable to the user right now": a'shared'variant is offered exactly when a tagged event handler is mounted in a live scope (refcount > 0). Variants in dead branches —show({when: false}), unmountedbranch()cases, removedeachitems — auto-vanish via the existingaddDisposermachinery. The explicit knobs for "agent should reach this regardless of UI state" are@alwaysAffordable(per-variant tag, now read by the runtime) andagentAffordances(state) => Msg[]on the component definition.@agentOnlyremains the canonical "no human path at all."
Migration
- Tag the bulk seed Msgs and agent-driven navigation that don't have a live UI binding with
@alwaysAffordable(or@agentOnlyif no human path exists at all). Concrete examples:Matrix/AddCriteria,Matrix/AddAlternatives,Matrix/SetManyCells,Matrix/Replace,Route/Navigate. - For
'shared'variants whose UI is currently open in the user's screen, no change is needed — their bindings are live, so they continue to surface. - For
'shared'variants whose UI is closed but you still want the agent to reach (e.g. cell-edit Msgs the agent should be able to dispatch without opening the editor first), tag them@alwaysAffordableor list them fromagentAffordances(state)for the screens where they should be reachable. - Escape hatch for the old behavior:
agentAffordances: () => allIntentVariantson the root component (returns every Msg unconditionally). Almost certainly wrong for non-trivial apps but useful as a temporary unblock.
@llui/agent@0.0.36
- Breaking
list_actionsdefault surface tightened — see top of release block. - Added
@alwaysAffordableJSDoc tag is now read by the runtime: tagged variants surface assource: 'always-affordable'regardless of binding state. Previously the tag was extracted by the compiler but ignored at runtime. - Fixed "UI gets messed up when the agent dispatches a Msg" — the agent's affordance surface now mirrors the user's, so dispatching a Msg never pops a hidden subtree into view in places the user didn't navigate to.
Docs
- Design doc 11 §1.1.4 (
@alwaysAffordable) and §4 (Source Tier) explain the new default and why off-screen'shared'variants are deliberately hidden. @llui/agentREADME's annotation table notes the default behavior and the@alwaysAffordable/agentAffordancesopt-in.- New
/agentssite section "Upgrading an existing install" explains thenpx -y llui-agent@latestcache-poke needed to pick up new releases.
2026-04-27 — @llui/agent@0.0.35, llui-agent@0.0.5, @llui/vite-plugin@0.0.34
Released: @llui/agent@0.0.35; llui-agent@0.0.5; @llui/vite-plugin@0.0.34
Two breaking agent-surface changes ship together: opaque random tokens replace JWTs, and the bridge's session tools drop their redundant llui_ prefix.
Breaking
@llui/agent@0.0.35— agent tokens are now opaque random bearer strings (llui-agent_<43-base64url>, ~54 chars) instead of JWTs (~250 chars). Tokens are stored as SHA-256 hashes server-side. ThesigningKeyoption is gone fromServerOptions/CoreOptions/ every LAP handler / WS upgrade.routeToAgentDO's third argument is now aresolveTid: (token) => Promise<string | null>callback (the worker no longer verifies signatures locally;TokenStore.findByTokenHashdoes the lookup).llui-agent@0.0.5— session-management MCP tools renamed:llui_connect_session→connect_session,llui_disconnect_session→disconnect_session. In Claude Code these now appear as the cleanermcp__llui__connect_sessioninstead of the doubledmcp__llui__llui_connect_session. The forwarded LAP tools (describe_app,get_state,send_message, …) keep their existing names.@llui/vite-plugin@0.0.34—AgentPluginConfig.signingKeyis gone (mirrors the agent-server removal). The type is now an empty reserved-for-future-options shape (Record<string, never>).
Migration
- Drop any
signingKeyfrom yourcreateLluiAgentServer({ … })call and fromllui({ agent: { signingKey } })invite.config.ts. - Drop
process.env.AGENT_SIGNING_KEYif you set it — it's no longer read. - Anywhere you reference the bridge tools by name in your own code, scripts, or prompt instructions, drop the
llui_prefix from the two session tools. - If you have stuck Claude sessions where
connect_session"isn't available" / "doesn't appear in the loaded tools", paste a fresh snippet — the new wording in@llui/agent@0.0.35tells the model to look formcp__<server>__connect_sessionand search for it via tool search if it's deferred (the cause of those failures).
@llui/agent@0.0.35
- Breaking opaque tokens replace JWT signing. See top of release block.
- Added
TokenStore.findByTokenHash()andTokenStore.rotateTokenHash(). - Improved connect snippet now names the LLui MCP server explicitly and flags Claude Code's deferred-tool behavior, so the model can resolve the namespaced tool on either platform.
llui-agent@0.0.5
- Breaking session tools renamed to
connect_session/disconnect_session. See top of release block. - Improved the "not bound" error and the bundled
llui-connectprompt body now name the LLui MCP server and the CC namespacing pattern, matching the new connect-snippet wording.
@llui/vite-plugin@0.0.34
- Breaking
AgentPluginConfig.signingKeyremoved. See top of release block.
Docs
- Replaced
/llm-guide(which duplicatedllms-full.txt) with two focused pages:/debugging(developer-facing —__lluiDebug,@llui/mcp,llui_lint,llui-mcp doctor, ESLint rules, trace export/replay) and/agents(end-user + app-author — bridge install, connect snippet flow,@requiresConfirm,@llui/agentintegration recipe).
2026-04-26 — @llui/eslint-plugin@0.0.18
Released: @llui/eslint-plugin@0.0.18
@llui/eslint-plugin@0.0.18
- Fixed
controlled-inputrule no longer false-positives ononBlur-committed inputs. The blur-commit pattern is a legitimate way to wire a reactivevaluebinding: state doesn't change during typing, so the binding doesn't overwrite mid-keystroke; blur fires the dispatch that commits the final value. The accepted commit handlers are nowonInput,onChange, oronBlur.
2026-04-26 — 0.0.32
Released: @llui/{dom,test,router,transitions,components}@0.0.32; @llui/vite-plugin@0.0.33; @llui/vike@0.0.34; @llui/agent@0.0.34; @llui/mcp@0.0.27; @llui/eslint-plugin@0.0.17; @llui/effects@0.0.10; llui-agent@0.0.4
The agent surface gets a major hardening pass driven by dogfooding decisive.space-2: send_message defaults to a tight diff-only response, missing @intent surfaces as null instead of synthesising the variant name, the schema tier surfaces documented shared variants without a live binding, and describe_visible_content falls back to a generic semantic walk when the app has no [data-agent] tags. The Vite plugin's compile-time diagnostics move to ESLint rules; the @llui/eslint-plugin recommended config promotes everything to error so LLMs (which only act on errors) actually fix what they see.
Breaking
@llui/agent@0.0.34—LapMessageResponse.stateAfteris now opt-in. By defaultsend_messagereturnsstateDiffonly; passincludeState: truein the request to get the full snapshot back. Callers that tracked state from the response need to either apply diffs against their snapshot fromconnect/observeor set the new flag explicitly.@llui/agent@0.0.34—LapActionsResponse.actions[].intentisstring | null. Variants without@intentannotation surface asnullrather than the variant name. Callers that surface affordances to LLMs should treatnullas "this action is undocumented" — neither synthesise a label from the variant name nor invent one.@llui/vite-plugin@0.0.33—failOnWarninganddisabledWarningsplugin options removed;DiagnosticRuleexport removed. The compile-timediagnose()pass is gone — install@llui/eslint-pluginand enable itsrecommendedconfig to get the equivalent (and more) checks at lint time. Apps usingdisabledWarningsshould remove the option fromvite.config.tsand selectively disable rules in theireslint.config.tsinstead.@llui/eslint-plugin@0.0.17—configs.recommendedpromotes every rule toerror. Thewarnseverity tier is gone. Rationale: warnings get reported but not fixed, so anything we shipped aswarneffectively never improved on its own. Per-package overrides remain the escape hatch for known false positives.
Migration
stateAfter. If your code readsresult.stateAfterafter asend_message, either pass{ includeState: true }in the request or applyresult.stateDiffto your prior snapshot.intent: null. Where you previously readaction.intentas a non-null string, handle the null case (skip the action, ask the user, or display a "no intent" placeholder).disabledWarnings→ ESLint config. Move per-rule mutes fromvite.config.ts'sdisabledWarningsarray toeslint.config.ts:'llui/<rule>': 'off'. Same rule names —empty-props,namespace-import,accessibility,controlled-input,child-static-props,static-on,exhaustive-update,bitmask-overflow,spread-in-children,map-on-state-array.- CI red on first upgrade. Apps not previously running
@llui/eslint-pluginwill see a wave of new errors from the ported diagnostics. Expected — fix or downgrade per-rule.
@llui/agent@0.0.34
- Added
includeState: truerequest flag onsend_message. Default is now to omitstateAfterand returnstateDiffonly. For a 100-cell matrix that's ~50kb saved per dispatch. - Added
fieldHints: Array<{path, hint}>on every action. Lifts@should("…")JSDoc hints from the schema tree to the action surface so callers don't have to dig throughdescription.messages.variants[X].field.hint. Path is dot/bracket notation rooted at the payload ("cells[].meta"). - Improved schema-tier action surfacing. Documented
'shared'variants (those with@intent) now appear inactionseven without a live UI binding, so an agent can dispatch e.g.Matrix/SetQuantityValuedirectly without first opening the cell editor. Previously only@agentOnlyvariants surfaced from the schema tier. - Improved
describe_visible_contentfalls back to a depth- and count-capped semantic walk of the entire root when the app has no[data-agent]tagged subtrees. Newsource: 'data-agent' | 'fallback' | 'truncated'field on the response signals which path produced the outline. - Breaking
intent: string | nullandstateAfteropt-in — see top of release block.
@llui/vite-plugin@0.0.33
- Improved cross-file resolver builds an enriched
TypeIndexthat follows named imports for type aliases referenced inside Msg variant payloads. Literal unions likeGridSorting = 'rank' | 'score'declared in a sibling file now resolve to{enum: ['rank', 'score']}in the schema, instead of'unknown'. - Breaking
failOnWarning/disabledWarningsremoved — see top of release block.
@llui/eslint-plugin@0.0.17
- Added eight rules ported from the Vite plugin's compile-time diagnostics:
empty-props,namespace-import,accessibility,controlled-input,child-static-props,static-on,exhaustive-update,bitmask-overflow. All run as editor squiggles instead of build-only console output, with autofix on the trivial cases. - Improved
spread-in-childrenis now scope-aware: only fires on genuinely-dynamic spreads. Bounded array literals (const items = [...]; div([...items.map(...)])) and known structural-call results stay silent — that footgun was migrated from the Vite version's scanner. - Improved
agent-msg-resolvableacceptsneveras a valid Msg type argument for stateless components — the canonical "this component dispatches no messages" declaration. Stops the rule from firing on legitimate display modules. - Breaking
recommendedpromoted to all-error — see top of release block.
@llui/mcp@0.0.27
- Improved picks up the agent surface improvements via the bumped
@llui/agentpeer.
@llui/effects@0.0.10
- Improved no behaviour changes; published in lockstep so consumers see a clean set.
llui-agent@0.0.4
- Added
send_messagetool advertises the newincludeStateparameter in its zod schema and description; default behaviour mirrors the@llui/agentserver (diff-only).
@llui/{dom,test,router,transitions,components,vike}@0.0.32 (and @llui/vike@0.0.34)
- Improved lockstep bump to keep peer-dep ranges aligned with
@llui/dom@0.0.32. No behaviour changes.
Docs
- Updated
docs/designs/02 Compiler.mdto reflect the diagnostics → ESLint move; the doc now points at the lint plugin for static-analysis rules and keeps the compiler's three-pass focus on prop classification, mask injection, and import cleanup. - Updated
packages/vite-plugin/README.mdwith the same redirection.
2026-04-25 — 0.0.31
Released: @llui/{dom,vite-plugin,test,router,transitions,components}@0.0.31; @llui/vike@0.0.33; @llui/agent@0.0.33; @llui/mcp@0.0.26; @llui/eslint-plugin@0.0.16; llui-agent@0.0.3
A consolidated batch shipping the agent surface improvements, hydrate parity work, cross-file/composition resolver, lint-rule hardening, components Msg JSDoc sweep, and the four-mount-path refactor accumulated since the previous release. Several behavior-breaking changes — read the Migration section before upgrading.
Breaking
@llui/dom@0.0.31—hydrateAppandhydrateAtAnchorno longer dispatch the effects returned byinit()on hydration. The SSR pass already ran them on the server; re-running on the client typically produced duplicate fetches / subscriptions. Opt back in viaMountOptions.runInitEffectsOnHydrate: true.@llui/agent@0.0.33—agentConnect.connect(),agentConfirm.connect(),agentLog.connect()now return a static prop bag with reactive accessors (matching the@llui/componentsconvention) instead of(state) => bag. Previous shape was incompatible with the documented "spread into element helpers" usage. ThecopyConnectSnippetButton.onClicknow dispatches a newCopyConnectSnippetMsg →AgentClipboardWriteeffect rather than reading state synchronously.@llui/agent@0.0.33—MessageAnnotations.humanOnly: booleanreplaced withdispatchMode: 'shared' | 'human-only' | 'agent-only'.LapMessageRejectReason: 'humanOnly'renamed'human-only'to match.@llui/agent@0.0.33— agent-only Msg variants (no UI affordance, LLM-only dispatch) are now expressible via@agentOnlyJSDoc tag and surface inlist_actions'sdispatchModefield.@llui/eslint-plugin@0.0.16—agent-missing-intentandagent-nonextractable-handlerare nowerror(notwarn) inconfigs.recommended. CI failures expected on first upgrade for unannotated Msg variants — fix is to add@intent("...")or@humanOnlyJSDoc. The@humanOnlyJSDoc exemption silences the rule on internal-only variants.@llui/eslint-plugin@0.0.16— Dropped name-based heuristics (name === 'Msg'/endsWith('Msg')) in favour of typed-lint cross-file detection. Rules now useparserOptions.projectServicewhen available; fall back to same-filecomponent<S, M, E>()arg names otherwise. Configure typed lint for full coverage.
Migration
- Hydrate effects. If your app relied on
init()effects firing on hydration, setMountOptions.runInitEffectsOnHydrate: true(orRenderClientOptions.runInitEffectsOnHydrate: trueif using@llui/vike). Otherwise no action — the default-off direction is the safer one for SSR setups. - Agent connect bag. Replace
connectParts(state).foopatterns withconnectParts.foo(static spread). ForconnectParts.foo.barreactive accessors, the runtime evaluates them per binding-mask hit; in tests, call them as functions:connectParts.foo.bar(state). humanOnlyreject reason. Anywhere your code readsLapMessageRejectReason === 'humanOnly', change to'human-only'.MessageAnnotations.humanOnly. Anywhere your code readsannotations.humanOnly, change toannotations.dispatchMode === 'human-only'.- Lint failures on upgrade. Add
@intent("...")JSDoc above each agent-dispatchable Msg variant, or@humanOnlyfor variants that are framework-internal / UI-only. - Typed-lint upgrade path (recommended): set
parserOptions.projectService: truein your ESLint config to get cross-file Msg detection and the most preciseagent-msg-resolvablechecks. Without typed lint the rules emit a one-line "Tip: enable parserOptions.projectService" reminder in their error messages.
@llui/dom@0.0.31
- Fixed
hydrateAppnow wires devtools and HMR registration the same waymountApp/mountAtAnchor/hydrateAtAnchordo. Previously SSR-hydrated layouts (e.g. the outermost@llui/vikeapp layout) silently dropped out ofwindow.__lluiComponents, never setwindow.__lluiDebug, andreplaceComponent(name, def)was a no-op against them. Newmount-path-parity.test.tsenforces the wiring across all four entry points. - Improved
MountOptions.runInitEffectsOnHydrateflag (defaultfalse) gates the post-swap dispatch ofinit()-time effects on hydration. See top of release block. - Improved the four mount entry points share a
buildAppHandle()helper for the AppHandle dispose / flush / send / getState / subscribe surface — ~120 lines of duplicate code eliminated. The parity test guarantees behavioral equivalence. - Improved
__llui_mcp_statusdiscovery now tries both/__llui_mcp_statusand/cdn-cgi/llui_mcp_statusso MCP auto-discovery survives@cloudflare/vite-plugin's catch-all worker routing. Distinguishes 404-from-live-server (don't fall back) from network-error (fall back to compile-time port).
@llui/vite-plugin@0.0.32
- Fixed all property-key emission goes through
ts.factory.createStringLiteralinstead of bare strings. Discriminants like'Router/RouteChanged','order-cancel', or reserved words like'delete'now serialize as quoted keys instead of bare identifiers (which produces invalid JS). - Improved new
cross-file-resolver.tsmodule follows imports + named re-exports +export *barrels (with rename, multi-hop, cycle detection) to locate the file declaring a Msg / State / Effect type. Composed unions liketype Msg = ImportedFoo | { type: 'extra' }get every variant in__msgAnnotationsand__msgSchemaregardless of where the variants are declared. Previously the file-local extractors silently dropped non-co-located variants. - Improved
/agent/*dev middleware also handles/cdn-cgi/agent/*with prefix-strip forwarding — same shadowing fix as__llui_mcp_statusfor cloudflare-vite consumers. - Improved
add-js-extensions.mjsnow discovers packages dynamically. The hardcodedlint-idiomatichad been silently skipping the renamedeslint-plugin-lluifor several releases.
@llui/test@0.0.32
- Improved README now shows a real, type-checking testComponent example with
send/flush/state/effects+assertEffects. Replaces the API-signature pseudocode that didn't compile and didn't help users get started.
@llui/router@0.0.31
- Improved
@llui/dompeer range bumped to^0.0.31.
@llui/transitions@0.0.31
- Improved README snippets tagged
// @doc-skipwhere they use illustrative[...]placeholders. - Improved
@llui/dompeer range bumped to^0.0.31.
@llui/components@0.0.31
- Improved all 57 Msg unions now carry
@intent("…")/@humanOnlyJSDoc on every variant (362 variants annotated). Composes correctly into downstream apps' annotation maps via@llui/vite-plugin's cross-file resolver — Claude no longer sees synthesized intent labels fordialog.open,tabs.setValue, etc. Intent text is approximate (camelCase variant names → "Camel case"); maintainers can polish per-variant. Keyboard-only / programmatic-config variants (focus*,highlight*,setItems,setDisabled, …) marked@humanOnly.
@llui/vike@0.0.33
- Added
getLayoutChain(): readonly AppHandle[]exported function and widenedRenderClientOptions.onMountto receive(chain: readonly AppHandle[]). Consumers wiring observability bridges, custom devtools, or the LAP agent client at the layout level now have a supported API; the old workaround (window.__lluiComponents[layoutName]) was unreliable due to the hydrateApp parity bug fixed in this release. - Added
RenderClientOptions.runInitEffectsOnHydrateforwarded to every layer in the layout chain. Defaults tofalsematching@llui/dom's default. - Added in the
llui_connect_sessionMCP tool result: fullobservebundle (state + actions + description + context) so Claude has everything it needs to act after the connect call. Previous shape returned only{appName, appVersion, status}and Claude had to follow up with separateobserve/describe_app/get_statecalls.
@llui/mcp@0.0.26
- Improved
@llui/dompeer range bumped to^0.0.31. - Improved
@llui/eslint-plugindependency picks up the typed-lint hint and rule changes via cascade.
@llui/eslint-plugin@0.0.16
- Added new rule
agent-msg-resolvable: at everycomponent<S, M, E>()call, errors when the M type is unresolvable (typo, missing import, namespace import, complex type). Three distinct messages so the fix is obvious. Inconfigs.recommendedandconfigs.agentat error severity. - Added typed-lint cross-file detection in
agent-missing-intentandagent-exclusive-annotations. WithparserOptions.projectServiceconfigured, the rules walk the wholets.Program(cached on a WeakMap) and match Msg unions by symbol identity — finds aliases declared in separate files with unconventional names. Fall-back to same-file heuristic when typed lint isn't configured. - Added
agentExclusiveAnnotationsRule.modeConflictflags@humanOnlyand@agentOnlyon the same variant. - Improved
createRuleURL repointed at.../src/rules/${name}.tssince the previousdocs/rules/${name}.mdpath 404'd. - Improved every error message appends a "Tip: enable
parserOptions.projectService" hint when typed lint isn't configured. - Fixed drop name-based heuristics (
name === 'Msg',endsWith('Msg')) — false-positive prone on unrelated*Msg-named types and redundant once typed lint is enabled.
@llui/agent@0.0.33
- Breaking
connect()static-bag refactor +dispatchModeenum +LapMessageRejectReason 'human-only'— see top of release block. - Added
@agentOnlyJSDoc tag for Msg variants the LLM can dispatch but the UI doesn't bind. Surfaces inlist_actions[].dispatchMode. - Added
EffectHandlerHost.agentBasePathconfiguration so consumers under@cloudflare/vite-plugincan route through/cdn-cgi/agent/*(the canonical/agent/*paths are shadowed by the cloudflare worker catch-all). - Added
CopyConnectSnippetMsg +AgentClipboardWriteeffect for the connect snippet copy affordance, replacing the old synchronous-state-read in the click handler. - Added
llui_connect_sessionreturns the fullobservebundle. Eliminates the round-trip pattern where Claude had to calllist_actions+describe_visible_contentseparately after connect. - Improved every variant of
AgentConnectMsg,AgentConfirmMsg,AgentLogMsgcarries@intent(user-actionable) or@humanOnly(framework-internal) JSDoc. Phase D1 composition merges these into downstream apps' annotation maps; previously Claude saw synthesized labels for the agent's own message types. - Improved
effect-handler.tssplit into per-effect handler functions with a thin top-level dispatcher (was a 9-case 150-line monolith). - Improved
agentLog.visibleEntriesmemoized by parent-state reference.each(bag.visibleEntries, …)no longer re-filters per item. - Improved
@llui/dompeer range bumped to^0.0.31.
llui-agent@0.0.3
- Improved
@llui/agentcascade — picks up the connect-bag refactor, dispatchMode enum, and effects-handler split via dependency.
Docs
- Added
scripts/check-readme-examples.mjsextracts every fencedts/tsxblock from each package's README, runstscagainst per-package mini-tsconfigs. Wired topnpm verify. Catches docs that drift from the actual API.// @doc-skipopt-out for illustrative-only blocks. - Added
scripts/annotate-component-msg.mjsis the one-shot sweep that produced the components Msg JSDoc above. Idempotent — skips variants that already carry an LAP tag.
2026-04-25 — peer-dep packaging fix
Released: @llui/vite-plugin@0.0.31, @llui/test@0.0.31, @llui/vike@0.0.32, @llui/mcp@0.0.25, @llui/eslint-plugin@0.0.15, @llui/agent@0.0.32
Critical packaging fix for @llui/{vike,test,mcp,agent}: ship @llui/dom as a peer dependency instead of a runtime dependency. The old packaging caused dual @llui/dom installs in any consumer whose own @llui/dom version differed from what the package was pinned to at publish time, producing provide() can only be called inside a component's view() function errors from inside view callbacks where the call was manifestly correct.
Breaking
@llui/vike@0.0.32,@llui/test@0.0.31,@llui/mcp@0.0.25,@llui/agent@0.0.32—@llui/domis now a peer dependency, not a transitive runtime dep. Consumers who relied on transitive resolution must declare@llui/domexplicitly in their own project'sdependencies.
Migration
- Add
@llui/domto your project's dependencies if it isn't there:pnpm add @llui/dom. Most projects already import from@llui/domdirectly and have it declared — only ones that relied purely on transitive resolution will hit "cannot find module". - If you'd applied a
pnpm.overridesworkaround to force a single@llui/dominstance, you can remove it — the peer pattern handles deduplication natively.
@llui/vite-plugin@0.0.31
- Fixed
transform.tspicked up a.jsextension on one relative import thatadd-js-extensions.mjshad missed.
@llui/test@0.0.31
- Fixed
@llui/domships aspeerDependencies+devDependenciesinstead ofdependencies. Same dual-install fix as@llui/vike.
@llui/vike@0.0.32
- Fixed
@llui/domships aspeerDependencies+devDependenciesinstead ofdependencies. Resolves dual-install /provide()-from-view errors. See top of release block for migration. - Added Cloudflare Workers section in the README — documents the
worker.tspattern withimport.meta.env.PRODguard around thedist/server/entry.mjsimport. Without the guard, dev workerd loads the stale prod build and trips Vike's prod-in-dev detector. The brillout-recommendedprocess.env.NODE_ENVsnippet silently fails under workerd (no Nodeprocess).
@llui/mcp@0.0.25
- Fixed
@llui/domships aspeerDependencies+devDependenciesinstead ofdependencies. Type-only usage in mcp's source, but the packaging anti-pattern was identical.
@llui/eslint-plugin@0.0.15
- Fixed 11 source files now have explicit
.jsextensions on relative imports. Theadd-js-extensions.mjsbuild pass had been silently skipping this package since thelint-idiomatic→eslint-pluginrename — its hardcoded list still pointed at the old name. No runtime effect (the package is CommonJS), but now consistent with the rest of the monorepo.
@llui/agent@0.0.32
- Fixed
@llui/domships aspeerDependencies+devDependenciesinstead ofdependencies. Type-only consumer (Send,AppHandlefrom agent's client adapters). - Fixed removed phantom
@llui/effectsdependency. The package never imported from@llui/effects— only the README example does, and that's user-side app code. Consumers usinghandleEffectsin their own app should declare@llui/effectsthemselves (most already do).
Docs
- Improved root
README.mdpackage table: replaced the stale@llui/lint-idiomaticrow with@llui/eslint-plugin, and added@llui/agent+llui-agentrows that had been missing. - Improved
/publishskill now refuses to bump versions if any non-private package has@llui/domindependenciesinstead ofpeerDependencies. Cascade list derived frompackage.jsonfiles instead of a hand-maintained enumeration, so a newly-added peer can't be silently skipped on the next release.
2026-04-24 — @llui/agent@0.0.31
Released: @llui/agent@0.0.31
Cross-runtime portability rework: @llui/agent now runs on Cloudflare Workers (via Durable Objects), Deno / Deno Deploy, and Bun in addition to Node. The ws library and node:crypto are no longer load-bearing in the runtime-neutral path — only the Node adapter imports them.
Breaking
@llui/agent@0.0.31direct consumers ofsignToken/verifyToken/signCookieValue: these are now async (returnPromise<T>). The signatures usecrypto.subtleHMAC-SHA256, which is web-standard and async by design. Wrap call sites inawait. LAP server usage viacreateLluiAgentServeris unchanged — the async migration is handled internally.
Migration
signToken(payload, key)→await signToken(payload, key)— same forverifyTokenandsignCookieValue.- No changes needed if you only use
createLluiAgentServer({ ... })at the top level. The Node path signature is unchanged. - Non-Node deployments: see Runtime support for the Cloudflare / Deno / Bun recipes.
@llui/agent@0.0.31
- Added
@llui/agent/server/coresub-path — runtime-neutral entry that builds the LAP router, registry, and accept-connection primitive without importingwsor anynode:*module. Works on Node, Bun, Deno, and Cloudflare. - Added
@llui/agent/server/websub-path — WHATWG WebSocket adapters. ExportscreateWHATWGPairingConnection(wraps any standardWebSocketin aPairingConnection),handleCloudflareUpgrade(usesWebSocketPair),handleDenoUpgrade(usesDeno.upgradeWebSocket), andextractToken. - Added
@llui/agent/server/cloudflaresub-path —AgentPairingDurableObjectclass +routeToAgentDOWorker helper. A single Cloudflare Durable Object owns one sessiontid's in-memory registry; the Worker's fetch handler routes LAP + WebSocket upgrade calls to the DO by token. Full recipe +wrangler.tomlsnippet in the docs. - Added
AgentCoreHandle.acceptConnection(token, conn)primitive. Runtime adapters call this after accepting a WebSocket in their native way; it validates the token, updates the token store, writes an audit entry, and registers thePairingConnection. - Added
PairingRegistryinterface extracted from theWsPairingRegistryclass. The in-memory implementation is nowInMemoryPairingRegistry(backward-compatibleWsPairingRegistryalias preserved). External implementations (e.g. the Durable Object registry) implement the interface directly. Routing primitives (register,send,subscribe,onClose) are separate from request/response helpers (rpc,waitForConfirm,waitForChange), which live inserver/ws/rpc.tsand can be reused across registries. - Improved WebCrypto migration — HMAC sign/verify now go through
crypto.subtle(standard across Node ≥ 15, Cloudflare, Deno, Bun). Removednode:cryptoimport.crypto.randomUUID()(global web standard) replacesrequire('node:crypto').randomUUID. - Improved LAP handler internals — the registry no longer owns in-flight RPC promise tracking or long-poll wait entries. Each handler subscribes to frames via
registry.subscribe(tid, filter)for the duration of its call, then unsubscribes. This keeps the registry interface small enough that a Cloudflare Durable Object can implement it cleanly.
Docs
- Added Runtime support matrix and full deployment recipes for Node, Deno, Bun, and Cloudflare + Durable Objects in
/api/agent.
2026-04-24 — 0.0.30
Released: @llui/{dom,vite-plugin,test,router,transitions,components}@0.0.30; @llui/vike@0.0.31; @llui/mcp@0.0.24; @llui/eslint-plugin@0.0.14; @llui/agent@0.0.30; llui-agent@0.0.2
Two headline changes: @llui/mcp grows from 23 → 38+ tools across four new phases (CDP screenshots + a11y, compiler cache introspection, source grep + test/lint, SSR hydration + render). @llui/agent adds the observe tool and drained send_message semantics, cutting the "check state → act → check state" loop from five MCP round-trips to two.
Breaking
@llui/lint-idiomaticis gone. The rules have been migrated into@llui/eslint-plugin. Drop the@llui/lint-idiomaticdependency, replace imports with@llui/eslint-plugin, and remove the old package from anyeslint.config.tsentries — the rule ids stay the same.
Migration
- Remove
@llui/lint-idiomaticfrom yourdevDependencies, add@llui/eslint-plugin, and adjust your ESLint config imports. - No code changes required for
@llui/agentusers: the newobservetool is additive and the newwaitFor: 'drained'default forsend_messageis a faster, backward-compatible drop-in.
@llui/dom@0.0.30
- Added
getCompiledSource,getMsgMaskMap,getBindingSource, andgetHydrationReportonLluiDebugAPI— the runtime hooks that back the new@llui/mcpcompiler/SSR tools. Zero cost in production; only populated wheninstallDevToolsruns.
@llui/vite-plugin@0.0.30
- Added 50-entry LRU compiler cache storing per-component pre/post transform source, Msg→mask map, and binding source locations. Emitted as non-enumerable
Object.definePropertycalls so production bundles aren't bloated but MCP tooling can read them in dev.
@llui/mcp@0.0.24
- Added 15 new tools across four phases:
- CDP (6) —
llui_screenshot,llui_a11y_tree,llui_network_tail,llui_console_tail,llui_uncaught_errors,llui_browser_close. Backed by a lazy Playwright attach (:9222user-chrome first, fallback to headless) with ring buffers for console/network/errors. - Compiler (3) —
llui_show_compiled,llui_explain_mask,llui_goto_binding_source. Read from the vite-plugin's new compiler cache. - Source (4) —
llui_find_msg_producers,llui_find_msg_handlers,llui_run_test,llui_lint_project. Grep + vitest + ESLint at workspace scope. - SSR (2) —
llui_hydration_report(diff client vs server-rendered HTML fromdata-llui-ssr-html),llui_ssr_render.
- CDP (6) —
- Added CLI flags
--url(dev-server target for Playwright) and--headed(visible browser window) so the CDP fallback can point at an existing dev server or run visibly for debugging.
@llui/agent@0.0.30
- Added
observeLAP endpoint + browser RPC handler. One call returns{state, actions, description, context}, folding in what used to take three separate calls (describe_app+get_state+list_actions). - Added drain semantics to
send_message. The defaultwaitFor: 'drained'waits for the message queue to go idle (http/delay/debounce round-trips feed back as messages, then quiesce), then returns the fresh state, actions, and adrainblock witheffectsObserved,durationMs,timedOut, and any unhandled effect errors captured during the window. New params:drainQuietMs(default 100ms) andtimeoutMs(default 5000ms, down from 15s). - Improved Response envelope on
dispatchednow carriesactionsalongsidestateAfter, so the LLM rarely needs a follow-upobserveafter a send.
llui-agent@0.0.2 (agent-bridge)
- Added
observeMCP tool routed to/lap/v1/observe.bridge.tscaches the returneddescriptionso subsequentdescribe_appcalls short-circuit. - Improved
send_messagetool schema advertiseswaitFor: 'drained' | 'idle' | 'none',drainQuietMs, andtimeoutMscontrols. Tool descriptions updated to steer Claude toward the efficient path.
@llui/eslint-plugin@0.0.14
- Added Rules migrated from the removed
@llui/lint-idiomaticpackage:agent-exclusive-annotations,agent-missing-intent,agent-nonextractable-handler,each-closure-violation, and related idiomatic-LLui rules. Rule ids unchanged — only the importing package moved.
@llui/vike@0.0.31, @llui/test@0.0.30, @llui/router@0.0.30, @llui/transitions@0.0.30, @llui/components@0.0.30
- Improved Cascade from
@llui/dom@0.0.30. No user-visible behavior changes;components,router,transitionspick up the new^0.0.30peer range.
Docs
- Added
/api/agentadoption guide (install, dev middleware, client wiring,@intent/@requiresConfirm/@humanOnlyannotations,agentDocs/agentContext/agentAffordances, DOM tagging, production server setup, efficient tool usage, security). - Added
/api/agent-bridgeCLI + Claude Desktop config + tool reference. - Updated Package table on the index page and
llms.txtto list the agent stack.
2026-04-22 — @llui/vike@0.0.30
Released: @llui/vike@0.0.30
Point-fix release for a client-navigation regression introduced in 0.0.26 that broke content-driven sites where multiple routes share a single ComponentDef. Reported against the llui.dev docs site; other lockstep packages ship unchanged at 0.0.29.
@llui/vike@0.0.30
- Fixed Page layer is no longer counted as a "surviving layer" by the chain diff on client navigation. Since 0.0.26, two routes whose
+Page.tsfiles resolved to the sameComponentDefreference — the normal pattern for content-driven sites where every page re-exports a shared component (e.g.DocPage) and per-route+data.tssupplies the content — were treated as a matching chain entry.firstMismatchadvanced past the page slot, the adapter hit theisNoOpshort-circuit, and onlyonMountfired: URL bar advanced, DOM stayed frozen on the previous route. The chain diff now boundsfirstMismatchto the layout prefix, so the page slot is always divergent andinit(data)re-runs on every nav regardless ofComponentDefidentity — matching the contract the README already documented ("Navigating from/dashboard/reportsto/dashboard/overviewonly disposes thePage"). Persistent layouts,propsMsgdispatch on surviving layouts, hydration envelope handling, and chain growth/shrink semantics are unchanged. Three regression tests cover the same-def nav scenario end-to-end.
2026-04-21 — 0.0.29
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.29; @llui/mcp@0.0.23; @llui/lint-idiomatic@0.0.13; @llui/agent@0.0.29 (first release); llui-agent@0.0.1 (first release)
Inaugural release of the LLui agent stack: a full LAP (LLui Agent Protocol) server + browser client + Claude Desktop bridge that lets Claude drive any LLui app directly.
@llui/agent@0.0.29 (new package)
First release. Provides both the server-side LAP endpoint and the browser-side client slices needed to make a LLui app driveable by Claude.
Server (@llui/agent/server)
- Added
createLluiAgentServer(opts)factory — mounts a full HTTP+WS agent server. HTTP routes:POST /agent/mint,POST /agent/revoke,GET /agent/sessions,POST /agent/resume/list,POST /agent/resume/claim. LAP routes:/lap/v1/describe,/lap/v1/message,/lap/v1/wait,/lap/v1/confirm-result. - Added WebSocket upgrade handler at
/agent/ws— authenticates via HMAC token, pairs the browser to a Claude session, relays RPC frames. - Added
signToken/verifyToken— HMAC-SHA256 mint/verify with configurable signing key; falls back to a random per-session key in dev. - Added
InMemoryTokenStore— default token store; pluggable via thetokenStoreoption. - Added
defaultIdentityResolver— signed-cookie identity; pluggable viaidentityResolver. - Added
defaultRateLimiter— 60 req/min per identity; pluggable viarateLimiter. - Added
consoleAuditSink— logs every LAP action to stdout; pluggable viaauditSink. - Added 6 LAP RPC handlers:
get_state(JSON-pointer path resolution),list_actions(bindings + affordances + annotations),describe_context,query_dom,describe_visible_content,send_message(annotation gating + confirm-propose flow). - Added
WsPairingRegistry— tid→pairing map with rpc correlation and pending-confirmation long-poll support.
Client (@llui/agent/client)
- Added
createAgentClient(opts)factory — composes the WebSocket client with the HTTP effect handler; acceptswrapConnectMsg,wrapConfirmMsg,wrapLogMsgslices for integration with the host app'supdate(). - Added
agentConnectheadless component — manages WS lifecycle (awaiting-ws → awaiting-claude → active), token minting, and the connect-snippet for Claude Desktop. - Added
agentConfirmheadless component — handles the pending-confirmation UI flow (propose → user accept/reject → resolved). - Added
agentLogheadless component — ring-buffered action log (entries: LogEntry[]); updated viawrapLogMsg. - Added
ws-client— hello frame dispatch, RPC round-trip,log-appendframe emission with human-readable intent labels built from@intentannotations and fixed labels for read tools ("Read app state","List available actions", etc.). - Added State-update and log-append frame emission so the host app's local
agent.logslice mirrors Claude's actions in real time. - Fixed Claude-bound activation signal —
ActivatedByClaudefires only after the server sends{t: "active"}, preventing prematureactivestatus. - Fixed
WsOpened/WsCloseddispatched toagentConnectslice on WebSocket events. - Fixed Unknown msg variants rejected early with a structured error; 500 responses now include real
Errorname/message/stack (first 5 frames) indetailso Claude sees actionable diagnostics.
llui-agent@0.0.1 (new package)
First release. A Claude Desktop MCP bridge CLI (npx llui-agent) that connects Claude to any running LLui app's agent endpoints.
- Added stdio MCP transport — lists and calls LAP tools on behalf of Claude Desktop.
- Added
BindingMap— per-session{url, token, describe}state keyed by session ID. - Added
forwardLap— generic POST dispatcher that proxies tool calls to the app's LAP routes. - Added
/llui-connectMCP prompt — guides Claude through the connection handshake. - Added Full MCP tool surface:
llui_connect_session,get_state,list_actions,send_message,describe_context,query_dom,describe_visible_content,wait,confirm_result.
@llui/dom@0.0.29
- Added
AppHandle.subscribe(listener)— post-update state-change listener. Called after every update cycle with(newState, prevState). Returns an unsubscribe function. Safe to call from outsideview(). - Added
LluiComponentDef.__msgAnnotations,.__bindingDescriptors,.__schemaHash— injected by the compiler; consumed by@llui/agentto populate the hello frame without runtime reflection.
@llui/vite-plugin@0.0.29
- Added
extractMsgAnnotations— reads JSDoc tags (@intent,@humanOnly,@alwaysAffordable,@readSurface) from theMsgunion and emits them as__msgAnnotationson the compiledcomponent()call. - Added
extractBindingDescriptors— walksview()to collect bound message variants and emits them as__bindingDescriptors. - Added
computeSchemaHash— stable SHA-256 over the message schema; emitted as__schemaHashso the agent can detect schema drift without a full describe round-trip. - Added
agent?: boolean | AgentPluginConfig— extends the existingagent: trueshorthand with an object form acceptingsigningKey. When set, also auto-mounts@llui/agent/serverHTTP and WS handlers on the Vite dev server so plainvite devhas working agent endpoints without a customserver.ts.
@llui/lint-idiomatic@0.0.13
- Added Rule
agent-missing-intent— warns when a user-dispatchableMsgvariant lacks an@intentJSDoc tag, which Claude needs to understand what the action does. - Added Rule
agent-exclusive-annotations— warns when@humanOnlyand@alwaysAffordableappear on the same variant (mutually exclusive). - Added Rule
agent-nonextractable-handler— warns when anonEffecthandler can't be statically associated with an effect type, preventing the compiler from extracting its affordances. - Fixed
@humanOnlyvariants are now exempt fromagent-missing-intent— intent annotations on human-only messages were never required. - Improved Perfect-score threshold updated to 20 to account for the new agent rules.
@llui/mcp@0.0.23
- Improved Perfect-score threshold updated to 20 to match the new
@llui/lint-idiomaticrule set.
@llui/{test,router,transitions,components,vike}@0.0.29
- Rebuilt against
@llui/dom@0.0.29. No source changes.
2026-04-19 — 0.0.28
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.28; @llui/mcp@0.0.22
Three consumer-reported issues fixed with TDD-first discipline — each lands with failing-test-then-implementation and no workarounds left in the library.
Breaking
@llui/components@0.0.28—SortableMsg.startandSortableMsg.movegain requiredx: number. Consumers usingconnect()for their handle/root wiring (the 99% case) see no change —connectfillsxfrome.clientXautomatically. Hand-wired dispatchers that construct these messages directly get a TS error pointing at the missing field; addx: <number>alongside the existingy.
Migration
- Hand-wired sortable dispatchers — add
x: <clientX or 0>to everySortableMsg.startand.moveliteral in your app.DragStatefixtures in tests getstartX/currentXalongside the existingstartY/currentY(both default to0when you don't have a meaningful position).
@llui/dom@0.0.28
- Fixed
branchandeachdisposers now remove their DOM nodes from the parent, not just their scopes. When an outer structural primitive swaps an arm whose children spread a nestedbranch/eachdirectly (no wrapping element), nodes the nested primitive inserted AFTER the outer's initial render — each-reconciled rows, inner-branch post-mount case swaps — used to leak. The parent's cleanup only walked its initial-rendercurrentNodessnapshot; anything the nested primitive inserted later was invisible to it. The disposer now walks live entries/nodes + anchor and removes them viaparentNode.removeChild, guarded so cascade-removed subtrees no-op.showandscoperide this fix throughbranch. 6 new tests intest/branch-nested-swap.test.tspinning every failure mode the repro covered. - Added
AppHandle.getState(): unknown— sanctioned escape hatch for reading state outsideview(). Safe from event handlers, adaptersendwrappers, async callbacks, timers. Returns the current instance state; throws afterdispose()so stale reads fail loud. Wired into all four mount paths (mountApp,hydrateApp,mountAtAnchor,hydrateAtAnchor) plus the HMR replacement handle. - Improved
sample()'s "called outside view" error now points specifically atAppHandle.getState()with an example. The previous message told users "you called a primitive outside a render context" but didn't say what to do instead; the common-case shape (adapter wrapssend, needs current state) now gets inline migration guidance with copy-pasteable code.
@llui/components@0.0.28
- Added
layout: '2d'option onsortable.connect(get, send, { id, layout }). Opt-in 2D support for flex-wrap and grid layouts where same-row items share a Y coordinate. Under the flag:findTargetAtranks by Euclidean distance instead of Y-only; the dragged item'sstyle.transformistranslate(dx, dy)instead oftranslateY(dy); non-dragged items between source and target get per-itemstyle.transform = translate(snapshotDelta)that opens the correct gap regardless of row wrap;data-shiftis suppressed in 2D so CSStranslateY(var(--sortable-shift))rules don't fight with the computed transform.DragStatenow always tracks{startX, startY, currentX, currentY}— 1D ignores X at render time. KeyboardmoveBystays linear-array in both modes (screen-reader-correct; 2D-spatial keyboard nav is a separate feature). - Breaking
SortableMsg.startand.movegain requiredx: number. See top of release block.
@llui/{vite-plugin,test,router,transitions,vike}@0.0.28
- Rebuilt against the new
@llui/domversion. No source changes.
@llui/mcp@0.0.22
- Rebuilt against the new
@llui/domversion. No source changes.
2026-04-19 — 0.0.27
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.27; @llui/mcp@0.0.21
Tightens the DomEnv contract introduced in 0.0.24 — follow-up hardening after the portal SSR fix in 0.0.26.
Breaking
@llui/dom@0.0.27—DomEnv.querySelector(selector)is now a required method on the interface. Previously optional, withportal()silently falling back toglobalThis.documentwhen a custom env didn't implement it. That fallback was exactly the shape that let a Workers-hostile env slip to production without an error — which is the failure mode 0.0.26 had to fix in the first place. Making the method required means any custom env that forgets to wire up selector resolution fails TS compile instead of crashing at render time. Consumers on the three LLui-shipped envs (browserEnv,jsdomEnv,linkedomEnv) need no action; they already implement it.
Migration
- Hand-rolled
DomEnvimplementations — addquerySelector(selector: string): Element | nullthat resolves against your env's document (or returnsnullif your env has no meaningful document concept — portal treatsnullas a no-op).
@llui/dom@0.0.27
- Breaking
DomEnv.querySelectorrequired. See top of release block. - Improved Portal's string-target resolution is now a straight
ctx.dom.querySelectorcall with no fallback branches — one less silent-failure mode.
@llui/{vite-plugin,test,router,transitions,components,vike}@0.0.27
- Rebuilt against the new
@llui/domversion. No source changes.
@llui/mcp@0.0.21
- Rebuilt against the new
@llui/domversion. No source changes.
2026-04-19 — 0.0.26
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.26; @llui/mcp@0.0.20
Fixes two SSR crashes under Cloudflare Workers + linkedomEnv that shipped in 0.0.24 / 0.0.25.
@llui/dom@0.0.26
- Fixed
<select value={accessor}>no longer throws under linkedomEnv. Two-part fix: (1) the element helper now defers applyingvalueon a<select>until after its children are appended — in real browsers and jsdom, settingselect.valueon an empty select was already a silent no-op (value fell through to the first option once options arrived), and on linkedom it was a hard throw. Deferring makes every env agree; the matching<option>ends upselectedregardless. (2)linkedomEnv()now patchesHTMLSelectElement.prototype.valuewith a custom get/set pair that walks<option>children and toggles[selected]per HTML-spec semantics. The patch is idempotent and only runs when the descriptor has no setter, so jsdom / real browser envs routed through the factory are untouched. - Fixed
portal()no longer reaches for baredocumentat render time, which crashed SSR withReferenceError: document is not definedwhenever a portal call appeared inside ashow/branch/ overlay render callback on Workers.DomEnvgains an optionalquerySelector?(selector): Element | null;browserEnv,jsdomEnv, andlinkedomEnvall implement it. Portal resolves string targets viactx.dom.querySelectorfirst, falls back toglobalThis.documentfor legacy envs that predate the method, and returns[]when neither is available — consistent with portal's existing "target not found" branch. Portal is semantically a client-only primitive; SSR emitting nothing is correct. - Added Optional
querySelector?(selector): Element | nullmethod on theDomEnvinterface. Added as optional so pre-existing consumer envs built by hand continue to type-check. All LLui-shipped envs implement it.
@llui/{vite-plugin,test,router,transitions,components,vike}@0.0.26
- Rebuilt against the new
@llui/domversion. No source changes.
@llui/mcp@0.0.20
- Rebuilt against the new
@llui/domversion. No source changes.
2026-04-19 — 0.0.25
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.25; @llui/mcp@0.0.19
Follow-up to 0.0.24 — fixes a pre-existing @llui/vike package.json bug that survived the DomEnv refactor. No API changes.
@llui/vike@0.0.25
- Fixed
jsdommoved fromdependenciestopeerDependencieswithpeerDependenciesMeta.jsdom.optional: true. Before this release, installing@llui/vikeauto-pulled jsdom intonode_moduleseven when the consumer usedcreateOnRenderHtml({ domEnv: linkedomEnv })on Cloudflare Workers. Now Workers consumers can skip jsdom entirely — matching@llui/dom's shape, where jsdom and linkedom are both optional peers. Consumers using the defaultonRenderHtmlexport see the standard peer-dep install prompt (pnpm install jsdom).
@llui/dom@0.0.25
- Fixed Dropped a stale
@ts-expect-errordirective insrc/ssr/linkedom.tsthat became an unused-directive lint error once pnpm started hoisting linkedom via the optional peer declaration. Replaced with an explicitas unknown as …cast that tolerates both resolved and unresolved module shapes at build time. Compiled JS is identical to 0.0.24 — this is a TS-only cleanup.
@llui/{vite-plugin,test,router,transitions,components}@0.0.25
- Rebuilt against the new
@llui/domversion. No source changes. Compiled output identical to 0.0.24.
@llui/mcp@0.0.19
- Rebuilt against the new
@llui/domversion. No source changes. Compiled output identical to 0.0.18.
2026-04-18 — 0.0.24
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.24; @llui/mcp@0.0.18
Removes globalThis mutation from SSR. @llui/dom now threads a DomEnv through its render pipeline as a context object instead of patching the process's window. Ships new sub-entries @llui/dom/ssr/jsdom + @llui/dom/ssr/linkedom for per-call env construction, which fixes a 9+ MiB Cloudflare Workers bundle regression (the old initSsrDom pulled jsdom's tr46 / whatwg-url / punycode transitive chain into the Worker bundle even when consumers used linkedom at runtime).
Breaking
@llui/dom@0.0.24—renderToString(def, state?)→renderToString(def, state, env). The thirdenv: DomEnvargument is required. Same change applies torenderNodes. Get an env from@llui/dom/ssr/jsdom(jsdomEnv()),@llui/dom/ssr/linkedom(linkedomEnv()), or the newbrowserEnv()helper for client-side tests.@llui/vike@0.0.24—createOnRenderHtml({ Layout, document })→createOnRenderHtml({ domEnv, Layout, document }). ThedomEnv: () => DomEnv | Promise<DomEnv>factory is required. The defaultonRenderHtmlexport still ships with a built-in jsdom env for zero-config setups; Workers consumers must usecreateOnRenderHtml({ domEnv: linkedomEnv }).
Migration
- Direct SSR users (jsdom): replace
await initSsrDom()+renderToString(def, state)withconst env = await jsdomEnv()+renderToString(def, state, env). ImportjsdomEnvfrom@llui/dom/ssr/jsdom. - Cloudflare Workers / strict-isolate runtimes: switch to
linkedomEnv()from@llui/dom/ssr/linkedom. Your Worker bundle no longer pulls jsdom — the rollup graph walker only sees linkedom. - Vike consumers: add
domEnv: jsdomEnv(orlinkedomEnv) to yourcreateOnRenderHtmloptions. Import the factory from@llui/dom/ssr/jsdom(or/linkedom). - Hand-patched globals (legacy linkedom workaround): delete the
Object.assign(globalThis, …)shim.linkedomEnv()returns a self-contained env that the renderer uses directly. initSsrDomcallers: update the import path from@llui/dom/ssrto@llui/dom/ssr/legacy. The shim still works, but living behind its own sub-entry means@llui/dom/ssrno longer pulls jsdom into bundles that don't explicitly opt in. Plan a real migration tojsdomEnv()before the shim is removed.
@llui/dom@0.0.24
- Breaking
renderToString/renderNodesrequire aDomEnv. See top of release block. - Added
clientOnly({ render, fallback? })primitive for browser-only subtrees. SSR emits<!--llui-client-only-start-->+ optional fallback +<!--llui-client-only-end-->and never invokesrender; on the clientrenderruns inline, participating in the host component'sView<S, M>bag and bitmask update cycle normally. Pair with dynamicimport()insiderenderto keep browser-only libraries (Leaflet, Chart.js, Monaco, etc.) out of the SSR bundle's module graph. Discriminates SSR vs client viactx.dom.isBrowser—browserEnv()sets it,jsdomEnv/linkedomEnvdon't. Also available asbag.clientOnlyon theView<S, M>helper (destructured form insideview). - Added
foreign.mountnow acceptsInstance | Promise<Instance>return values. When the promise is pending, the container element is inserted into the DOM immediately andsyncis deferred; the initialsyncfires on resolve with whatever props the binding observed during the await. Dispose-before-resolve correctly destroys the instance once it arrives. Rejected promises log toconsole.error(they can't reacherrorBoundarythrough the microtask queue). Removes the workaround where users had to structureforeign.mountas a synchronous closure that referenced a pre-loaded imperative handle — nowawait import('leaflet')inline works directly. - Added
__clientOnlyStub(name)helper +'use client'module directive handled by@llui/vite-plugin. A file whose first non-comment statement is'use client'is replaced entirely during SSR builds: everyexport const NAME = ...,export function NAME,export class NAME, and namedexport { ... }list is rewritten toexport const NAME = __clientOnlyStub('NAME'), andexport defaultbecomesexport default __clientOnlyStub('default'). Top-level imports in the directive'd module are dropped from SSR output — any library that crashes on Node/Workers module-init no longer poisons the SSR bundle. Client builds are unaffected (directive is a no-op); atomic-swap hydration replaces the stub's empty placeholder with the real component DOM. Warns onexport ... from '...'re-exports that bypass the stubbing pass. - Added
DomEnvinterface +browserEnv()factory, both exported from@llui/domand@llui/dom/ssr. Defines a minimal DOM contract (createElement, createTextNode, createComment, createDocumentFragment, Element, Node, Text, Comment, HTMLElement, HTMLTemplateElement, ShadowRoot, MouseEvent, parseHtmlFragment) that the runtime consumes instead of reaching forglobalThis. - Added
@llui/dom/ssr/jsdomsub-entry exportingjsdomEnv(): Promise<DomEnv>. Lazy-imports jsdom on call; each call returns a fresh env. - Added
@llui/dom/ssr/linkedomsub-entry exportinglinkedomEnv(): Promise<DomEnv>. Lazy-imports linkedom on call; safe on workerd and other strict-isolate runtimes where jsdom's transitive deps can't resolve. - Improved Every internal
document.*reference migrated toctx.dom.*threading — 19 files, ~40 call sites.mountApp/hydrateApp/renderToStringeach seed the render context with adom: DomEnvfield the primitives read. Concurrent SSR with different DOM implementations in a single process works correctly. - Improved
elTemplate's template cache is now per-env (WeakMap keyed onDomEnv) so concurrent SSR across jsdom + linkedom never cross-pollinates HTMLTemplateElement instances between envs. - Breaking
initSsrDom()moved from@llui/dom/ssr→@llui/dom/ssr/legacy. The shim still works (emits a one-timeconsole.warnpointing at the migration path) but must be imported from the new path. Rationale: co-locating the shim with the clean entry meantawait import('jsdom')stayed reachable from every Worker bundle that only wantedrenderToString. Splitting into a named sub-entry ensures the jsdom chunk only appears in bundles that explicitly import the legacy path. Migrate:import { initSsrDom } from '@llui/dom/ssr/legacy', then plan a proper migration tojsdomEnv()before it's removed.
@llui/vite-plugin@0.0.24
- Improved Compiler replaces its internal
document.createElement('template')IIFE emission with a call to__cloneStaticTemplate(html), a new@llui/domhelper that threads throughctx.dom. Static-content template clones now work correctly under SSR without needing a patched globalThis. The plugin auto-injects the helper import when it emits the call. - Improved
elTemplatepatch-function signature gains a third__dom: DomEnvparameter. Compiler-emitted patch bodies call__dom.createTextNode(...)instead ofdocument.createTextNode(...)for reactive-text placeholders. App-authoredelTemplatecalls are unaffected — the new parameter is optional in positional terms (unused params don't need to be declared).
@llui/vike@0.0.24
- Breaking
createOnRenderHtmlrequires adomEnvoption. See top of release block. - Improved
pageSlot()threads throughctx.dom.createCommentfor its anchor comment instead of touchingdocumentdirectly. Works under any env (jsdom, linkedom, or a custom one) without globalThis state. - Improved Chain-composition
renderNodesloop accepts an env parameter and uses it to synthesize end-sentinel comments. No more implicit dependency on a global document being alive during the composition pass.
@llui/{test,router,transitions,components}@0.0.24
- Rebuilt against the new
@llui/domversion. No source changes.
@llui/mcp@0.0.18
- Rebuilt against the new
@llui/domversion. No source changes.
2026-04-18 — 0.0.23
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.23; @llui/mcp@0.0.17
Post-0.0.22 polish pass. Ships a real bug fix: HTTP-mode @llui/mcp sessions used to route tool calls through dead relay instances (the per-session LluiMcpServer's relay was never startBridge()'d), so any tool that needed the browser would fail with RelayUnavailableError even when a browser was attached. Upgrade strongly recommended for anyone running MCP in HTTP mode.
@llui/dom@0.0.23
- Added
each.render's callback bag now carriesh: View<S, M>. Inside each-render you can now reach forh.text,h.scope,h.sample, etc. without using the top-level imports — symmetric with howbranch.cases[k],show.render, andscope.renderreceive the View. Both forms still work; destructure whichever is cleaner. - Improved
slice()wraps theeachrender callback so a liftedh: View<Sub, M>is threaded through correctly — code that usesslice(h, selector).each({ render })now sees the Sub-typed View inside the render bag. - Improved Dropped the placeholder
<_S, _M>generics on the internalBranchOptionsBaseinterface — they weren't used in the body. The three variants that extend it (BranchOptionsExhaustive,BranchOptionsNonExhaustive,BranchOptionsWide) continue to carry S, M as before. No user-visible API change.
@llui/mcp@0.0.17
- Fixed HTTP-mode session-relay bug. Each HTTP MCP session used to construct a fresh
LluiMcpServer, which in turn constructed its ownWebSocketRelayTransport. Only thebridgeHost's relay ever hadstartBridge()called — session relays were dead instances. Any tool call that needed the browser failed even when a browser was attached because the dispatcher'sctx.relaypointed at the unstarted session relay. Fix: newLluiMcpServer.createSessionMcp()returns a fresh SDKServerrouting through THIS instance's registry and relay.cli.tscalls it per session instead of spawning a newLluiMcpServer. A regression test intest/http-transport.test.tspins the shape by assertingbridge.running: truein the error diagnostic (the discriminator between a livebridgeHostrelay and a dead session-local one). - Fixed MCP server version advertised in the
initializehandshake is now read from@llui/mcp/package.jsonat module init instead of hardcoded as a literal — the hardcoded'0.0.15'silently drifted through the0.0.16release. Reads once, falls back to'unknown'on read failure. - Added
llui-mcp doctorhonors the standardNO_COLORenv var and a new--plainflag. Falls back toOK/FAILglyphs instead of emoji ✓/✗ for CI logs, screen readers, and corporate terminals that don't render U+2713/U+2717. - Deprecated
new LluiMcpServer(<port>)numeric-port constructor. The options formnew LluiMcpServer({ bridgePort, attachTo? })is the only shape that expresses HTTP-transport port sharing; numeric form is mostly dead code outside a couple of bridge tests and will be removed in a future release. JSDoc carries the@deprecatedtag.
@llui/{vite-plugin,test,router,transitions,components,vike}@0.0.23
- Rebuilt against the new
@llui/domversion. No source changes.
2026-04-18 — 0.0.22
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.22; @llui/mcp@0.0.16
Follow-up pass on the dicerun2 feedback batch. The branch() exhaustiveness-typing gate lands (deferred from 0.0.21). @llui/mcp adopts @modelcontextprotocol/sdk, gains an HTTP transport, and becomes plugin-spawnable — one pnpm dev starts the whole stack. @llui/vite-plugin picks up verbose, auto-detects @llui/mcp as a dep, warns on mismatched MCP state, and auto-spawns the child in HTTP mode. @llui/mcp also ships a doctor CLI + structured bridge diagnostic for self-describing failures.
Breaking
@llui/dom@0.0.22—branch.casesnow enforces exhaustiveness at the type level whenonreturns a literal string union. Existing calls with partialcasesand nodefaultthat previously compiled silently now require adefaultbuilder. Widestringreturns stay lenient (exhaustiveness can't be checked on an infinite domain).@llui/mcp@0.0.16—LluiMcpServer.start()is removed. The hand-rolled stdio JSON-RPC loop is replaced by SDK-backed transports; callers drive the protocol viaconnect(transport)(e.g.StdioServerTransport,StreamableHTTPServerTransport). Direct stdio consumers of the class must refactor; CLI users are unaffected.@llui/vite-plugin@0.0.22— when@llui/mcpis installed andmcpPortis omitted, the plugin now spawnsllui-mcp --http 5200as a child of the dev server (previously: wire-only to an externally-managed server). If your.mcp.jsonalready runsllui-mcpvia stdio, the spawn is skipped when the marker file is already present — but switching to HTTP transport in.mcp.json({ "type": "http", "url": "http://127.0.0.1:5200/mcp" }) is the recommended path forward.
Migration
branch({ on, cases: { a: …, b: … } })withoutdefaultover a literal union like'a' | 'b' | 'c': adddefault: () => [](or whatever the fallback should be) so the missing cases compile.- If you embed
@llui/mcpprogrammatically (rare — most consumers use the CLI), replaceserver.start()withawait server.connect(new StdioServerTransport()). Thestart()method no longer exists. - If you previously ran
npx llui-mcpin a separate terminal plus configured the Vite plugin withmcpPort: 5200: keep that setup — the plugin detects the existing marker and won't double-spawn. Or switch to the plugin-spawn + HTTP.mcp.jsonflow to drop the second terminal.
@llui/dom@0.0.22
- Breaking exhaustiveness typing for
branch()— see top of release block. - Added
ExhaustiveKeys<K, C>type helper (public) surfaced for consumers composing their ownbranch-like abstractions. - Improved
branch.tsreconciler tags the Lifetime with_kind: 'scope'when__disposalCause === 'scope-rebuild'; devtools disposer-log now distinguishes scope rebuilds from branch swaps end-to-end (runtime side was right in 0.0.21; this fills in the kind-string missing link). - Fixed
BranchOptionsBase<_S, _M>stops tripping the no-unused-vars lint in downstream consumers.
@llui/vite-plugin@0.0.22
- Added
verbose?: booleanoption — emits[llui]-prefixedconsole.infologs per compiled component file listing reactive state paths and their bit assignments. Off by default. - Added auto-detect: when
mcpPortis omitted and@llui/mcpresolves from the Vite project root, the plugin now defaults to enabling MCP — previously silent opt-out. ExplicitmcpPort: falsestill disables, explicit numeric port still selects wire-only. - Added auto-spawn: when auto-detect succeeds, the plugin reads
@llui/mcp'sbin.llui-mcpentry and spawnsllui-mcp --http <port>as a child ofserver.httpServer, piping stdout/stderr to Vite with[mcp]prefix, killing the child on server close. Skipped when the marker file already exists (something else is managing the server). - Added MCP mismatch warning: when
mcpPortresolves to null but the marker file exists, the plugin emits a one-shotconsole.warnexplaining the opted-out state and how to wire things up. - Improved
scope()is recognized by the path scanner,__maskinjection, and the static-onlint — it sees the same reactive-accessor treatment asbranch,show,each,memo. - Improved Pass 2 mask injection: new lint variant fires when
scope.on/branch.onreads no state (key never changes, subtree mounts once and never rebuilds). Usually a bug.
@llui/mcp@0.0.16
- Breaking
LluiMcpServer.start()removed — see top of release block. - Added
@modelcontextprotocol/sdkdependency.LluiMcpServerwraps the SDK'sServerclass; tool list/call handlers register viasetRequestHandlerwith Zod-backed schemas. The hand-rolled JSON-RPC loop is gone. - Added HTTP transport via SDK's
StreamableHTTPServerTransport.llui-mcp --http [port](default 5200) listens onPOST /mcpfor JSON-RPC requests, emits SSE-framed responses, and upgrades/bridgefor the browser WebSocket relay — one port, dual protocol. - Added
llui-mcp doctorsubcommand — offline diagnostic that walks the full failure-mode tree (marker presence, JSON validity, plugin devUrl stamping, bridge-port TCP connectability, recorded-pid liveness). Prints a ✓/✗ punch list; exits 0 on all-pass. - Added
RelayUnavailableError(exported) — thrown when a tool call needs the browser and no browser is attached. Carries adiagnostic: BridgeDiagnosticpayload (connection status, bridge state, browser tabs, marker state,suggestedFixsentence). Thetools/callhandler surfaces it as an MCPisError: truetool result whose content is JSON-serialized diagnostic — callers see why the call failed, not just that it did. - Added
BridgeDiagnostictype (exported from@llui/mcp/transports) for consumers building their own diagnostics UI. - Added
LluiMcpServerOptionsshape — constructor now accepts{ bridgePort?, attachTo? }to share anhttp.Serverwith an externally-managed HTTP transport. Numeric-port constructor still works for backward compat. - Improved
WebSocketRelayTransportgains anattachTo: http.Servermode alongside the standaloneportmode — HTTP-transport deployments share a single port for MCP + bridge via upgrade routing on/bridge.
@llui/{test,router,transitions,components,vike}@0.0.22
- Rebuilt against the new
@llui/domversion. No source changes.
Docs
@llui/mcpREADME documents both usage patterns (plugin-launched HTTP and manual stdio) with.mcp.jsonexamples and adoctortroubleshooting section.- Site API docs + llms-full.txt regenerated.
2026-04-18 — 0.0.21
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.21; @llui/mcp@0.0.15
Big release. Lands the scope() + sample() primitives for keyed subtree rebuild, renames the internal Scope disposal concept to Lifetime, threads the D (init-data) generic through every public API, and closes every item from the dicerun2 feedback batch — path-scanner false positives, spread-in-children noise, bitmask diagnostic improvements, plus new plugin options for CI (failOnWarning, disabledWarnings). Three breaking changes in @llui/dom; mechanical migrations.
Breaking
@llui/dom@0.0.21— InternalScopedisposal-lifetime type renamed toLifetime. The rename surfaces in two public places: the exportedScopeNodetype becomesLifetimeNode, andMountOptions.parentScopebecomesMountOptions.parentLifetime. The runtime itself, DOM output, disposal semantics, and the_kindstrings on nodes are unchanged — this is a pure naming fix.@llui/dom@0.0.21—branch.onnarrows fromstring | number | booleantostring. Numeric/boolean discriminants coerce at the call site (on: s => String(s.code)oron: s => s.flag ? 'yes' : 'no').branch.casesbecomes optional, and a newdefault?: (h) => Node[]field runs whenever no case matches — the canonical "dynamic rebuild" shapebranch({ on, default })works without enumerated cases.@llui/dom,@llui/test@0.0.21— Every public API that takes aComponentDefnow threads theD(init-data) generic. CoversmountApp,mountAtAnchor,hydrateApp,hydrateAtAnchor,renderToString,renderNodes,addressOf,replaceComponent,testComponent,testView. Previously a typed-data component required anas unknown as ComponentDef<S, M, E>cast at each call site; that cast is no longer needed (and, for non-void D, no longer compiles without it).
Migration
- Replace every
MountOptions.parentScopewithparentLifetime; same for anyScopeNodetype import (→LifetimeNode). The only consumer outside@llui/domis@llui/vike's layout chain, which this release updates. - Wrap numeric/boolean
branch({ on })inString(...)and keep case keys as stringified literals. Ifcasesdidn't cover every possible key, add adefaultbuilder — the new runtime will now fall back to it instead of rendering nothing. - Remove
as unknown as ComponentDef<S, M, E>casts frommountApp(container, MyDef, data)andtestComponent(MyDef, data)call sites; theDgeneric now flows through. Regenerate types (pnpm turbo check --force) to confirm nothing else was papering over a real mismatch.
@llui/dom@0.0.21
- Added
scope({ on, render })— rebuilds a subtree when the string-valued key returned byon(state)changes. Each rebuild runs in a freshLifetimewith fresh bindings andonMountcallbacks. Sugar overbranch({ on, cases: {}, default: render })with the'scope-rebuild'disposer cause. Replaces the "each + epoch + closure-captured snapshot" workaround for "rebuild this region when this counter changes" use cases. - Added
sample(selector)— one-shot imperative state read inside a render context. Available as a top-level@llui/domimport and ash.sample(...)on theViewbag (destructure-friendly inside builders). No binding is created, no mask is assigned; ideal for reading a whole-state snapshot inside ascope()arm without making the entire subtree reactive. - Added
branch.default— fallback builder described under Breaking. Withcasesalso now optional,branch({ on: s => String(s.epoch), default: render })is a valid dynamic-rebuild shape (thoughscope()is the preferred spelling). - Added
ItemAccessor<T>.current()— returns the whole current item. Fixes primitive-T ergonomics (where the mapped-field branch collapses to method names liketoString) and lets object-T callers sample the full record without writingitem(r => r)(). - Improved
Dgeneric threaded through every publicComponentDef-taking API (see Breaking / Migration). Also cascades intocreateComponentInstanceinternally —child()andlazy()widen their pre-existing casts to carry theDslot. - Improved
View.branch/View.scope/View.sampleavailable on the destructuredhbag. - Fixed
show()wraps the booleanwhenviaString(...)internally to match the new string-onlybranch.on— runtime semantics unchanged for user code.
@llui/vite-plugin@0.0.21
- Added
failOnWarningplugin option — routes every diagnostic throughthis.errorinstead ofthis.warnso lint regressions fail CI without a custombuild.rollupOptions.onwarnhandler. - Added
disabledWarningsplugin option — silences specific rules without disabling the lint pass. Every diagnostic is tagged with aDiagnosticRule(also exported); the tag appears in brackets at the start of each warning message (e.g.[spread-in-children]), so authors know what to pass. - Added
scoperecognized by the path scanner and__maskinjection — theonaccessor's state paths contribute to the component bitmask, and Phase 1 reconcile is gated by the same mask machinerybranch/each/show/memoalready use. - Added
static-onlint — warns whenscope.onorbranch.onreads no state. The key never changes, so the subtree mounts once and never rebuilds; usually a bug. - Improved Every diagnostic message is now prefixed with
<file>:<line>:<col>: [<rule>]— survives customonwarnhandlers that logwarning.messagealone. - Improved Bitmask-overflow diagnostic does co-occurrence analysis — when every sub-path of a top-level field always fires in the same set of accessors, suggests reading the parent object as a single unit (one bit vs. N bits) before recommending
child()extraction. Cheaper refactor, same budget relief. - Fixed Spread-in-children is now scope-aware. Identifier spreads (
...foo) and array-method spreads (...foo.map(...),.concat(...), etc.) resolving to bounded bindings — array literal, function-call result, or.mapon a named bounded receiver — no longer fire. Inline...[…].map(...)still warns. Closes four concrete noise cases reported from dicerun2: conditionalpushinto a localNode[],.mapover aconst x = […] as consttuple, storing a helper-call result in a local first, and.concaton two namedNode[]arrays. - Fixed Path scanner unified between
collect-deps.ts(runtime bit assignment) anddiagnostics.ts(bitmask-overflow warning). The diagnostics side previously had its own naïve walker that produced false positives foreach({ key }),item((t) => t.field), array-method callbacks (.some,.filter, etc.) inside reactive accessors, and user-land helper properties likesliceHandler({ narrow }). All four are now silent. - Fixed
onMsghandlers no longer inflate the path bitmask via the same unified-scanner change.
@llui/test@0.0.21
- Added
reducer({ init, update, name? })— builds a view-lessComponentDefso reducer-only suites can drop a definition intotestComponent()without padding aview: () => []field. Default name__reducer__surfaces in devtools/HMR if one ever leaks into a real mount. - Improved
testComponentandtestViewthread theDgeneric through (see Breaking). Typed init data passes without a cast.
@llui/vike@0.0.21
- Breaking Consumes the
Lifetimerename viaMountOptions.parentLifetime— see top of release block.
@llui/mcp@0.0.15
- Breaking Consumes the
Lifetimerename viaLifetimeNode— see top of release block.
@llui/{router,transitions,components}@0.0.21
- Rebuilt against the new
@llui/domversion. No source changes.
Docs
- New design spec
docs/superpowers/specs/2026-04-18-scope-primitive-design.mdand matching plan underdocs/superpowers/plans/. - Cookbook recipe "Rebuild a subtree when a derived value changes" documents the canonical
scope() + sample()pattern and deprecates the oldeach + epoch + closure-snapshotworkaround. - Site footer exposes
llms-full.txtalongsidellms.txtfor discoverability.
2026-04-18 — 0.0.20
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.20; @llui/mcp@0.0.14
Anchor-based mount primitives land in @llui/dom, enabling @llui/vike's pageSlot() to emit a bare comment marker instead of a wrapper div. Also in @llui/dom: a new unsafeHtml primitive for rendering trusted HTML strings (markdown output, syntax-highlighted code, server snippets).
Breaking
@llui/vike@0.0.20—pageSlot()now emits<!-- llui-page-slot -->instead of<div data-llui-page-slot="">. Apps that styled or queried the slot element directly must wrappageSlot()in their own styled element. The scope-tree behavior is unchanged.
Migration
- If you were styling the page slot (e.g.
.page-slot { display: flex }or[data-llui-page-slot] { ... }), move the styles to an enclosing element you add inside your layout view:main([pageSlot()]),div({ class: 'page-slot' }, [...pageSlot()]), etc. - If you were querying the slot via
document.querySelector('[data-llui-page-slot]'), switch to walking comment nodes (TreeWalker(..., SHOW_COMMENT)) or query your own wrapping element.
@llui/dom@0.0.20
- Added
mountAtAnchor(anchor, def, data?, opts?)andhydrateAtAnchor(anchor, def, serverState, opts?)— mount or hydrate a component relative to a comment anchor rather than inside a container element. Uses a synthesized end sentinel (<!-- llui-mount-end -->) to bracket the owned DOM region; dispose walks between the sentinels so top-leveleach/show/branchmutations within the component are always cleaned up correctly. Publicly exported — usable outside@llui/vikefor anywhere you want to embed a reactive component at a comment anchor (e.g. inside rendered markdown). - Added
unsafeHtml(html, mask?)primitive — escape hatch for rendering trusted HTML strings into the DOM. Accepts a static string or a reactive accessor. The reactive path short-circuits on strict string equality so unchanged HTML preserves subtree identity (focus, selection, listeners attached outside LLui). Callers own sanitization — the parsed subtree is opaque to the framework (no nested bindings, events, or primitives). Wired intoView<S, M>andslice()'s view bag. - Improved
HmrEntrybecomes a discriminated union (kind: 'container' | 'anchor') with a newregisterForAnchorexport.replaceComponenthandles both kinds with appropriate DOM cleanup + insertion strategies, so hot-swap works for anchor-mounted instances without touching their outer DOM. - Improved new
_removeBetweenand_findEndSentinelhelpers inmount.ts. Both guard a nullparentNodedefensively so a detached anchor at dispose time is a no-op rather than a thrownTypeError.
@llui/vike@0.0.20
- Breaking
pageSlot()emits a comment anchor. See top of release block. - Improved SSR stitching in
on-render-html.tsusesinsertBeforerelative to the anchor plus a synthesized end sentinel per layer, replacing the oldappendChild-into-marker approach. - Improved client adapter in
on-render-client.tsdispatches betweenhydrateApp/mountApp(root container) andhydrateAtAnchor/mountAtAnchor(inner anchors) based on node kind. Nav swaps rely on per-layerhandle.dispose()for region cleanup instead of the old top-downleaveTarget.textContent = ''. - Improved exports
_renderChainand_mountChainSuffix@internalfor direct testing.
@llui/{vite-plugin,test,router,transitions,components}@0.0.20
- Added cascade bump — no user-visible changes; picks up the new
@llui/dom@0.0.20peerDependency range.
@llui/mcp@0.0.14
- Added cascade bump — no direct changes. Picks up
@llui/dom@0.0.20via workspace resolution.
2026-04-17 — 0.0.19
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.19; @llui/effects@0.0.9; @llui/mcp@0.0.13
Phase 1 of the MCP debug-API expansion lands: 21 new MCP tools, 16 new LluiDebugAPI methods, four dev-mode runtime trackers in @llui/dom, and a dev-only effect interceptor hook in @llui/effects. Plus three correctness fixes carried along from parallel work on child(), per-case mask analysis, and Vike page context typing.
@llui/dom@0.0.19
- Added 16 new
LluiDebugAPImethods, populated oninstallDevTools:- DOM:
inspectElement,getRenderedHtml,dispatchDomEvent,getFocus - Bindings/scope:
forceRerender,getEachDiff,getScopeTree,getDisposerLog,getBindingGraph - Effects:
getPendingEffects,getEffectTimeline,mockEffect,resolveEffect - Time-travel/utility:
stepBack,getCoverage - Eval:
evalInPage(runs user JS vianew Function()with an observability envelope — state diff, new history entries, new pending effects, dirty bindings).
- DOM:
- Added four dev-mode ring-buffer trackers: each-diff log (100), disposer log (500), effect timeline (500), Msg coverage. All zero-cost in production — populated only when
installDevToolsruns, gated on a module-level flag. - Added scope
_kindtagging (root | show | each | branch | child | portal | foreign) set by each structural primitive at creation; reset on pool recycle. PowersgetScopeTree's classification without a separate lookup. - Added new exported types:
ElementReport,ScopeNode,EachDiff,DisposerEvent,PendingEffect,EffectTimelineEntry,EffectMatch,StateDiff,CoverageSnapshot,MessageRecord. - Added
kind='effect'binding variant for side-effect-only watchers.applyBindingis a typed no-op; Phase 2 runs the accessor without diffing or writinglastValue. Used internally bychild()'s prop-watch binding, eliminating per-tick object stringification onto a detached anchor. - Fixed
child()propsMsg loop vector. Framework-synthesized propsMsg messages now dispatch throughoriginalSend, bypassing theonMsgwrapper — a naiveonMsg: m => echo(m)no longer bounces props/set back to the parent and loops forever. - Improved mocked effects auto-deliver their response via the effect's own
onSuccesscallback on a microtask (same timing contract as a real async resolve), makingllui_mock_effectusable as a testing primitive.
@llui/effects@0.0.9
- Added
_setEffectInterceptor(hook | null)dev-only hook. Zero-cost in production — one null check per dispatch; no allocation when the hook is null. Reserved for Phase 2 (Worker / off-loop effect interception); Phase 1@llui/domintercepts upstream at the update loop, so Phase 1 callers of the hook won't see invocations. Documented in JSDoc.
@llui/vite-plugin@0.0.19
- Added MCP marker file now carries an optional
devUrlfield. The plugin stamps the dev URL when Vite's HTTP server starts listening; marker updates handle both orderings (MCP-before-Vite and MCP-after-Vite). Thellui:mcp-readyHMR event broadcasts the full marker so the browser relay doesn't depend onfs.watchside-effects. - Added diagnostic that warns when a
child()propsaccessor returns an object literal whose values are themselves freshly-constructed object/array literals. Prop diffing compares top-level keys byObject.is— a fresh reference reports "changed" every render, firingpropsMsgon every parent update. - Fixed
analyzeModifiedFieldsnow bails out onSpreadAssignments whose source isn't the state parameter (e.g....msg.props). The previous code treated every spread as a noop, which produced narrowcaseDirtymasks excluding fields the spread actually overwrites. Symptom: stale DOM on props/set after a spread-based reducer.show()reconcile seemed to work only because mounting a fresh arm created new bindings that happened to read current state.
@llui/mcp@0.0.13
- Added 21 new MCP tools routed through a new
ToolRegistrywith layer-tag dispatch (debug-api | cdp | source | compiler):- View/DOM (5):
llui_inspect_element,llui_get_rendered_html,llui_dom_diff,llui_dispatch_event,llui_get_focus - Bindings/scope (6):
llui_force_rerender,llui_each_diff,llui_scope_tree,llui_disposer_log,llui_list_dead_bindings,llui_binding_graph - Effects (4):
llui_pending_effects,llui_effect_timeline,llui_mock_effect,llui_resolve_effect - Time-travel/utility (5):
llui_step_back,llui_coverage,llui_diff_state,llui_assert,llui_search_history - Eval (1):
llui_eval
- View/DOM (5):
- Improved internal layout:
packages/mcp/src/index.tsshrinks from 747 → ~244 lines. Tool handlers live intools/debug-api.ts; WebSocket relay lives intransports/relay.tsasWebSocketRelayTransport implements RelayTransport. Same public API (LluiMcpServer,connectDirect,handleToolCall). - Added
setDevUrl(url)onLluiMcpServer. Extends the marker write so CDP-fallback consumers (Phase 2) can find the dev URL.
@llui/vike@0.0.19
- Fixed
pageContext.datanow honorsVike.PageContextaugmentations. The server and client hook interfaces previously declareddata?: unknowninline, so consumer augmentations of Vike's global namespace never reached the hook callbacks — everydocument({ pageContext })/ nav callback had to cast. A conditional lookup onVike.PageContextresolves tounknownwhen unaugmented and to the user's type when declared. An ambient stub of theVikenamespace lets the package type-check standalone and merge cleanly whenvikeis installed alongside.
@llui/{test,router,transitions,components}@0.0.19
- Added cascade bump — no user-visible changes; picks up the new
@llui/dom@0.0.19peerDependency range.
Docs
packages/mcp/README.md,site/content/api/mcp.md,site/content/cookbook.md,site/content/llm-guide.md,CLAUDE.md,docs/designs/07 LLM Friendliness.md,docs/designs/09 API Reference.mdall updated with Phase 1 additions (tool tables, API types, browser console examples, package row).
2026-04-15 — 0.0.18
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.18; @llui/mcp@0.0.12
Hotfix release for a compiler regression in 0.0.17 that silently broke form-error rendering inside child components whose view factored structural blocks into helper functions. Anyone running 0.0.17 against @llui/components's dialog.overlay with a form body inside should upgrade.
Migration
- Delete any
stripFastPath-style workaround that strips__update/__dirty/__handlersfrom concreteComponentDefs before passing them tochild({ def }). The compiler fast path is now correct — pass the concrete def directly. - Delete any
widenDef-style wrapper still in use at achild({ def })boundary. 0.0.17'sAnyComponentDefalias already made the wrapper unnecessary for typing; 0.0.18 removes the runtime reason it was accidentally helping (it was stripping the broken fast path, not widening).
@llui/vite-plugin@0.0.18
- Fixed
detectArrayOpno longer short-circuits structural reconcile when a case'scaseDirtydoesn't intersect the computedstructuralMask. The optimization was unsafe becausecomputeStructuralMaskonly walks the view function's lexical AST — it does not descend into helper function calls. A view likeview: () => [...show({ when: s => s.mode === 'signin', render: () => [signinFormBody(send)] })]wheresigninFormBody(send)internally does...show({ when: s => s.errors.email !== undefined, ... })produces astructuralMaskthat contains themodebit but misseserrors.email. The submit case'scaseDirtythen had no overlap withstructuralMaskeven though the inner show block's mask DOES depend onerrors, and the compiler emittedmethod = -1("skip structural blocks") for the submit handler. At runtime_handleMsgskipped Phase 1 entirely, the helper-hidden show blocks never reconciled, and error paragraphs never mounted despite state having changed. The symptom was "submit button click doesn't show validation errors" — reproducible against any component that factors its form body into a helper function. Fixed by removing the unsafe short-circuit. Non-empty cases now always fall through to'general'(method = 0) unless an explicit array op (clear/remove/mutate/strided) is detected. Phase 1 runs unconditionally;_handleMsg's existing per-block(block.mask & dirty)check filters uninterested blocks at near-zero cost. ThemodifiedFields.length === 0short-circuit is preserved — a case that returns[state, []]unchanged is a real tautology and still emitsmethod = -1. Regression tests inpackages/vite-plugin/test/show-helper-reconcile.test.tscover the helper-hidden shape, a minimal cross-function mode+errors variant, and the preserved noop tautology.
@llui/dom@0.0.18
- Improved
useContextValuedocstring now has a dedicated "Value capture contract" section spelling out that the returned value is captured once at view-construction time. Storing the return in a closure insideview()and reading from event handlers is the correct and efficient pattern for stable dispatcher bags; consumers that need to see later re-publishes from a parent must use the reactiveuseContext(ctx)form. The docstring also documents the pairing rule:useContextValuemust be used withprovideValueon the producer side; using it against a state-reading provider will passundefinedto the accessor and likely throw or return garbage.
@llui/{test,router,transitions,components,vike}@0.0.18
- Improved Cascade bump from
@llui/dom@0.0.18(tier-1 lockstep). No direct code changes — same contracts as 0.0.17.components,router, andtransitionsalso have theirpeerDependencies["@llui/dom"]range updated from^0.0.17to^0.0.18.
@llui/mcp@0.0.12
- Improved Cascade bump from
@llui/dom@0.0.18runtime dependency. No direct code changes — same contracts as 0.0.11.
Docs
- Improved Cookbook "Persistent Layouts → Layout ↔ Page communication" recipe now documents the
useContextValuecapture contract inline — when to reach for it vs the reactiveuseContextform. - Improved LLM guide rules bullet extended with the capture contract note so LLMs picking up the context-dispatcher pattern from the guide see the warning.
2026-04-15 — 0.0.17
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.17; @llui/mcp@0.0.11
Follow-up release for four reports against 0.0.16's persistent-layout work. Covers a functional gap (no prop updates on surviving layers), a type-system ergonomics issue (widenDef invariance across three APIs), a docs filename collision (+Layout.ts vs Vike's own convention), and an API shape wart (useContext awkward for static dispatcher bags).
Migration
- Revert any
widenDef-style helper you wrote to pass a concreteComponentDef<S, M, E, D>intochild({ def }),createOnRenderClient({ Layout }), orcreateOnRenderHtml({ Layout }). Concrete component definitions now assign structurally into these APIs via the newAnyComponentDefalias — no widening needed. - Revert any module-level pub/sub bridge you wrote to deliver nav data into a persistent layout.
createOnRenderClientnow pushes freshlluiLayoutData[i]into surviving layers through theirpropsMsghandler on every nav — opt in by settingpropsMsg: (data) => ({ type: 'navChanged', data })on the layout def. - Consider switching static dispatcher-bag contexts from the reactive
provide(ctx, accessor, children)/useContext(ctx)pair to the newprovideValue(ctx, value, children)/useContextValue(ctx)forms. Call sites becomeuseContextValue(ctx).method(...)instead ofuseContext(ctx)(undefined as never).method(...). The reactive forms still exist for context values that track state. - Rename any layout file called
+Layout.ts(per the previous release's docs) toLayout.tsor similar — the+prefix is Vike's own framework-adapter convention and collides with@llui/vike'sLayoutoption.
@llui/vike@0.0.17
- Fixed Surviving layers on client nav now receive fresh
lluiLayoutData[i]through theirpropsMsghandler. Previously the chain diff identified which layers to keep alive but never delivered the updated data slice — a persistent layout tracking pathname, session, breadcrumbs, or nav-highlight state was frozen at whatever it initialized with on first mount. The adapter now walks the shared prefix after the diff, shallow-keyObject.is-diffs each surviving layer's new data against its stored slice, and dispatches the layer'spropsMsg(newData)result through the newAppHandle.sendchannel on change. Layers withoutpropsMsgare skipped silently — opt-in. Mirrorschild()'s prop-diff and dispatch behavior exactly. - Fixed
createOnRenderClient({ Layout })andcreateOnRenderHtml({ Layout })now accept concreteComponentDef<S, M, E, D>without a widening helper. Previously theLayoutoption was typed asComponentDef<unknown, unknown, unknown, unknown>, which uses property syntax and is contravariant in each type parameter — concrete definitions were rejected with "Type 'void' is not assignable to type 'unknown'" on theinitfield. The option is now typed asAnyComponentDef(a new type-erased alias exported from@llui/domusing method syntax for bivariance) so structural assignment succeeds without anywidenDefwrapper.ChildOptions.defuses the same alias — the same gap inchild({ def })is fixed by the same change. - Improved Docs no longer recommend
pages/+Layout.tsas the layout filename. Vike reserves the+prefix for its own framework-adapter config conventions, and+Layout.tsspecifically is interpreted byvike-react/vike-vue/vike-solidas a framework-native layout config — collides with@llui/vike'sLayoutoption. All JSDoc examples, the README, cookbook recipe, LLM guide, andpageSlot()primitive doc now showpages/Layout.ts(no prefix) with an explicit warning paragraph explaining why.
@llui/dom@0.0.17
- Added
AnyComponentDefexported from@llui/dom(and from@llui/dom/internalfor framework adapters). A type-erased component-definition shape using method syntax for bivariance — concreteComponentDef<S, M, E, D>s assign structurally without any widening helper. Used bychild(),createOnRenderClient({ Layout }), andcreateOnRenderHtml({ Layout })as the consumer-facing type for opaque component definitions at module boundaries. The existingLazyDef<D>(used bylazy()) remains parameterized onDfor the lazy-loader case. - Added
AppHandle.send(msg)exposes the mounted instance's send channel through the handle object, allowing adapter-level code to dispatch messages into long-lived instances from outside their normal view-boundsendpath. No-op afterdispose(). Used by@llui/vike's persistent-layout chain to push layout-data updates into surviving layer instances on client navigation.mountApp,hydrateApp, andhmr.replaceComponentall populate the new method; existing consumers that only usedispose()andflush()are unaffected. - Added
provideValue<T>(ctx, value, children)anduseContextValue<T>(ctx)as static-bag companions to the existing reactiveprovide/useContextprimitives. For the common case of publishing a stable dispatcher record (toast queues, session managers, DI containers — anything that doesn't depend on parent state),provideValuewraps the value in a constant accessor anduseContextValueresolves it with a single function call. Replaces theuseContext(ctx)(undefined as never).method(...)pattern withuseContextValue(ctx).method(...). The reactive primitives still exist and are still the right call when the context value DOES need to track state.
@llui/{vite-plugin,test,router,transitions,components}@0.0.17
- Improved Cascade bump from
@llui/dom@0.0.17(tier-1 lockstep). No direct code changes — same contracts as 0.0.16.components,router, andtransitionsalso have theirpeerDependencies["@llui/dom"]range updated from^0.0.16to^0.0.17.
@llui/mcp@0.0.11
- Improved Cascade bump from
@llui/dom@0.0.17runtime dependency. No direct code changes — same contracts as 0.0.10.
Docs
- Added Doc updates across the
@llui/vikeREADME, cookbook "Persistent Layouts" recipe, LLM guide section + rules bullet: everything showsprovideValue/useContextValuefor the layout-owned dispatcher pattern, usespages/Layout.tsas the filename with an explicit warning against+Layout.ts, and the cookbook + llm-guide spell out when to reach for the static-bag primitives vs the reactive ones. - Improved
examples/vike-layoutswitched bothToastContextandSessionContexttoprovideValue+useContextValue. DroppedSessionDispatcher.getUserfrom the contexts module with a note explaining why — context accessors can't reach across instance boundaries to read live layout state, so exposing a state-reader dispatcher from a layout context was always subtly broken. - Improved
scripts/publish.shnow runspnpm whoamias an auth preflight and auto-runspnpm logininteractively when the token is expired or missing. Previously a stale token produced nine consecutiveE404errors (npm returns 404 on PUT for unauthenticated writers to avoid leaking scope existence) which was confusing if you didn't know the pattern. Not a package change — only visible to maintainers running publish.
2026-04-15 — 0.0.16
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.16; @llui/mcp@0.0.10; @llui/lint-idiomatic@0.0.12
Headline: persistent layouts in @llui/vike. Declare app chrome (header, sidebar, session state, portalled dialogs) as a Layout component that stays mounted across client navigation — only the route's page disposes and re-mounts. Nested layout chains and per-route chain resolvers both supported from day one. Plus the supporting runtime primitives in @llui/dom, a compiler walker fix, and two lint-rule false-positive fixes.
@llui/vike@0.0.16
- Added
Layoutoption oncreateOnRenderClient/createOnRenderHtml. Accepts a singleComponentDef, an array[outer, ..., inner]for nested chains, or a(pageContext) => chainfunction for per-route resolution. Persistent layouts stay mounted across client nav; only the divergent suffix of the chain disposes and re-mounts. Outer-layer DOM — and every portal, focus trap, scroll position, and effect subscription rooted inside it — survives page swaps. - Added
pageSlot()primitive (exported from@llui/vike/client) — a declarative structural marker a layout places in its view to declare where the nested Page or nested Layout renders. Creates its scope as a child of the current render scope so contexts flow from layout providers through the slot into the page via standarduseContextlookups. Call exactly once per layout; layouts with zero or two-plus slots throw with descriptive errors. - Added Chain diff on nav walks old and new chains in parallel by component identity and preserves every shared prefix layer. Navigating between
/dashboard/reportsand/dashboard/overviewdisposes only the innermostPage; navigating from/dashboard/*to/settingscollapses the chain to[AppLayout]. Per-route resolvers enable this cleanly. - Added Chain-aware hydration envelope:
window.__LLUI_STATE__is now{ layouts: [{ name, state }, ...], page: { name, state } }for layout-using pages. Entries carry the component name so server/client chain mismatches fail loud with a clear error instead of silently binding wrong state to wrong instance. The legacy flat envelope shape is still read for pages without a configuredLayout— no migration required for existing apps. - Added Regression tests covering single-layout mount + nav, nested 3-layer chains, context flow through the slot, chain diffing with per-route resolvers, SSR composed rendering, and error paths (missing
pageSlotin a layout,pageSlotcalled from the innermost page). 10 tests inpackages/vike/test/layout.test.ts.
@llui/dom@0.0.16
- Added
MountOptions.parentScopeonmountApp/hydrateApp— when provided, the mounted instance'srootScopebecomes a child of that scope. This is the keystone that makes persistent layouts compose:@llui/vike'spageSlot()uses it to parent a page instance into its enclosing layout's scope tree, souseContextlookups walk layer boundaries and scope disposal cascades in the right direction on nav. - Added
@llui/dom/internalsubpath export. Surfaces low-level primitives (getRenderContext,setRenderContext,clearRenderContext,createScope,disposeScope,addDisposer) for framework-adapter packages that need to build structural primitives likepageSlot()on top of the runtime. Not part of the public app-author API — stability contract applies only to the main@llui/dombarrel. - Added
renderNodesandserializeNodesfactored out ofrenderToString. Chain renders (e.g.@llui/vike/server's layout-composed SSR) can now render multiple instances, append their outputs into each other's slot markers, and serialize the composed tree once with the union of every layer's bindings.renderToStringis a trivial one-liner on top and its public contract is unchanged. - Fixed
elSplitchildren now flatten nested arrays one level, matchingcreateElement's existing behavior. Patterns likemain([helperReturningNodeArray()])worked in unit tests (raw path flattens) but silently crashed at SSR build time because the compiled path didn't. Both paths now agree — catches this class of test-vs-production mismatch permanently.
@llui/vite-plugin@0.0.16
- Fixed
computeAccessorMask's AST walker no longer crashes on chained method calls inside template literals inside reactive accessors. Previously a pattern liketext((_s) => \$${item.x.toLocaleString()}`)inside aneach()row crashed the whole build with "Cannot read properties of undefined (reading 'kind')" — the row-factory rewrite synthesizes new sub-trees whose innerPropertyAccessExpressionnodes have no parent pointers, and the walker'sts.isPropertyAccessExpression(node.parent)crashed on undefined. Guarded every parent access in the walker; mask accounting is unchanged because resolving a chain from an inner PAE produces a prefix of the outer chain (idempotent|=). Regression tests inaccessor-walker-parent.test.ts`.
@llui/lint-idiomatic@0.0.12
- Fixed
state-mutationrule's "Increment/decrement on state" check no longer flags all prefix and postfix unary operators on state access — only++and--count as mutations. Before the fix, the canonical toggle reducerreturn [{ ...state, flag: !state.flag }, []]was flagged as a mutation because!is a prefix unary operator;-state.x,~state.x,+state.xwere caught the same way. - Fixed
spread-in-childrenrule now exemptsprovideandpageSlotalongside the existing structural-primitive exemptions (each,show,branch,virtualEach,onMount). Both returnNode[]and must be spread, and the rule was tripping on every layout authoring pattern that placed a context provider or page slot inside an element-helper children array. - Fixed
@llui/lint-idiomatic/viteplugin now reads source from disk viareadFileSync(id)inside the transform hook instead of trusting the pipelinecodeargument. Before the fix,enforce: 'post'meant the plugin was linting the AST AFTER@llui/vite-pluginhad rewritten component bodies — compiler-generated row-updater++/--loops triggered false-positivestate-mutationwarnings that didn't correspond to anything in user source. Reading from disk guarantees we only ever see what the author wrote.
@llui/mcp@0.0.10
- Improved Cascade bump from
@llui/dom@0.0.16and@llui/lint-idiomatic@0.0.12runtime dependencies. No direct code changes — same contracts as 0.0.9.
@llui/{test,router,transitions,components}@0.0.16
- Improved Cascade bump from
@llui/dom@0.0.16(tier-1 lockstep). No direct code changes — same contracts as 0.0.15.components,router, andtransitionsalso have theirpeerDependencies["@llui/dom"]range updated from^0.0.15to^0.0.16.
Docs
- Added New "Persistent Layouts" + "Layout → Page communication via context" recipes in the cookbook under the SSR section.
- Added New "Persistent layouts (@llui/vike)" section in the LLM guide with the canonical shape and a new rules bullet so LLMs reach for
pageSlot()as the idiom. - Added New "Cross-instance scope parenting" subsection in the architecture doc explaining how
parentScope+pageSlot()make context flow layer → layer and how disposal cascades asymmetrically on nav. - Added New
examples/vike-layoutworkspace — full working example with root layout (toast stack + session context dispatchers from layout state), nested dashboard layout with sidebar, four routes exercising different chain shapes, per-route chain resolver. All four routes prerender via Vike SSG.
2026-04-14 — 0.0.15
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.15; @llui/mcp@0.0.9
Addresses two production reports against @llui/vike + @llui/transitions page routing, and bakes the browser-e2e test back into the default pnpm verify pipeline.
@llui/vike@0.0.15
- Added
RenderClientOptions.onLeave(el)— awaited before dispose, so leave animations can run against the outgoing page's still-mounted DOM. Return a promise to defer the dispose-and-mount swap until the animation finishes. - Added
RenderClientOptions.onEnter(el)— fires after the new page mounts, for enter animations. Sync; promise returns are ignored. Neither hook fires on the initial hydration render. - Added
fromTransition(t)adapter — converts anyTransitionOptions(the shape returned byrouteTransition,fade,slide, etc. from@llui/transitions) into the{ onLeave, onEnter }pair, so wiring route transitions into Vike filesystem routing is one line:createOnRenderClient({ ...fromTransition(routeTransition({ duration: 200 })) }). - Improved README documents the full client-navigation lifecycle:
onLeave→dispose→textContent = ''→mountApp→onEnter→onMount, with notes onAbortSignalsemantics for in-flight effects (the signal gatessend()dispatches but does not cancel in-flight network requests — intentional, avoids losing a successful POST on nav) and scroll handling (Vike's problem viascrollToTop, not ours).
@llui/transitions@0.0.15
- Improved
routeTransition()JSDoc now documents both call sites: manualbranch()-based routing (spread{ enter, leave }into the branch call) and@llui/vikefilesystem routing (wrap viafromTransitionfrom@llui/vike/client). Previous wording implied the primary path wasbranch()and left Vike users reaching for a helper with nowhere to plug it in.
@llui/components@0.0.15
- Added
dialog-dispose.test.tsregression test: asserts that disposing a mounted app with an opendialog.overlayleavesdocument.bodyclean — no leftover portal content, focus-trap stack empty, body scroll lock count zero, siblingaria-hidden/inertrestored, idempotent on seconddispose(). Empirically confirms the scope-disposer chain correctly tears down overlay state when@llui/vikeclears a page during client navigation.
@llui/vite-plugin@0.0.15
- Fixed
test/mcp-watch.test.tswas leakingfs.watchhandles on the marker directory's parent on everysetup()call. Over ~200 test invocations the accumulated handles hit macOS's EMFILE cap and sporadically crashed other tests running in parallel. Track active fake servers per test and fire their registeredclosehandlers inafterEachso the plugin's cleanup path runs.
@llui/mcp@0.0.9
- Fixed
test/playwright-e2e.test.tsreworked to use vite's programmaticcreateServerAPI withserver.watch: nullandoptimizeDeps.noDiscovery: true. The previousspawn('pnpm', ['dev'])path was unreliable on macOS: vite's default chokidar watcher tries to register directory watches across the whole monorepo at startup and blows through the launchctl-default 256-fd soft limit before printing its ready message, surfacing as a spuriousvite startup timeoutthat had broken this suite on every developer machine since it landed. - Fixed Narrowly-scoped
process.on('uncaughtException')filter installed during the suite swallows only{ code: 'EMFILE', syscall: 'watch' }errors originating from vite'swatchPackageDataPlugin, which registersfs.watchon everypackage.jsonregardless ofserver.watch. Legit exceptions still propagate; the filter is removed inafterAll. - Improved Suite is re-included in the default
pnpm verifypipeline — runs in ~3s against a real Vite dev server and a real Chromium browser. The earlierLLUI_RUN_E2Eopt-in flag is gone;loadPlaywright()probesplaywright.chromium.executablePath()+existsSyncso fresh checkouts (beforepnpm install) and CI jobs without Chromium installed still skip the suite cleanly. - Added
pnpm test:e2eroot script — shortcut forpnpm --filter @llui/mcp testwhen iterating on the browser-integration suite.
CI
- Added Playwright Chromium install + cache step in
.github/workflows/ci.yml. Cache keyed onpnpm-lock.yaml, stored at~/.cache/ms-playwright. Cold install is ~30s with--with-deps; cache hits runinstall-deps chromiumonly to refresh system libraries.
2026-04-14 — 0.0.14
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.14; @llui/{effects,mcp}@0.0.8; @llui/lint-idiomatic@0.0.11
Ten production-sourced bug fixes spanning SSR, the compiler, structural reconciliation, runtime timing, and published build output.
Breaking
@llui/vite-plugin@0.0.14—mcpPortis now opt-in. Default isnull. The/__llui_mcp_statusmiddleware and WebSocket companion process are only installed whenmcpPortis passed explicitly. If you were relying on MCP in dev, add{ mcpPort: 5200 }(or any port) to yourllui()call invite.config.ts. Fixes 404 noise in dev logs for apps that don't use MCP.
Migration
- If you worked around the
show()source-order bug by reordering sibling branches, you can revert that change — the original source order now works correctly. - If you worked around the hoisted
class:accessor bug by inlining the arrow or using a module-level variable, you can revert to the hoisted-const-arrow form. - If you were using MCP in dev, add
{ mcpPort: <port> }tollui()invite.config.ts— the default is now opt-out rather than opt-in.
@llui/dom@0.0.14
- Fixed
show()/branch()block source-order reconciliation. Nested structural blocks were landing before their parent in the flatinst.structuralBlocksarray because every structural primitive didblocks.push(block)after running its builder. When a parent reconciled and disposed nested children, the array collapsed mid-iteration and subsequent sibling blocks could be skipped entirely — a siblingshow()placed after a form would silently fail to mount. All structural primitives (branch,each,virtualEach) now push their block before running the builder, so parents always precede nested children. Parents now also reconcile before children, avoiding wasted work on subtrees the parent is about to unmount. - Fixed
hydrateAppdroppedinit-time effects. It was short-circuitinginit()to reuseserverState, silently discarding any effectsinit()returned — so HTTP fetches, subscriptions, and timers never fired on the client after hydration.hydrateAppnow runs the originalinit()purely to extract its effect list, discards the returned state, and dispatches those effects after mount. - Fixed
elSplitcrashed on raw string children. The children parameter was typedNode[]but callers pass mixed(Node | string)[]arrays from template helpers. In jsdom (SSR), passing a raw string toappendChildthrows.elSplitnow acceptsArray<Node | string>and wraps strings indocument.createTextNode(...). - Fixed
onMountmicrotask race. Callbacks were deferred viaqueueMicrotask, which meant a synchronousdispatchEventfired immediately after mount (or abranch()case swap) could reach the DOM before the listener registered insideonMounthad attached.mountApp,hydrateApp, andbranch()'s reconcile path now push anonMountqueue and flush it synchronously after new nodes are inserted. ThequeueMicrotaskfallback still exists for callbacks registered outside any active mount cycle. - Improved
getRenderContexterror message now enumerates the three common causes when a primitive is called outside aview()render context: (1) module-scope primitive calls, (2) module-scope overlay helpers likedialog.overlay/popover.overlay(which internally useshow()/branch()), (3) primitives called fromsetTimeout/Promise.then/ async event handlers. - Improved
applyBindingdefensive guard. Throws aTypeErrorthe moment any function value reaches the DOM-write layer, naming the binding kind, key, and a source snippet of the offending function. Catches future compiler paths that might leak a function value past the binding emitter.
@llui/vite-plugin@0.0.14
- Breaking
mcpPortis now opt-in. See top of release block. - Fixed hoisted
class:accessor miscompile. A reactive attribute whose value was anIdentifierresolving to aconst-bound arrow (e.g.const cls = (s) => ...; a({ class: cls })) compiled to__e.className = clsin the static setup, coercing the function to its source string at runtime and producing<a class="(s) => ...">in the DOM with no binding wired. The compiler now resolves localconst-bound arrow identifiers to their initializer and emits a reactive binding identical to the inline-arrow form. Applies to both theelSplitsplit pass and theelTemplatesubtree-collapse pass. Affectsclass,style, attribute, and reactive DOM-property accessors. Event handlers were never affected. - Fixed per-item heuristic scope leak.
isPerItemFieldAccesswas detecting anyitem.fieldexpression as a per-item binding candidate, regardless of whetheritemactually referred to aneach()render-callback parameter. A plainarr.map((item) => ...)outsideeach()would produce a broken binding tuple and crash at runtime. The heuristic now walks up the AST and verifiesitemis bound as a parameter of aneach({ render })callback, handling destructured and renamed bindings.
All packages — build output
- Fixed ESM imports missing
.jsextensions.moduleResolution: bundlerwas stripping.jsextensions from emittedimport/exportstatements, breaking strict Node ESM consumers. A newscripts/add-js-extensions.mjspass rewrites all relative imports during publish — 578 edits across 208 source files in all 10 packages. Published tarballs now resolve cleanly under Node's strict ESM loader. - Fixed sourcemaps referenced missing
.tsfiles. Published.mapfiles referenced../src/*.tspaths not shipped in the tarball, breaking source-map debugging for downstream consumers. All 10tsconfig.build.jsonfiles now setinlineSources: true, embedding the full TypeScript source inline viasourcesContent. Sourcemaps are self-contained.
2026-04-13 — @llui/lint-idiomatic@0.0.10, @llui/mcp@0.0.7
Released: @llui/lint-idiomatic@0.0.10; @llui/mcp@0.0.7
@llui/mcp@0.0.7
- Added
llui_linttool; llm-guide reframed for the dual API.
@llui/lint-idiomatic@0.0.10
- Improved Tightened rule set, fixed example snippets, adopted across all in-repo projects.
2026-04-13 — @llui/lint-idiomatic@0.0.9
Released: @llui/lint-idiomatic@0.0.9
@llui/lint-idiomatic@0.0.9
- Added Ship as a Vite plugin via a
/vitesubpath export. - Improved Publish flow uses
pnpm publishand restoresworkspace:*in runtime deps.
2026-04-12 — 0.0.13
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.13; @llui/mcp@0.0.6
@llui/mcp@0.0.6
- Added Auto-connect MCP relay via Vite middleware + file marker; promoted auto-connect e2e to vitest CI.
@llui/dom@0.0.13
- Added Bitmask diagnostic surfaced through MCP;
childHandlersmigration landed end-to-end.
2026-04-11 — 0.0.12
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.12
@llui/dom@0.0.12
- Added On-demand MCP relay with devtools documentation.
- Added
ChildState/ChildMsgtype utilities andchildHandlersruntime.
Docs
- Improved New cookbook recipes for
slice,selector,lazy,virtualEach, andsortable.
2026-04-11 — 0.0.11
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.11
@llui/dom@0.0.11
- Added
sliceHandlershorthand for child update wiring. - Improved Clearer error messages across the runtime.
2026-04-11 — 0.0.10
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.10
@llui/dom@0.0.10
- Added
LazyDef<D>type eliminates user-side casts when usinglazy().
Docs
- Fixed Stale
ComponentDefsignature, component count, and version refs.
2026-04-11 — 0.0.9
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.9
@llui/dom@0.0.9
- Fixed Expose the
Dtype parameter on thecomponent()wrapper.
2026-04-11 — 0.0.8
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike,lint-idiomatic}@0.0.8
@llui/dom@0.0.8
- Added
virtualEach()primitive for large-list windowing. - Fixed Phase 1 iteration crash plus demo/theme bugs.
@llui/vite-plugin@0.0.8
- Fixed
__handlersnow unions modified fields across all return paths.
@llui/components@0.0.8
- Added
sortablewith visual drag feedback, cross-container drag-and-drop, and keyboard a11y (space to grab, arrows to move, escape to cancel). - Fixed
sortablesnapshots item positions at drag start (no flicker) and resolves stale index after sequential drags.
@llui/lint-idiomatic@0.0.8
- Fixed
spread-in-childrenexempts structural primitives;each-closure-violationhandles destructured params and render boundaries.
Docs & examples
- Added Root exports for
validateSchema/reorder/ theme helpers; newform-validationandi18n-lazyexample apps. - Improved API reference and examples for
virtualEach,sortable,themeSwitch, andform.
2026-04-10 — 0.0.7
Released: @llui/{dom,effects,vite-plugin,test,router,transitions,components,vike,lint-idiomatic}@0.0.7
@llui/dom@0.0.7
- Added
lazy()primitive for code-split component boundaries.
@llui/vite-plugin@0.0.7
- Fixed SVG class binding.
@llui/components@0.0.7
- Added
form(Standard Schema),sortable,themeSwitch; dashboard example app. - Fixed Accessibility audit: 13 violations → 0.
@llui/test@0.0.7
- Fixed Typechecking enabled, fixing 109 latent type errors.
@llui/lint-idiomatic@0.0.7
- Added Two new rules.
Docs
- Improved Docs site — dark mode.
2026-04-09 — 0.0.6
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.6
@llui/dom@0.0.6
- Added SVG and MathML element helpers.
@llui/components@0.0.6
- Added
inViewcomponent; RTL keyboard navigation across all directional components. - Added Locale context for i18n (English defaults, zero-setup for English apps) and locale-aware
formatutilities wrappingIntl. - Fixed Benchmark chart animations and format stability.
Docs & CI
- Added Animated benchmark charts.
- Added GitHub Actions workflow for format, build, check, lint, and test.
- Fixed Example app — use
ItemAccessor<Repo>sorepoItemshorthand type-checks.
2026-04-08 — 0.0.5
Released: @llui/{dom,vite-plugin,test,router,transitions,components,vike}@0.0.5
@llui/vite-plugin@0.0.5
- Added Compiler-generated per-message-type handlers (
__handlers) and compiler-generated__updatereplacing the generic Phase 1/2 loop. - Added Row factory: compiler-generated shared update function for
each()rows with a runtime fast path (entry + __rowUpdate). - Added Detects array operation patterns (e.g. filter) for specialized reconcilers.
- Fixed Row factory correctly scopes selector definitions (IIFE wrap), rewrites accessor calls, and preserves user variables.
@llui/dom@0.0.5
- Fixed Restore per-row disposer with a generation guard, fixing the Clear memory leak.
- Fixed Selector memory leaks: lazy bucket compaction, empty bucket cleanup, bulk clear on
each()reconcile. - Fixed Set
currentDirtyMaskin__handleMsgfor memo consistency. - Improved Phase 1 mask gating skips structural blocks on irrelevant changes; shared Phase 2; swap reduced to O(2) with bulk scope disposal.
- Improved Scope pooling reuses disposed scope objects to reduce GC pressure;
reconcileRemovewalks in O(n) without a Map. - Improved Strided
reconcileChangedfor every-Nth-item updates; item updaters moved from scope to entry for direct access; render bag object reused acrosseach()entries.
Docs & infra
- Added Docs site — benchmarks page auto-generated from
jfb-baseline.json. - Improved
bench:setupscript validates the detectedjfbrepo before use.
2026-04-07 — 0.0.4
Released: @llui/{dom,effects,vite-plugin,test,router,transitions,components,vike}@0.0.4
@llui/dom@0.0.4
- Added
branch/showcallbacks receive theView<S,M>bag.
@llui/vike@0.0.4
- Added Sub-path exports; SSG extensions powering the new
llui.devdocs site with auto-generated API docs for all 10 packages. - Fixed Dispose previous page on client navigation; enable Vike client routing for SPA navigation.
Docs
- Fixed Shiki CSS variables theme (no
!important, proper light/dark), strip duplicateh1, fix entity encoding and content accuracy.
2026-04-06 — 0.0.3
Released: @llui/{dom,effects,vite-plugin,test,router,transitions,components,vike,lint-idiomatic}@0.0.3
Breaking
@llui/effects@0.0.3— effects API v2: typed constructors, flexible body, addswebsocketandretry. All existing effect call sites need to move to the typed constructors.
@llui/effects@0.0.3
- Added
uploadeffect with progress tracking;clipboard,notification, andgeolocationeffects.
@llui/router@0.0.3
- Added Route guards via
beforeEnter/beforeLeavehooks.
@llui/transitions@0.0.3
- Added Route transitions and
staggerforeach(); spring physics.
@llui/components@0.0.3
- Added Complete default theme for all 54 components;
aria-ownswiring.
@llui/lint-idiomatic@0.0.3
- Added 9 new rules:
effect-without-handler,forgotten-spread,string-effect-callback,nested-send-in-update,imperative-dom-in-view,accessor-side-effect, plus 3 aria/error-message rules.
@llui/dom@0.0.3
- Improved Runtime error messages.
Docs
- Improved Document the styling layer in architecture, API reference, and README; update all effect examples for typed constructors; update system prompt for effects v2 +
View<S,M>.
2026-04-06 — 0.0.2
Released: @llui/{dom,effects,vite-plugin,test,router,transitions,components,vike,lint-idiomatic}@0.0.2
Initial multi-package release — core TEA runtime (scope tree, bindings, update loop, mountApp), element helpers, structural primitives (show, branch, each, memo, portal, onMount), Vite plugin with prop-split and bitmask injection, test harness, effects builders, router, transitions, 54 headless components, idiomatic lint rules, and Vike SSR adapter. 977 tests at release.