Skip to content

fix(chat): A2UI surface renderer must route through render-spec (nested children)#371

Merged
blove merged 1 commit into
mainfrom
claude/fix-a2ui-progressive-render-props
May 16, 2026
Merged

fix(chat): A2UI surface renderer must route through render-spec (nested children)#371
blove merged 1 commit into
mainfrom
claude/fix-a2ui-progressive-render-props

Conversation

@blove
Copy link
Copy Markdown
Contributor

@blove blove commented May 16, 2026

Summary

Fixes a silent regression in v0.0.32 + v0.0.33: any A2UI surface containing a Column/Row/Modal/Tabs with nested children rendered as an empty container. Affects essentially every real-world GenUI prompt (contact forms, settings cards, polls, etc.).

Root cause (verified live + via NG0303 warning)

The slot-based progressive renderer in `A2uiSurfaceComponent` mounted root components via `a2uiSlot`, but its `pushProps(view.props)` had no knowledge of:

  1. How to unwrap the wire-format `{ TypeKey: { ...inner } }` def shape
  2. How to translate `children.explicitList` → catalog component's `childKeys` input
  3. That catalog components require `spec: input.required()` (they internally use `` to recurse)

Net effect: `setInput("Column", {...})` silently failed (no such input on `A2uiColumnComponent`), `childKeys` stayed empty, `@for` rendered nothing.

Confirmed by:

  • Live DOM inspection: `view.props = { Column: { children: {...}, distribution: 'start', alignment: 'stretch' } }`
  • Angular's NG0303 warning: "Can't set value of the 'Column' input on the 'A2uiColumnComponent' component"
  • Existing test contract mismatch: `surface-store.spec.ts` asserted `view.props['TextField']` (wrapped) while `a2ui-slot.directive.spec.ts` asserted `props: { label: 'Ada' }` (flat) — the boundary was never integration-tested with real catalog components

Fix

Route ALL state-set surfaces through the proven legacy `` path. Extend `spec()` computed to also build from `state().surface` (was only checking the legacy `surface` input). `surfaceToSpec` already correctly:

  • Unwraps the type key
  • Treats `children` / `child` / `entryPointChild` / `contentChild` / `tabItems` as reserved keys
  • Translates them into `spec.elements[id].children` which `render-element` maps to `childKeys`
  • Rewrites path refs (`{$.foo}`) to `\$bindState` markers

The slot-based path is dead-coded (template no longer reaches it for state surfaces). The directive + view-readiness tracking stay for back-compat / future use. Per-element readiness still works via `render-element.notReady` (mounts fallback if any resolved prop is undefined).

Verified live

http://localhost:4200/embed → "Demo: render a contact form" → renders:

  • "Contact Us" heading
  • Name / Email address / Subject text fields
  • Message multi-line textarea
  • Send button

(Before this fix: empty container, just the tool-call header.)

Test plan

  • New integration test: `A2uiSurfaceComponent — nested children with real catalog (regression)` — uses `a2uiBasicCatalog` + real surfaceUpdate envelope, asserts the rendered output contains the child's text
  • Rewrote 4 existing surface.component tests to match the new contract (they were exercising slot-path behavior with fake types and empty components maps — never caught the bug)
  • `vitest run libs/chat` — 744 tests pass
  • `npx nx build chat` — green
  • Manual: contact form renders end-to-end against local LangGraph backend
  • CI green

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel Bot commented May 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cacheplane Ready Ready Preview, Comment May 16, 2026 6:03pm

Request Review

…r-spec

The slot-based progressive renderer (introduced alongside surface-store
readiness tracking) mounted root components but never populated their
child-related inputs — leaving Columns/Rows/Modals/etc. with empty
childKeys and rendering as empty containers. Catalog components
declare `childKeys: input<string[]>([])` and `spec: input.required<Spec>()`
because they internally use `<render-element>` for recursive children;
the slot's `pushProps(view.props)` had no knowledge of either.

Worse, `view.props` was stored as `{ TypeKey: { ...inner } }` (wire
format) while `a2ui-slot.pushProps` iterated those entries and called
`setInput("Column", {...})` — which silently failed because no such
input exists. Net effect: ZERO inputs ever made it to the catalog
component, so even `distribution`/`alignment` were unset.

This shipped as a silent regression in v0.0.32 and v0.0.33: any GenUI
surface containing a Column/Row with nested children (i.e. virtually
all real-world A2UI surfaces) rendered empty.

Root cause confirmed via live DOM inspection + Angular's own NG0303
warning ("Can't set value of the 'Column' input"). The legacy
`<render-spec>` path was the original (and correct) flow:
`surfaceToSpec` unwraps the type key, treats `children.explicitList`
/ `child` / `entryPointChild` / `contentChild` / `tabItems` as
reserved keys, and translates them into `spec.elements[id].children`
which `render-element` then maps to the catalog component's
`childKeys` input. The progressive renderer was an aspirational layer
on top of this that was never finished.

Fix: route ALL state-set surfaces through `<render-spec>` too. Extend
`spec()` computed to also build from `state().surface`. The progressive
component-view readiness tracking in surface-store is preserved for
future use, but rendering goes through the proven legacy path. Per-
element readiness still works via `render-element.notReady` (mounts
fallback if any resolved prop is undefined).

Tests:
  - New integration test asserts a Column with children.explicitList
    renders its child Text leaf using the real a2uiBasicCatalog
    (reproduces the contact-form bug; was failing before this commit).
  - Surface.component.spec rewritten to exercise the new contract
    (fake type / empty Map tests no longer apply — they were testing
    slot-path behavior that is now bypassed).
  - All 744 chat lib tests pass.

Verified live: the "Demo: render a contact form" suggestion now
renders a fully functional contact form (heading + 4 text fields +
multi-line message + Send button) in the local dev stack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@blove blove force-pushed the claude/fix-a2ui-progressive-render-props branch from a7a538a to d360ac1 Compare May 16, 2026 18:01
@blove blove merged commit 00703c0 into main May 16, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant