Live SSE demo
A real Server-Sent Events round-trip. A docs API route streams A2UI envelopes frame by frame, createSseTransport renders them live as they arrive, and the button POSTs an action back to the agent route.
connecting
Connecting to the stream…
Example code
The whole demo is three small pieces: a route that streams A2UI envelopes, a client that renders the stream, and a route that receives actions back.
Server — the streaming route
Emit one A2UI envelope per SSE data: frame. Any transport-agnostic stream works; this is the plainest one.
// app/api/agent/stream/route.ts — the agent's SSE streamexport const dynamic = 'force-dynamic';
// In a real app these frames come from your agent, not a constant.const FRAMES = [ { version: 'v1.0', createSurface: { surfaceId: 'main', catalogId: 'urn:flab-ui:a2ui:catalog:v1', }, }, { version: 'v1.0', updateComponents: { surfaceId: 'main', components: [ { id: 'root', component: 'Text', text: 'Hello from the agent', variant: 'title' }, ], }, },];
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export async function GET() { const encoder = new TextEncoder(); const stream = new ReadableStream({ async start(controller) { for (const frame of FRAMES) { // One A2UI envelope per SSE 'data:' frame. controller.enqueue(encoder.encode(`data: ${JSON.stringify(frame)}\n\n`)); await sleep(600); } controller.close(); }, }); return new Response(stream, { headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' }, });}Client — render the stream
createSseTransport reads the stream and feeds the client; subscribe to its surface store and hand the surface to <A2UISurface>.
'use client';
import { A2UISurface, A2uiClient, createSseTransport, type RendererAction,} from '@flabbergasted-ai/a2ui';import { useEffect, useMemo, useSyncExternalStore } from 'react';
export function AgentView() { // Receive over SSE; send user actions back with a POST to sendUrl. const client = useMemo( () => new A2uiClient( createSseTransport('/api/agent/stream', { sendUrl: '/api/agent/action' }), ), [], );
// connect() returns a disconnect fn, so it doubles as the effect cleanup. useEffect(() => client.connect(), [client]);
const surfaces = useSyncExternalStore( client.surfaces.subscribe, client.surfaces.getSnapshot, client.surfaces.getSnapshot, ); const surface = surfaces.get('main'); const handleAction = (action: RendererAction) => { void client.onAction(action).catch((error) => console.error('A2UI action failed', error)); };
return surface ? <A2UISurface surface={surface} onAction={handleAction} /> : null;}Server — the action channel
SSE is one-way, so user actions come back as a POST to sendUrl.
// app/api/agent/action/route.ts — the renderer -> agent return channelexport async function POST(request: Request) { const action = await request.json(); // Resume the agent run with the user's action… return Response.json({ ok: true });}