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.
Packages version in lockstep at release time: @llui/dom, @llui/vite-plugin, @llui/test, @llui/router, @llui/transitions, @llui/components, @llui/vike share a version line. @llui/effects, @llui/mcp, @llui/eslint-plugin, @llui/agent, and llui-agent have their own cadence.
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.