Skip to content

Migrate React shell sample to A2UI v0.9#1262

Merged
andrewkolos merged 6 commits intogoogle:mainfrom
andrewkolos:feat/upgrade-react-shell-v0.9
Apr 24, 2026
Merged

Migrate React shell sample to A2UI v0.9#1262
andrewkolos merged 6 commits intogoogle:mainfrom
andrewkolos:feat/upgrade-react-shell-v0.9

Conversation

@andrewkolos
Copy link
Copy Markdown
Collaborator

@andrewkolos andrewkolos commented Apr 22, 2026

Supersedes #1186 (credit to @jacobsimionato for the original migration work).This PR adds some fixes for bugs surfaced while smoke testing it against the restaurant finder agent. It also addresses some feedback provided on the original PR. There are still more follow-up issues for me to file. However, none of them block getting the react shell working with the restaurant finder agent, and I'd prefer to keep the PR reviewable.

Summary

The shell now uses MessageProcessor and A2uiSurface from @a2ui/web_core/v0_9 and @a2ui/react/v0_9 in place of the v0.8 A2UIProvider / A2UIRenderer / useA2UIActions stack.

The shell works end-to-end against the restaurant finder agent (search -> restaurant list -> "Book Now" -> booking form -> submit -> confirmation).

Being a migration, this is a really big change. I made an effort to structure changes into commits so they can be reviewed in sequence if desired.

Changes since #1186

If you are unfamiliar with #1186, you might be better off with a brief glance at that and then focusing on the commits/diffs here rather than reading the following line items.

Fixes for issues I found while testing #1186

  • Wired @a2ui/markdown-it via MarkdownContext. Without a markdown renderer, the basic catalog's Text displayed unformatted markdown.
  • Corrected mock surface root component ids to 'root'. A2uiSurface in @a2ui/react/v0_9 hard-requires the root component's ID to be 'root'. The mock builders used other IDs, so all three mock surfaces were stuck on "Loading root…".
  • Wrapped user actions in the v0.9 client message envelope. MessageProcessor's action listener emits a raw A2uiClientAction, but A2uiClientMessage (the wire shape) is the envelope {version: 'v0.9', action: {...}}. The shell now wraps correctly. This issue was masked by any.
  • Dedupe createSurface across SSE chunks. A2A status-update events carry cumulative parts, so createSurface is redelivered on every chunk. MessageProcessor throws on duplicate surface IDs, so the client-side deduplication keeps the stream flowing. This is a bit of a hack, as the client should ideally not be receiving redundant create messages to begin with, but a more proper solution is left out of scope on this PR. Prior discussion on Update React Shell to A2UI v0.9 and Add A2A Middleware #1186.
  • Dropped a shallow-clone hack from the streaming chunk handler. A prior workaround shallow-cloned SurfaceModel instances into React state, replacing the real models with plain-object stubs and breaking rendering mid-stream. A2uiSurface already subscribes via useSyncExternalStore; no manual re-render is needed.
  • Recognized v0.9 action envelopes in agent_executor.py — this is a new finding. The restaurant finder's executor only checked for the v0.8 envelope {userAction: {...}}, so v0.9 actions fell through to context.get_user_input() → empty string, corrupting the conversation state. Independent repro in this issue: (TODO: link issue once filed).

Smaller things inspired by bot comments on #1186

  • Body size limit. middleware now enforces a 1MB request body cap to prevent memory exhaustion. This is perhaps a bit over-protective, but memory exhaustion is one of the nastier ways for an application to fail since it can take the machine with it.
  • SSE error format. Errors mid-stream now go out as a data: line with an [{kind: 'error', text: ...}] Part array, matching what the client parser expects.
  • Added some new entries to .gitignore.
  • Action envelope format. now wrapped correctly (see above).
  • Hardcoded agent card URL. Intentionally kept hardcoded; the AGENT_CARD_URL env var the suggestion proposed was added but removed here for sample simplicity. Happy to reconsider.
  • Fragile SSE parsing. Sufficiently addressed. The client now splits on \n\n or \r\n\r\n, so Windows-style line endings don't break it. Full SSE spec compliance is still not implemented, but isn't a practical concern since we control both ends of the stream.

Issues left for follow-up (@andrewkolos to file)

  • Restaurant finder agent's broader surface-management design. Briefly mentioned before. The agent sends createSurface on every step instead of reusing or explicitly deleting. Jacob (rightfully IMO) called this out on #1186 as "not really in the spirit of A2UI" and suggested a future redesign. To be re-considered in a follow up issue if this PR lands.
  • Image stretching in the restaurant cards. Images get distorted vertically because the parent Row defaults align-items: stretch and Image has no align-self: flex-start. This is a renderer-level CSS issue (not shell-specific). fix(react,v0.9): honor the schema's weight property in basic catalog #1215 did help with image display issues but fix didn't totally resolve things. Will file a followup if this lands.

Manually testing things

Live agent

  1. Start the restaurant finder agent (cd samples/agent/adk/restaurant_finder && uv run .).
  2. Start the shell (cd samples/client/react/shell && npm run dev).
  3. Open http://localhost:5003.
  4. Submit the default prompt. Restaurant list appears.
  5. Click "Book Now" on any card. Booking form appears.
  6. Fill in party size / time / dietary / submit. Confirmation appears.

Mock mode

Open http://localhost:5003/?mock=true and step through the same flow. No agent required.

Pre-launch Checklist

If you need help, consider asking for advice on the discussion board.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the React shell and restaurant finder agent to support the A2UI v0.9 specification. It introduces a Vite middleware for A2A communication, refactors the client to support streaming responses (SSE), and updates UI components and mock data to the new schema. Feedback suggests checking for client disconnection in the streaming loop to free server resources and improving error handling in the client by verifying response status codes.

Comment thread samples/client/react/shell/middleware/a2a.ts
Comment thread samples/client/react/shell/src/client.ts
@andrewkolos andrewkolos force-pushed the feat/upgrade-react-shell-v0.9 branch from 9d612c5 to 8f23e2f Compare April 22, 2026 20:19
- .vite/: Vite's dev-server cache
- worktrees/: directory used for local git worktrees
- *.tsbuildinfo: TypeScript incremental build metadata

None of these should be checked in.
Introduces a Vite dev-server plugin at /a2a that proxies client requests
to the A2A agent via the @a2a-js/sdk client. This extracts A2A SDK usage
(agent card resolution, JSON-RPC, streaming transport) out of the shell's
browser code, mirroring the middleware pattern used by the Lit samples.

Request handling:

- JSON bodies are forwarded as A2A `data` parts with the
  `application/json+a2ui` mime type (used for v0.9 client action
  envelopes carrying structured context like party size, time).
- Plain text bodies are forwarded as `text` parts (used for user prompts
  like "find me a restaurant").
- A 1MB body size limit is enforced to prevent the dev server from
  accumulating unbounded memory if the shell misbehaves.
- All outbound requests to the agent carry the
  `X-A2A-Extensions: https://a2ui.org/a2a-extension/a2ui/v0.9` header,
  signalling A2UI v0.9 support to the agent. Without this, A2UI-aware
  agents fall back to plain-text responses.

Response handling:

- Defaults to A2A streaming: opens a text/event-stream to the shell and
  forwards each status-update or message event's parts as a \`data:\` line.
  This lets the shell render partial UI as the agent generates it.
- Setting \`ENABLE_STREAMING=false\` switches to non-streaming mode; the
  shell handles both.
- Errors are returned as 500 JSON before the stream opens, or as a
  terminal \`data:\` line containing an error-kind part array once the
  stream is open.

This commit only introduces the middleware infrastructure — the
middleware is registered in the Vite dev server but not yet reached by
any code path in the shell. The shell migration in the next commit wires
it up.

@types/node is added as a devDependency for the middleware's \`http\` and
\`crypto\` imports; \`middleware/\` is added to the TypeScript project so
the middleware is type-checked alongside the rest of the shell.
Migrates the React shell sample from the A2UI v0.8 protocol to v0.9.
The shell now uses @a2ui/web_core/v0_9's MessageProcessor and
@a2ui/react/v0_9's A2uiSurface in place of the v0.8
A2UIProvider / A2UIRenderer / useA2UIActions stack.

## Shell code (src/App.tsx, src/client.ts)

- Replaces A2UIProvider with a direct MessageProcessor instance and
  A2UIRenderer with A2uiSurface, the v0.9 rendering entry point.

- Tracks surfaces reactively: surfaces state is seeded from
  processor.model.surfacesMap and refreshed via onSurfaceCreated and
  onSurfaceDeleted subscriptions, so the shell renders whatever
  surfaces the agent creates (supporting multi-surface responses).

- User actions emitted by UI components (e.g. "Book Now" clicks) are
  wrapped in the v0.9 client message envelope
  \`{version: 'v0.9', action: {...}}\` before being sent to the agent.
  MessageProcessor's action listener emits a raw A2uiClientAction, so
  the shell has to wrap it for the wire format.

- client.ts posts to the /a2a middleware introduced in the previous
  commit instead of instantiating A2AClient directly. It parses SSE
  chunks and deduplicates \`createSurface\` messages across chunks:
  A2A status-update events carry cumulative message parts, so
  \`createSurface\` is redelivered on every chunk, and the v0.9
  MessageProcessor throws on duplicate surface IDs.

- Stream errors come back as an error-kind part array matching the
  Part shape the shell already parses, so error handling doesn't need
  a separate path.

## Mock data (src/mock/restaurantMessages.ts)

Rewrites the three hardcoded flows (restaurant list, booking form,
confirmation) for the v0.9 message format: createSurface /
updateComponents / updateDataModel instead of beginRendering /
surfaceUpdate / dataModelUpdate.

Each surface's root component uses \`id: 'root'\`; A2uiSurface in
@a2ui/react/v0_9 hard-requires that ID as the rendering entry point.

## Dependencies

Adds @a2ui/web_core as a file dep for MessageProcessor access.
@andrewkolos andrewkolos force-pushed the feat/upgrade-react-shell-v0.9 branch from 8f23e2f to 36bd8f8 Compare April 22, 2026 20:21
@andrewkolos andrewkolos marked this pull request as ready for review April 22, 2026 21:17
)
for i, part in enumerate(context.message.parts):
if isinstance(part.root, DataPart):
if "userAction" in part.root.data:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!!

@andrewkolos andrewkolos merged commit 6ec39d8 into google:main Apr 24, 2026
10 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in A2UI Apr 24, 2026
@andrewkolos andrewkolos deleted the feat/upgrade-react-shell-v0.9 branch April 24, 2026 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants