import { useEffect, useEffectEvent, useMemo, useRef, useState } from 'react';
import {
ApplicationRenderer,
AdkUiKitProvider,
MCP_APPS_PROTOCOL_VERSION,
ReasoningPanel,
Renderer as UiRenderer,
applyReasoningEvent,
buildMcpAppsInitializeRequest,
buildOutboundEvent,
createReasoningRuntime,
defaultUiKit,
extractProtocolSurface,
isActionResponseMessage,
parseUiApplication,
uiEventToMessage,
writeJsonPointer,
type Component,
type UiApplication,
type ProtocolSurfaceSnapshot as SharedProtocolSurfaceSnapshot,
type ReasoningMessage,
type UiEvent,
} from '@zavora-ai/adk-ui-react';
import { convertA2UIComponent } from './adk-ui-renderer/a2ui-converter';
import { ShowcaseGallery } from './showcase/ShowcaseGallery';
import './App.css';
type UiProtocol = 'adk_ui' | 'a2ui' | 'ag_ui' | 'mcp_apps' | 'awp';
const STUDIO_UI_KIT = {
...defaultUiKit,
id: 'zavora.ai:adk-ui/kit/studio@2.1.0',
name: 'ADK UI Studio',
version: '2.1.0',
brand: {
vibe: 'editorial, technical, warm',
industry: 'agentic software',
},
tokens: {
...defaultUiKit.tokens,
colors: {
...defaultUiKit.tokens.colors,
primary: '#ff6846',
primaryForeground: '#101923',
secondary: '#dce7ff',
secondaryForeground: '#172c66',
background: '#f4f0e7',
foreground: '#101923',
surface: '#fffdf8',
surfaceForeground: '#101923',
muted: '#e7ede8',
mutedForeground: '#53606c',
border: '#d7d2c8',
input: '#c6c1b7',
ring: '#4c72ff',
destructive: '#c83b2d',
destructiveForeground: '#ffffff',
success: '#20b486',
warning: '#b86b00',
info: '#4c72ff',
chart: ['#ff6846', '#20b486', '#4c72ff', '#b86b00', '#c83b2d', '#087557'],
},
typography: {
bodyFamily: 'Avenir Next, Century Gothic, sans-serif',
headingFamily: 'Avenir Next, Century Gothic, sans-serif',
monoFamily: 'IBM Plex Mono, ui-monospace, monospace',
},
},
};
interface SurfaceSnapshot {
protocol?: SharedProtocolSurfaceSnapshot['protocol'];
source?: SharedProtocolSurfaceSnapshot['source'];
surfaceId: string;
components: Component[];
dataModel: Record<string, unknown>;
bridge?: SharedProtocolSurfaceSnapshot['bridge'];
/** AWP-rendered HTML for iframe display */
html?: string;
}
interface PendingA2uiAction {
actionId: string;
surfaceId: string;
name: string;
responsePath: string;
}
interface StreamLogEvent {
id: number;
at: string;
protocol: UiProtocol;
kind: string;
preview: string;
raw: unknown;
}
interface AgUiActivityMessage {
messageId: string;
activityType: string;
content: Record<string, unknown>;
replace: boolean;
}
type JsonPatchOperation = {
op?: unknown;
path?: unknown;
from?: unknown;
value?: unknown;
};
interface ExampleTarget {
id: string;
name: string;
description: string;
port: number;
prompts: string[];
}
interface ProtocolCapability {
protocol: string;
versions: string[];
available?: boolean;
implementationTier?: string;
specTrack?: string;
reviewedOn?: string;
versionDetails?: Array<{
version: string;
specTrack: string;
status: string;
specificationUrl: string;
}>;
negotiation?: {
mode: string;
capabilityKeys: string[];
contentTypes: string[];
transports: string[];
fallbackProtocols: string[];
};
capabilityClaims?: Array<{
id: string;
status: 'supported' | 'partial' | 'host_owned' | 'candidate' | 'planned' | 'legacy';
scope: 'runtime' | 'client' | 'transport' | 'host' | 'security';
note?: string;
}>;
details?: Record<string, unknown>;
summary?: string;
features: string[];
limitations?: string[];
deprecation?: {
stage: string;
announcedOn: string;
sunsetTargetOn?: string;
replacementProtocols: string[];
note?: string;
};
}
interface ActionRequestBridge {
outbound: Record<string, unknown>;
requestBody: Record<string, unknown>;
textMessage: string;
preview: string;
bridgeRequest?: {
endpoint: string;
body: Record<string, unknown>;
kind: string;
preview: string;
skipModelRun?: boolean;
};
}
interface McpAppsBridgeSessionState {
sessionId?: string;
}
type TableColumnDef = {
header: string;
accessor_key: string;
sortable?: boolean;
};
const EXAMPLES: ExampleTarget[] = [
{
id: 'agentic_a2ui_support',
name: 'Agentic A2UI Support',
description: 'Multi-step A2UI intake → confirm → toast (v1.0 actionResponse)',
port: 8080,
prompts: [
'A production payroll export failed 45 minutes before finance cutoff. Build a guided escalation intake that carries forward those known facts, explains the severity and SLA, collects only the missing impact and evidence, and ends with an editable review step before escalation.',
'Turn this vague report into a support workflow: "Checkout sometimes fails after yesterday\'s release." Show progress, group the missing issue details, environment and evidence, then present a review summary. Never escalate without explicit confirmation.',
],
},
{
id: 'agentic_ag_ui_ops',
name: 'Agentic AG-UI Ops',
description: 'Ops dashboard + confirm with AG-UI events / reasoning panel',
port: 8080,
prompts: [
'Investigate a checkout latency spike in us-east-1. Stream an incident workspace with live service health, recent deploys, affected regions, an evidence timeline, and a guarded rollback proposal whose scope I can edit before approval.',
'The order queue is 3.4x above baseline. Build a live triage workspace with impact KPIs, a bottleneck trend, queue owners, current investigation status, and an approve-with-edits remediation plan.',
],
},
{
id: 'agentic_mcp_apps_confirm',
name: 'Agentic MCP Apps',
description: 'MCP Apps ui:// confirm → form host bridge flow',
port: 8080,
prompts: [
'Create an interactive release-readiness app for the mobile launch. Let me filter blockers by team, inspect evidence, edit rollout scope, and request final approval while preserving state in the conversation.',
'Build a cloud-spend anomaly explorer with date and team filters, drill-down transactions, annotations, export, and a guarded action to open an investigation.',
],
},
{
id: 'ui_demo',
name: 'UI Demo',
description: 'General purpose multi-surface demo',
port: 8080,
prompts: [
'Build a revenue operations cockpit with realistic sample data: pipeline KPIs with deltas, a six-month trend, at-risk deals, forecast gaps, and clear follow-up actions for today.',
'Design a customer onboarding workspace for a B2B analytics product. Show step progress, contextual guidance, validation, saved-state cues, and a complete review before activation.',
'Create a release command center for a Friday production launch: readiness KPIs, blockers by owner, rollout timeline, live risk signals, and a guarded go/no-go decision.',
],
},
{
id: 'ui_working_support',
name: 'Support Intake',
description: 'Ticket intake and triage workflows',
port: 8080,
prompts: [
'A customer cannot complete SSO setup before tomorrow\'s rollout. Build a focused intake that preserves that context, collects affected users, environment, error evidence and contact preference, explains the SLA, and submits to a review step rather than directly escalating.',
'Show an incident response board for a payments outage with impact, owners, service status, a timestamped timeline, unresolved decisions, and the next operator action.',
'Guide a blameless postmortem for a 38-minute API outage. Separate known facts from hypotheses, collect contributing factors and evidence, assign corrective actions, and show completion progress.',
],
},
{
id: 'ui_working_appointment',
name: 'Appointments',
description: 'Scheduling and availability workflows',
port: 8080,
prompts: [
'Help a new patient book a physiotherapy assessment next week. Compare suitable clinicians and accessible time slots, show price and preparation notes, then review the choice before booking.',
'Reschedule Tuesday\'s cardiology follow-up without losing the current appointment until a replacement is confirmed. Show alternatives, conflict warnings, and a final review.',
'Build today\'s clinician operations board with capacity, late arrivals, room conflicts, waitlist opportunities, and the next action for each exception.',
],
},
{
id: 'ui_working_events',
name: 'Events',
description: 'Registration and agenda workflows',
port: 8080,
prompts: [
'Build registration for a two-day AI engineering summit. Compare ticket entitlements, collect only attendee and accessibility needs, show the running total, and review everything before purchase.',
'Create a personalized conference agenda with schedule conflicts, speaker context, room changes, and a one-click way to save sessions without losing my filters.',
'Design an organizer workspace with RSVP conversion, capacity risk, waitlist movement, dietary counts, and actionable attendee exceptions.',
],
},
{
id: 'ui_working_facilities',
name: 'Facilities',
description: 'Maintenance requests and escalation',
port: 8080,
prompts: [
'There is water leaking near the third-floor electrical closet. Build an urgent facilities intake that highlights immediate safety guidance, preserves the known location, collects evidence and access details, and reviews the dispatch scope before escalation.',
'Show a maintenance dispatch board with SLA risk, technician capacity, site access constraints, progress, ownership, and recommended reassignment actions.',
'Create a work-order workspace with asset history, diagnosis, parts, safety checklist, evidence, and a guarded completion review.',
],
},
{
id: 'ui_working_inventory',
name: 'Inventory',
description: 'Stock monitoring and restock workflows',
port: 8080,
prompts: [
'Build a multi-warehouse inventory control tower with days-of-cover KPIs, demand trend, stockout risks, transfer opportunities, and prioritized reorder actions using realistic sample data.',
'Create a replenishment workflow for SKU AX-104, which has four days of cover. Compare transfer versus purchase options, collect quantity and urgency, and show cost and approval impact before submission.',
'Evaluate three suppliers using on-time delivery, defect rate, lead-time trend, capacity risk, and a clear sourcing recommendation with supporting evidence.',
],
},
];
const PROTOCOLS: Array<{ id: UiProtocol; label: string; hint: string; mark: string; status: string }> = [
{
id: 'adk_ui',
label: 'Legacy ADK UI',
hint: 'Backward-compatible internal profile (deprecated).',
mark: 'ADK',
status: 'Legacy',
},
{
id: 'a2ui',
label: 'A2UI',
hint: 'Production v0.9.1 surfaces with candidate v1 correlated actions.',
mark: 'A2',
status: 'Preferred',
},
{
id: 'ag_ui',
label: 'AG-UI',
hint: 'Hybrid subset with protocol-native runtime transport, AG-UI run input, and stable text/tool event parsing.',
mark: 'AG',
status: 'Native subset',
},
{
id: 'mcp_apps',
label: 'MCP Apps',
hint: 'Compatibility subset with framework bridge endpoints, bridge-aware structured content, ui:// resources, and inline HTML fallback.',
mark: 'MCP',
status: 'Bridge ready',
},
{
id: 'awp',
label: 'AWP',
hint: 'Agentic Web Protocol — dual-user rendering with HTML output, capability manifest export, and bandwidth-adaptive mode.',
mark: 'AWP',
status: 'Adaptive',
},
];
const NEXT_GENERATION_DEMO: UiApplication = {
id: 'luma-transit-command',
name: 'Luma Transit',
kit_id: STUDIO_UI_KIT.id,
initial_route: '/',
navigation: [
{ label: 'Briefing', route: '/' },
{ label: 'Network', route: '/network' },
{ label: 'Spatial twin', route: '/spatial' },
],
state: { region: 'all', activeIncident: 'INC-2048' },
pages: [
{
id: 'briefing',
route: '/',
title: 'Morning network briefing',
template: 'marketing',
atmosphere: 'gradient',
eyebrow: 'Friday / 06:42 UTC',
hero: {
title: 'Move a city without losing the signal.',
description: 'A live operating picture for 2.8 million daily journeys. The agent connects demand, fleet health, and service risk into one decisive briefing.',
alignment: 'split',
content: [
{ type: 'badge', label: 'Network confidence 94%', variant: 'success' },
],
actions: [
{ type: 'button', label: 'Open network control', action_id: 'navigate:/network', variant: 'primary' },
{ type: 'button', label: 'Explore spatial twin', action_id: 'navigate:/spatial', variant: 'outline' },
],
visual: [
{
type: 'card',
title: 'Passenger flow',
description: 'Boardings by hour / today versus baseline',
content: [{
type: 'chart', kind: 'area', x_key: 'hour', y_keys: ['today', 'baseline'], show_legend: true,
data: [
{ hour: '05', today: 18, baseline: 16 }, { hour: '06', today: 41, baseline: 36 },
{ hour: '07', today: 78, baseline: 69 }, { hour: '08', today: 96, baseline: 88 },
{ hour: '09', today: 71, baseline: 75 }, { hour: '10', today: 57, baseline: 59 },
],
}],
footer: [{ type: 'text', content: 'Peak load shifted 14 minutes earlier', variant: 'caption' }],
},
],
},
regions: [
{
id: 'briefing-signals',
title: 'Signals that change the day',
description: 'Not a wall of metrics. Three conditions the operating team can act on now.',
layout: 'bento',
columns: 3,
components: [
{
type: 'card', title: 'Airport line is absorbing demand', description: '+12% journeys after the express closure',
content: [
{ type: 'text', content: 'Add two reserve sets between Central and Terminal 2 before 07:30.', variant: 'body' },
{ type: 'progress', value: 78, label: 'Current vehicle load' },
],
footer: [{ type: 'button', label: 'Review deployment', action_id: 'navigate:/network', variant: 'primary' }],
},
{ type: 'card', title: '94%', description: 'On-time performance', content: [{ type: 'badge', label: '+2.1 pts', variant: 'success' }] },
{ type: 'card', title: '6 min', description: 'Median recovery time', content: [{ type: 'badge', label: 'Inside target', variant: 'info' }] },
{ type: 'alert', title: 'Weather watch', description: 'Crosswinds may constrain the river crossing after 16:00.', variant: 'warning' },
],
},
],
footer: [{ type: 'text', content: 'Prepared by the network operations agent from live service, fleet, and demand feeds.', variant: 'caption' }],
},
{
id: 'network',
route: '/network',
title: 'Network control',
description: 'Exceptions, capacity, and owner actions across the live network.',
template: 'dashboard',
atmosphere: 'grid',
eyebrow: 'Live operating picture',
regions: [
{
id: 'network-kpis',
layout: 'stats',
columns: 4,
components: [
{ type: 'card', title: '1,284', description: 'Vehicles in service', content: [{ type: 'badge', label: '98.7% available', variant: 'success' }] },
{ type: 'card', title: '42', description: 'Active exceptions', content: [{ type: 'badge', label: '7 need action', variant: 'warning' }] },
{ type: 'card', title: '2.8m', description: 'Forecast journeys', content: [{ type: 'badge', label: '+4.2% Friday', variant: 'info' }] },
{ type: 'card', title: '94%', description: 'Network confidence', content: [{ type: 'progress', value: 94, label: 'Confidence' }] },
],
},
{
id: 'network-actions',
title: 'Where attention compounds',
description: 'The agent has ranked interventions by passenger impact and recovery confidence.',
layout: 'split',
columns: 2,
tone: 'muted',
components: [
{
type: 'chart', title: 'Capacity pressure by corridor', kind: 'bar', x_key: 'corridor', y_keys: ['load', 'threshold'],
data: [
{ corridor: 'Airport', load: 88, threshold: 80 }, { corridor: 'North', load: 72, threshold: 80 },
{ corridor: 'River', load: 81, threshold: 80 }, { corridor: 'West', load: 64, threshold: 80 },
],
},
{
type: 'table', sortable: true, striped: true, page_size: 4,
columns: [
{ header: 'Priority', accessor_key: 'priority' }, { header: 'Intervention', accessor_key: 'action' },
{ header: 'Owner', accessor_key: 'owner' }, { header: 'Impact', accessor_key: 'impact' },
],
data: [
{ priority: 'P1', action: 'Deploy reserve sets', owner: 'Fleet', impact: '18k journeys' },
{ priority: 'P1', action: 'Hold cross-line transfer', owner: 'Control', impact: '9k journeys' },
{ priority: 'P2', action: 'Stage response crew', owner: 'Field', impact: '4k journeys' },
{ priority: 'P3', action: 'Update rider guidance', owner: 'Comms', impact: 'All channels' },
],
},
],
},
],
aside: [
{
type: 'card', title: 'Agent focus', description: 'Tune the operating picture without losing page state.',
content: [
{ type: 'select', name: 'region', label: 'Region', options: [{ label: 'All network', value: 'all' }, { label: 'Airport corridor', value: 'airport' }, { label: 'River corridor', value: 'river' }] },
{ type: 'switch', name: 'live_refresh', label: 'Live refresh', default_checked: true },
],
footer: [{ type: 'button', label: 'Ask agent to re-rank', action_id: 'rerank_network', variant: 'primary' }],
},
],
footer: [{ type: 'button', label: 'Open the spatial twin', action_id: 'navigate:/spatial', variant: 'outline' }],
},
{
id: 'spatial',
route: '/spatial',
title: 'Spatial network twin',
template: 'immersive',
atmosphere: 'spatial',
eyebrow: 'Experimental / WebGPU ready',
hero: {
title: 'See pressure move through the network.',
description: 'A safe declarative scene generated by the agent. Select a corridor to focus the investigation; no arbitrary code or shaders are accepted.',
alignment: 'split',
actions: [{ type: 'button', label: 'Back to network control', action_id: 'navigate:/network', variant: 'outline' }],
visual: [{
type: 'scene_3d',
title: 'Live corridor pressure',
description: 'Drag to orbit. Select a labelled corridor below.',
height: 560,
background: 'contrast',
auto_rotate: true,
controls: true,
camera: { position: [5.5, 4.2, 7.5], target: [0, 0, 0], fov: 39 },
objects: [
{ kind: 'primitive', id: 'core', shape: 'sphere', position: [0, 0.4, 0], scale: [1.25, 1.25, 1.25], material: 'primary', label: 'Central interchange', action_id: 'focus_central' },
{ kind: 'primitive', id: 'airport', shape: 'torus', position: [2.2, 0.2, 0], rotation: [1.57, 0, 0], scale: [1.4, 1.4, 1.4], material: 'warning', label: 'Airport corridor', action_id: 'focus_airport' },
{ kind: 'primitive', id: 'north', shape: 'cylinder', position: [-2.2, 0, -0.7], scale: [0.6, 2.1, 0.6], material: 'info', label: 'North corridor', action_id: 'focus_north' },
{ kind: 'primitive', id: 'river', shape: 'box', position: [0.4, -0.5, -2.4], rotation: [0.25, 0.65, 0], scale: [2.2, 0.28, 0.55], material: 'success', label: 'River crossing', action_id: 'focus_river' },
{ kind: 'primitive', id: 'ground', shape: 'plane', position: [0, -1.1, 0], rotation: [-1.57, 0, 0], scale: [4.5, 4.5, 4.5], material: 'muted' },
],
fallback: 'The spatial twin is unavailable on this device. Use Network control for the same live interventions.',
}],
},
regions: [
{
id: 'spatial-context',
title: 'The scene is part of the workflow',
description: 'Spatial selection can drive agent actions, route changes, and detail panels just like any other generated component.',
layout: 'editorial',
columns: 2,
components: [
{ type: 'text', content: 'The active model is bounded to semantic brand materials, approved GLB assets, a fixed primitive catalog, and host-owned interaction controls.', variant: 'body' },
{ type: 'key_value', pairs: [{ key: 'Renderer', value: 'WebGPU with WebGL 2 fallback' }, { key: 'Motion', value: 'Reduced-motion aware' }, { key: 'Assets', value: 'Approved kit references only' }, { key: 'Object budget', value: '64 scene objects / 8 models' }] },
],
},
],
},
],
};
const MAX_EVENT_LOG = 120;
const DEFAULT_USER_ID = 'user1';
let streamEventSequence = 0;
function nextStreamEventId(): number {
streamEventSequence += 1;
return streamEventSequence;
}
function readClockMs(): number {
return performance.now();
}
function nowLabel(): string {
return new Date().toLocaleTimeString('en-US', { hour12: false });
}
function makeSessionId(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
// Fallback for environments without crypto.randomUUID — uses crypto.getRandomValues
// for better randomness than Math.random().
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
return `session-${Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')}`;
}
return `session-${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function pickField(record: Record<string, unknown>, ...keys: string[]): unknown {
for (const key of keys) {
if (key in record) {
return record[key];
}
}
return undefined;
}
function parseMaybeJson(value: unknown): unknown {
if (typeof value !== 'string') {
return value;
}
const trimmed = value.trim();
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
return value;
}
try {
return JSON.parse(trimmed);
} catch {
return value;
}
}
function normalizeProtocol(value: unknown): UiProtocol | null {
if (typeof value !== 'string') {
return null;
}
const normalized = value.trim().toLowerCase();
if (normalized === 'adk_ui' || normalized === 'adk-ui') return 'adk_ui';
if (normalized === 'a2ui') return 'a2ui';
if (normalized === 'ag_ui' || normalized === 'ag-ui') return 'ag_ui';
if (normalized === 'mcp_apps' || normalized === 'mcp-apps') return 'mcp_apps';
if (normalized === 'awp') return 'awp';
return null;
}
function takeCompleteSseEvents(buffer: string): { events: string[]; rest: string } {
let rest = buffer.replace(/\r\n/g, '\n');
const events: string[] = [];
while (true) {
const boundary = rest.indexOf('\n\n');
if (boundary < 0) {
break;
}
events.push(rest.slice(0, boundary));
rest = rest.slice(boundary + 2);
}
return { events, rest };
}
function extractSseEventPayload(rawEvent: string): string | null {
const dataLines: string[] = [];
for (const rawLine of rawEvent.split('\n')) {
const line = rawLine.trimEnd();
if (!line || line.startsWith(':') || !line.startsWith('data:')) {
continue;
}
dataLines.push(line.slice(5).trimStart());
}
if (dataLines.length === 0) {
return null;
}
const payload = dataLines.join('\n').trim();
return payload.length > 0 ? payload : null;
}
function extractTextPartsFromContent(content: unknown): string[] {
if (!Array.isArray(content)) {
return [];
}
const snippets: string[] = [];
for (const entry of content) {
if (!isRecord(entry)) {
continue;
}
const text = pickField(entry, 'text');
if (typeof text === 'string' && text.trim().length > 0) {
snippets.push(text.trim());
}
}
return snippets;
}
function extractOutboundTextMessage(
outbound: Record<string, unknown>,
fallback: string,
): string {
const protocol = normalizeProtocol(pickField(outbound, 'protocol'));
if (protocol === 'ag_ui') {
const input = pickField(outbound, 'input');
if (isRecord(input)) {
const messages = pickField(input, 'messages');
if (Array.isArray(messages)) {
const snippets: string[] = [];
for (const entry of messages) {
if (!isRecord(entry)) {
continue;
}
const content = pickField(entry, 'content');
if (typeof content === 'string' && content.trim().length > 0) {
snippets.push(content.trim());
}
}
if (snippets.length > 0) {
return snippets.join('\n\n');
}
}
}
}
if (protocol === 'mcp_apps') {
const params = pickField(outbound, 'params');
if (isRecord(params)) {
const content = pickField(params, 'content');
const snippets = extractTextPartsFromContent(content);
if (snippets.length > 0) {
return snippets.join('\n\n');
}
}
}
return fallback;
}
function describeOutboundEvent(protocol: UiProtocol, outbound: Record<string, unknown>): string {
if (protocol === 'mcp_apps') {
const method = pickField(outbound, 'method');
if (typeof method === 'string' && method.trim().length > 0) {
return method;
}
}
if (protocol === 'ag_ui') {
return 'ag_ui run input';
}
return `${protocol} event`;
}
function buildActionRequestBridge(
protocol: UiProtocol,
action: UiEvent,
context: {
appName: string;
userId: string;
sessionId: string;
surfaceId: string;
threadId?: string;
parentRunId?: string;
},
): ActionRequestBridge {
const timestamp = Date.now();
const outbound = buildOutboundEvent(protocol, action, {
surfaceId: context.surfaceId,
threadId: context.threadId ?? `thread-${context.sessionId}`,
runId: `run-${context.sessionId}-${timestamp}`,
parentRunId: context.parentRunId,
messageId: `msg-${context.sessionId}-${timestamp}`,
});
const fallbackMessage = uiEventToMessage(action);
const textMessage = extractOutboundTextMessage(outbound, fallbackMessage);
const preview = describeOutboundEvent(protocol, outbound);
const requestBody: Record<string, unknown> = {};
let bridgeRequest: ActionRequestBridge['bridgeRequest'];
if (protocol === 'ag_ui') {
requestBody.uiTransport = 'protocol_native';
requestBody.input = buildAgUiRuntimeInput(textMessage, {
sessionId: context.sessionId,
surfaceId: context.surfaceId,
threadId: context.threadId,
parentRunId: context.parentRunId,
runId: `run-${context.sessionId}-${timestamp}`,
messageId: `msg-${context.sessionId}-${timestamp}`,
forwardedProps: {
uiEvent: action,
},
});
} else if (protocol === 'mcp_apps') {
const method = pickField(outbound, 'method');
const params = pickField(outbound, 'params');
if (typeof method === 'string' && isRecord(params)) {
const endpoint = resolveMcpAppsBridgeEndpoint(method);
if (endpoint) {
bridgeRequest = {
endpoint,
kind:
method === 'ui/update-model-context'
? 'mcp_apps_model_context'
: 'mcp_apps_message',
preview: method,
skipModelRun: method === 'ui/update-model-context',
body: {
appName: context.appName,
userId: context.userId,
sessionId: context.sessionId,
...params,
},
};
}
}
} else {
const protocolEvent = pickField(outbound, 'event');
if (protocolEvent !== undefined) {
requestBody.protocolEvent = protocolEvent;
}
}
return {
outbound,
requestBody,
textMessage,
preview,
bridgeRequest,
};
}
function buildMcpAppsHandshakePayload(): {
initializeRequest: Record<string, unknown>;
} {
return {
initializeRequest: buildMcpAppsInitializeRequest({
protocolVersion: MCP_APPS_PROTOCOL_VERSION,
appInfo: {
name: 'adk-ui-react-example',
version: '0.4.0',
title: 'ADK UI Example View',
description: 'Example MCP Apps-compatible view host for adk-ui.',
},
appCapabilities: {
availableDisplayModes: ['inline'],
tools: {
listChanged: false,
},
},
}) as unknown as Record<string, unknown>,
};
}
function buildAgUiRuntimeInput(
message: string,
context: {
sessionId: string;
surfaceId: string;
threadId?: string;
parentRunId?: string;
runId?: string;
messageId?: string;
forwardedProps?: Record<string, unknown>;
activityMessages?: Array<Record<string, unknown>>;
},
): Record<string, unknown> {
const timestamp = Date.now();
return {
threadId: context.threadId ?? `thread-${context.sessionId}`,
runId: context.runId ?? `run-${context.sessionId}-${timestamp}`,
parentRunId: context.parentRunId,
messages: [
{
id: context.messageId ?? `msg-${context.sessionId}-${timestamp}`,
role: 'user',
name: 'adk-ui',
content: [
{
type: 'text',
text: message,
},
],
},
...(context.activityMessages ?? []),
],
tools: [],
context: [],
forwardedProps: {
source: 'adk-ui-react-example',
surfaceId: context.surfaceId,
...(context.forwardedProps ?? {}),
},
};
}
function buildPromptContextActivityMessages(
prompt: string,
context: {
sessionId: string;
exampleId: string;
retryAttempt: number;
protocol: UiProtocol;
},
): Array<Record<string, unknown>> {
const timestamp = Date.now();
const messageId = `activity-${context.sessionId}-${timestamp}`;
return [
{
id: messageId,
role: 'activity',
activityType: 'PROMPT_CONTEXT',
replace: true,
content: {
label: context.retryAttempt > 0 ? 'Retry prompt queued' : 'Prompt queued',
prompt,
exampleId: context.exampleId,
retryAttempt: context.retryAttempt,
status: context.retryAttempt > 0 ? 'queued_retry' : 'queued',
},
},
{
id: messageId,
role: 'activity',
activityType: 'PROMPT_CONTEXT',
replace: false,
patch: [
{ op: 'replace', path: '/status', value: 'streaming' },
{ op: 'add', path: '/transport', value: context.protocol === 'ag_ui' ? 'protocol_native' : context.protocol },
],
},
];
}
function resolveMcpAppsBridgeEndpoint(method: string): string | null {
if (method === 'ui/initialize') return '/api/ui/initialize';
if (method === 'ui/message') return '/api/ui/message';
if (method === 'ui/update-model-context') return '/api/ui/update-model-context';
return null;
}
function buildMcpAppsInitializeBody(
appName: string,
userId: string,
sessionId: string,
): Record<string, unknown> {
const handshake = buildMcpAppsHandshakePayload();
const params = isRecord(handshake.initializeRequest.params)
? handshake.initializeRequest.params
: {};
return {
appName,
userId,
sessionId,
...params,
};
}
function formatCapabilityToken(value: string): string {
return value
.split('_')
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}
function protocolInstruction(protocol: UiProtocol): string {
if (protocol === 'mcp_apps') {
return [
'Use adk-ui tools and explicitly set `protocol` to `mcp_apps`.',
'Assume the host supports MCP Apps initialize/message/model-context bridge flows.',
'Include `mcp_apps.domain` as `https://example.com` when you call render tools.',
'Design a focused interactive app that preserves host context and state instead of reproducing the chat composer.',
'Use `render_app` with two to five pages when the request describes a complete product, portal, site, or multi-route workflow.',
'Use a populated multi-section surface for exploration; use a focused card for a single decision.',
'Call at least one `render_*` tool before any plain text response.',
'If rendering fails, call `render_alert` with an error message.',
'Give the surface clear hierarchy, domain-specific content, one primary next action, and no more than one secondary action.',
].join(' ');
}
if (protocol === 'awp') {
return [
'Use adk-ui tools and explicitly set `protocol` to `awp`.',
'AWP (Agentic Web Protocol) produces both a structured component tree and rendered HTML in a single response.',
'The runtime will generate embeddable HTML from your components automatically.',
'Design a complete decision surface with realistic domain content instead of a text-only component inventory.',
'Use `render_app` with two to five pages when the request describes a complete product, portal, site, or multi-route workflow.',
'Call at least one `render_*` tool before any plain text response.',
'If rendering fails, call `render_alert` with an error message.',
'Give the surface clear hierarchy, current state, and one obvious next action.',
].join(' ');
}
return [
`Use adk-ui tools and explicitly set \`protocol\` to \`${protocol}\`.`,
protocol === 'ag_ui'
? 'Assume the runtime supports protocol-native AG-UI transport with stable lifecycle, text, and tool events.'
: 'Assume the runtime preserves protocol-aware payloads alongside the base ADK stream.',
'Design for the user outcome rather than echoing requested component names.',
'When the request is for a complete product, site, portal, or multiple routes, call `render_app` with two to five purpose-built pages instead of compressing the result into one surface.',
'Use `scene_3d` only when spatial understanding or product exploration improves the workflow; keep 3D declarative, labelled, and actionable.',
'Preserve supplied facts and state; use realistic populated data when the request is a prototype.',
'Call at least one `render_*` tool before any plain text response.',
'If rendering fails, call `render_alert` with an error message.',
'Give the surface clear hierarchy, domain-specific content, and one obvious next action.',
].join(' ');
}
function retryInstruction(
protocol: UiProtocol,
prompt: string,
mode: 'no_surface' | 'quality' = 'no_surface',
): string {
if (mode === 'quality') {
const intent = prompt.toLowerCase();
const isFormWorkflow = /\b(form|intake|onboard|register|book|reschedule|replenish|postmortem)\b/.test(intent);
const isDecisionWorkspace = /\b(dashboard|workspace|cockpit|command center|board|monitor|explorer|control tower)\b/.test(intent);
if (isFormWorkflow) {
return [
'Retry mode: the previous form was too sparse or generic for the requested workflow.',
'Do not repeat it. Render a focused form with a specific context-setting description, at least four purposeful fields for a multi-step intake, useful select options, and a textarea for evidence or detail when relevant.',
'Preserve facts already supplied in the request and collect only missing information.',
'Use a domain-specific review CTA such as "Review escalation" or "Review booking", never a generic "Submit" action.',
'Do not emit plain text before tool calls.',
].join(' ');
}
if (isDecisionWorkspace) {
return [
'Retry mode: the previous decision surface lacked enough structure or useful data.',
`Produce a populated \`${protocol}\` workspace with domain-specific KPIs, a meaningful chart or table, current risks or exceptions, and one clear next action.`,
'Use render_layout for the complete composition. If the user must edit a proposal or filters, include a form section in that layout.',
'Do not replace structured sections with one oversized text or form description, and do not use generic placeholders.',
'Do not emit plain text before tool calls.',
].join(' ');
}
return [
'Retry mode: the previous surface was too sparse or generic.',
`Produce a more complete \`${protocol}\` surface with clear hierarchy, realistic domain content, useful state, and one obvious next action.`,
'Do not repeat the previous component inventory or use generic placeholder labels.',
'Do not emit plain text before tool calls.',
].join(' ');
}
if (protocol === 'mcp_apps') {
return [
'Retry mode: previous attempt produced no renderable UI.',
'Now emit exactly one valid `render_screen` tool call immediately with `protocol: "mcp_apps"` and a `root` component.',
'Ensure the surface includes structured UI (table/card/alert), not only text and buttons.',
'Do not emit plain text before the tool call.',
].join(' ');
}
return [
'Retry mode: previous attempt produced no renderable UI.',
`Now emit exactly one valid \`render_screen\` tool call immediately with \`protocol: "${protocol}"\` and a \`root\` component.`,
'Ensure the surface includes structured UI (table/card/alert), not only text and buttons.',
'Do not emit plain text before the tool call.',
].join(' ');
}
function extractTextValue(value: unknown): string {
if (typeof value === 'string') {
return value;
}
if (!isRecord(value)) {
return '';
}
const literal = pickField(value, 'literalString', 'literal_string');
if (typeof literal === 'string') {
return literal;
}
const dynamic = pickField(value, 'dynamicString', 'dynamic_string');
if (typeof dynamic === 'string') {
return dynamic;
}
const text = pickField(value, 'text');
return typeof text === 'string' ? text : '';
}
function extractLegacyUiResponseComponents(dataModel: unknown): Component[] | null {
if (!isRecord(dataModel)) {
return null;
}
const legacy = pickField(dataModel, 'adk_ui_response', 'adkUiResponse');
if (!isRecord(legacy)) {
return null;
}
const rawComponents = pickField(legacy, 'components');
if (!Array.isArray(rawComponents)) {
return null;
}
const components = rawComponents
.filter((entry): entry is Record<string, unknown> => isRecord(entry))
.filter((entry) => typeof pickField(entry, 'type') === 'string')
.map((entry) => entry as unknown as Component);
return components.length > 0 ? components : null;
}
function parseTableColumnDef(entry: Record<string, unknown>): TableColumnDef | null {
const componentField = pickField(entry, 'component');
let tableColumnNode: Record<string, unknown> | null = null;
if (isRecord(componentField)) {
const candidate = pickField(componentField, 'TableColumn', 'tableColumn', 'table_column', 'column');
if (isRecord(candidate)) {
tableColumnNode = candidate;
} else {
const maybeAccessor = pickField(componentField, 'accessorKey', 'accessor_key', 'accessor');
if (typeof maybeAccessor === 'string') {
tableColumnNode = componentField;
}
}
} else if (typeof componentField === 'string') {
const normalized = componentField.toLowerCase();
if (normalized === 'tablecolumn' || normalized === 'table_column' || normalized === 'table-column') {
tableColumnNode = entry;
}
}
if (!tableColumnNode) {
const direct = pickField(entry, 'TableColumn', 'tableColumn', 'table_column');
if (isRecord(direct)) {
tableColumnNode = direct;
}
}
if (!tableColumnNode) {
return null;
}
const accessor = pickField(tableColumnNode, 'accessorKey', 'accessor_key', 'accessor');
if (typeof accessor !== 'string' || accessor.trim().length === 0) {
return null;
}
const headerRaw = pickField(tableColumnNode, 'header', 'title', 'label');
const header = extractTextValue(headerRaw) || (typeof headerRaw === 'string' ? headerRaw : '') || humanizeId(accessor);
const sortableRaw = pickField(tableColumnNode, 'sortable');
const sortable = typeof sortableRaw === 'boolean' ? sortableRaw : undefined;
return {
header,
accessor_key: accessor,
sortable,
};
}
function extractTableColumnDefs(
rawComponents: unknown[],
): Map<string, TableColumnDef> {
const refs = new Map<string, TableColumnDef>();
for (const entry of rawComponents) {
if (!isRecord(entry)) {
continue;
}
const columnId = typeof pickField(entry, 'id') === 'string' ? (pickField(entry, 'id') as string) : null;
if (!columnId) {
continue;
}
const def = parseTableColumnDef(entry);
if (!def) {
continue;
}
refs.set(columnId, def);
}
return refs;
}
function findTableColumnDef(rawComponents: unknown[], columnRef: string): TableColumnDef | null {
for (const entry of rawComponents) {
if (!isRecord(entry)) {
continue;
}
const id = pickField(entry, 'id');
if (id !== columnRef) {
continue;
}
const parsed = parseTableColumnDef(entry);
if (parsed) {
return parsed;
}
}
return null;
}
function inferAccessorFromRef(columnRef: string, sampleKeys: string[], columnIndex: number): string | null {
if (sampleKeys.length === 0) {
return null;
}
const lowered = columnRef.toLowerCase();
const normalized = lowered.replace(/^col[-_]/, '').replace(/^column[-_]/, '');
const compact = normalized.replace(/[-_]/g, '');
const direct = sampleKeys.find((key) => key.toLowerCase() === lowered);
if (direct) {
return direct;
}
const normalizedMatch = sampleKeys.find((key) => key.toLowerCase() === normalized);
if (normalizedMatch) {
return normalizedMatch;
}
const compactMatch = sampleKeys.find((key) => key.toLowerCase().replace(/[-_]/g, '') === compact);
if (compactMatch) {
return compactMatch;
}
if (columnIndex >= 0 && columnIndex < sampleKeys.length) {
return sampleKeys[columnIndex];
}
return null;
}
function inferTableColumnDef(
columnRef: string,
sampleRow: Record<string, unknown> | null,
columnIndex: number,
): TableColumnDef | null {
if (!sampleRow) {
return null;
}
const keys = Object.keys(sampleRow);
const accessor = inferAccessorFromRef(columnRef, keys, columnIndex);
if (!accessor) {
return null;
}
return {
header: humanizeId(accessor),
accessor_key: accessor,
};
}
function normalizeRequestError(error: unknown, baseUrl: string, endpoint: string, port: number): string {
const fallback = `Request to ${baseUrl}${endpoint} failed.`;
if (!(error instanceof Error)) {
return fallback;
}
const message = error.message || fallback;
const networkPattern =
/failed to fetch|load failed|networkerror|fetch failed|could not connect|connection refused|request blocked/i;
if (networkPattern.test(message)) {
return `Cannot reach ${baseUrl}${endpoint}. Start the example server on port ${port} and retry.`;
}
return message;
}
function extractButtonChildRefs(rawComponents: unknown[]): Map<string, string> {
const refs = new Map<string, string>();
for (const entry of rawComponents) {
if (!isRecord(entry)) {
continue;
}
const buttonId = typeof pickField(entry, 'id') === 'string' ? (pickField(entry, 'id') as string) : null;
if (!buttonId) {
continue;
}
const componentField = pickField(entry, 'component');
if (isRecord(componentField)) {
const buttonNode = pickField(componentField, 'Button');
if (isRecord(buttonNode)) {
const childRef = pickField(buttonNode, 'child');
if (typeof childRef === 'string' && childRef.trim().length > 0) {
refs.set(buttonId, childRef);
}
}
continue;
}
if (typeof componentField === 'string' && componentField === 'Button') {
const childRef = pickField(entry, 'child');
if (typeof childRef === 'string' && childRef.trim().length > 0) {
refs.set(buttonId, childRef);
}
}
}
return refs;
}
function humanizeId(id: string): string {
const text = id
.replace(/[_-]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
if (text.length === 0) {
return 'Submit';
}
return text.replace(/\b\w/g, (char) => char.toUpperCase());
}
function convertRawComponent(raw: unknown, fallbackId: string): { id?: string; component: Component } | null {
if (!isRecord(raw)) {
return null;
}
if (typeof raw.type === 'string') {
const component = raw as unknown as Component;
return {
id: typeof raw.id === 'string' ? raw.id : undefined,
component,
};
}
const componentField = pickField(raw, 'component');
let componentObject: Record<string, unknown> | null = null;
if (isRecord(componentField)) {
componentObject = componentField;
} else if (typeof componentField === 'string') {
// Support adk-ui flat payload shape: { component: "Text", text: "...", ... }
const inlineProps: Record<string, unknown> = {};
for (const [key, value] of Object.entries(raw)) {
if (key === 'id' || key === 'component') {
continue;
}
inlineProps[key] = value;
}
componentObject = { [componentField]: inlineProps };
}
if (!componentObject) {
return null;
}
const sourceId =
typeof pickField(raw, 'id') === 'string' ? (pickField(raw, 'id') as string) : fallbackId;
const converted = convertA2UIComponent({
id: sourceId,
component: componentObject,
});
if (!converted) {
return null;
}
return {
id: sourceId,
component: converted,
};
}
function resolveNestedComponent(
component: Component,
byId: Map<string, Component>,
path: Set<string>,
): Component {
const clone = { ...(component as Record<string, unknown>) } as Record<string, unknown>;
const currentId = typeof clone.id === 'string' ? clone.id : undefined;
if (currentId) {
if (path.has(currentId)) {
return component;
}
path.add(currentId);
}
if (Array.isArray(clone.children)) {
clone.children = clone.children
.map((entry) => {
if (typeof entry === 'string') {
return byId.get(entry);
}
return entry;
})
.filter((entry): entry is Component => Boolean(entry))
.map((entry) => resolveNestedComponent(entry, byId, new Set(path)));
}
if (Array.isArray(clone.content)) {
clone.content = clone.content
.map((entry) => {
if (typeof entry === 'string') {
return byId.get(entry);
}
return entry;
})
.filter((entry): entry is Component => Boolean(entry) && isRecord(entry))
.map((entry) => resolveNestedComponent(entry, byId, new Set(path)));
}
if (Array.isArray(clone.footer)) {
clone.footer = clone.footer
.map((entry) => {
if (typeof entry === 'string') {
return byId.get(entry);
}
return entry;
})
.filter((entry): entry is Component => Boolean(entry) && isRecord(entry))
.map((entry) => resolveNestedComponent(entry, byId, new Set(path)));
}
if (Array.isArray(clone.tabs)) {
clone.tabs = clone.tabs
.filter((entry) => isRecord(entry) && Array.isArray(entry.content))
.map((tab) => ({
...tab,
content: (tab.content as unknown[])
.map((entry) => {
if (typeof entry === 'string') {
return byId.get(entry);
}
return entry;
})
.filter((entry): entry is Component => Boolean(entry) && isRecord(entry))
.map((entry) => resolveNestedComponent(entry, byId, new Set(path))),
}));
}
return clone as Component;
}
function buildRenderableComponents(rawComponents: unknown[]): Component[] {
const byId = new Map<string, Component>();
const withoutId: Component[] = [];
const buttonChildRefs = extractButtonChildRefs(rawComponents);
const tableColumnDefs = extractTableColumnDefs(rawComponents);
rawComponents.forEach((entry, index) => {
const converted = convertRawComponent(entry, `component-${index + 1}`);
if (!converted) {
return;
}
if (converted.id) {
byId.set(converted.id, converted.component);
} else {
withoutId.push(converted.component);
}
});
// A2UI buttons may reference a child Text node by id. Hydrate label from that node.
for (const [buttonId, childId] of buttonChildRefs) {
const buttonNode = byId.get(buttonId);
if (!buttonNode || buttonNode.type !== 'button') {
continue;
}
const childNode = byId.get(childId);
const textLabel =
childNode && childNode.type === 'text' && childNode.content.trim().length > 0
? childNode.content
: null;
const currentLabel = buttonNode.label?.trim() ?? '';
if (currentLabel.length === 0 || currentLabel === childId) {
byId.set(buttonId, {
...buttonNode,
label: textLabel ?? humanizeId(childId),
});
}
}
// A2UI tables can declare columns as ids and define TableColumn components separately.
for (const [componentId, component] of byId.entries()) {
if (component.type !== 'table') {
continue;
}
const rawColumns = (component as unknown as { columns?: unknown[] }).columns;
if (!Array.isArray(rawColumns)) {
continue;
}
const rawData = (component as unknown as { data?: unknown[] }).data;
const sampleRow =
Array.isArray(rawData) && rawData.length > 0 && isRecord(rawData[0])
? (rawData[0] as Record<string, unknown>)
: null;
const hydrated = rawColumns
.map((column, index) => {
if (typeof column === 'string') {
const fromMap = tableColumnDefs.get(column);
if (fromMap) {
return fromMap;
}
const fromRaw = findTableColumnDef(rawComponents, column);
if (fromRaw) {
return fromRaw;
}
return inferTableColumnDef(column, sampleRow, index);
}
if (isRecord(column)) {
const accessor = pickField(column, 'accessor_key', 'accessorKey');
if (typeof accessor !== 'string' || accessor.trim().length === 0) {
return null;
}
const header = extractTextValue(pickField(column, 'header')) || humanizeId(accessor);
const sortableRaw = pickField(column, 'sortable');
const sortable = typeof sortableRaw === 'boolean' ? sortableRaw : undefined;
return {
header,
accessor_key: accessor,
sortable,
};
}
return null;
})
.filter(
(column): column is { header: string; accessor_key: string; sortable?: boolean } =>
Boolean(column),
);
if (hydrated.length > 0) {
byId.set(componentId, {
...component,
columns: hydrated,
});
}
}
const referenced = new Set<string>();
byId.forEach((component) => {
const node = component as Record<string, unknown>;
const maybeChildren = node.children;
const maybeContent = node.content;
const maybeFooter = node.footer;
const maybeTabs = node.tabs;
const collectRefs = (entries: unknown) => {
if (!Array.isArray(entries)) {
return;
}
for (const entry of entries) {
if (typeof entry === 'string') {
referenced.add(entry);
}
}
};
collectRefs(maybeChildren);
collectRefs(maybeContent);
collectRefs(maybeFooter);
if (Array.isArray(maybeTabs)) {
for (const tab of maybeTabs) {
if (!isRecord(tab)) {
continue;
}
collectRefs(tab.content);
}
}
});
if (byId.has('root')) {
return [resolveNestedComponent(byId.get('root') as Component, byId, new Set())];
}
const rootCandidates = Array.from(byId.entries())
.filter(([id]) => !referenced.has(id))
.map(([, component]) => resolveNestedComponent(component, byId, new Set()));
if (rootCandidates.length > 0) {
return [...rootCandidates, ...withoutId];
}
const fallback = Array.from(byId.values()).map((component) =>
resolveNestedComponent(component, byId, new Set()),
);
return [...fallback, ...withoutId];
}
function extractSurfaceFromProtocolSnapshot(
surface: SharedProtocolSurfaceSnapshot,
): SurfaceSnapshot | null {
const dataModel = isRecord(surface.dataModel) ? surface.dataModel : {};
const legacyComponents = extractLegacyUiResponseComponents(dataModel);
const components = legacyComponents ?? buildRenderableComponents(surface.components);
if (components.length === 0) {
return null;
}
return {
protocol: surface.protocol,
source: surface.source,
surfaceId: surface.surfaceId || 'main',
components,
dataModel,
bridge: surface.bridge ?? null,
};
}
function extractSurfaceFromToolResponse(response: unknown): SurfaceSnapshot | null {
const surface = extractProtocolSurface(response);
if (!surface) {
return null;
}
const snapshot = extractSurfaceFromProtocolSnapshot(surface);
if (!snapshot) {
return null;
}
// Extract AWP HTML from the payload for iframe rendering
if (snapshot.protocol === 'awp' && isRecord(response)) {
const payload = isRecord(response) ? (pickField(response as Record<string, unknown>, 'payload') as Record<string, unknown> | undefined) : undefined;
const html = payload && typeof payload === 'object' ? (payload as Record<string, unknown>).html : undefined;
if (typeof html === 'string' && html.length > 0) {
snapshot.html = html;
}
}
return snapshot;
}
function normalizeAlertVariant(value: unknown): 'info' | 'success' | 'warning' | 'error' {
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
if (normalized === 'success') return 'success';
if (normalized === 'warning' || normalized === 'warn') return 'warning';
if (normalized === 'error' || normalized === 'danger') return 'error';
return 'info';
}
function normalizeButtonVariant(
value: unknown,
): 'primary' | 'secondary' | 'danger' | 'ghost' | 'outline' {
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
if (normalized === 'secondary') return 'secondary';
if (normalized === 'danger' || normalized === 'error' || normalized === 'destructive') {
return 'danger';
}
if (normalized === 'ghost') return 'ghost';
if (normalized === 'outline') return 'outline';
return 'primary';
}
function normalizeBadgeVariant(
value: unknown,
): 'default' | 'info' | 'success' | 'warning' | 'error' | 'secondary' | 'outline' {
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
if (normalized === 'operational' || normalized === 'ok' || normalized === 'success') {
return 'success';
}
if (normalized === 'degraded' || normalized === 'warning' || normalized === 'warn') {
return 'warning';
}
if (normalized === 'down' || normalized === 'error' || normalized === 'outage') {
return 'error';
}
if (normalized === 'info') {
return 'info';
}
return 'secondary';
}
function normalizeChartKind(value: unknown): 'bar' | 'line' | 'area' | 'pie' {
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
if (normalized === 'line') return 'line';
if (normalized === 'area') return 'area';
if (normalized === 'pie') return 'pie';
return 'bar';
}
function normalizeModalSize(value: unknown): 'small' | 'medium' | 'large' | 'full' {
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
if (normalized === 'small') return 'small';
if (normalized === 'large') return 'large';
if (normalized === 'full') return 'full';
return 'medium';
}
function stringifyDisplayValue(value: unknown): string {
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value);
}
if (value === null || value === undefined) {
return '';
}
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
function normalizeSurfaceIdFromResponse(response: Record<string, unknown>): string {
const surfaceId = pickField(response, 'surface_id', 'surfaceId');
return typeof surfaceId === 'string' && surfaceId.trim().length > 0 ? surfaceId : 'main';
}
function normalizeTableRows(value: unknown): Record<string, unknown>[] {
if (!Array.isArray(value)) {
return [];
}
return value.filter((entry): entry is Record<string, unknown> => isRecord(entry));
}
function normalizeTableColumns(
value: unknown,
rows: Record<string, unknown>[],
): Array<{ header: string; accessor_key: string }> {
if (Array.isArray(value)) {
const normalized = value
.filter((entry): entry is Record<string, unknown> => isRecord(entry))
.map((entry) => {
const accessor = pickField(entry, 'accessor_key', 'accessorKey', 'key');
if (typeof accessor !== 'string' || accessor.trim().length === 0) {
return null;
}
const header =
extractTextValue(pickField(entry, 'header', 'title', 'label')) || humanizeId(accessor);
return {
header,
accessor_key: accessor,
};
})
.filter(
(entry): entry is { header: string; accessor_key: string } => Boolean(entry),
);
if (normalized.length > 0) {
return normalized;
}
}
const sample = rows[0];
if (!sample) {
return [];
}
return Object.keys(sample).map((key) => ({
header: humanizeId(key),
accessor_key: key,
}));
}
function convertLayoutSection(section: Record<string, unknown>, index: number): Component | null {
const sectionTitle =
extractTextValue(pickField(section, 'title')) || `Section ${index + 1}`;
const sectionTypeRaw = pickField(section, 'type', 'section_type', 'sectionType');
const sectionType =
typeof sectionTypeRaw === 'string' ? sectionTypeRaw.trim().toLowerCase() : '';
if (sectionType === 'alert') {
return {
type: 'alert',
title: sectionTitle,
description:
extractTextValue(pickField(section, 'message', 'description')) || undefined,
variant: normalizeAlertVariant(pickField(section, 'severity', 'variant')),
};
}
if (sectionType === 'stats') {
const statsRaw = pickField(section, 'stats');
const stats = Array.isArray(statsRaw)
? statsRaw.filter((entry): entry is Record<string, unknown> => isRecord(entry))
: [];
const cards = stats.map((entry, statIndex) => {
const label =
extractTextValue(pickField(entry, 'label', 'title')) || `Stat ${statIndex + 1}`;
const value =
extractTextValue(pickField(entry, 'value')) || stringifyDisplayValue(pickField(entry, 'value'));
const statusRaw = pickField(entry, 'status');
const status =
typeof statusRaw === 'string' && statusRaw.trim().length > 0 ? statusRaw : null;
const content: Component[] = [
{ type: 'text', content: value, variant: 'h3' },
];
if (status) {
content.push({
type: 'badge',
label: humanizeId(status),
variant: normalizeBadgeVariant(status),
});
}
return {
type: 'card',
title: label,
content,
} satisfies Component;
});
return {
type: 'card',
title: sectionTitle,
content: [
{
type: 'grid',
columns: Math.min(Math.max(cards.length, 1), 3),
children: cards.length > 0 ? cards : [{ type: 'text', content: '(No stats)', variant: 'caption' }],
},
],
};
}
if (sectionType === 'text') {
return {
type: 'card',
title: sectionTitle,
content: [
{
type: 'text',
content:
extractTextValue(pickField(section, 'text', 'message', 'description')) || '(No content)',
variant: 'body',
},
],
};
}
if (sectionType === 'table') {
const rows = normalizeTableRows(pickField(section, 'rows', 'data'));
const columns = normalizeTableColumns(pickField(section, 'columns'), rows);
return {
type: 'card',
title: sectionTitle,
content: [
{
type: 'table',
columns,
data: rows,
},
],
};
}
if (sectionType === 'chart') {
const rows = normalizeTableRows(pickField(section, 'data'));
const xKeyRaw = pickField(section, 'x_key', 'xKey');
const xKey =
typeof xKeyRaw === 'string' && xKeyRaw.trim().length > 0
? xKeyRaw
: Object.keys(rows[0] ?? {})[0] ?? 'x';
const yKeysRaw = pickField(section, 'y_keys', 'yKeys');
const yKeys = Array.isArray(yKeysRaw)
? yKeysRaw.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
: [];
return {
type: 'card',
title: sectionTitle,
content: [
{
type: 'chart',
title: undefined,
kind: normalizeChartKind(pickField(section, 'chart_type', 'chartType', 'type')),
data: rows,
x_key: xKey,
y_keys: yKeys,
},
],
};
}
if (sectionType === 'key_value') {
const pairsRaw = pickField(section, 'pairs');
const pairs = Array.isArray(pairsRaw)
? pairsRaw
.filter((entry): entry is Record<string, unknown> => isRecord(entry))
.map((entry) => ({
key:
extractTextValue(pickField(entry, 'key', 'label')) || 'Key',
value:
extractTextValue(pickField(entry, 'value')) || stringifyDisplayValue(pickField(entry, 'value')),
}))
: [];
return {
type: 'card',
title: sectionTitle,
content: [
{
type: 'key_value',
pairs,
},
],
};
}
if (sectionType === 'list') {
const itemsRaw = pickField(section, 'items');
const items = Array.isArray(itemsRaw)
? itemsRaw.map((entry) => stringifyDisplayValue(entry)).filter((entry) => entry.trim().length > 0)
: [];
return {
type: 'card',
title: sectionTitle,
content: [
{
type: 'list',
items,
ordered: pickField(section, 'ordered') === true,
},
],
};
}
if (sectionType === 'code_block') {
return {
type: 'card',
title: sectionTitle,
content: [
{
type: 'code_block',
code: extractTextValue(pickField(section, 'code')) || '',
language:
typeof pickField(section, 'language') === 'string'
? (pickField(section, 'language') as string)
: undefined,
},
],
};
}
return {
type: 'card',
title: sectionTitle,
content: [
{
type: 'text',
content: `Unsupported section type: ${sectionType || 'unknown'}`,
variant: 'caption',
},
],
};
}
function convertRenderFormField(
field: Record<string, unknown>,
index: number,
): Component | null {
const typeRaw = pickField(field, 'type');
const type = typeof typeRaw === 'string' ? typeRaw.trim().toLowerCase() : '';
const label =
extractTextValue(pickField(field, 'label', 'title')) || `Field ${index + 1}`;
const nameRaw = pickField(field, 'name', 'id');
const name =
typeof nameRaw === 'string' && nameRaw.trim().length > 0 ? nameRaw : `field_${index + 1}`;
const required = pickField(field, 'required') === true;
if (
type === 'text' ||
type === 'email' ||
type === 'password' ||
type === 'tel' ||
type === 'url'
) {
return {
type: 'text_input',
name,
label,
input_type: type === 'text' ? 'text' : (type as 'email' | 'password' | 'tel' | 'url'),
placeholder:
extractTextValue(pickField(field, 'placeholder', 'hint')) || undefined,
required,
};
}
if (type === 'textarea') {
return {
type: 'textarea',
name,
label,
placeholder:
extractTextValue(pickField(field, 'placeholder', 'hint')) || undefined,
required,
rows:
typeof pickField(field, 'rows') === 'number'
? (pickField(field, 'rows') as number)
: undefined,
};
}
if (type === 'select') {
const optionsRaw = pickField(field, 'options');
const options = Array.isArray(optionsRaw)
? optionsRaw
.filter((entry): entry is Record<string, unknown> => isRecord(entry))
.map((entry) => {
const value = pickField(entry, 'value');
const labelValue = pickField(entry, 'label');
return {
value: typeof value === 'string' ? value : String(value ?? ''),
label:
extractTextValue(labelValue) ||
(typeof labelValue === 'string' ? labelValue : String(value ?? '')),
};
})
.filter((entry) => entry.value.trim().length > 0)
: [];
return {
type: 'select',
name,
label,
required,
options,
};
}
if (type === 'number') {
return {
type: 'number_input',
name,
label,
required,
min: typeof pickField(field, 'min') === 'number' ? (pickField(field, 'min') as number) : undefined,
max: typeof pickField(field, 'max') === 'number' ? (pickField(field, 'max') as number) : undefined,
step: typeof pickField(field, 'step') === 'number' ? (pickField(field, 'step') as number) : undefined,
};
}
if (type === 'date') {
return {
type: 'date_input',
name,
label,
required,
};
}
if (type === 'switch' || type === 'checkbox') {
return {
type: 'switch',
name,
label,
default_checked: pickField(field, 'default_checked', 'defaultChecked') === true,
};
}
if (type === 'slider') {
return {
type: 'slider',
name,
label,
min: typeof pickField(field, 'min') === 'number' ? (pickField(field, 'min') as number) : undefined,
max: typeof pickField(field, 'max') === 'number' ? (pickField(field, 'max') as number) : undefined,
step: typeof pickField(field, 'step') === 'number' ? (pickField(field, 'step') as number) : undefined,
};
}
return null;
}
function extractSurfaceFromRenderToolInvocation(
toolName: string,
response: unknown,
): SurfaceSnapshot | null {
if (!isRecord(response)) {
return null;
}
if (toolName === 'render_alert') {
const title = extractTextValue(pickField(response, 'title')) || 'Alert';
const description =
extractTextValue(pickField(response, 'description', 'message')) || undefined;
return {
surfaceId: typeof pickField(response, 'surface_id', 'surfaceId') === 'string'
? (pickField(response, 'surface_id', 'surfaceId') as string)
: 'main',
dataModel: {},
components: [
{
type: 'alert',
title,
description,
variant: normalizeAlertVariant(pickField(response, 'variant', 'severity')),
},
],
};
}
if (toolName === 'render_card') {
const actionsRaw = pickField(response, 'actions');
const footer = Array.isArray(actionsRaw)
? actionsRaw.reduce<Component[]>((items, entry) => {
if (!isRecord(entry)) {
return items;
}
const actionId = pickField(entry, 'action_id', 'actionId');
const label = pickField(entry, 'label', 'title');
if (typeof actionId !== 'string' || actionId.trim().length === 0) {
return items;
}
items.push({
type: 'button',
action_id: actionId,
label:
extractTextValue(label) ||
(typeof label === 'string' ? label : humanizeId(actionId)),
variant: normalizeButtonVariant(pickField(entry, 'variant')),
});
return items;
}, [])
: undefined;
return {
surfaceId: normalizeSurfaceIdFromResponse(response),
dataModel: {},
components: [
{
type: 'card',
title: extractTextValue(pickField(response, 'title')) || 'Card',
description:
extractTextValue(pickField(response, 'description', 'subtitle')) || undefined,
content: [
{
type: 'text',
content:
extractTextValue(pickField(response, 'content', 'message')) || '(No content)',
variant: 'body',
},
],
footer: footer && footer.length > 0 ? footer : undefined,
},
],
};
}
if (toolName === 'render_table') {
const rows = normalizeTableRows(pickField(response, 'data', 'rows'));
const columns = normalizeTableColumns(pickField(response, 'columns'), rows);
const tableComponent: Component = {
type: 'table',
columns,
data: rows,
};
const title = extractTextValue(pickField(response, 'title'));
return {
surfaceId: normalizeSurfaceIdFromResponse(response),
dataModel: {},
components: title
? [
{
type: 'card',
title,
content: [tableComponent],
},
]
: [tableComponent],
};
}
if (toolName === 'render_chart') {
const rows = normalizeTableRows(pickField(response, 'data'));
const xKeyRaw = pickField(response, 'x_key', 'xKey');
const xKey =
typeof xKeyRaw === 'string' && xKeyRaw.trim().length > 0
? xKeyRaw
: Object.keys(rows[0] ?? {})[0] ?? 'x';
const yKeysRaw = pickField(response, 'y_keys', 'yKeys');
const yKeys = Array.isArray(yKeysRaw)
? yKeysRaw.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
: [];
return {
surfaceId: normalizeSurfaceIdFromResponse(response),
dataModel: {},
components: [
{
type: 'chart',
title: extractTextValue(pickField(response, 'title')) || undefined,
kind: normalizeChartKind(pickField(response, 'type', 'chart_type', 'chartType')),
data: rows,
x_key: xKey,
y_keys: yKeys,
},
],
};
}
if (toolName === 'render_toast') {
return {
surfaceId: normalizeSurfaceIdFromResponse(response),
dataModel: {},
components: [
{
type: 'toast',
message:
extractTextValue(pickField(response, 'message', 'description')) || 'Notification',
variant: normalizeAlertVariant(pickField(response, 'variant', 'severity')),
duration:
typeof pickField(response, 'duration') === 'number'
? (pickField(response, 'duration') as number)
: undefined,
dismissible:
typeof pickField(response, 'dismissible') === 'boolean'
? (pickField(response, 'dismissible') as boolean)
: undefined,
},
],
};
}
if (toolName === 'render_confirm') {
const footer: Component[] = [];
const cancelActionRaw = pickField(response, 'cancel_action', 'cancelAction');
const cancelLabelRaw = pickField(response, 'cancel_label', 'cancelLabel');
if (typeof cancelActionRaw === 'string' && cancelActionRaw.trim().length > 0) {
footer.push({
type: 'button',
label:
extractTextValue(cancelLabelRaw) ||
(typeof cancelLabelRaw === 'string' ? cancelLabelRaw : '') ||
'Cancel',
action_id: cancelActionRaw,
variant: 'ghost',
});
}
const confirmActionRaw = pickField(response, 'confirm_action', 'confirmAction');
if (typeof confirmActionRaw === 'string' && confirmActionRaw.trim().length > 0) {
const confirmLabelRaw = pickField(response, 'confirm_label', 'confirmLabel');
footer.push({
type: 'button',
label:
extractTextValue(confirmLabelRaw) ||
(typeof confirmLabelRaw === 'string' ? confirmLabelRaw : '') ||
'Confirm',
action_id: confirmActionRaw,
variant: pickField(response, 'destructive') === true ? 'danger' : 'primary',
});
}
return {
surfaceId: normalizeSurfaceIdFromResponse(response),
dataModel: {},
components: [
{
type: 'modal',
title: extractTextValue(pickField(response, 'title')) || 'Confirm Action',
content: [
{
type: 'text',
content:
extractTextValue(pickField(response, 'message', 'description')) ||
'(No confirmation message provided)',
variant: 'body',
},
],
footer: footer.length > 0 ? footer : undefined,
size: 'small',
closable: true,
},
],
};
}
if (toolName === 'render_progress') {
const stepsRaw = pickField(response, 'steps');
const steps = Array.isArray(stepsRaw)
? stepsRaw
.filter((entry): entry is Record<string, unknown> => isRecord(entry))
.map((entry) => {
const label = extractTextValue(pickField(entry, 'label'));
if (!label) {
return null;
}
if (pickField(entry, 'completed') === true) {
return `Done: ${label}`;
}
if (pickField(entry, 'current') === true) {
return `Current: ${label}`;
}
return `Pending: ${label}`;
})
.filter((entry): entry is string => Boolean(entry))
: [];
const valueRaw = pickField(response, 'value');
const value =
typeof valueRaw === 'number' && Number.isFinite(valueRaw)
? Math.max(0, Math.min(100, valueRaw))
: 0;
const content: Component[] = [
{
type: 'progress',
value,
label: `${value}%`,
},
];
const description =
extractTextValue(pickField(response, 'description', 'message')) || undefined;
if (description) {
content.push({
type: 'text',
content: description,
variant: 'caption',
});
}
if (steps.length > 0) {
content.push({
type: 'list',
items: steps,
ordered: true,
});
}
return {
surfaceId: normalizeSurfaceIdFromResponse(response),
dataModel: {},
components: [
{
type: 'card',
title: extractTextValue(pickField(response, 'title')) || 'Progress',
content,
},
],
};
}
if (toolName === 'render_form') {
const fieldsRaw = pickField(response, 'fields');
const fields = Array.isArray(fieldsRaw)
? fieldsRaw
.filter((entry): entry is Record<string, unknown> => isRecord(entry))
.map((entry, index) => convertRenderFormField(entry, index))
.filter((entry): entry is Component => Boolean(entry))
: [];
const submitActionRaw = pickField(response, 'submit_action', 'submitAction');
const submitAction =
typeof submitActionRaw === 'string' && submitActionRaw.trim().length > 0
? submitActionRaw
: 'form_submit';
const submitLabelRaw = pickField(response, 'submit_label', 'submitLabel');
const submitLabel =
extractTextValue(submitLabelRaw) ||
(typeof submitLabelRaw === 'string' ? submitLabelRaw : '') ||
humanizeId(submitAction);
return {
surfaceId: typeof pickField(response, 'surface_id', 'surfaceId') === 'string'
? (pickField(response, 'surface_id', 'surfaceId') as string)
: 'main',
dataModel: {},
components: [
{
type: 'card',
title:
extractTextValue(pickField(response, 'title')) || 'Form',
description:
extractTextValue(pickField(response, 'description', 'subtitle')) || undefined,
content: [
...fields,
{
type: 'button',
label: submitLabel,
action_id: submitAction,
variant: 'primary',
},
],
},
],
};
}
if (toolName === 'render_modal') {
const footer: Component[] = [];
const cancelLabelRaw = pickField(response, 'cancel_label', 'cancelLabel');
if (typeof cancelLabelRaw === 'string' && cancelLabelRaw.trim().length > 0) {
footer.push({
type: 'button',
label: cancelLabelRaw,
action_id:
typeof pickField(response, 'cancel_action', 'cancelAction') === 'string'
? (pickField(response, 'cancel_action', 'cancelAction') as string)
: 'modal_cancel',
variant: 'secondary',
});
}
const confirmLabelRaw = pickField(response, 'confirm_label', 'confirmLabel');
if (typeof confirmLabelRaw === 'string' && confirmLabelRaw.trim().length > 0) {
footer.push({
type: 'button',
label: confirmLabelRaw,
action_id:
typeof pickField(response, 'confirm_action', 'confirmAction') === 'string'
? (pickField(response, 'confirm_action', 'confirmAction') as string)
: 'modal_confirm',
variant: 'primary',
});
}
return {
surfaceId: normalizeSurfaceIdFromResponse(response),
dataModel: {},
components: [
{
type: 'modal',
title: extractTextValue(pickField(response, 'title')) || 'Modal',
content: [
{
type: 'text',
content:
extractTextValue(pickField(response, 'message', 'description')) ||
'(No modal content)',
variant: 'body',
},
],
footer: footer.length > 0 ? footer : undefined,
size: normalizeModalSize(pickField(response, 'size')),
closable: pickField(response, 'closable') !== false,
},
],
};
}
if (toolName === 'render_layout') {
const sectionsRaw = pickField(response, 'sections');
const sections = Array.isArray(sectionsRaw)
? sectionsRaw
.filter((entry): entry is Record<string, unknown> => isRecord(entry))
.map((entry, index) => convertLayoutSection(entry, index))
.filter((entry): entry is Component => Boolean(entry))
: [];
const children: Component[] = [];
const title = extractTextValue(pickField(response, 'title'));
const description = extractTextValue(pickField(response, 'description'));
if (title) {
children.push({
type: 'text',
content: title,
variant: 'h2',
});
}
if (description) {
children.push({
type: 'text',
content: description,
variant: 'caption',
});
}
children.push(...sections);
return {
surfaceId: normalizeSurfaceIdFromResponse(response),
dataModel: {},
components: [
{
type: 'stack',
direction: 'vertical',
gap: 20,
children,
},
],
};
}
return null;
}
function extractEventText(event: Record<string, unknown>): string {
const eventType = typeof pickField(event, 'type') === 'string'
? (pickField(event, 'type') as string).trim().toUpperCase()
: '';
if (
eventType === 'TEXT_MESSAGE_CONTENT'
|| eventType === 'TEXT_MESSAGE_DELTA'
|| eventType === 'TEXT_MESSAGE_CHUNK'
) {
const delta = pickField(event, 'delta', 'content');
if (typeof delta === 'string') {
return delta;
}
}
const llmResponse = pickField(event, 'llm_response', 'llmResponse');
const nestedContent = isRecord(llmResponse) ? pickField(llmResponse, 'content') : undefined;
const content = pickField(event, 'content') ?? nestedContent;
if (!isRecord(content)) {
return '';
}
const parts = pickField(content, 'parts');
if (!Array.isArray(parts)) {
return '';
}
const snippets: string[] = [];
for (const part of parts) {
if (!isRecord(part)) {
continue;
}
const text = pickField(part, 'text');
if (typeof text === 'string') {
snippets.push(text);
}
}
return snippets.join(' ').trim();
}
type ToolResponseSource = 'call' | 'response';
type AgUiToolCallBuffer = {
toolCallName?: string;
delta: string;
};
type AgUiRuntimeContext = {
toolCallNames: Map<string, string>;
toolCallArgs: Map<string, AgUiToolCallBuffer>;
};
function normalizeEventType(event: Record<string, unknown>): string {
const type = pickField(event, 'type');
return typeof type === 'string' ? type.trim().toUpperCase() : '';
}
function resolveAgUiToolCallName(
event: Record<string, unknown>,
context?: AgUiRuntimeContext,
): string {
const directName = pickField(event, 'toolCallName', 'tool_call_name', 'toolName', 'tool_name', 'name');
if (typeof directName === 'string' && directName.trim().length > 0) {
return directName;
}
const toolCallId = pickField(event, 'toolCallId', 'tool_call_id');
if (typeof toolCallId === 'string' && context?.toolCallNames.has(toolCallId)) {
return context.toolCallNames.get(toolCallId) as string;
}
if (typeof toolCallId === 'string' && toolCallId.trim().length > 0) {
return toolCallId;
}
return 'tool_response';
}
function collectResponseSurfacesByTool(
toolResponses: Array<{ name: string; response: unknown; source: ToolResponseSource }>,
): Map<string, SurfaceSnapshot> {
const surfaces = new Map<string, SurfaceSnapshot>();
for (const toolResponse of toolResponses) {
if (toolResponse.source !== 'response') {
continue;
}
const surface = extractSurfaceFromToolResponse(toolResponse.response);
if (!surface) {
continue;
}
surfaces.set(toolResponse.name, surface);
}
return surfaces;
}
function decodeJsonPointer(path: string): string[] | null {
if (path === '') {
return [];
}
if (!path.startsWith('/')) {
return null;
}
return path
.slice(1)
.split('/')
.map((segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~'));
}
function cloneJsonValue<T>(value: T): T {
if (typeof structuredClone === 'function') {
return structuredClone(value);
}
return JSON.parse(JSON.stringify(value)) as T;
}
function getPatchParent(
root: unknown,
segments: string[],
): { parent: Record<string, unknown> | unknown[]; key: string } | null {
if (segments.length === 0) {
return null;
}
let current: unknown = root;
for (let index = 0; index < segments.length - 1; index += 1) {
const segment = segments[index];
if (Array.isArray(current)) {
const nextIndex = Number(segment);
if (!Number.isInteger(nextIndex) || nextIndex < 0 || nextIndex >= current.length) {
return null;
}
current = current[nextIndex];
continue;
}
if (!isRecord(current) || !(segment in current)) {
return null;
}
current = current[segment];
}
if (Array.isArray(current) || isRecord(current)) {
return { parent: current, key: segments[segments.length - 1] };
}
return null;
}
function getValueAtJsonPointer(root: unknown, path: string): unknown {
const segments = decodeJsonPointer(path);
if (segments === null) {
return undefined;
}
let current: unknown = root;
for (const segment of segments) {
if (Array.isArray(current)) {
const index = Number(segment);
if (!Number.isInteger(index) || index < 0 || index >= current.length) {
return undefined;
}
current = current[index];
continue;
}
if (!isRecord(current) || !(segment in current)) {
return undefined;
}
current = current[segment];
}
return current;
}
function applySingleJsonPatch(root: Record<string, unknown>, operation: JsonPatchOperation): void {
const op = typeof operation.op === 'string' ? operation.op : null;
const path = typeof operation.path === 'string' ? operation.path : null;
if (!op || path === null) {
return;
}
if (path === '') {
if (op === 'replace' || op === 'add') {
const value = isRecord(operation.value) ? operation.value : { value: operation.value ?? null };
for (const key of Object.keys(root)) {
delete root[key];
}
Object.assign(root, cloneJsonValue(value));
} else if (op === 'remove') {
for (const key of Object.keys(root)) {
delete root[key];
}
}
return;
}
const segments = decodeJsonPointer(path);
if (!segments) {
return;
}
const target = getPatchParent(root, segments);
if (!target) {
return;
}
if (op === 'move' || op === 'copy') {
if (typeof operation.from !== 'string') {
return;
}
const fromValue = getValueAtJsonPointer(root, operation.from);
if (fromValue === undefined) {
return;
}
const movedValue = cloneJsonValue(fromValue);
applySingleJsonPatch(root, { op: 'add', path, value: movedValue });
if (op === 'move') {
applySingleJsonPatch(root, { op: 'remove', path: operation.from });
}
return;
}
if (op === 'test') {
return;
}
if (Array.isArray(target.parent)) {
const array = target.parent;
const index = target.key === '-' ? array.length : Number(target.key);
if (!Number.isInteger(index) || index < 0 || index > array.length) {
return;
}
if (op === 'add') {
array.splice(index, 0, cloneJsonValue(operation.value));
return;
}
if (op === 'replace') {
if (index >= array.length) {
return;
}
array[index] = cloneJsonValue(operation.value);
return;
}
if (op === 'remove') {
if (index >= array.length) {
return;
}
array.splice(index, 1);
}
return;
}
if (op === 'add' || op === 'replace') {
target.parent[target.key] = cloneJsonValue(operation.value);
return;
}
if (op === 'remove') {
delete target.parent[target.key];
}
}
function applyActivityPatch(
content: Record<string, unknown>,
patch: JsonPatchOperation[],
): Record<string, unknown> {
const next = cloneJsonValue(content);
for (const operation of patch) {
applySingleJsonPatch(next, operation);
}
return next;
}
function extractAgUiActivityMessage(event: Record<string, unknown>): AgUiActivityMessage | null {
if (normalizeEventType(event) !== 'ACTIVITY_SNAPSHOT') {
return null;
}
const messageId = pickField(event, 'messageId', 'message_id', 'id');
if (typeof messageId !== 'string' || messageId.trim().length === 0) {
return null;
}
const activityType = pickField(event, 'activityType', 'activity_type', 'name');
if (typeof activityType !== 'string' || activityType.trim().length === 0) {
return null;
}
const contentRaw = pickField(event, 'content');
const content = isRecord(contentRaw)
? contentRaw
: { value: contentRaw ?? null };
const replaceRaw = pickField(event, 'replace');
return {
messageId,
activityType,
content,
replace: replaceRaw === false ? false : true,
};
}
function extractAgUiActivityDelta(
event: Record<string, unknown>,
): { messageId: string; activityType: string; patch: JsonPatchOperation[] } | null {
if (normalizeEventType(event) !== 'ACTIVITY_DELTA') {
return null;
}
const messageId = pickField(event, 'messageId', 'message_id', 'id');
const activityType = pickField(event, 'activityType', 'activity_type', 'name');
const patch = pickField(event, 'patch');
if (
typeof messageId !== 'string'
|| messageId.trim().length === 0
|| typeof activityType !== 'string'
|| activityType.trim().length === 0
|| !Array.isArray(patch)
) {
return null;
}
return {
messageId,
activityType,
patch: patch.filter(isRecord),
};
}
function upsertAgUiActivityMessage(
current: AgUiActivityMessage[],
next: AgUiActivityMessage,
): AgUiActivityMessage[] {
const existingIndex = current.findIndex((entry) => entry.messageId === next.messageId);
if (existingIndex === -1) {
return [...current, next];
}
if (!next.replace) {
return current;
}
const updated = current.slice();
updated[existingIndex] = next;
return updated;
}
function applyAgUiActivityDelta(
current: AgUiActivityMessage[],
delta: { messageId: string; activityType: string; patch: JsonPatchOperation[] },
): AgUiActivityMessage[] {
const existingIndex = current.findIndex((entry) => entry.messageId === delta.messageId);
const existing = existingIndex === -1
? {
messageId: delta.messageId,
activityType: delta.activityType,
content: {},
replace: true,
}
: current[existingIndex];
const updatedEntry: AgUiActivityMessage = {
...existing,
activityType: delta.activityType,
content: applyActivityPatch(existing.content, delta.patch),
};
if (existingIndex === -1) {
return [...current, updatedEntry];
}
const updated = current.slice();
updated[existingIndex] = updatedEntry;
return updated;
}
function extractAgUiToolCallDelta(event: Record<string, unknown>): string | undefined {
const delta = pickField(event, 'delta', 'args', 'arguments', 'payload', 'data');
if (typeof delta === 'string') {
return delta;
}
if (delta === undefined) {
return undefined;
}
try {
return JSON.stringify(delta);
} catch {
return String(delta);
}
}
function parseAgUiToolCallPayload(delta: string): unknown | undefined {
const parsed = parseMaybeJson(delta);
const trimmed = delta.trim();
if (typeof parsed === 'string' && (trimmed.startsWith('{') || trimmed.startsWith('['))) {
return undefined;
}
return parsed;
}
function extractToolResponses(
event: Record<string, unknown>,
agUiContext?: AgUiRuntimeContext,
): Array<{ name: string; response: unknown; source: ToolResponseSource }> {
const eventType = normalizeEventType(event);
if (eventType === 'TOOL_CALL_ARGS' || eventType === 'TOOL_CALL_CHUNK') {
const toolCallId = pickField(event, 'toolCallId', 'tool_call_id');
const delta = extractAgUiToolCallDelta(event);
const resolvedName = resolveAgUiToolCallName(event, agUiContext);
if (typeof toolCallId === 'string' && agUiContext) {
const existing = agUiContext.toolCallArgs.get(toolCallId) ?? { delta: '' };
if (resolvedName.trim().length > 0 && resolvedName !== 'tool_response') {
existing.toolCallName = resolvedName;
}
if (delta) {
existing.delta += delta;
}
agUiContext.toolCallArgs.set(toolCallId, existing);
const parsed = parseAgUiToolCallPayload(existing.delta);
if (parsed === undefined) {
return [];
}
return [
{
name: existing.toolCallName ?? resolvedName,
response: parsed,
source: 'call',
},
];
}
if (delta !== undefined) {
const parsed = parseAgUiToolCallPayload(delta);
if (parsed === undefined) {
return [];
}
return [
{
name: resolvedName,
response: parsed,
source: 'call',
},
];
}
}
if (eventType === 'TOOL_CALL_RESULT') {
const content = pickField(event, 'content', 'result', 'payload', 'data');
if (content !== undefined) {
return [
{
name: resolveAgUiToolCallName(event, agUiContext),
response: parseMaybeJson(content),
source: 'response',
},
];
}
}
const llmResponse = pickField(event, 'llm_response', 'llmResponse');
const nestedContent = isRecord(llmResponse) ? pickField(llmResponse, 'content') : undefined;
const content = pickField(event, 'content') ?? nestedContent;
if (!isRecord(content)) {
return [];
}
const parts = pickField(content, 'parts');
if (!Array.isArray(parts)) {
return [];
}
const responses: Array<{ name: string; response: unknown; source: ToolResponseSource }> = [];
for (const part of parts) {
if (!isRecord(part)) {
continue;
}
const functionCallWrapped = pickField(part, 'functionCall', 'function_call', 'toolCall', 'tool_call');
if (isRecord(functionCallWrapped)) {
const callName = pickField(functionCallWrapped, 'name', 'toolName', 'tool_name');
const callArgs = pickField(functionCallWrapped, 'args', 'arguments', 'parameters', 'input', 'payload');
if (typeof callName === 'string') {
responses.push({
name: callName,
response: parseMaybeJson(callArgs ?? functionCallWrapped),
source: 'call',
});
}
continue;
}
// adk-core FunctionCall part shape is untagged: { name, args, id? }
const directCallName = pickField(part, 'name');
const directCallArgs = pickField(part, 'args', 'arguments', 'parameters', 'input', 'payload');
if (typeof directCallName === 'string' && directCallArgs !== undefined) {
responses.push({
name: directCallName,
response: parseMaybeJson(directCallArgs),
source: 'call',
});
continue;
}
const functionResponse = pickField(part, 'functionResponse', 'function_response', 'toolResponse', 'tool_response');
if (!isRecord(functionResponse)) {
continue;
}
const name =
(typeof pickField(functionResponse, 'name', 'toolName', 'tool_name') === 'string'
? (pickField(functionResponse, 'name', 'toolName', 'tool_name') as string)
: 'tool_response') || 'tool_response';
responses.push({
name,
response:
parseMaybeJson(
pickField(functionResponse, 'response', 'result', 'output', 'payload', 'data') ??
functionResponse,
),
source: 'response',
});
}
// Also accept event-level tool response envelopes.
const eventToolResponse = pickField(event, 'tool_response', 'toolResponse');
if (isRecord(eventToolResponse)) {
const nameRaw = pickField(eventToolResponse, 'name', 'toolName', 'tool_name');
const name = typeof nameRaw === 'string' ? nameRaw : 'tool_response';
responses.push({
name,
response:
parseMaybeJson(
pickField(eventToolResponse, 'response', 'result', 'output', 'payload', 'data') ??
eventToolResponse,
),
source: 'response',
});
}
const eventToolResponses = pickField(event, 'tool_responses', 'toolResponses');
if (Array.isArray(eventToolResponses)) {
for (const item of eventToolResponses) {
if (!isRecord(item)) {
continue;
}
const nameRaw = pickField(item, 'name', 'toolName', 'tool_name');
const name = typeof nameRaw === 'string' ? nameRaw : 'tool_response';
responses.push({
name,
response: parseMaybeJson(pickField(item, 'response', 'result', 'output', 'payload', 'data') ?? item),
source: 'response',
});
}
}
return responses;
}
function extractEventError(event: Record<string, unknown>): string | null {
const eventType = normalizeEventType(event);
const candidates = [
(eventType === 'RUN_ERROR' || eventType === 'ERROR')
? pickField(event, 'message', 'error_message', 'errorMessage')
: undefined,
pickField(event, 'error_message', 'errorMessage'),
isRecord(pickField(event, 'llm_response', 'llmResponse'))
? pickField(pickField(event, 'llm_response', 'llmResponse') as Record<string, unknown>, 'error_message', 'errorMessage')
: undefined,
];
for (const candidate of candidates) {
if (typeof candidate === 'string' && candidate.trim().length > 0) {
return candidate.trim();
}
}
return null;
}
function extractArtifactDelta(event: Record<string, unknown>): Array<{ name: string; version: number }> {
const actions = pickField(event, 'actions');
if (!isRecord(actions)) {
return [];
}
const delta = pickField(actions, 'artifact_delta', 'artifactDelta');
if (!isRecord(delta)) {
return [];
}
const entries: Array<{ name: string; version: number }> = [];
for (const [name, value] of Object.entries(delta)) {
const numericVersion = typeof value === 'number' ? value : Number(value);
if (!Number.isFinite(numericVersion)) {
continue;
}
entries.push({ name, version: numericVersion });
}
return entries;
}
function toReadablePreview(raw: unknown, max = 10_000): string {
let text: string;
if (typeof raw === 'string') {
text = raw;
} else {
try {
text = JSON.stringify(raw);
} catch {
text = String(raw);
}
}
return text.length > max ? `${text.slice(0, max)}...` : text;
}
function toPrettyJson(raw: unknown): string {
try {
return JSON.stringify(raw, null, 2);
} catch {
return String(raw);
}
}
function trimForPanel(text: string, max = 200_000): string {
if (text.length <= max) {
return text;
}
return `${text.slice(0, max)}\n... (truncated)`;
}
function flattenSurfaceComponents(components: Component[]): Component[] {
const result: Component[] = [];
const stack: Component[] = [...components];
while (stack.length > 0) {
const node = stack.pop();
if (!node) {
continue;
}
result.push(node);
const asRecord = node as Record<string, unknown>;
const children = asRecord.children;
const content = asRecord.content;
const footer = asRecord.footer;
const tabs = asRecord.tabs;
if (Array.isArray(children)) {
for (const entry of children) {
if (isRecord(entry) && typeof pickField(entry as Record<string, unknown>, 'type') === 'string') {
stack.push(entry as Component);
}
}
}
if (Array.isArray(content)) {
for (const entry of content) {
if (isRecord(entry) && typeof pickField(entry as Record<string, unknown>, 'type') === 'string') {
stack.push(entry as Component);
}
}
}
if (Array.isArray(footer)) {
for (const entry of footer) {
if (isRecord(entry) && typeof pickField(entry as Record<string, unknown>, 'type') === 'string') {
stack.push(entry as Component);
}
}
}
if (Array.isArray(tabs)) {
for (const tab of tabs) {
if (!isRecord(tab) || !Array.isArray(tab.content)) {
continue;
}
for (const entry of tab.content) {
if (isRecord(entry) && typeof pickField(entry as Record<string, unknown>, 'type') === 'string') {
stack.push(entry as Component);
}
}
}
}
}
return result;
}
interface SurfaceQuality {
score: number;
lowFidelity: boolean;
placeholder: boolean;
}
function assessSurfaceQuality(surface: SurfaceSnapshot): SurfaceQuality {
if (surface.html) {
return { score: 100, lowFidelity: false, placeholder: false };
}
const flat = flattenSurfaceComponents(surface.components);
const weights: Partial<Record<Component['type'], number>> = {
table: 8,
chart: 8,
card: 5,
grid: 5,
tabs: 5,
modal: 4,
alert: 3,
key_value: 3,
list: 2,
text_input: 3,
number_input: 3,
select: 3,
multi_select: 3,
textarea: 3,
};
const typeSet = new Set(flat.map((component) => component.type));
const textValues = flat
.filter((component): component is Extract<Component, { type: 'text' }> => component.type === 'text')
.map((component) => component.content.trim())
.filter(Boolean);
const placeholder = flat.length <= 3
&& textValues.length > 0
&& textValues.every((value) => /^(rendered|generated|created)\s+(card|screen|surface|ui|layout|form|page)\.?$/i.test(value));
const emptyDataDisplay = flat.some((component) => (
(component.type === 'chart' && (component.data.length === 0 || component.y_keys.length === 0 || !component.x_key))
|| (component.type === 'table' && (component.columns.length === 0 || component.data.length === 0))
));
const textCount = flat.filter((component) => component.type === 'text').length;
const buttonCount = flat.filter((component) => component.type === 'button').length;
const inputCount = flat.filter((component) => (
component.type === 'text_input'
|| component.type === 'number_input'
|| component.type === 'select'
|| component.type === 'multi_select'
|| component.type === 'textarea'
)).length;
const formHasOwnContext = flat.some((component) => (
component.type === 'card'
&& typeof component.description === 'string'
&& component.description.trim().length >= 24
));
const formHasWorkspaceContext = inputCount > 0
&& typeSet.has('alert')
&& (typeSet.has('table') || typeSet.has('chart') || typeSet.has('key_value'));
const hasOversizedFormDescription = inputCount > 0 && flat.some((component) => (
component.type === 'card'
&& typeof component.description === 'string'
&& component.description.trim().length > 900
));
const hasGenericFormAction = inputCount > 0 && flat.some((component) => (
component.type === 'button'
&& /^(submit|send|continue|next)$/i.test(component.label.trim())
));
const textHeavy = flat.length > 0 && textCount / flat.length >= 0.55;
const lowVariety = typeSet.size <= 3;
const weightedScore = flat.reduce((score, component) => score + (weights[component.type] ?? 1), 0);
const score = weightedScore + typeSet.size * 2 + (inputCount > 0 && buttonCount > 0 ? 4 : 0);
const sparseForm = inputCount > 0
&& (!(formHasOwnContext || formHasWorkspaceContext)
|| hasGenericFormAction
|| hasOversizedFormDescription
|| score < 32);
const lowFidelity = flat.length === 0
|| placeholder
|| emptyDataDisplay
|| sparseForm
|| (flat.length <= 3 && inputCount === 0 && !flat.some((component) => weights[component.type]))
|| (flat.length >= 5 && textHeavy && lowVariety && buttonCount >= 1);
return { score, lowFidelity, placeholder };
}
function buildAssistantFallback(
lastRuntimeEvent: Record<string, unknown> | null,
lastToolResponse: unknown | null,
renderDetected: boolean,
): string | null {
if (!lastRuntimeEvent && !lastToolResponse) {
return null;
}
const lines: string[] = [];
lines.push('No assistant text was emitted for this run.');
if (!renderDetected) {
lines.push('No renderable UI surface was detected in streamed tool payloads.');
}
if (lastToolResponse) {
lines.push('');
lines.push('Last tool payload:');
lines.push(toPrettyJson(lastToolResponse));
}
if (lastRuntimeEvent) {
lines.push('');
lines.push('Last runtime event:');
lines.push(toPrettyJson(lastRuntimeEvent));
}
return trimForPanel(lines.join('\n'));
}
function App() {
const [selectedExample, setSelectedExample] = useState<ExampleTarget>(EXAMPLES[0]);
const [selectedProtocol, setSelectedProtocol] = useState<UiProtocol>('a2ui');
const [sessionId, setSessionId] = useState<string | null>(null);
const [surface, setSurface] = useState<SurfaceSnapshot | null>(null);
const [application, setApplication] = useState<UiApplication | null>(null);
const [promptInput, setPromptInput] = useState('');
const [assistantText, setAssistantText] = useState('');
const [activityMessages, setActivityMessages] = useState<AgUiActivityMessage[]>([]);
const [reasoningMessages, setReasoningMessages] = useState<ReasoningMessage[]>([]);
const [pendingA2uiActions, setPendingA2uiActions] = useState<PendingA2uiAction[]>([]);
const [lastActionResponse, setLastActionResponse] = useState<unknown>(null);
const [eventLog, setEventLog] = useState<StreamLogEvent[]>([]);
const [isStreaming, setIsStreaming] = useState(false);
const [isRetrying, setIsRetrying] = useState(false);
const [statusText, setStatusText] = useState('Idle');
const [runCount, setRunCount] = useState(0);
const [toolHitCount, setToolHitCount] = useState(0);
const [lastLatencyMs, setLastLatencyMs] = useState<number | null>(null);
const [errorText, setErrorText] = useState<string | null>(null);
const [capabilities, setCapabilities] = useState<ProtocolCapability[]>([]);
const [capabilitiesError, setCapabilitiesError] = useState<string | null>(null);
const [previewMode, setPreviewMode] = useState<'desktop' | 'mobile'>('desktop');
const [isCanvasExpanded, setIsCanvasExpanded] = useState(false);
const [isCanvasFullscreen, setIsCanvasFullscreen] = useState(false);
const [inspectorTab, setInspectorTab] = useState<'events' | 'output' | 'reasoning'>('events');
const canvasPanelRef = useRef<HTMLElement | null>(null);
const agUiContextRef = useRef<{
threadId?: string;
lastRunId?: string;
toolCallNames: Map<string, string>;
toolCallArgs: Map<string, AgUiToolCallBuffer>;
}>({
toolCallNames: new Map(),
toolCallArgs: new Map(),
});
const reasoningRuntimeRef = useRef(createReasoningRuntime());
const pendingA2uiActionsRef = useRef<Map<string, PendingA2uiAction>>(new Map());
const mcpAppsBridgeRef = useRef<McpAppsBridgeSessionState>({});
const selectedProtocolMeta = useMemo(
() => PROTOCOLS.find((protocol) => protocol.id === selectedProtocol) ?? PROTOCOLS[0],
[selectedProtocol],
);
const capabilityForSelectedProtocol = useMemo(() => {
return capabilities.find((capability) => normalizeProtocol(capability.protocol) === selectedProtocol);
}, [capabilities, selectedProtocol]);
const selectedCapabilityClaims = capabilityForSelectedProtocol?.capabilityClaims ?? [];
const supportedCapabilityCount = selectedCapabilityClaims.filter(
(claim) => claim.status === 'supported',
).length;
const baseUrl = `http://localhost:${selectedExample.port}`;
function appendEvent(kind: string, protocol: UiProtocol, payload: unknown, preview?: string) {
const entry: StreamLogEvent = {
id: nextStreamEventId(),
at: nowLabel(),
protocol,
kind,
preview: preview ?? toReadablePreview(payload),
raw: payload,
};
setEventLog((current) => [entry, ...current].slice(0, MAX_EVENT_LOG));
}
async function postBridgeEndpoint(
endpoint: string,
protocol: UiProtocol,
kind: string,
body: Record<string, unknown>,
preview: string,
): Promise<unknown> {
const response = await fetch(`${baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const responseText = await response.text();
let parsed: unknown = responseText;
if (responseText.trim().length > 0) {
try {
parsed = JSON.parse(responseText);
} catch {
parsed = responseText;
}
} else {
parsed = { ok: response.ok, status: response.status };
}
if (!response.ok) {
const message =
isRecord(parsed) && typeof pickField(parsed, 'message', 'error') === 'string'
? (pickField(parsed, 'message', 'error') as string)
: `Bridge request failed (${response.status}${response.statusText ? ` ${response.statusText}` : ''})`;
throw new Error(message);
}
appendEvent(kind, protocol, parsed, preview);
return parsed;
}
async function ensureMcpAppsBridgeInitialized(activeSessionId: string): Promise<boolean> {
if (mcpAppsBridgeRef.current.sessionId === activeSessionId) {
return false;
}
await postBridgeEndpoint(
'/api/ui/initialize',
'mcp_apps',
'mcp_apps_initialize',
buildMcpAppsInitializeBody(selectedExample.id, DEFAULT_USER_ID, activeSessionId),
'ui/initialize',
);
mcpAppsBridgeRef.current = { sessionId: activeSessionId };
return true;
}
function registerUiAction(action: UiEvent) {
void (async () => {
const activeSessionId = sessionId ?? makeSessionId();
if (!sessionId) {
setSessionId(activeSessionId);
}
const surfaceId = surface?.surfaceId ?? 'main';
const actionName = action.action === 'form_submit' || action.action === 'button_click'
? action.action_id
: action.action;
const sourceComponentId = action.action === 'form_submit' || action.action === 'button_click'
? action.source_component_id ?? action.action_id
: actionName;
// A2UI v1.0: confirm/submit clicks request a correlated actionResponse.
const wantsA2uiResponse =
selectedProtocol === 'a2ui'
&& (action.action === 'button_click' || action.action === 'form_submit');
const a2uiActionId = wantsA2uiResponse
? `action_${actionName}_${Date.now().toString(36)}`
: undefined;
const a2uiResponsePath = wantsA2uiResponse ? '/__a2ui/lastActionResponse' : undefined;
if (wantsA2uiResponse && a2uiActionId) {
const pending: PendingA2uiAction = {
actionId: a2uiActionId,
surfaceId,
name: actionName,
responsePath: a2uiResponsePath!,
};
pendingA2uiActionsRef.current.set(a2uiActionId, pending);
setPendingA2uiActions(Array.from(pendingA2uiActionsRef.current.values()));
}
const bridge = buildActionRequestBridge(selectedProtocol, action, {
appName: selectedExample.id,
userId: DEFAULT_USER_ID,
sessionId: activeSessionId,
surfaceId,
threadId: agUiContextRef.current.threadId,
parentRunId: agUiContextRef.current.lastRunId,
});
if (action.action === 'input_change') {
setSurface((current) => current
? {
...current,
dataModel: {
...current.dataModel,
__localFormValues: {
...(isRecord(current.dataModel.__localFormValues)
? current.dataModel.__localFormValues
: {}),
[action.name]: action.value,
},
},
}
: current);
appendEvent(
'input_change_local',
selectedProtocol,
{ surfaceId, name: action.name, value: action.value },
`Updated ${action.name} locally`,
);
// MCP Apps has an explicit model-context bridge for non-submitting edits.
// Other protocols send the complete form state only on submit.
if (selectedProtocol !== 'mcp_apps') {
setStatusText('Editing');
return;
}
}
if (wantsA2uiResponse && a2uiActionId) {
bridge.requestBody.a2uiAction = {
version: 'v1.0',
action: {
name: actionName,
surfaceId,
sourceComponentId,
timestamp: new Date().toISOString(),
context: action.action === 'form_submit' ? action.data : {},
wantResponse: true,
actionId: a2uiActionId,
},
};
bridge.requestBody.uiProtocol = 'a2ui';
}
if (surfaceId === 'feature-demo' && wantsA2uiResponse && a2uiActionId) {
const value = actionName === 'confirm_escalation'
? {
accepted: true,
ticketId: 'SUP-2048',
escalation: 'on_call_notified',
}
: {
accepted: false,
escalation: 'cancelled',
};
pendingA2uiActionsRef.current.delete(a2uiActionId);
setPendingA2uiActions(Array.from(pendingA2uiActionsRef.current.values()));
setLastActionResponse({
actionId: a2uiActionId,
responsePath: a2uiResponsePath,
value,
});
setSurface((current) => current
? {
...current,
dataModel: writeJsonPointer(current.dataModel ?? {}, a2uiResponsePath!, value),
}
: current);
setStatusText(actionName === 'confirm_escalation' ? 'Escalation confirmed' : 'Escalation cancelled');
setErrorText(null);
appendEvent(
'a2ui:action_response',
'a2ui',
{ actionId: a2uiActionId, actionResponse: { value } },
`Correlated local actionResponse for ${actionName}`,
);
return;
}
try {
if (selectedProtocol === 'mcp_apps') {
await ensureMcpAppsBridgeInitialized(activeSessionId);
if (bridge.bridgeRequest) {
await postBridgeEndpoint(
bridge.bridgeRequest.endpoint,
'mcp_apps',
bridge.bridgeRequest.kind,
bridge.bridgeRequest.body,
bridge.bridgeRequest.preview,
);
if (bridge.bridgeRequest.skipModelRun) {
setStatusText('MCP Apps model context updated');
setErrorText(null);
return;
}
}
}
try {
await runPrompt(bridge.textMessage, {
activeSessionId,
skipPromptAugmentation: true,
requestBody: bridge.requestBody,
allowRetry: false,
clearSurfaceOnStart: false,
requestEventKind: 'ui_action_request',
requestEventPayload: {
action,
outbound: bridge.outbound,
message: bridge.textMessage,
},
requestPreview: `${bridge.preview}: ${action.action}`,
initialStatusText: 'Submitting UI action...',
});
} catch (error) {
if (wantsA2uiResponse && a2uiActionId) {
pendingA2uiActionsRef.current.delete(a2uiActionId);
setPendingA2uiActions(Array.from(pendingA2uiActionsRef.current.values()));
}
throw error;
}
} catch (error) {
const endpoint = bridge.bridgeRequest?.endpoint ?? '/api/ui/message';
const message = normalizeRequestError(error, baseUrl, endpoint, selectedExample.port);
setStatusText('Failed');
setErrorText(message);
appendEvent('error', selectedProtocol, { message }, message);
}
})();
}
async function loadCapabilities() {
const endpoint = '/api/ui/capabilities';
try {
const response = await fetch(`${baseUrl}${endpoint}`);
if (!response.ok) {
throw new Error(`status ${response.status}${response.statusText ? ` ${response.statusText}` : ''}`);
}
const data = await response.json();
const list = Array.isArray(data.protocols) ? (data.protocols as ProtocolCapability[]) : [];
setCapabilities(list);
setCapabilitiesError(null);
appendEvent('capabilities', selectedProtocol, data, 'Loaded /api/ui/capabilities');
} catch (error) {
const message = normalizeRequestError(error, baseUrl, endpoint, selectedExample.port);
setCapabilities([]);
setCapabilitiesError(message);
}
}
async function runPrompt(
rawPrompt: string,
options?: {
retryAttempt?: number;
activeSessionId?: string;
retryMode?: 'no_surface' | 'quality';
skipPromptAugmentation?: boolean;
requestBody?: Record<string, unknown>;
requestHeaders?: Record<string, string>;
allowRetry?: boolean;
clearSurfaceOnStart?: boolean;
requestEventKind?: string;
requestEventPayload?: unknown;
requestPreview?: string;
initialStatusText?: string;
},
) {
const retryAttempt = options?.retryAttempt ?? 0;
const isAutoRetry = retryAttempt > 0;
const prompt = rawPrompt.trim();
if (!prompt || (isStreaming && !isAutoRetry)) {
return;
}
const activeSessionId = options?.activeSessionId ?? sessionId ?? makeSessionId();
if (!sessionId && !options?.activeSessionId) {
setSessionId(activeSessionId);
}
setIsStreaming(true);
setStatusText(options?.initialStatusText ?? (isAutoRetry ? 'Streaming UI (retrying)...' : 'Streaming...'));
if (!isAutoRetry) {
setIsRetrying(false);
}
setErrorText(null);
if (!isAutoRetry) {
setAssistantText('');
setActivityMessages([]);
setEventLog([]);
if (options?.clearSurfaceOnStart !== false) {
setSurface(null);
setApplication(null);
setReasoningMessages([]);
reasoningRuntimeRef.current = createReasoningRuntime();
setLastActionResponse(null);
}
setToolHitCount(0);
}
const instructedPrompt = options?.skipPromptAugmentation
? prompt
: `${prompt}\n\n${protocolInstruction(selectedProtocol)}${
isAutoRetry ? `\n\n${retryInstruction(selectedProtocol, prompt, options?.retryMode ?? 'no_surface')}` : ''
}`;
const fetchedArtifactVersions = new Set<string>();
let assistantTextCaptured = false;
let renderDetected = false;
let lowFidelityDetected = false;
let latestSurface: SurfaceSnapshot | null = null;
let latestSurfaceId = 'main';
let latestSurfaceQuality: SurfaceQuality | null = null;
let lastRuntimeEventSeen: Record<string, unknown> | null = null;
let lastToolResponseSeen: unknown | null = null;
const startedAt = readClockMs();
const acceptSurfaceCandidate = (candidate: SurfaceSnapshot, source: string): boolean => {
renderDetected = true;
const quality = assessSurfaceQuality(candidate);
const shouldAccept = latestSurface === null
|| latestSurfaceQuality === null
|| (!quality.placeholder && quality.score >= latestSurfaceQuality.score)
|| (!quality.lowFidelity && latestSurfaceQuality.lowFidelity);
if (!shouldAccept) {
appendEvent(
'surface_candidate_ignored',
selectedProtocol,
{ source, surfaceId: candidate.surfaceId, quality },
`Kept higher-quality surface; ignored ${source}`,
);
return false;
}
latestSurface = candidate;
latestSurfaceId = candidate.surfaceId;
latestSurfaceQuality = quality;
lowFidelityDetected = quality.lowFidelity;
setSurface(candidate);
setStatusText(`Rendered ${candidate.surfaceId}`);
return true;
};
const acceptApplicationCandidate = (candidate: unknown, source: string): boolean => {
const parsed = parseUiApplication(candidate);
if (!parsed) return false;
renderDetected = true;
lowFidelityDetected = false;
latestSurfaceId = `${parsed.id}:${parsed.initial_route}`;
setApplication(parsed);
setSurface(null);
setStatusText(`Rendered ${parsed.pages.length}-page application`);
appendEvent('application_rendered', selectedProtocol, candidate, `Accepted ${source}: ${parsed.pages.length} routes`);
return true;
};
const endpoint = '/api/run_sse';
const requestPayload: Record<string, unknown> = {
appName: selectedExample.id,
userId: DEFAULT_USER_ID,
sessionId: activeSessionId,
uiProtocol: selectedProtocol,
streaming: true,
...(options?.requestBody ?? {}),
};
const requestHeaders: Record<string, string> = {
'Content-Type': 'application/json',
...(options?.requestHeaders ?? {}),
};
const hasAgUiInput = requestPayload.input !== undefined || requestPayload.agUiInput !== undefined;
if (selectedProtocol === 'ag_ui') {
requestPayload.uiTransport = 'protocol_native';
requestHeaders['x-adk-ui-transport'] = 'protocol_native';
if (!hasAgUiInput) {
requestPayload.input = buildAgUiRuntimeInput(instructedPrompt, {
sessionId: activeSessionId,
surfaceId: surface?.surfaceId ?? 'main',
threadId: agUiContextRef.current.threadId,
parentRunId: agUiContextRef.current.lastRunId,
activityMessages: buildPromptContextActivityMessages(prompt, {
sessionId: activeSessionId,
exampleId: selectedExample.id,
retryAttempt,
protocol: selectedProtocol,
}),
forwardedProps: {
promptKind: isAutoRetry ? 'retry' : 'prompt',
retryAttempt,
},
});
}
}
if (
requestPayload.newMessage === undefined
&& requestPayload.input === undefined
&& requestPayload.agUiInput === undefined
) {
requestPayload.newMessage = {
role: 'user',
parts: [{ text: instructedPrompt }],
};
}
const requestEventPayload =
options?.requestEventPayload ??
{
appName: selectedExample.id,
sessionId: activeSessionId,
protocol: selectedProtocol,
prompt,
};
try {
if (selectedProtocol === 'mcp_apps') {
await ensureMcpAppsBridgeInitialized(activeSessionId);
}
const response = await fetch(`${baseUrl}${endpoint}`, {
method: 'POST',
headers: requestHeaders,
body: JSON.stringify(requestPayload),
});
if (!response.ok || !response.body) {
throw new Error(
`request failed (${response.status}${response.statusText ? ` ${response.statusText}` : ''})`,
);
}
appendEvent(
options?.requestEventKind ?? 'request',
selectedProtocol,
requestEventPayload,
options?.requestPreview,
);
const decoder = new TextDecoder();
const reader = response.body.getReader();
let buffer = '';
let receivedDoneSentinel = false;
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
buffer += decoder.decode(value, { stream: true });
const sseBatch = takeCompleteSseEvents(buffer);
buffer = sseBatch.rest;
for (const rawEvent of sseBatch.events) {
const payload = extractSseEventPayload(rawEvent);
if (!payload || payload === ':keep-alive') {
continue;
}
if (payload === '[DONE]') {
receivedDoneSentinel = true;
break;
}
let parsed: unknown;
try {
parsed = JSON.parse(payload);
} catch {
appendEvent('parse_error', selectedProtocol, payload, 'Could not parse SSE JSON payload');
continue;
}
let runtimeProtocol = selectedProtocol;
let runtimeEvent = parsed;
if (isRecord(parsed) && isRecord(parsed.event)) {
runtimeProtocol = normalizeProtocol(parsed.ui_protocol) ?? selectedProtocol;
runtimeEvent = parsed.event;
}
if (!isRecord(runtimeEvent)) {
continue;
}
const runtimeEventType = normalizeEventType(runtimeEvent);
const runtimeToolCallId = pickField(runtimeEvent, 'toolCallId', 'tool_call_id');
if (runtimeProtocol === 'ag_ui') {
const threadId = pickField(runtimeEvent, 'threadId', 'thread_id');
const runId = pickField(runtimeEvent, 'runId', 'run_id');
const toolCallName = pickField(runtimeEvent, 'toolCallName', 'tool_call_name', 'name');
if (typeof threadId === 'string') {
agUiContextRef.current.threadId = threadId;
}
if (typeof runId === 'string') {
agUiContextRef.current.lastRunId = runId;
}
if (runtimeEventType === 'RUN_STARTED') {
agUiContextRef.current.toolCallNames.clear();
agUiContextRef.current.toolCallArgs.clear();
setActivityMessages([]);
}
if (
typeof runtimeToolCallId === 'string'
&& typeof toolCallName === 'string'
&& toolCallName.trim().length > 0
) {
agUiContextRef.current.toolCallNames.set(runtimeToolCallId, toolCallName);
}
}
lastRuntimeEventSeen = runtimeEvent;
const runtimeError = extractEventError(runtimeEvent);
const text = extractEventText(runtimeEvent);
const activityMessage = extractAgUiActivityMessage(runtimeEvent);
const activityDelta = extractAgUiActivityDelta(runtimeEvent);
const toolResponses = extractToolResponses(runtimeEvent, agUiContextRef.current);
const artifactDeltas = extractArtifactDelta(runtimeEvent);
if (
runtimeProtocol === 'ag_ui'
&& typeof runtimeToolCallId === 'string'
&& (runtimeEventType === 'TOOL_CALL_END' || runtimeEventType === 'TOOL_CALL_RESULT')
) {
agUiContextRef.current.toolCallArgs.delete(runtimeToolCallId);
}
const turnCompleteRaw = pickField(runtimeEvent, 'turn_complete', 'turnComplete');
const isTerminalEvent =
turnCompleteRaw === true ||
typeof runtimeError === 'string';
if (isTerminalEvent && !runtimeError && !text && toolResponses.length === 0 && artifactDeltas.length === 0) {
appendEvent('turn_complete', runtimeProtocol, runtimeEvent, 'Turn complete');
}
if (runtimeError) {
assistantTextCaptured = true;
setErrorText(runtimeError);
setAssistantText((current) =>
trimForPanel(
`${current}${current ? '\n\n' : ''}[Runtime error]\n${runtimeError}`,
),
);
appendEvent('runtime_error', runtimeProtocol, runtimeEvent, runtimeError);
}
if (text) {
assistantTextCaptured = true;
setAssistantText((current) => `${current}${current ? '\n' : ''}${text}`);
}
if (activityMessage) {
setActivityMessages((current) => upsertAgUiActivityMessage(current, activityMessage));
appendEvent(
`activity:${activityMessage.activityType.toLowerCase()}`,
runtimeProtocol,
runtimeEvent,
toReadablePreview(activityMessage.content, 180),
);
}
if (activityDelta) {
setActivityMessages((current) => applyAgUiActivityDelta(current, activityDelta));
appendEvent(
`activity_delta:${activityDelta.activityType.toLowerCase()}`,
runtimeProtocol,
runtimeEvent,
toReadablePreview(activityDelta.patch, 180),
);
}
// AG-UI closes active reasoning when the next non-reasoning event arrives.
if (runtimeProtocol === 'ag_ui') {
const next = applyReasoningEvent(reasoningRuntimeRef.current, runtimeEvent);
setReasoningMessages(next);
if (
runtimeEventType.startsWith('REASONING_')
|| runtimeEventType.startsWith('THINKING_')
) {
appendEvent(
`reasoning:${runtimeEventType.toLowerCase()}`,
runtimeProtocol,
runtimeEvent,
toReadablePreview(pickField(runtimeEvent, 'delta', 'content') ?? runtimeEvent, 160),
);
}
}
// A2UI v1.0 actionResponse (direct envelope or nested in tool payload)
const actionResponseCandidates: unknown[] = [runtimeEvent];
if (isRecord(runtimeEvent)) {
const nested = pickField(runtimeEvent, 'payload', 'data', 'a2ui', 'message');
if (nested !== undefined) {
actionResponseCandidates.push(nested);
}
}
for (const candidate of actionResponseCandidates) {
if (!isRecord(candidate) || !isActionResponseMessage(candidate as never)) {
continue;
}
const actionId = String(pickField(candidate, 'actionId', 'action_id') ?? '');
const actionResponse = pickField(candidate, 'actionResponse', 'action_response');
if (!actionId || !isRecord(actionResponse)) {
continue;
}
const pending = pendingA2uiActionsRef.current.get(actionId);
if (!pending) {
appendEvent(
'a2ui:action_response_ignored',
runtimeProtocol,
candidate,
`ignored unmatched actionResponse for ${actionId}`,
);
continue;
}
const path = pending.responsePath;
const value = pickField(actionResponse, 'value');
const error = pickField(actionResponse, 'error');
const applied = error ?? value ?? actionResponse;
setLastActionResponse({
actionId,
responsePath: path,
value: applied,
});
pendingA2uiActionsRef.current.delete(actionId);
setPendingA2uiActions(Array.from(pendingA2uiActionsRef.current.values()));
setSurface((current) => {
if (!current) {
return current;
}
return {
...current,
dataModel: writeJsonPointer(current.dataModel ?? {}, path, applied),
};
});
appendEvent(
`a2ui:action_response`,
runtimeProtocol,
candidate,
error
? `actionResponse error for ${actionId}`
: `actionResponse for ${actionId}`,
);
}
const responseToolResponses = toolResponses.filter((entry) => entry.source === 'response');
const responseSurfaceByTool = collectResponseSurfacesByTool(toolResponses);
if (responseToolResponses.length > 0) {
setToolHitCount((count) => count + responseToolResponses.length);
}
for (const toolResponse of toolResponses) {
if (acceptApplicationCandidate(toolResponse.response, `${toolResponse.source} ${toolResponse.name}`)) {
if (toolResponse.source === 'response') {
lastToolResponseSeen = toolResponse.response;
}
continue;
}
if (toolResponse.source === 'call') {
appendEvent(`tool_call:${toolResponse.name}`, runtimeProtocol, toolResponse.response);
const responseSurface = responseSurfaceByTool.get(toolResponse.name);
if (responseSurface?.protocol === 'mcp_apps') {
continue;
}
// Tool call args for render_* tools contain the full component tree.
// Try to extract a surface from the call args as well, since the SSE
// stream may not include a separate functionResponse event.
const maybeSurface =
extractSurfaceFromToolResponse(toolResponse.response) ??
extractSurfaceFromRenderToolInvocation(toolResponse.name, toolResponse.response);
if (maybeSurface) {
acceptSurfaceCandidate(maybeSurface, `tool call ${toolResponse.name}`);
setToolHitCount((count) => count + 1);
}
continue;
}
lastToolResponseSeen = toolResponse.response;
appendEvent(`tool:${toolResponse.name}`, runtimeProtocol, toolResponse.response);
const maybeSurface =
extractSurfaceFromToolResponse(toolResponse.response) ??
extractSurfaceFromRenderToolInvocation(toolResponse.name, toolResponse.response);
if (maybeSurface) {
acceptSurfaceCandidate(maybeSurface, `tool response ${toolResponse.name}`);
}
}
if (runtimeProtocol === 'ag_ui' && toolResponses.length === 0) {
const eventSurface = extractSurfaceFromToolResponse({
protocol: 'ag_ui',
events: [runtimeEvent],
});
if (eventSurface) {
acceptSurfaceCandidate(eventSurface, 'AG-UI event');
}
}
for (const artifactDelta of artifactDeltas) {
const cacheKey = `${artifactDelta.name}@${artifactDelta.version}`;
if (fetchedArtifactVersions.has(cacheKey)) {
continue;
}
fetchedArtifactVersions.add(cacheKey);
try {
const artifactResponse = await fetch(
`${baseUrl}/api/sessions/${selectedExample.id}/user1/${activeSessionId}/artifacts/${encodeURIComponent(artifactDelta.name)}`,
);
if (!artifactResponse.ok) {
appendEvent(
`artifact:${artifactDelta.name}`,
runtimeProtocol,
{ status: artifactResponse.status, version: artifactDelta.version },
`artifact fetch failed (${artifactResponse.status})`,
);
continue;
}
const artifactBody = await artifactResponse.text();
appendEvent(
`artifact:${artifactDelta.name}`,
runtimeProtocol,
artifactBody,
`artifact version ${artifactDelta.version}`,
);
let rendered = false;
const extractedSurface = extractSurfaceFromToolResponse(artifactBody);
if (extractedSurface) {
acceptSurfaceCandidate(extractedSurface, `artifact ${artifactDelta.name}`);
rendered = true;
}
if (!rendered) {
try {
const parsedArtifact = JSON.parse(artifactBody);
const maybeSurface = extractSurfaceFromToolResponse(parsedArtifact);
if (maybeSurface) {
acceptSurfaceCandidate(maybeSurface, `artifact ${artifactDelta.name}`);
}
} catch {
// ignore non-JSON artifacts
}
}
} catch (error) {
appendEvent(
`artifact:${artifactDelta.name}`,
runtimeProtocol,
{ error: error instanceof Error ? error.message : 'artifact fetch failed' },
'artifact fetch error',
);
}
}
if (isTerminalEvent) {
// Some runtimes emit the model terminal event before trailing tool responses.
// Keep draining the stream until EOF or an explicit [DONE] sentinel so response-side
// render payloads can override speculative tool-call projection.
continue;
}
}
if (receivedDoneSentinel) {
try {
await reader.cancel();
} catch {
// ignore cancellation errors
}
break;
}
}
const shouldRetry = options?.allowRetry !== false && !renderDetected && !isAutoRetry;
if (shouldRetry) {
setIsRetrying(true);
setStatusText('Streaming UI (retrying)...');
appendEvent(
'retry',
selectedProtocol,
{ reason: 'No renderable UI surface detected in streamed tool payloads.', attempt: 2 },
'No renderable UI surface detected. Retrying once...',
);
return await runPrompt(rawPrompt, { retryAttempt: 1, activeSessionId, retryMode: 'no_surface' });
}
const shouldRetryQuality =
options?.allowRetry !== false && renderDetected && lowFidelityDetected && !isAutoRetry;
if (shouldRetryQuality) {
setIsRetrying(true);
setStatusText('Streaming UI (retrying quality)...');
appendEvent(
'retry',
selectedProtocol,
{
reason: 'Low-fidelity UI detected (mostly text/buttons without structured components).',
attempt: 2,
surfaceId: latestSurfaceId,
},
'Low-fidelity UI detected. Retrying once with stricter structure constraints...',
);
return await runPrompt(rawPrompt, { retryAttempt: 1, activeSessionId, retryMode: 'quality' });
}
if (!assistantTextCaptured) {
const fallback = buildAssistantFallback(
lastRuntimeEventSeen,
lastToolResponseSeen,
renderDetected,
);
if (fallback) {
setAssistantText(fallback);
}
}
setRunCount((value) => value + 1);
setStatusText('Completed');
setLastLatencyMs(Math.round(readClockMs() - startedAt));
} catch (error) {
const message = normalizeRequestError(error, baseUrl, endpoint, selectedExample.port);
setStatusText('Failed');
setErrorText(message);
appendEvent('error', selectedProtocol, { message }, message);
} finally {
setIsStreaming(false);
if (!isAutoRetry) {
setIsRetrying(false);
}
}
}
const resetForServerChange = useEffectEvent(() => {
agUiContextRef.current = { toolCallNames: new Map(), toolCallArgs: new Map() };
mcpAppsBridgeRef.current = {};
reasoningRuntimeRef.current = createReasoningRuntime();
pendingA2uiActionsRef.current = new Map();
setSessionId(null);
setSurface(null);
setApplication(null);
setAssistantText('');
setActivityMessages([]);
setReasoningMessages([]);
setPendingA2uiActions([]);
setLastActionResponse(null);
setErrorText(null);
setIsRetrying(false);
setStatusText('Idle');
setRunCount(0);
setToolHitCount(0);
setLastLatencyMs(null);
setEventLog([]);
void loadCapabilities();
});
useEffect(() => {
const timer = window.setTimeout(resetForServerChange, 0);
return () => window.clearTimeout(timer);
}, [baseUrl]);
useEffect(() => {
const syncFullscreenState = () => {
setIsCanvasFullscreen(document.fullscreenElement === canvasPanelRef.current);
};
const restoreExpandedCanvas = (event: KeyboardEvent) => {
if (event.key === 'Escape' && document.fullscreenElement === null) {
setIsCanvasExpanded(false);
}
};
document.addEventListener('fullscreenchange', syncFullscreenState);
window.addEventListener('keydown', restoreExpandedCanvas);
return () => {
document.removeEventListener('fullscreenchange', syncFullscreenState);
window.removeEventListener('keydown', restoreExpandedCanvas);
};
}, []);
async function toggleCanvasFullscreen() {
try {
if (document.fullscreenElement) {
await document.exitFullscreen();
return;
}
if (!canvasPanelRef.current?.requestFullscreen) {
throw new Error('This browser does not support the Fullscreen API.');
}
await canvasPanelRef.current.requestFullscreen();
} catch (error) {
const message = error instanceof Error ? error.message : 'Fullscreen could not be opened.';
setErrorText(message);
appendEvent('fullscreen_error', selectedProtocol, { message }, message);
}
}
/** Offline showcase for reasoning UI + A2UI actionResponse (no LLM required). */
function loadLocalFeatureDemo() {
setSelectedProtocol('a2ui');
setErrorText(null);
setInspectorTab('events');
pendingA2uiActionsRef.current = new Map();
setPendingA2uiActions([]);
setLastActionResponse(null);
setStatusText('Local feature demo loaded');
setApplication(null);
setAssistantText(
'Feature demo: support intake surface + sample reasoning stream.\nClick "Confirm escalation" to complete a correlated A2UI v1.0 actionResponse entirely in the browser.',
);
const runtime = createReasoningRuntime();
applyReasoningEvent(runtime, { type: 'REASONING_START', messageId: 'demo-r1' });
applyReasoningEvent(runtime, {
type: 'REASONING_MESSAGE_CONTENT',
messageId: 'demo-r1',
delta: 'User reported a production payroll failure with a same-day finance deadline.\n',
});
applyReasoningEvent(runtime, {
type: 'REASONING_MESSAGE_CONTENT',
messageId: 'demo-r1',
delta: 'I will collect severity/environment, then request explicit confirmation before escalation.\n',
});
const messages = applyReasoningEvent(runtime, {
type: 'REASONING_END',
messageId: 'demo-r1',
});
reasoningRuntimeRef.current = runtime;
setReasoningMessages(messages);
setActivityMessages([
{
messageId: 'demo-activity-1',
activityType: 'PLAN',
content: {
steps: ['Collect ticket details', 'Confirm escalation', 'Notify on-call'],
status: 'ready',
},
replace: true,
},
]);
setSurface({
protocol: 'a2ui',
source: 'a2ui_surface',
surfaceId: 'feature-demo',
dataModel: {
ticket: {
title: 'Payroll export failed',
severity: 'high',
environment: 'production',
},
},
components: [
{
type: 'stack',
direction: 'vertical',
gap: 12,
children: [
{
type: 'alert',
title: 'Feature demo surface',
description:
'A2UI confirm action requests actionResponse. Reasoning panel is populated above.',
variant: 'info',
},
{
type: 'card',
title: 'Support intake summary',
description: 'High severity · production · finance deadline today',
content: [
{
type: 'key_value',
pairs: [
{ key: 'Title', value: 'Payroll export failed' },
{ key: 'Severity', value: 'high' },
{ key: 'Environment', value: 'production' },
],
},
],
footer: [
{
type: 'button',
label: 'Confirm escalation',
action_id: 'confirm_escalation',
variant: 'primary',
},
{
type: 'button',
label: 'Cancel',
action_id: 'cancel_escalation',
variant: 'ghost',
},
],
},
],
},
],
});
appendEvent(
'local_feature_demo',
'a2ui',
{ reasoning: true, actionResponse: true },
'Loaded local reasoning + A2UI actionResponse demo',
);
}
function loadNextGenerationDemo() {
setApplication(NEXT_GENERATION_DEMO);
setSurface(null);
setErrorText(null);
setSelectedProtocol('a2ui');
setInspectorTab('events');
setStatusText('Three-page application loaded');
setAssistantText('Generated a complete transit operations product with preserved multi-page state and a declarative 3D network twin. Navigate between Briefing, Network, and Spatial twin inside the canvas.');
setActivityMessages([]);
setReasoningMessages([]);
setEventLog([]);
appendEvent(
'local_application_demo',
'a2ui',
{ applicationId: NEXT_GENERATION_DEMO.id, routes: NEXT_GENERATION_DEMO.pages.map((page) => page.route), scene3d: true },
'Loaded multi-page application + 3D scene',
);
}
const surfaceSourceLabel = surface?.source ? formatCapabilityToken(surface.source) : null;
const surfaceQuality = surface ? assessSurfaceQuality(surface) : null;
const surfaceDisplayMode =
typeof surface?.bridge?.hostContext?.displayMode === 'string'
? (surface.bridge.hostContext.displayMode as string)
: null;
const surfaceProtocolVersion =
typeof surface?.bridge?.protocolVersion === 'string'
? surface.bridge.protocolVersion
: null;
const surfaceHostLabel =
typeof surface?.bridge?.hostInfo?.title === 'string'
? (surface.bridge.hostInfo.title as string)
: typeof surface?.bridge?.hostInfo?.name === 'string'
? (surface.bridge.hostInfo.name as string)
: null;
const surfaceBridgeState =
surface?.bridge?.initialized === true
? 'initialized'
: null;
return (
<div id="top" className="v2-shell">
<div className="ambient-orb ambient-orb-a" />
<div className="ambient-orb ambient-orb-b" />
<div className="ambient-grid" />
<nav className="topbar fade-in-rise" aria-label="Primary navigation">
<a className="brand" href="#top" aria-label="ADK UI home">
<span className="brand-mark" aria-hidden="true"><i /><i /><i /></span>
<span>ADK UI</span>
</a>
<span className="release-pill"><i /> Version 2.1</span>
<div className="topbar-links">
<a href="#studio">Studio</a>
<a href="#showcase">Showcase</a>
<a href="#whats-new">What's new</a>
<a href="https://github.com/zavora-ai/adk-ui" target="_blank" rel="noreferrer">
GitHub <span aria-hidden="true">↗</span>
</a>
</div>
</nav>
<header className="v2-hero fade-in-rise">
<div className="hero-copy">
<div className="release-kicker">
<span>ADK-Rust 2.0 native</span>
<span className="kicker-divider" />
<span>Five UI protocols</span>
</div>
<h1>One agent.<br /><em>Every interface.</em></h1>
<p>
Build interactive agent experiences once, then deliver them through A2UI, AG-UI,
MCP Apps, AWP, or the classic ADK renderer. V2 makes the protocol visible, testable,
and ready for real user actions.
</p>
<div className="hero-actions">
<button type="button" className="primary-cta" onClick={loadNextGenerationDemo} data-testid="load-feature-demo">
Explore a generated app <span aria-hidden="true">→</span>
</button>
<a className="secondary-cta" href="https://github.com/zavora-ai/adk-ui#quick-start" target="_blank" rel="noreferrer">
Read the quick start
</a>
</div>
</div>
<div className="hero-proof" aria-label="Version 2 highlights">
<div className="proof-orbit" aria-hidden="true">
<span className="orbit-core">UI</span>
<span className="orbit-node orbit-a">A2</span>
<span className="orbit-node orbit-b">AG</span>
<span className="orbit-node orbit-c">MCP</span>
<span className="orbit-node orbit-d">AWP</span>
</div>
<div className="proof-caption">
<span className="live-dot" />
Protocol-native surface delivery
</div>
</div>
</header>
<section className="protocol-section fade-in-rise delay-1" aria-labelledby="protocol-heading">
<div className="section-heading-row">
<div>
<p className="section-kicker">Protocol matrix</p>
<h2 id="protocol-heading">Choose how your UI travels</h2>
</div>
<p>Switch profiles without rebuilding the agent.</p>
</div>
<div className="protocol-switcher" role="group" aria-label="UI protocol profiles">
{PROTOCOLS.map((protocol) => (
<button
key={protocol.id}
type="button"
className={`protocol-card ${selectedProtocol === protocol.id ? 'is-selected' : ''}`}
aria-pressed={selectedProtocol === protocol.id}
onClick={() => setSelectedProtocol(protocol.id)}
>
<span className="protocol-mark">{protocol.mark}</span>
<span className="protocol-card-copy">
<strong>{protocol.label}</strong>
<small>{protocol.status}</small>
</span>
<span className="protocol-check" aria-hidden="true">{selectedProtocol === protocol.id ? '✓' : '→'}</span>
</button>
))}
</div>
<div className="protocol-signal">
<div className="protocol-signal-topline">
<div className="signal-summary">
<span className={`signal-light ${capabilityForSelectedProtocol && capabilityForSelectedProtocol.available !== false ? 'is-online' : ''}`} />
<div>
<span className="signal-eyebrow">Live capability contract</span>
<strong>{selectedProtocolMeta.label}</strong>
<p>{capabilityForSelectedProtocol?.summary ?? selectedProtocolMeta.hint}</p>
</div>
</div>
<div className="capability-score" aria-label={`${supportedCapabilityCount} supported capabilities`}>
<strong>{supportedCapabilityCount}<span>/{selectedCapabilityClaims.length || '—'}</span></strong>
<small>native claims</small>
</div>
</div>
<div className="capability-contract-grid">
<div className="capability-version-rail" aria-label="Protocol versions">
{(capabilityForSelectedProtocol?.versionDetails ?? []).map((version) => (
<a
href={version.specificationUrl}
target="_blank"
rel="noreferrer"
className={`version-contract version-${version.status}`}
key={`${version.version}-${version.specTrack}`}
>
<span>{version.version}</span>
<strong>{formatCapabilityToken(version.status)}</strong>
<small>{formatCapabilityToken(version.specTrack)}</small>
</a>
))}
</div>
{capabilityForSelectedProtocol?.negotiation ? (
<div className="negotiation-contract">
<span>Discovery</span>
<strong>{formatCapabilityToken(capabilityForSelectedProtocol.negotiation.mode)}</strong>
<p>{capabilityForSelectedProtocol.negotiation.transports.map(formatCapabilityToken).join(' · ')}</p>
<code>{capabilityForSelectedProtocol.negotiation.capabilityKeys[0]}</code>
</div>
) : null}
<div className="capability-claims" aria-label="Capability support claims">
{selectedCapabilityClaims.map((claim) => (
<div
className={`capability-claim claim-${claim.status}`}
key={`${claim.scope}-${claim.id}`}
title={claim.note}
>
<span>{formatCapabilityToken(claim.scope)}</span>
<strong>{formatCapabilityToken(claim.id)}</strong>
<small>{formatCapabilityToken(claim.status)}</small>
</div>
))}
</div>
</div>
<div className="capability-contract-footer">
<div className="chip-row">
{capabilityForSelectedProtocol?.implementationTier ? (
<span className={`chip chip-${capabilityForSelectedProtocol.implementationTier}`}>
{formatCapabilityToken(capabilityForSelectedProtocol.implementationTier)}
</span>
) : null}
{capabilityForSelectedProtocol?.specTrack ? (
<span className={`chip chip-${capabilityForSelectedProtocol.specTrack}`}>
{formatCapabilityToken(capabilityForSelectedProtocol.specTrack)}
</span>
) : null}
</div>
{capabilityForSelectedProtocol?.reviewedOn ? (
<span>Reviewed {capabilityForSelectedProtocol.reviewedOn}</span>
) : null}
</div>
</div>
</section>
<section id="studio" className="studio-shell fade-in-rise delay-2" aria-labelledby="studio-heading">
<div className="studio-topline">
<div className="studio-title">
<span className="window-dots" aria-hidden="true"><i /><i /><i /></span>
<div>
<p className="section-kicker">Interactive workspace</p>
<h2 id="studio-heading">Agent UI Studio</h2>
</div>
</div>
<div className="studio-metrics" aria-label="Run metrics">
<span><small>Runs</small><strong>{runCount}</strong></span>
<span><small>Tools</small><strong>{toolHitCount}</strong></span>
<span><small>Latency</small><strong>{lastLatencyMs !== null ? `${lastLatencyMs}ms` : '—'}</strong></span>
<span className={isStreaming ? 'is-live' : ''}><small>Status</small><strong>{statusText}</strong></span>
</div>
</div>
<div className="workspace-bar">
<label className="workspace-select" htmlFor="example-select">
<span>Demo agent</span>
<select
id="example-select"
value={selectedExample.id}
onChange={(event) => {
const next = EXAMPLES.find((example) => example.id === event.target.value);
if (next) setSelectedExample(next);
}}
>
{EXAMPLES.map((example) => (
<option key={example.id} value={example.id}>{example.name}</option>
))}
</select>
</label>
<div className="workspace-description">
<span className="workspace-port">:{selectedExample.port}</span>
<p>{selectedExample.description}</p>
</div>
<button type="button" className="refresh-button" onClick={() => void loadCapabilities()}>
Refresh capabilities
</button>
</div>
{capabilitiesError ? (
<div className="offline-notice" role="status">
<span className="offline-icon" aria-hidden="true">⌁</span>
<div>
<strong>Server offline? The studio still works.</strong>
<p>Explore a complete local surface, reasoning stream, and correlated action without an API key.</p>
</div>
<button type="button" onClick={loadLocalFeatureDemo}>Launch offline demo</button>
</div>
) : null}
<div className={`studio-grid${isCanvasExpanded ? ' is-canvas-expanded' : ''}`}>
<aside className="launcher-panel" aria-labelledby="launcher-heading">
<div className="panel-heading">
<span className="step-number">01</span>
<div>
<p className="section-kicker">Give it a goal</p>
<h3 id="launcher-heading">Launch a scenario</h3>
</div>
</div>
<div className="scenario-list">
{selectedExample.prompts.map((prompt, index) => (
<button
key={prompt}
type="button"
className="scenario-card"
onClick={() => void runPrompt(prompt)}
disabled={isStreaming}
>
<span className="scenario-index">0{index + 1}</span>
<span>{prompt}</span>
<i aria-hidden="true">↗</i>
</button>
))}
</div>
<div className="composer-block">
<div className="composer-label-row">
<label htmlFor="prompt-input">Or describe your own UI</label>
<span>{promptInput.length}/1200</span>
</div>
<textarea
id="prompt-input"
value={promptInput}
maxLength={1200}
onChange={(event) => setPromptInput(event.target.value)}
placeholder="Example: Build an incident dashboard with a guarded failover action..."
rows={5}
disabled={isStreaming}
/>
<button
type="button"
className="run-button"
disabled={isStreaming || promptInput.trim().length === 0}
onClick={() => {
const value = promptInput;
setPromptInput('');
void runPrompt(value);
}}
>
<span>{isRetrying ? 'Retrying' : isStreaming ? 'Building interface' : 'Build interface'}</span>
<span aria-hidden="true">{isStreaming ? '•••' : '→'}</span>
</button>
</div>
</aside>
<main ref={canvasPanelRef} className="canvas-panel" aria-labelledby="canvas-heading">
<div className="canvas-toolbar">
<div className="panel-heading compact">
<span className="step-number">02</span>
<div>
<p className="section-kicker">See the result</p>
<h3 id="canvas-heading">Live surface</h3>
</div>
</div>
<div className="canvas-controls">
<div className="preview-toggle" aria-label="Preview size">
<button type="button" aria-pressed={previewMode === 'desktop'} onClick={() => setPreviewMode('desktop')}>
<span aria-hidden="true">▱</span> Desktop
</button>
<button type="button" aria-pressed={previewMode === 'mobile'} onClick={() => setPreviewMode('mobile')}>
<span aria-hidden="true">▯</span> Mobile
</button>
</div>
<div className="canvas-actions" aria-label="Canvas display">
<button
type="button"
aria-pressed={isCanvasExpanded}
onClick={() => setIsCanvasExpanded((current) => !current)}
>
{isCanvasExpanded ? 'Restore' : 'Maximize'}
</button>
<button
type="button"
aria-pressed={isCanvasFullscreen}
disabled={!document.fullscreenEnabled}
onClick={() => void toggleCanvasFullscreen()}
>
{isCanvasFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
</button>
</div>
</div>
</div>
<div className="canvas-stage">
<div className={`preview-viewport preview-${previewMode}`}>
<div className="preview-chrome">
<span><i /><i /><i /></span>
<strong>{application ? application.name : surface ? surface.surfaceId : `${selectedProtocol}.surface`}</strong>
<em>{application ? `${application.pages.length} routes / 3D ready` : surfaceQuality ? `quality ${Math.min(100, surfaceQuality.score)}` : 'ready'}</em>
</div>
{application || surfaceSourceLabel || surfaceQuality ? (
<div className="surface-meta-strip">
{application ? <span>application graph</span> : null}
{surfaceSourceLabel ? <span>{surfaceSourceLabel}</span> : null}
{surfaceQuality ? (
<span className={surfaceQuality.lowFidelity ? 'quality-low' : 'quality-good'}>
{surfaceQuality.lowFidelity ? 'needs refinement' : 'quality checked'}
</span>
) : null}
{surfaceDisplayMode ? <span>{surfaceDisplayMode}</span> : null}
{surfaceProtocolVersion ? <span>{surfaceProtocolVersion}</span> : null}
{surfaceHostLabel ? <span>{surfaceHostLabel}</span> : null}
{surfaceBridgeState ? <span>{surfaceBridgeState}</span> : null}
</div>
) : null}
{application ? (
<div className="render-application" data-application={application.id}>
<ApplicationRenderer
application={application}
kits={[STUDIO_UI_KIT]}
fallbackKit={STUDIO_UI_KIT}
onAction={registerUiAction}
onNavigate={(route) => appendEvent('application_navigate', selectedProtocol, { route }, `Navigate to ${route}`)}
/>
</div>
) : surface ? (
<div className="render-surface" data-surface={surface.surfaceId}>
{surface.html ? (
<iframe
srcDoc={surface.html}
title="AWP rendered surface"
sandbox="allow-scripts"
/>
) : (
<AdkUiKitProvider kit={STUDIO_UI_KIT}>
{surface.components.map((component, index) => (
<UiRenderer
key={`${component.id ?? 'component'}-${index}`}
component={component}
onAction={registerUiAction}
/>
))}
</AdkUiKitProvider>
)}
</div>
) : (
<div className="empty-state">
<div className="empty-illustration" aria-hidden="true">
<span className="empty-card card-one" />
<span className="empty-card card-two" />
<span className="empty-spark">✦</span>
</div>
{isStreaming ? (
<>
<h4 className="streaming-title" aria-live="polite">
{isRetrying ? 'Refining your surface' : 'Building your surface'}
<span className="streaming-dots" aria-hidden="true"><span>.</span><span>.</span><span>.</span></span>
</h4>
<p>Streaming {selectedProtocolMeta.label} events into the canvas.</p>
</>
) : (
<>
<h4>Your agent's interface appears here</h4>
<p>Choose a scenario or explore a complete generated product with three routes and 3D.</p>
<button type="button" onClick={loadNextGenerationDemo}>Load next-generation app</button>
</>
)}
</div>
)}
</div>
</div>
</main>
<aside className="inspector-panel" aria-labelledby="inspector-heading">
<div className="panel-heading inspector-heading">
<span className="step-number">03</span>
<div>
<p className="section-kicker">Understand the run</p>
<h3 id="inspector-heading">Inspector</h3>
</div>
<span className="event-count">{eventLog.length}</span>
</div>
<div className="inspector-tabs" role="tablist" aria-label="Run inspector">
{(['events', 'output', 'reasoning'] as const).map((tab) => (
<button
key={tab}
type="button"
role="tab"
aria-selected={inspectorTab === tab}
onClick={() => setInspectorTab(tab)}
>
{tab === 'events' ? 'Events' : tab === 'output' ? 'Output' : 'Reasoning'}
</button>
))}
</div>
<div className="inspector-content">
{inspectorTab === 'events' ? (
<>
{errorText ? <div className="error-banner"><strong>Run failed</strong><span>{errorText}</span></div> : null}
<div className="event-list">
{eventLog.length === 0 ? (
<div className="inspector-empty">
<span aria-hidden="true">{'{ }'}</span>
<strong>Waiting for events</strong>
<p>Protocol frames will appear here as the agent runs.</p>
</div>
) : (
eventLog.map((entry) => (
<details key={entry.id} className="event-item">
<summary>
<div><span className="event-kind">{entry.kind}</span><span className="event-preview">{entry.preview}</span></div>
<div className="event-meta"><span>{entry.protocol}</span><span>{entry.at}</span></div>
</summary>
<pre>{JSON.stringify(entry.raw, null, 2)}</pre>
</details>
))
)}
</div>
</>
) : null}
{inspectorTab === 'output' ? (
<div className="output-stack">
<div className="assistant-output">
<h4>Assistant output</h4>
<pre>{assistantText || 'No text output captured yet.'}</pre>
</div>
{pendingA2uiActions.length > 0 || lastActionResponse !== null ? (
<div className="a2ui-action-output">
<h4>A2UI actionResponse</h4>
{pendingA2uiActions.length > 0 ? <p>Waiting for {pendingA2uiActions.map((item) => item.actionId).join(', ')}</p> : null}
{lastActionResponse !== null ? <pre>{JSON.stringify(lastActionResponse, null, 2)}</pre> : null}
</div>
) : null}
{selectedProtocol === 'ag_ui' ? (
<div className="activity-output">
<h4>AG-UI activity</h4>
{activityMessages.length === 0 ? <p className="activity-empty">No activity snapshots yet.</p> : (
<div className="activity-list">
{activityMessages.map((activity) => (
<article key={activity.messageId} className="activity-item">
<div className="activity-item-header"><span>{activity.activityType}</span><small>{activity.messageId}</small></div>
<pre>{JSON.stringify(activity.content, null, 2)}</pre>
</article>
))}
</div>
)}
</div>
) : null}
</div>
) : null}
{inspectorTab === 'reasoning' ? (
<ReasoningPanel messages={reasoningMessages} className="reasoning-output" title="Agent reasoning" />
) : null}
</div>
</aside>
</div>
</section>
<ShowcaseGallery />
<section id="whats-new" className="whats-new-section fade-in-rise" aria-labelledby="whats-new-heading">
<div className="whats-new-intro">
<p className="section-kicker">What changed in v2</p>
<h2 id="whats-new-heading">More than a compatibility update.</h2>
<p>V2 turns protocol support into a workflow developers can see, test, and trust.</p>
</div>
<div className="upgrade-grid">
<article>
<span className="upgrade-number">01</span>
<div className="upgrade-icon">⌁</div>
<h3>Protocol-native streams</h3>
<p>Inspect A2UI actions, AG-UI reasoning, and MCP Apps bridge state without translating everything into one opaque format.</p>
</article>
<article>
<span className="upgrade-number">02</span>
<div className="upgrade-icon">✓</div>
<h3>Safer user actions</h3>
<p>Correlated action responses, real component identity, and no fabricated approval states for high-impact interactions.</p>
</article>
<article>
<span className="upgrade-number">03</span>
<div className="upgrade-icon">✦</div>
<h3>A studio, not a test page</h3>
<p>Scenario-led demos, responsive previews, zero-setup exploration, and diagnostics that stay available without taking over the canvas.</p>
</article>
</div>
</section>
<footer className="site-footer">
<div><span className="brand-mark small" aria-hidden="true"><i /><i /><i /></span><strong>ADK UI v2</strong></div>
<p>Open source agent interfaces for the ADK-Rust ecosystem.</p>
<a href="https://github.com/zavora-ai/adk-ui" target="_blank" rel="noreferrer">Apache 2.0 · GitHub ↗</a>
</footer>
</div>
);
}
export default App;