8000 feat(a2ui): add A2UI spec v0.9 rendering support alongside v0.8 by lwangverizon · Pull Request #517 · google/adk-web · GitHub
[go: up one dir, main page]

Skip to content

feat(a2ui): add A2UI spec v0.9 rendering support alongside v0.8 - #517

Open
lwangverizon wants to merge 16 commits into
google:mainfrom
lwangverizon:feat/a2ui-v09-support
Open

lwangverizon wants to merge 16 commits into
google:mainfrom
lwangverizon:feat/a2ui-v09-support

Conversation

@lwangverizon
@lwangverizon lwangverizon commented Aug 27, 2026 •
Copy link
Copy Markdown

Summary

ADK Web drops A2UI v0.9 message kinds, leaving agents that emit createSurface, updateComponents, and updateDataModel without a rendered UI. This change detects the spec version per payload and adds a v0.9 canvas alongside the existing v0.8 path.

  • Normalize ordered message batches from A2A DataParts and inline <a2ui-json> blocks. Accept both application/json+a2ui and application/a2ui+json.
  • Use upstream main's existing @a2ui/angular 0.10.5 dependency and provideA2Ui(). Preserve the ngx-markdown bridge and provide it to both renderer versions.
  • Render every created surface in a multi-surface batch. Update-only events mutate the original widget without mounting duplicate copies.
  • Scope the v0.9 renderer to each chat. Clear models when replacing history, preserve them when retaining messages, and unmount widgets when a later event deletes their surface.
  • Map v0.9 theme variables to Material tokens for light/dark mode.
  • Contain v0.8 processor exceptions so malformed messages do not break chat change detection; existing v0.8 rendering bindings remain in place.

The canonical basic catalog ID is https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json.

Validation

  • Full headless Chrome suite: 875 passed, 37 skipped.
  • Production build passed, with Angular template/CommonJS warnings.
  • Tests cover both MIME types in live events/history, multi-surface and streamed rendering, deletion across bubbles, renderer state reset/retention, v0.9 markdown through the existing provider, and v0.8 regression coverage.
  • Contributor browser verification: feat(a2ui): add A2UI spec v0.9 rendering support alongside v0.8 #517 (comment). The contributor reports light/dark rendering, live theme changes, history reload, streaming, and v0.8 visual parity. Modal stacking remains a known upstream issue. These standalone browser checks were not independently repeated during this review.

Scope and follow-ups

Client actions reach an actionHandler stub; round-tripping to the agent remains a separate follow-up agreed in the discussion. Upstream modal stacking and ADK backend extension/streaming transport issues are also separate. A2UI v1.0 is outside this change.

The integration preserves both the original branch history and the contributor's rebased commits; the normalizer and renderer dispatch remain separate commits in that history.

< F438 /div>
…kdown-it

@a2ui/angular 0.9.1 ships both spec versions from one package via its ./v0_8
and ./v0_9 subpath exports, so adding A2UI 0.9 support does not mean dropping
0.8. Its bare "." export is still the complete 0.8 API, so existing imports of
Types, Theme, Catalog and MessageProcessor keep resolving unchanged. The peer
range is @angular/core ^21.2.0 and the lockfile already resolves 21.2.4, so no
Angular bump is needed.

@a2ui/lit is promoted from a transitive to a direct dependency: 0.9 drops it,
which would break a2ui-theme.ts's `import {Types, Styles} from '@a2ui/lit/0.8'`.
Types is available from @a2ui/angular, but Styles is not, and Styles.merge
cannot be replaced with an object spread — it strips same-family keys by
prefix, and 8 of its 13 call sites in a2ui-theme.ts merge *different* keys
within the same `color-c-*` family. A spread would leave both set and emit two
conflicting classes.

@a2ui/markdown-it is an optional peer of @a2ui/angular, but in practice it is
required: TextComponent injects the abstract MarkdownRenderer unconditionally
and synthesizes headings by prepending `#`/`##` before rendering, so with the
passthrough fallback every 0.9 heading renders as literal "# My Title".
DefaultMarkdownRenderer also does a bare `await import('@a2ui/markdown-it')`,
which esbuild errors on when the package is undeclared.
Mechanical result of the previous commit; isolated so it can be reviewed
separately from hand-written changes.

Added: @a2ui/web_core 0.9.2 (replacing 0.8.0 at the top level),
@preact/signals-core, @a2ui/markdown-it, date-fns (transitive of web_core
0.9.2), a nested zod 3 under @a2ui/angular, and a nested web_core 0.8.0 +
zod 3 under @a2ui/lit.

The app's own zod stays at 4.x. The two majors never meet at a type boundary —
adk-web never passes zod schemas into A2UI — and npm's nesting keeps them
apart. Forcing a dedupe via `overrides` would break web_core's schema parsing
at runtime, since zod 4 is not backward compatible with its `^3.25.76`
requirement.
… 0.9 bump

Repoint the bare '@a2ui/angular' imports to the explicit '/v0_8' subpath so a
future major that flips the bare export to 0.9 cannot silently retarget the 0.8
renderer. Also drop the dead A2uiCanvasComponent import in chat-panel (not in
that component's `imports`, unused in its template).

Bumping @a2ui/angular to 0.9.1 changed v0.8 runtime behavior in three ways that
this commit repairs, so that going green here is real evidence 0.8 still works:

1. `Types` now comes from '@a2ui/angular/v0_8' instead of '@a2ui/lit/0.8'.
   @a2ui/lit@0.8.3 carries its own nested web_core@0.8.0, while
   @a2ui/angular/v0_8 is backed by web_core@0.9.2. The two copies of the v0.8
   types are structurally incompatible (`ComponentInstance.component` is
   optional in one, required in the other), which broke compilation. @a2ui/lit
   is now used only for `Styles`, which @a2ui/angular does not re-export.

2. `provideMarkdownRenderer()` is now registered in main.ts. `Text`
   unconditionally injects `MarkdownRenderer`, whose base class is declared
   `providedIn: 'root'` but has no `render` method -- so the injection succeeds
   and then throws "render is not a function" on the first Text component. Any
   0.8 agent sending Text would have crashed without this.

3. `processMessages` is wrapped in try/catch. web_core@0.9.2's v0.8 processor
   zod-validates and throws where 0.8.0 tolerated. Uncaught, that escapes
   ngOnChanges and breaks change detection for the whole chat view; a malformed
   payload should break only its own widget. Two new canvas specs cover this:
   one asserts nothing is logged on a clean message, the other that a throwing
   processor is contained, still logged, and leaves surfaceId derivable so a
   later valid message recovers.

Test fixtures that were invalid under the stricter schema are corrected rather
than left masked by the new catch (`Text.text` must be `{literalString}` or
`{path}`, not a bare string), and the chat spec now mirrors main.ts's A2UI
providers, which a schema-valid payload reaches during renderer construction.

645 passing, ng build clean.
Both A2UI ingestion paths duck-typed the three v0.8 message kinds into a
three-slot, last-wins object, so v0.9 kinds (createSurface, updateComponents,
updateDataModel, deleteSurface) were silently dropped: a2uiData was never
populated, nothing rendered, and no error surfaced.

Replace the duplicated bucketing in processA2uiPartIntoMessage and
extractA2uiJsonFromText with one normalizeA2uiMessages(), returning the new
version-tagged A2uiPayload. Version detection prefers the explicit `version`
discriminator and falls back to the kind key, which is unambiguous because the
two versions' kind sets are disjoint. Verified against web_core's v0.9 schema:
the discriminator sits on the envelope only -- every message object declares
`additionalProperties: false`, so a nested `version` is invalid and needs no
handling.

`messages` is an ordered array because v0.9 legitimately sends N
updateComponents / updateDataModel messages across a surface, which the fixed
slots could not represent. The three v0.8 aliases are still populated for v0.8
payloads, so the existing v0.8 render bindings are untouched -- that is the
regression story for this commit.

No rendering change yet: a v0.9 payload normalizes but still falls through to
the v0.8 canvas, which pushes no messages and draws nothing.

One intentional behavior change, covered by a test: <a2ui-json>[]</a2ui-json>
previously produced a truthy `{}` that mounted an empty canvas, and now leaves
a2uiData unset. The text is still stripped either way, so the markup never
leaks into the bubble.

665 passing, ng build clean.
A sibling of A2uiCanvasComponent rather than a branch inside it, because the two
renderers have genuinely different lifecycles: v0.8's <a2ui-surface> needs a
resolved surface object re-read from a map on every change, while v0.9's
<a2ui-v09-surface> takes only a surfaceId and resolves it internally. Leaving
the v0.8 component and its spec literally unmodified is also the strongest
guarantee against v0.8 regressions.

Messages are processed one at a time, not as a single batch, and a createSurface
naming an existing surface is skipped. v0.9's processor throws on a duplicate
createSurface, an unknown catalogId, and an update naming an absent surface. The
renderer service is root-scoped, so its surfaces outlive any one bubble: a
history reload or a re-processed batch would throw on the create and, batched,
abandon every message after it -- leaving a blank surface. Skipping the create
instead lets the idempotent updates land on the existing surface, so the mounted
child keeps a valid reference and no keyed-@for remount is needed.

The @if gate on surfaceId is load-bearing: ComponentHostComponent looks its
surface up once in ngOnInit and, on a miss, warns and never retries -- unlike a
missing component, which recovers via onCreated. Angular runs the parent's
ngOnChanges before creating children, so deriving surfaceId only after
processing guarantees the surface exists before the child mounts. A spec asserts
the element is absent before processing and present after.

DI is registered by hand: v0.9.1 has no provideA2Ui() (that lands in 0.10), and
A2uiRendererService is a plain @Injectable() with no providedIn, so omitting it
would make ComponentHostComponent's inject() throw NG0201.

Not reachable from any template yet -- the dispatch lands in the next commit.

Verified that Parameters<A2uiRendererService['processMessages']>[0][number]
resolves to the real A2uiMessage type rather than degrading to any, so
@a2ui/web_core does not need to become a direct dependency.

674 passing, ng build clean.
This is the commit that turns v0.9 rendering on. Reverting it disables
v0.9 without touching anything else.

`content-bubble.component.html` now branches on `a2uiData.version`: a
`v0.9` payload mounts the new canvas with the whole message batch, and
everything else keeps the historical v0.8 markup character-for-character.
An untagged payload therefore still takes the v0.8 path.

Adds `content-bubble.component.spec.ts` -- there was none, and this is
the file that catches a mis-dispatch. Four cases: v0.8 renders only
`app-a2ui-canvas`, v0.9 renders only `app-a2ui-canvas-v09`, an absent
`a2uiData` renders neither, and an untagged payload falls back to v0.8.
Both renderers are stubbed; this spec is about which canvas is chosen,
not what it draws.

`chat.component.spec.ts` gains the real v0.9 provider graph (mirroring
main.ts) rather than a stub, so the v0.9 history test also proves the
canonical `catalogId` is accepted -- a mismatch is the top integration
gotcha for agent authors and its only symptom is a silent empty surface,
which the `console.error` assertion now turns into a test failure.

Also documents why `chat-panel.component.spec.ts`'s A2UI test stays
disabled: chat-panel is four components removed from the canvas and
provides none of the A2UI DI graph, so re-enabling it is high-cost and
redundant with the new content-bubble spec.
v0.9 has no theme object. Unlike v0.8, which is themed in TypeScript
through the `Theme` token, v0.9 injects no root stylesheet and defines no
`a2ui-light`/`a2ui-dark` classes -- every component styles itself with
inline `var(--a2ui-X, <fallback>)`, and all but a handful of those
fallbacks are hardcoded light-mode values (#fff, google#333, #ccc). Without
this, v0.9 surfaces are unreadable in adk-web's default dark theme, so
it is a correctness fix rather than a cosmetic one.

Defines the ~50 `--a2ui-*` properties as `var(--mat-sys-*)`. Because
`var()` resolves at use time, the layer is pure indirection: a theme flip
repaints `--mat-sys-*`, which repaints `--a2ui-*`. No JavaScript, no new
classes, no second definition site -- ThemeService and styles.scss are
untouched.

Scoped to `:host` rather than a global stylesheet so `--a2ui-*` stays out
of the global namespace of an app embedding this component. Verified safe
for v0.9's Modal, which is a plain `position: fixed` element inside its
own template (no CDK overlay, no body portal), so it stays in this
subtree and inherits.

Three findings from reading the compiled v0.9 CSS, each commented at its
mapping:

- The six `--a2ui-font-size-*` properties are mandatory. Upstream reads
  them as bare `var(--a2ui-font-size-l)` with no fallback, and an
  undefined custom property invalidates the whole declaration -- every
  heading would silently drop to the browser default.
- `--a2ui-color-on-secondary` is the foreground of the *default* button,
  over an `--a2ui-color-surface` background. It maps to
  `--mat-sys-on-surface`; the intuitive `--mat-sys-on-secondary-container`
  gives low-contrast text.
- `.a2ui-button:disabled` hardcodes #e9ecef/#6c757d with no property to
  hook, and the CheckBox is a native `<input type="checkbox">` whose
  background/border variables are inert without `appearance: none`. Both
  need a `:host ::ng-deep` override until upstream tokenizes them.

Card shadow and margin are dropped: a surface already sits inside an
elevated chat bubble, where upstream's drop shadow reads as a second
border and its 16px margin double-pads the bubble.
@batou9150
Copy link
Copy Markdown

Hi @lwangverizon, thanks for this PR — I'd like to help get v0.9 rendering landed.

Some context on where things stand today:

  • feat(a2ui): upgrade a2ui packages to ^0.10.0. #520 has since moved main to @a2ui/angular ^0.10.0 (lockfile resolves 0.10.5), and that bump ships in the web assets bundled with google-adk 2.12.0. So the dependency commits here can be dropped, which should also resolve the current merge conflict.
  • 0.10.5 exposes provideA2Ui() from @a2ui/angular/v0_9, which answers your second question: the hand-rolled A2UI_RENDERER_CONFIG / A2uiRendererService providers can go.
  • Ingestion on main still only matches the v0.8 kinds (beginRendering / surfaceUpdate / dataModelUpdate), so v0.9 payloads (createSurface / updateComponents / updateDataModel) are still silently dropped (A2UI does not work with ADK web #447).

I can take on:

  1. Rebasing this branch onto main (keeping your commit structure and authorship, with the deps commits dropped and provideA2Ui() in place of the manual providers).
  2. Running the outstanding browser verification checklist against a minimal ADK agent that emits v0.9 basic-catalog payloads through <a2ui-json>, and posting the results here.
  3. As a separate follow-up PR, wiring the v0.9 actionHandler back to the agent (your first question), so buttons and forms actually round-trip.

Before I start, two questions so I don't duplicate work:

@ditman
ditman commented Oct 27, 2026
Copy link
Copy Markdown
Contributor

@ditman @wyf7107 — #520 mentioned continuing in an internal CL. Does that already cover v0.9 rendering?

I'm sorry this was quite opaque. The internal CL had to do with how the way that adk-web imported A2UI (from the root of the package when it meant the v0.8 endpoint) was blocking our ability to reclaim that root of the package for other things. I didn't really fix the rendering code of adk-web at all, just how it imported the code it needed (the only real change I remember making is injecting the markdown rendering how the newer A2UI wanted).

((So yeah, I think this is still needed. Also, A2UI v1.0 is coming, so maybe that also needs some changes!))

@lwangverizon
lwangverizon commented Oct 28, 2026 via email
Copy link
Copy Markdown
Author

lwangverizon and others added 7 commits October 31, 2026 20:39
Since the @a2ui 0.10 bump (google#520), web_core's v0.8 processor zod-validates and
throws where 0.8.0 tolerated. Uncaught, that escapes ngOnChanges and breaks
change detection for the whole chat view; a malformed payload should break
only its own widget. `processMessages` is now wrapped in try/catch, and two
new canvas specs cover it: one asserts nothing is logged on a clean message,
the other that a throwing processor is contained, still logged, and leaves
surfaceId derivable so a later valid message recovers.

Also drop the dead A2uiCanvasComponent import in chat-panel (not in that
component's `imports`, unused in its template).

The /v0_8 import pinning, the `Types` source fix and the markdown provider
from the original version of this commit are already on main via google#520
(`provideA2uiMarkdown()`), so they are dropped here.
Both A2UI ingestion paths duck-typed the three v0.8 message kinds into a
three-slot, last-wins object, so v0.9 kinds (createSurface, updateComponents,
updateDataModel, deleteSurface) were silently dropped: a2uiData was never
populated, nothing rendered, and no error surfaced.

Replace the duplicated bucketing in processA2uiPartIntoMessage and
extractA2uiJsonFromText with one normalizeA2uiMessages(), returning the new
version-tagged A2uiPayload. Version detection prefers the explicit `version`
discriminator and falls back to the kind key, which is unambiguous because the
two versions' kind sets are disjoint. Verified against web_core's v0.9 schema:
the discriminator sits on the envelope only -- every message object declares
`additionalProperties: false`, so a nested `version` is invalid and needs no
handling.

`messages` is an ordered array because v0.9 legitimately sends N
updateComponents / updateDataModel messages across a surface, which the fixed
slots could not represent. The three v0.8 aliases are still populated for v0.8
payloads, so the existing v0.8 render bindings are untouched -- that is the
regression story for this commit.

No rendering change yet: a v0.9 payload normalizes but still falls through to
the v0.8 canvas, which pushes no messages and draws nothing.

One intentional behavior change, covered by a test: <a2ui-json>[]</a2ui-json>
previously produced a truthy `{}` that mounted an empty canvas, and now leaves
a2uiData unset. The text is still stripped either way, so the markup never
leaks into the bubble.

665 passing, ng build clean.
A sibling of A2uiCanvasComponent rather than a branch inside it, because the two
renderers have genuinely different lifecycles: v0.8's <a2ui-surface> needs a
resolved surface object re-read from a map on every change, while v0.9's
<a2ui-v09-surface> takes only a surfaceId and resolves it internally. Leaving
the v0.8 component and its spec literally unmodified is also the strongest
guarantee against v0.8 regressions.

Messages are processed one at a time, not as a single batch, and a createSurface
naming an existing surface is skipped. v0.9's processor throws on a duplicate
createSurface, an unknown catalogId, and an update naming an absent surface. The
renderer service is root-scoped, so its surfaces outlive any one bubble: a
history reload or a re-processed batch would throw on the create and, batched,
abandon every message after it -- leaving a blank surface. Skipping the create
instead lets the idempotent updates land on the existing surface, so the mounted
child keeps a valid reference and no keyed-@for remount is needed.

The @if gate on surfaceId is load-bearing: ComponentHostComponent looks its
surface up once in ngOnInit and, on a miss, warns and never retries -- unlike a
missing component, which recovers via onCreated. Angular runs the parent's
ngOnChanges before creating children, so deriving surfaceId only after
processing guarantees the surface exists before the child mounts. A spec asserts
the element is absent before processing and present after.

DI goes through provideA2Ui() from @a2ui/angular/v0_9 (available since 0.10.5,
which main is on), with BasicCatalog and a console.debug actionHandler stub;
routing client actions back to the agent is a follow-up. A2uiRendererService is
providedIn: 'root' in 0.10, so it needs no explicit provider.

Markdown reuses main's provideA2uiMarkdown() ngx-markdown bridge, extended to

8000
also provide the v0.9 MarkdownRenderer. Without it, v0.9 Text falls back to
DefaultMarkdownRenderer, which needs the optional @a2ui/markdown-it peer and
renders raw markdown when it is absent.

Not reachable from any template yet -- the dispatch lands in the next commit.

Verified that Parameters<A2uiRendererService['processMessages']>[0][number]
resolves to the real A2uiMessage type rather than degrading to any, so
@a2ui/web_core does not need to become a direct dependency.
This is the commit that turns v0.9 rendering on. Reverting it disables
v0.9 without touching anything else.

`content-bubble.component.html` now branches on `a2uiData.version`: a
`v0.9` payload mounts the new canvas with the whole message batch, and
everything else keeps the historical v0.8 markup character-for-character.
An untagged payload therefore still takes the v0.8 path.

Adds `content-bubble.component.spec.ts` -- there was none, and this is
the file that catches a mis-dispatch. Four cases: v0.8 renders only
`app-a2ui-canvas`, v0.9 renders only `app-a2ui-canvas-v09`, an absent
`a2uiData` renders neither, and an untagged payload falls back to v0.8.
Both renderers are stubbed; this spec is about which canvas is chosen,
not what it draws.

`chat.component.spec.ts` gains the real v0.9 provider graph (mirroring
main.ts) rather than a stub, so the v0.9 history test also proves the
canonical `catalogId` is accepted -- a mismatch is the top integration
gotcha for agent authors and its only symptom is a silent empty surface,
which the `console.error` assertion now turns into a test failure.

Also documents why `chat-panel.component.spec.ts`'s A2UI test stays
disabled: chat-panel is four components removed from the canvas and
provides none of the A2UI DI graph, so re-enabling it is high-cost and
redundant with the new content-bubble spec.
v0.9 has no theme object. Unlike v0.8, which is themed in TypeScript
through the `Theme` token, v0.9 injects no root stylesheet and defines no
`a2ui-light`/`a2ui-dark` classes -- every component styles itself with
inline `var(--a2ui-X, <fallback>)`, and all but a handful of those
fallbacks are hardcoded light-mode values (#fff, google#333, #ccc). Without
this, v0.9 surfaces are unreadable in adk-web's default dark theme, so
it is a correctness fix rather than a cosmetic one.

Defines the ~50 `--a2ui-*` properties as `var(--mat-sys-*)`. Because
`var()` resolves at use time, the layer is pure indirection: a theme flip
repaints `--mat-sys-*`, which repaints `--a2ui-*`. No JavaScript, no new
classes, no second definition site -- ThemeService and styles.scss are
untouched.

Scoped to `:host` rather than a global stylesheet so `--a2ui-*` stays out
of the global namespace of an app embedding this component. Verified safe
for v0.9's Modal, which is a plain `position: fixed` element inside its
own template (no CDK overlay, no body portal), so it stays in this
subtree and inherits.

Three findings from reading the compiled v0.9 CSS, each commented at its
mapping:

- The six `--a2ui-font-size-*` properties are mandatory. Upstream reads
  them as bare `var(--a2ui-font-size-l)` with no fallback, and an
  undefined custom property invalidates the whole declaration -- every
  heading would silently drop to the browser default.
- `--a2ui-color-on-secondary` is the foreground of the *default* button,
  over an `--a2ui-color-surface` background. It maps to
  `--mat-sys-on-surface`; the intuitive `--mat-sys-on-secondary-container`
  gives low-contrast text.
- `.a2ui-button:disabled` hardcodes #e9ecef/#6c757d with no property to
  hook, and the CheckBox is a native `<input type="checkbox">` whose
  background/border variables are inert without `appearance: none`. Both
  need a `:host ::ng-deep` override until upstream tokenizes them.

Card shadow and margin are dropped: a surface already sits inside an
elevated chat bubble, where upstream's drop shadow reads as a second
border and its 16px margin double-pads the bubble.
A v0.9 event may carry several surfaces -- each message names its own
surfaceId -- but A2uiCanvasV09Component derived a single surfaceId per batch
and mounted one <a2ui-v09-surface>, so every surface after the first was
processed and silently never shown.

Track all surfaceIds in first-mention order (`surfaceIds`), skipping any that
a deleteSurface in the same batch removes, and mount one surface per id with
@for. `surfaceId` remains as a computed alias for the first one. They are
still derived only after processing, so a child never mounts before its
surface exists.

Found with a test agent emitting three staged-ui-widgets surfaces in one
<a2ui-json> block: only the first rendered before this change.
A v0.9 surface is one live, root-scoped model. When an agent streams it --
createSurface, updateComponents and updateDataModel in separate events --
every bubble derived the surfaceId from its own messages and mounted the
same surface again, so it was shown once per event.

Mount only the surfaces a batch creates. A batch that merely updates a
surface is still processed, and its changes land on the copy already
mounted in the creating bubble. History reload is unaffected: the creating
event's batch still contains the createSurface (skipped as redundant when
the surface already exists), so that bubble still mounts it.

This replaces the "derive the surfaceId from updateComponents alone" spec
with one asserting an update-only batch is processed but not mounted, and
adds a three-event streaming spec.

The creating bubble can still log a transient "Component root not found
... Waiting for it" between createSurface and updateComponents: the
renderer waits for `root` by design and recovers once it arrives.
@batou9150
Copy link
Copy Markdown

@lwangverizon — the rebase is up as lwangverizon#1, against feat/a2ui-v09-support. Changes: deps commits dropped, provideA2Ui() in place of the manual providers, provideA2uiMarkdown() kept and extended to v0.9, plus two small surface fixes found below. Details are in that PR.

Browser verification

Setup:

  • adk web backend on google-adk 2.7.1 / 2.11.0; this branch served with ng serve and the dev proxy.
  • A deterministic test agent (no LLM) that replies with <a2ui-json> blocks.
  • Its v0.9 product surfaces are real SDK output from the staged-ui-widgets recipe; the interactive form and the v0.8 control are hand-written.
  • All fixtures validate against the published schemas with a2ui-agent-sdk.
Item Result
Card / Button / TextField / Tabs / Slider / CheckBox (v0.9) ✅ render. Initial values come from updateDataModel. Two-way binding works: typing in a TextField updates a Text bound to the same path on another tab.
Modal ⚠️ opens, but appears under the side drawer (see below)
Dark and light mode ✅
Theme toggle while a surface is mounted ✅ restyles live and keeps its state
History reload with v0.9 surfaces ✅ a full page reload rebuilds the surface. A new in-app session reusing the same surfaceId also renders. No blank surface, so surfaceGeneration wasn't needed.
Streaming: createSurface / updateComponents / updateDataModel in separate events ✅ after the second fix in the PR. Before it, the same surface was mounted in every bubble.
Several surfaces in one event ✅ after the first fix in the PR. Before it, only the first rendered.
Existing v0.8 agent renders identically ✅ identical to main: same normalized DOM and size, byte-identical screenshot. Markdown in v0.8 Text renders.
Mixed v0.8 + v0.9 in one block ✅ v0.8 kept, one console.warn per dropped v0.9 message
Client actions ✅ submit_preferences / open_details reach the actionHandler stub with surfaceId, sourceComponentId and context. Round-trip to the agent is the follow-up.
Devtools console ✅ no a2ui errors. One transient warning: in the streaming case, Component root not found in surface … Waiting for it... is logged between createSurface and updateComponents. The renderer waits for root by design and recovers.

Out of scope here, but found along the way:

  • Modal stacking (upstream). .a2ui-modal-overlay is position: fixed; z-index: 1000 but sits inside mat-drawer-content's stacking context (z=1), so the side drawer paints over it. No z-index in adk-web can fix that. The v0.9 Modal would need the top layer (<dialog>.showModal() or a CDK overlay), so it's probably worth an issue on a2ui.
  • A2A DataPart path. Tested with a RemoteA2aAgent in front of an A2UI v0.9.1 agent:
    • The A2UI SDK tags v0.9.1+ parts application/a2ui+json and keeps application/json+a2ui as deprecated for 0.8/0.9. adk-web only accepts application/json+a2ui, so those parts are dropped without a console message (A2A converter: emit EventActions.render_ui_widgets (provider="a2ui") as A2UI parts adk-python#7308). Accepting both in isA2uiDataPart is a one-liner if you want it in this PR; otherwise I can send it as a follow-up.
    • Separately, on the ADK side: RemoteA2aAgent doesn't activate the A2UI extension unless a request interceptor adds it to X-A2A-Extensions. Surfaces streamed as working status updates also don't reach the event with the default non-streaming call.

@lwangverizon
lwangverizon marked this pull request as ready for review November 1, 2026 23:07
@lwangverizon
Copy link
Copy Markdown
Author

Thanks @batou9150 for the rebase, the surface fixes, and the thorough browser verification, and @ditman for clarifying the internal CL's scope.

I've reviewed and integrated lwangverizon#1 into this branch, preserving the existing history. The updated code uses main's dependency versions and provideA2Ui(), and retains provideA2uiMarkdown() for both renderer versions.

I also added a few fixes during review:

  • Accept both application/json+a2ui and application/a2ui+json, with live-event and history coverage, so no separate MIME compatibility PR is needed.
  • Scope the v0.9 renderer to each chat and clear old surface models when replacing history, while preserving them when retaining existing messages.
  • Unmount a surface when a later event deletes it, and release deletion subscriptions when the canvas is destroyed.

Local validation passes: 875 tests passed, 37 skipped, and the production build passed. Added real-renderer regression checks cover v0.9 markdown and deletion across bubbles. The standalone backend/browser checklist remains your reported verification; I haven't independently repeated it. GitHub's change and Google CLA checks are also green.

I've updated the description and marked this ready for review. As agreed, action round-tripping remains a separate follow-up; the upstream modal stacking and ADK backend extension/streaming issues also remain separate.

Thanks again for helping move this forward!

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.

3 participants

0