- 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.
16 KiB
A2UI Protocol Usage and Troubleshooting Guide
This guide provides comprehensive information about the A2UI protocol for AI agent developers using CCW.
Table of Contents
- Protocol Overview
- Surface Update Structure
- Component Reference
- Action Handling
- State Management
- Code Examples
- Troubleshooting
- Best Practices
Protocol Overview
A2UI (AI-to-UI) is a protocol that enables AI agents to generate dynamic user interfaces through structured JSON messages. The protocol defines:
- Surface Updates: Complete UI descriptions that can be rendered
- Components: Reusable UI building blocks
- Actions: User interaction handlers
- State: Data binding and state management
Message Flow
AI Agent ──(generates)──▶ A2UI Surface Update ──(WebSocket)──▶ Frontend
│
▼
A2UI Parser
│
▼
Component Registry
│
▼
Rendered UI
│
▼
User Interaction
│
▼
Action Event ──(WebSocket)──▶ Backend
│
▼
AI Agent
Surface Update Structure
Basic Structure
{
"surfaceId": "unique-surface-identifier",
"components": [
{
"id": "component-1",
"component": {
"ComponentType": {
// Component-specific properties
}
}
}
],
"initialState": {
"key": "value",
"nested": {
"data": true
}
}
}
Properties
| Property | Type | Required | Description |
|---|---|---|---|
surfaceId |
string | Yes | Unique identifier for this surface |
components |
SurfaceComponent[] | Yes | Array of component definitions |
initialState |
Record<string, unknown> | No | Initial state for bindings |
Component Reference
Content Types
Literal String
Direct text value:
{
"literalString": "Hello, World!"
}
Binding
Reference to state value:
{
"path": "user.name"
}
Standard Components
Text
Display text with semantic hints.
{
"Text": {
"text": { "literalString": "Hello" },
"usageHint": "h1"
}
}
| Property | Type | Required | Values |
|---|---|---|---|
text |
Content | Yes | Literal or binding |
usageHint |
string | No | h1, h2, h3, h4, h5, h6, p, span, code, small |
Button
Clickable button with nested content.
{
"Button": {
"onClick": { "actionId": "submit", "parameters": { "formId": "login" } },
"content": {
"Text": { "text": { "literalString": "Submit" } }
},
"variant": "primary",
"disabled": { "literalBoolean": false }
}
}
| Property | Type | Required | Values |
|---|---|---|---|
onClick |
Action | Yes | Action definition |
content |
Component | Yes | Nested component (usually Text) |
variant |
string | No | primary, secondary, destructive, ghost, outline |
disabled |
BooleanContent | No | Literal or binding |
Dropdown
Select dropdown with options.
{
"Dropdown": {
"options": [
{ "label": { "literalString": "Option 1" }, "value": "opt1" },
{ "label": { "literalString": "Option 2" }, "value": "opt2" }
],
"selectedValue": { "literalString": "opt1" },
"onChange": { "actionId": "select-change" },
"placeholder": "Select an option"
}
}
TextField
Single-line text input.
{
"TextField": {
"value": { "literalString": "Initial value" },
"onChange": { "actionId": "input-change", "parameters": { "field": "username" } },
"placeholder": "Enter username",
"type": "text"
}
}
| Property | Type | Required | Values |
|---|---|---|---|
type |
string | No | text, email, password, number, url |
TextArea
Multi-line text input.
{
"TextArea": {
"onChange": { "actionId": "textarea-change" },
"placeholder": "Enter description",
"rows": 5
}
}
Checkbox
Boolean checkbox with label.
{
"Checkbox": {
"checked": { "literalBoolean": true },
"onChange": { "actionId": "checkbox-change" },
"label": { "literalString": "Accept terms" }
}
}
Progress
Progress bar indicator.
{
"Progress": {
"value": { "literalNumber": 75 },
"max": 100
}
}
Card
Container with title and nested content.
{
"Card": {
"title": { "literalString": "Card Title" },
"description": { "literalString": "Card description" },
"content": [
{
"id": "text-1",
"component": {
"Text": { "text": { "literalString": "Card content" } }
}
}
]
}
}
Custom Components
CLIOutput
Terminal-style output with syntax highlighting.
{
"CLIOutput": {
"output": { "literalString": "$ npm install\nInstalling...\nDone!" },
"language": "bash",
"streaming": false,
"maxLines": 100
}
}
| Property | Type | Required | Description |
|---|---|---|---|
output |
Content | Yes | Text to display |
language |
string | No | bash, javascript, python, etc. |
streaming |
boolean | No | Show streaming indicator |
maxLines |
number | No | Limit output lines |
DateTimeInput
Date and time picker.
{
"DateTimeInput": {
"value": { "literalString": "2024-01-15T10:30:00Z" },
"onChange": { "actionId": "datetime-change" },
"placeholder": "Select date and time",
"includeTime": true,
"minDate": { "literalString": "2024-01-01T00:00:00Z" },
"maxDate": { "literalString": "2024-12-31T23:59:59Z" }
}
}
| Property | Type | Required | Description |
|---|---|---|---|
includeTime |
boolean | No | Include time (default: true) |
minDate |
Content | No | Minimum selectable date |
maxDate |
Content | No | Maximum selectable date |
Action Handling
Action Structure
{
"actionId": "unique-action-id",
"parameters": {
"key1": "value1",
"key2": 42
}
}
Action Flow
- User interacts with component (click, type, select)
- Component triggers
onActioncallback - Action sent via WebSocket to backend
- Backend processes action and responds
Action Response
Backend can respond with:
- State Update: Update component state
- New Surface: Replace or add components
- Close Surface: Dismiss notification/dialog
State Management
State Binding
Components can bind to state values:
{
"TextField": {
"value": { "path": "form.username" },
"onChange": { "actionId": "update-field" }
}
}
State Update
Backend sends state updates:
{
"type": "a2ui-state-update",
"surfaceId": "form-surface",
"updates": {
"form": {
"username": "newvalue",
"email": "updated@example.com"
}
}
}
Code Examples
Example 1: Simple Form
{
"surfaceId": "login-form",
"components": [
{
"id": "title",
"component": {
"Text": {
"text": { "literalString": "Login" },
"usageHint": "h2"
}
}
},
{
"id": "username",
"component": {
"TextField": {
"onChange": { "actionId": "field-change", "parameters": { "field": "username" } },
"placeholder": "Username",
"type": "text"
}
}
},
{
"id": "password",
"component": {
"TextField": {
"onChange": { "actionId": "field-change", "parameters": { "field": "password" } },
"placeholder": "Password",
"type": "password"
}
}
},
{
"id": "submit",
"component": {
"Button": {
"onClick": { "actionId": "login" },
"content": {
"Text": { "text": { "literalString": "Login" } }
},
"variant": "primary"
}
}
}
],
"initialState": {
"username": "",
"password": ""
}
}
Example 2: Data Display with Actions
{
"surfaceId": "user-list",
"components": [
{
"id": "title",
"component": {
"Text": {
"text": { "literalString": "Users" },
"usageHint": "h3"
}
}
},
{
"id": "user-card",
"component": {
"Card": {
"title": { "path": "users.0.name" },
"description": { "path": "users.0.email" },
"content": [
{
"id": "edit-btn",
"component": {
"Button": {
"onClick": { "actionId": "edit-user", "parameters": { "userId": "1" } },
"content": { "Text": { "text": { "literalString": "Edit" } } },
"variant": "secondary"
}
}
}
]
}
}
}
],
"initialState": {
"users": [
{ "id": 1, "name": "Alice", "email": "alice@example.com" },
{ "id": 2, "name": "Bob", "email": "bob@example.com" }
]
}
}
Example 3: CLI Output with Progress
{
"surfaceId": "build-progress",
"components": [
{
"id": "title",
"component": {
"Text": {
"text": { "literalString": "Building Project" },
"usageHint": "h3"
}
}
},
{
"id": "progress",
"component": {
"Progress": {
"value": { "path": "build.progress" },
"max": 100
}
}
},
{
"id": "output",
"component": {
"CLIOutput": {
"output": { "path": "build.output" },
"language": "bash",
"streaming": { "path": "build.running" }
}
}
}
],
"initialState": {
"build": {
"progress": 45,
"output": "$ npm run build\nBuilding module 1/3...",
"running": true
}
}
}
Troubleshooting
Common Errors
Parse Errors
Error: A2UI validation failed
Causes:
- Invalid JSON structure
- Missing required fields
- Invalid component type
- Wrong data type for property
Solution:
// Use safeParse to get detailed errors
const result = a2uiParser.safeParse(jsonString);
if (!result.success) {
console.error('Validation errors:', result.error.errors);
}
Unknown Component Type
Error: Unknown component type: XYZ
Causes:
- Component not registered in registry
- Typo in component type name
- Custom component not exported
Solution:
// Check registered components
console.log(a2uiRegistry.getRegisteredTypes());
// Should include: ['Text', 'Button', 'XYZ', ...]
// Register custom component
a2uiRegistry.register('XYZ', XYZRenderer);
State Binding Failures
Error: State not updating or showing undefined
Causes:
- Wrong binding path
- State not initialized
- Case-sensitive path mismatch
Solution:
// Ensure state exists in initialState
initialState: {
user: {
name: "Alice"
}
}
// Use correct binding path
{ "path": "user.name" } // ✓
{ "path": "User.name" } // ✗ (case mismatch)
Debugging Tips
Enable Verbose Logging
// In development
if (process.env.NODE_ENV === 'development') {
(window as any).A2UI_DEBUG = true;
}
Inspect Component Props
// Add logging in renderer
export const A2UICustom: ComponentRenderer = (props) => {
console.log('[A2UICustom] Props:', props);
// ... rest of implementation
};
Validate Before Sending
// Backend: Validate before sending
const result = a2uiParser.safeParseObject(surfaceUpdate);
if (!result.success) {
console.error('Invalid surface:', result.error);
return;
}
// Safe to send
ws.send(JSON.stringify(surfaceUpdate));
Performance Issues
Too Many Re-renders
Symptoms: UI lagging, high CPU usage
Solutions:
- Memoize expensive components
- Debounce rapid actions
- Limit component count per surface
import { memo } from 'react';
export const ExpensiveComponent = memo<A2UIComponentType>(({
component, state, onAction, resolveBinding
}) => {
// Component implementation
});
Large Output in CLIOutput
Symptoms: Page freeze with large CLI output
Solutions:
{
"CLIOutput": {
"output": { "path": "output" },
"maxLines": 1000
}
}
Best Practices
1. Component IDs
Use unique, descriptive IDs:
{
"id": "user-form-username",
"component": { ... }
}
2. Surface IDs
Include timestamp for uniqueness:
{
"surfaceId": "form-1704067200000",
"components": [...]
}
3. State Structure
Organize state logically:
{
"initialState": {
"form": {
"username": "",
"email": ""
},
"ui": {
"loading": false,
"error": null
}
}
}
4. Error Handling
Always include error states:
{
"components": [
{
"id": "error-display",
"component": {
"Text": {
"text": { "path": "ui.error" },
"usageHint": "p"
}
}
}
]
}
5. Progressive Enhancement
Start simple, add complexity:
// Simple: Just text
{ "Text": { "text": { "literalString": "Status: OK" } } }
// Enhanced: Add status indicator
{
"Card": {
"title": { "literalString": "Status" },
"content": [
{
"Text": { "text": { "literalString": "OK" } }
},
{
"Progress": { "value": { "literalNumber": 100 } }
}
]
}
}
6. Accessibility
- Use semantic usageHint values (h1-h6, p)
- Provide labels for inputs
- Include descriptions for complex interactions
Quick Reference
Action IDs
Use descriptive, action-oriented IDs:
submit-form,cancel-action,delete-itemrefresh-data,load-more,sort-by-date
Binding Paths
Use dot notation for nested paths:
user.profile.nameitems.0.titleform.settings.theme
Component Variants
| Variant | Use Case |
|---|---|
| primary | Main action, important |
| secondary | Alternative action |
| destructive | Dangerous actions (delete) |
| ghost | Subtle, unobtrusive |
| outline | Bordered, less emphasis |
Language Values for CLIOutput
| Language | Use For |
|---|---|
| bash | Shell commands, terminal output |
| javascript | JS/TS code, console logs |
| python | Python code, error traces |
| text | Plain text, no highlighting |
Support
For issues, questions, or contributions:
- Check existing Issues
- Review Integration Guide
- See Component Examples