Files
Claude-Code-Workflow/ccw/frontend/src/packages/a2ui-runtime/README.md
catlog22 345437415f Add end-to-end tests for workspace switching and backend tests for ask_question tool
- Implemented E2E tests for workspace switching functionality, covering scenarios such as switching workspaces, data isolation, language preference maintenance, and UI updates.
- Added tests to ensure workspace data is cleared on logout and handles unsaved changes during workspace switches.
- Created comprehensive backend tests for the ask_question tool, validating question creation, execution, answer handling, cancellation, and timeout scenarios.
- Included edge case tests to ensure robustness against duplicate questions and invalid answers.
2026-01-31 16:02:20 +08:00

11 KiB

A2UI Runtime

A lightweight, framework-agnostic runtime for rendering A2UI (AI-to-UI) surfaces in React applications.

Overview

A2UI Runtime enables AI agents to generate and update interactive UI components dynamically through a structured protocol. Based on Google's A2UI specification, this runtime provides:

  • Dynamic Surface Rendering: Create and update UI components in real-time
  • Component Registry: Extensible system for custom component renderers
  • Type Safety: Full TypeScript support with Zod validation
  • State Management: Built-in state binding and action handling
  • Protocol Validation: Parse and validate A2UI surface updates

Installation

Located at src/packages/a2ui-runtime/, this runtime is included as part of the CCW frontend codebase.

Core Components

1. A2UIParser

Parse and validate A2UI surface updates from JSON.

import { a2uiParser } from '@/packages/a2ui-runtime/core';

// Parse JSON string
const surfaceUpdate = a2uiParser.parse(jsonString);

// Validate object
const isValid = a2uiParser.validate(surfaceUpdate);

// Safe parse with result
const result = a2uiParser.safeParse(jsonString);
if (result.success) {
  console.log(result.data);
}

2. A2UIComponentRegistry

Register and manage component renderers.

import { a2uiRegistry } from '@/packages/a2ui-runtime/core';
import { CustomRenderer } from './renderers/CustomRenderer';

// Register a component
a2uiRegistry.register('CustomComponent', CustomRenderer);

// Check if registered
if (a2uiRegistry.has('CustomComponent')) {
  const renderer = a2uiRegistry.get('CustomComponent');
}

// Get all registered types
const types = a2uiRegistry.getRegisteredTypes();

3. A2UIRenderer

Render A2UI surfaces as React components.

import { A2UIRenderer } from '@/packages/a2ui-runtime/renderer';

function MyComponent() {
  const [surface, setSurface] = useState<SurfaceUpdate | null>(null);

  const handleAction = async (actionId: string, params: Record<string, unknown>) => {
    console.log(`Action: ${actionId}`, params);
    // Send action back to AI agent
  };

  return surface ? (
    <A2UIRenderer
      surface={surface}
      onAction={handleAction}
      className="my-surface"
    />
  ) : null;
}

Built-in Components

Standard Components

Component Props Description
Text text, usageHint Text with semantic hint (h1-h6, p, span, code, small)
Button onClick, content, variant, disabled Button with variants (primary, secondary, destructive, ghost, outline)
Dropdown options, selectedValue, onChange, placeholder Select dropdown with options
TextField value, onChange, placeholder, type Text input (text, email, password, number, url)
TextArea value, onChange, placeholder, rows Multi-line text input
Checkbox checked, onChange, label Checkbox with label
Progress value, max Progress bar
Card title, description, content Container with title and nested content

Custom Components

Component Props Description
CLIOutput output, language, streaming, maxLines CLI output with syntax highlighting
DateTimeInput value, onChange, placeholder, includeTime Date/time picker input

A2UI Protocol

Surface Update Structure

interface SurfaceUpdate {
  surfaceId: string;
  components: SurfaceComponent[];
  initialState?: Record<string, unknown>;
}

interface SurfaceComponent {
  id: string;
  component: A2UIComponent;
}

Component Types

All components use a discriminated union structure:

// Text Component
{
  Text: {
    text: { literalString: "Hello" } | { path: "state.key" };
    usageHint?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "span" | "code" | "small";
  };
}

// Button Component
{
  Button: {
    onClick: { actionId: "submit", parameters?: {} };
    content: A2UIComponent; // Nested component (usually Text)
    variant?: "primary" | "secondary" | "destructive" | "ghost" | "outline";
    disabled?: { literalBoolean: true } | { path: "state.disabled" };
  };
}

Content Bindings

Values can be literal or bound to state:

// Literal string
{ literalString: "Hello, World!" }

// Bound to state
{ path: "user.name" }

Actions

Actions are triggered by user interactions:

{
  actionId: "save-form",
  parameters: {
    formId: "contact-form",
    validate: true
  }
}

Creating Custom Components

1. Define the Renderer

import type { ComponentRenderer } from '@/packages/a2ui-runtime/core/A2UIComponentRegistry';
import type { A2UIComponent } from '@/packages/a2ui-runtime/core/A2UITypes';

interface CustomComponentConfig {
  title: { literalString: string } | { path: string };
  items: Array<{ label: string; value: string }>;
  onSelect: { actionId: string; parameters?: Record<string, unknown> };
}

export const CustomRenderer: ComponentRenderer = ({ 
  component, 
  state, 
  onAction, 
  resolveBinding 
}) => {
  const customComp = component as { Custom: CustomComponentConfig };
  const { Custom: config } = customComp;

  const handleSelect = (value: string) => {
    onAction(config.onSelect.actionId, {
      ...config.onSelect.parameters,
      selectedValue: value,
    });
  };

  return (
    <div className="custom-component">
      {/* Your rendering logic */}
    </div>
  );
};

2. Register the Component

import { a2uiRegistry } from '@/packages/a2ui-runtime/core/A2UIComponentRegistry';
import { CustomRenderer } from './CustomRenderer';

a2uiRegistry.register('Custom', CustomRenderer);

3. Add Type Definition (Optional)

For type safety, extend the A2UI types:

// In A2UITypes.ts
export const CustomComponentSchema = z.object({
  Custom: z.object({
    title: TextContentSchema,
    items: z.array(z.object({
      label: z.string(),
      value: z.string(),
    })),
    onSelect: ActionSchema,
  }),
});

// Add to ComponentSchema union
export const ComponentSchema = z.union([
  // ... existing components
  CustomComponentSchema,
]);

// Add to A2UIComponentType
export type A2UIComponentType =
  | 'Text' | 'Button' | // ... existing types
  | 'Custom';

Usage Examples

Rendering a Simple Form

const formSurface: SurfaceUpdate = {
  surfaceId: 'contact-form',
  components: [
    {
      id: 'title',
      component: {
        Text: {
          text: { literalString: 'Contact Form' },
          usageHint: 'h2',
        },
      },
    },
    {
      id: 'name',
      component: {
        TextField: {
          value: { literalString: '' },
          onChange: { actionId: 'update-name', parameters: { field: 'name' } },
          placeholder: 'Your Name',
          type: 'text',
        },
      },
    },
    {
      id: 'email',
      component: {
        TextField: {
          value: { literalString: '' },
          onChange: { actionId: 'update-email', parameters: { field: 'email' } },
          placeholder: 'your@email.com',
          type: 'email',
        },
      },
    },
    {
      id: 'submit',
      component: {
        Button: {
          onClick: { actionId: 'submit-form' },
          content: { Text: { text: { literalString: 'Submit' } } },
          variant: 'primary',
        },
      },
    },
  ],
  initialState: {
    name: '',
    email: '',
  },
};

function ContactForm() {
  const [surface] = useState(formSurface);

  const handleAction = async (actionId: string, params: any) => {
    if (actionId === 'submit-form') {
      // Submit logic
    } else {
      // Update local state
      console.log(`${actionId}:`, params);
    }
  };

  return <A2UIRenderer surface={surface} onAction={handleAction} />;
}

Using CLIOutput Component

const cliOutputSurface: SurfaceUpdate = {
  surfaceId: 'build-output',
  components: [
    {
      id: 'output',
      component: {
        CLIOutput: {
          output: { literalString: '$ npm run build\nBuilding...\nDone!' },
          language: 'bash',
          streaming: false,
        },
      },
    },
  ],
};

Using DateTimeInput Component

const datetimeSurface: SurfaceUpdate = {
  surfaceId: 'appointment-picker',
  components: [
    {
      id: 'datetime',
      component: {
        DateTimeInput: {
          value: { literalString: '2024-01-15T10:30:00Z' },
          onChange: { actionId: 'update-date' },
          placeholder: 'Select appointment time',
          includeTime: true,
        },
      },
    },
  ],
};

State Management

Binding Resolution

State is resolved through the resolveBinding function:

// In your component renderer
const value = resolveBinding({ path: 'user.name' });

State Updates

Send state updates through actions:

const handleAction = (actionId: string, params: any) => {
  // Update local state
  setLocalState(prev => ({ ...prev, ...params }));
  
  // Send action to backend
  sendA2UIAction(actionId, surfaceId, params);
};

API Reference

A2UIParser

Method Parameters Returns Description
parse() json: string SurfaceUpdate Parse and validate JSON string
parseObject() data: unknown SurfaceUpdate Validate object
validate() value: unknown boolean Type guard check
safeParse() json: string SafeParseResult Parse without throwing
safeParseObject() data: unknown SafeParseResult Validate without throwing
validateComponent() component: unknown boolean Check if valid component

A2UIComponentRegistry

Method Parameters Returns Description
register() type, renderer void Register component renderer
unregister() type void Remove component renderer
get() type ComponentRenderer | undefined Get renderer
has() type boolean Check if registered
getRegisteredTypes() - A2UIComponentType[] List all types
clear() - void Remove all renderers
size - number Count of renderers

Error Handling

A2UIParseError

Custom error class for parsing failures:

import { A2UIParseError } from '@/packages/a2ui-runtime/core/A2UIParser';

try {
  a2uiParser.parse(invalidJson);
} catch (error) {
  if (error instanceof A2UIParseError) {
    console.error(error.message);
    console.error(error.getDetails()); // Detailed Zod errors
    console.error(error.originalError);
  }
}

Best Practices

  1. Always validate surfaces before rendering:

    if (a2uiParser.validate(surfaceUpdate)) {
      return <A2UIRenderer surface={surfaceUpdate} onAction={handleAction} />;
    }
    
  2. Handle unknown components gracefully:

    const renderer = a2uiRegistry.get(componentType);
    if (!renderer) {
      return <FallbackComponent />;
    }
    
  3. Use TypeScript types for type safety:

    import type { SurfaceUpdate, A2UIComponent } from '@/packages/a2ui-runtime/core/A2UITypes';
    
  4. Clean up resources when surfaces are removed:

    useEffect(() => {
      return () => {
        // Cleanup timers, subscriptions, etc.
      };
    }, [surfaceId]);
    

License

Part of the CCW project.