mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
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.
This commit is contained in:
617
ccw/docs/a2ui-integration.md
Normal file
617
ccw/docs/a2ui-integration.md
Normal file
@@ -0,0 +1,617 @@
|
||||
# A2UI Integration Guide for CCW Developers
|
||||
|
||||
This guide explains how to integrate and extend the A2UI (AI-to-UI) system in the CCW application.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Architecture Overview](#architecture-overview)
|
||||
2. [WebSocket Integration](#websocket-integration)
|
||||
3. [Notification System](#notification-system)
|
||||
4. [Creating Custom Components](#creating-custom-components)
|
||||
5. [Backend Tool Integration](#backend-tool-integration)
|
||||
6. [Testing](#testing)
|
||||
7. [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ CCW Frontend │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────────────┐ ┌─────────────────┐ │
|
||||
│ │ WebSocket │─────▶│ A2UI Parser │ │
|
||||
│ │ Handler │ │ (validation) │ │
|
||||
│ └────────────────┘ └────────┬────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────┐ ┌─────────────────┐ │
|
||||
│ │ Notification │◀─────│ A2UI Component │ │
|
||||
│ │ Store │ │ Registry │ │
|
||||
│ └────────────────┘ └────────┬────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────┐ ┌─────────────────┐ │
|
||||
│ │ UI Components │◀─────│ A2UI Renderer │ │
|
||||
│ │ (Dialogs, │ │ (React) │ │
|
||||
│ │ Panels) │ └─────────────────┘ │
|
||||
│ └────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ WebSocket
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ CCW Backend │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌────────────────┐ ┌─────────────────┐ │
|
||||
│ │ MCP Tools │─────▶│ A2UI Types │ │
|
||||
│ │ (ask_question, │ │ (Zod schemas) │ │
|
||||
│ │ etc.) │ └─────────────────┘ │
|
||||
│ └────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## WebSocket Integration
|
||||
|
||||
### Message Flow
|
||||
|
||||
The A2UI system uses WebSocket messages for bidirectional communication:
|
||||
|
||||
#### Frontend to Backend (Actions)
|
||||
|
||||
```typescript
|
||||
// Send action via WebSocket
|
||||
const sendA2UIAction = (
|
||||
actionId: string,
|
||||
surfaceId: string,
|
||||
parameters: Record<string, unknown>
|
||||
) => {
|
||||
const event = new CustomEvent('a2ui-action', {
|
||||
detail: {
|
||||
type: 'a2ui-action',
|
||||
actionId,
|
||||
surfaceId,
|
||||
parameters,
|
||||
},
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
|
||||
// WebSocket handler picks up this event
|
||||
};
|
||||
```
|
||||
|
||||
#### Backend to Frontend (Surface Updates)
|
||||
|
||||
```typescript
|
||||
// WebSocket message handler processes incoming messages
|
||||
interface A2UISurfaceMessage {
|
||||
type: 'a2ui-surface';
|
||||
surfaceId: string;
|
||||
title?: string;
|
||||
surface: SurfaceUpdate;
|
||||
}
|
||||
|
||||
// In useWebSocket hook
|
||||
useEffect(() => {
|
||||
ws.addEventListener('message', (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
|
||||
if (message.type === 'a2ui-surface') {
|
||||
// Add to notification store
|
||||
addA2UINotification(message.surface, message.title);
|
||||
}
|
||||
});
|
||||
}, [ws]);
|
||||
```
|
||||
|
||||
## Notification System
|
||||
|
||||
### A2UI Notifications
|
||||
|
||||
A2UI surfaces appear as notifications in the notification panel:
|
||||
|
||||
```typescript
|
||||
// Add A2UI notification
|
||||
import { useNotificationStore } from '@/stores/notificationStore';
|
||||
|
||||
const { addA2UINotification } = useNotificationStore();
|
||||
|
||||
addA2UINotification(surfaceUpdate, 'Notification Title');
|
||||
```
|
||||
|
||||
### State Management
|
||||
|
||||
A2UI state is managed through the notification store:
|
||||
|
||||
```typescript
|
||||
// Update A2UI state
|
||||
updateA2UIState(surfaceId, {
|
||||
counter: 5,
|
||||
userInput: 'value',
|
||||
});
|
||||
```
|
||||
|
||||
### Ask Question Dialog
|
||||
|
||||
The `ask_question` tool uses a dedicated dialog:
|
||||
|
||||
```typescript
|
||||
// Set current question (triggered by WebSocket)
|
||||
setCurrentQuestion({
|
||||
surfaceId: 'question-123',
|
||||
title: 'Confirmation Required',
|
||||
questions: [
|
||||
{
|
||||
id: 'q1',
|
||||
type: 'confirm',
|
||||
question: 'Do you want to continue?',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Dialog renders automatically
|
||||
// {currentQuestion && <AskQuestionDialog payload={currentQuestion} />}
|
||||
```
|
||||
|
||||
## Creating Custom Components
|
||||
|
||||
### Step 1: Create Component Renderer
|
||||
|
||||
Create file: `src/packages/a2ui-runtime/renderer/components/A2UICustom.tsx`
|
||||
|
||||
```typescript
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import type { ComponentRenderer } from '../../core/A2UIComponentRegistry';
|
||||
import { resolveTextContent } from '../A2UIRenderer';
|
||||
|
||||
// Define component config interface (for TypeScript)
|
||||
interface CustomComponentConfig {
|
||||
title: { literalString: string } | { path: string };
|
||||
dataSource: { literalString: string } | { path: string };
|
||||
onRefresh: { actionId: string; parameters?: Record<string, unknown> };
|
||||
}
|
||||
|
||||
export const A2UICustom: ComponentRenderer = ({
|
||||
component,
|
||||
state,
|
||||
onAction,
|
||||
resolveBinding
|
||||
}) => {
|
||||
const customComp = component as { Custom: CustomComponentConfig };
|
||||
const { Custom: config } = customComp;
|
||||
|
||||
// Resolve title (literal or binding)
|
||||
const title = resolveTextContent(config.title, resolveBinding);
|
||||
|
||||
// Resolve data source
|
||||
const dataSource = resolveTextContent(config.dataSource, resolveBinding);
|
||||
|
||||
// Local state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Handle refresh action
|
||||
const handleRefresh = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
onAction(config.onRefresh.actionId, {
|
||||
...config.onRefresh.parameters,
|
||||
dataSource,
|
||||
});
|
||||
// Reset loading after action (implementation dependent)
|
||||
setTimeout(() => setIsLoading(false), 1000);
|
||||
}, [onAction, config.onRefresh, dataSource]);
|
||||
|
||||
return (
|
||||
<div className="a2ui-custom p-4 border rounded-md">
|
||||
<h3 className="text-lg font-semibold mb-2">{title}</h3>
|
||||
<div className="text-sm">
|
||||
Data source: {dataSource}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={isLoading}
|
||||
className="mt-2 px-3 py-1 bg-primary text-primary-foreground rounded"
|
||||
>
|
||||
{isLoading ? 'Loading...' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Step 2: Register Component
|
||||
|
||||
Update: `src/packages/a2ui-runtime/renderer/components/registry.ts`
|
||||
|
||||
```typescript
|
||||
import { A2UICustom } from './A2UICustom';
|
||||
|
||||
export function registerBuiltInComponents(): void {
|
||||
// ... existing registrations
|
||||
|
||||
// Register custom component
|
||||
a2uiRegistry.register('Custom', A2UICustom);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Export Component
|
||||
|
||||
Update: `src/packages/a2ui-runtime/renderer/components/index.ts`
|
||||
|
||||
```typescript
|
||||
export * from './A2UICustom';
|
||||
```
|
||||
|
||||
### Step 4: Add Type Definition (Optional)
|
||||
|
||||
Update: `src/packages/a2ui-runtime/core/A2UITypes.ts`
|
||||
|
||||
```typescript
|
||||
// Add schema
|
||||
export const CustomComponentSchema = z.object({
|
||||
Custom: z.object({
|
||||
title: TextContentSchema,
|
||||
dataSource: TextContentSchema,
|
||||
onRefresh: ActionSchema,
|
||||
description: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Add to ComponentSchema union
|
||||
export const ComponentSchema: z.ZodType<
|
||||
| z.infer<typeof TextComponentSchema>
|
||||
| z.infer<typeof ButtonComponentSchema>
|
||||
// ... other components
|
||||
| z.infer<typeof CustomComponentSchema>
|
||||
> = z.union([
|
||||
// ... existing schemas
|
||||
CustomComponentSchema,
|
||||
]);
|
||||
|
||||
// Add to TypeScript type
|
||||
export type CustomComponent = z.infer<typeof CustomComponentSchema>;
|
||||
export type A2UIComponent = z.infer<typeof ComponentSchema>;
|
||||
|
||||
// Add to discriminated union
|
||||
export type A2UIComponentType =
|
||||
| 'Text' | 'Button' | // ... existing types
|
||||
| 'Custom';
|
||||
```
|
||||
|
||||
## Backend Tool Integration
|
||||
|
||||
### Creating an A2UI Tool
|
||||
|
||||
Create file: `src/tools/my-a2ui-tool.ts`
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
import type { ToolSchema, ToolResult } from '../types/tool.js';
|
||||
import type { SurfaceUpdate } from '../core/a2ui/A2UITypes.js';
|
||||
|
||||
// Generate A2UI surface for your tool
|
||||
function generateSurface(params: MyToolParams): {
|
||||
surfaceUpdate: SurfaceUpdate;
|
||||
} {
|
||||
const components: unknown[] = [
|
||||
{
|
||||
id: 'title',
|
||||
component: {
|
||||
Text: {
|
||||
text: { literalString: params.title || 'My Tool' },
|
||||
usageHint: 'h3',
|
||||
},
|
||||
},
|
||||
},
|
||||
// Add more components...
|
||||
];
|
||||
|
||||
return {
|
||||
surfaceUpdate: {
|
||||
surfaceId: `my-tool-${Date.now()}`,
|
||||
components,
|
||||
initialState: {
|
||||
toolName: 'my-tool',
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Execute tool
|
||||
export async function execute(params: MyToolParams): Promise<ToolResult> {
|
||||
try {
|
||||
// Generate surface
|
||||
const { surfaceUpdate } = generateSurface(params);
|
||||
|
||||
// TODO: Send surface via WebSocket to frontend
|
||||
// For now, return surface in result
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
surfaceId: surfaceUpdate.surfaceId,
|
||||
surface: surfaceUpdate,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Tool schema
|
||||
export const schema: ToolSchema = {
|
||||
name: 'my_a2ui_tool',
|
||||
description: 'Description of what your tool does',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'Title for the A2UI surface' },
|
||||
// Add more properties...
|
||||
},
|
||||
required: ['title'],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Handling Actions from Frontend
|
||||
|
||||
Create handler in backend:
|
||||
|
||||
```typescript
|
||||
// In WebSocket handler or tool manager
|
||||
function handleA2UIAction(
|
||||
actionId: string,
|
||||
surfaceId: string,
|
||||
parameters: Record<string, unknown>
|
||||
): void {
|
||||
// Find pending question or surface
|
||||
const pending = pendingSurfaces.get(surfaceId);
|
||||
|
||||
if (!pending) {
|
||||
console.warn(`No surface found for ID: ${surfaceId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process action based on actionId
|
||||
switch (actionId) {
|
||||
case 'confirm':
|
||||
// Handle confirmation
|
||||
pending.resolve({
|
||||
success: true,
|
||||
surfaceId,
|
||||
cancelled: false,
|
||||
answers: [parameters],
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
break;
|
||||
|
||||
case 'cancel':
|
||||
// Handle cancellation
|
||||
pending.resolve({
|
||||
success: false,
|
||||
surfaceId,
|
||||
cancelled: true,
|
||||
answers: [],
|
||||
timestamp: new Date().toISOString(),
|
||||
error: 'User cancelled',
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
// Handle custom actions
|
||||
console.log(`Action ${actionId} with params:`, parameters);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
pendingSurfaces.delete(surfaceId);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Test component renderers:
|
||||
|
||||
```typescript
|
||||
// src/packages/a2ui-runtime/__tests__/components/A2UICustom.test.tsx
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { A2UICustom } from '../A2UICustom';
|
||||
import type { A2UIComponent } from '@/packages/a2ui-runtime/core/A2UITypes';
|
||||
|
||||
describe('A2UICustom', () => {
|
||||
it('should render with title', () => {
|
||||
const component: A2UIComponent = {
|
||||
Custom: {
|
||||
title: { literalString: 'Test Title' },
|
||||
dataSource: { literalString: 'test-source' },
|
||||
onRefresh: { actionId: 'refresh' },
|
||||
},
|
||||
};
|
||||
|
||||
const props = {
|
||||
component,
|
||||
state: {},
|
||||
onAction: vi.fn(),
|
||||
resolveBinding: vi.fn(),
|
||||
};
|
||||
|
||||
render(<A2UICustom {...props} />);
|
||||
expect(screen.getByText('Test Title')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### E2E Tests
|
||||
|
||||
Test A2UI flow in browser:
|
||||
|
||||
```typescript
|
||||
// tests/e2e/my-a2ui-tool.spec.ts
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('My A2UI Tool E2E', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Simulate WebSocket message
|
||||
await page.evaluate(() => {
|
||||
const event = new CustomEvent('ws-message', {
|
||||
detail: {
|
||||
type: 'a2ui-surface',
|
||||
surfaceId: 'test-custom',
|
||||
title: 'Custom Tool',
|
||||
surface: {
|
||||
surfaceId: 'test-custom',
|
||||
components: [
|
||||
{
|
||||
id: 'custom',
|
||||
component: {
|
||||
Custom: {
|
||||
title: { literalString: 'Test Custom' },
|
||||
dataSource: { literalString: 'data' },
|
||||
onRefresh: { actionId: 'refresh' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
initialState: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
});
|
||||
|
||||
// Verify rendering
|
||||
await expect(page.getByText('Test Custom')).toBeVisible();
|
||||
await expect(page.getByText('Data source: data')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. Component Not Rendering
|
||||
|
||||
**Symptoms**: A2UI surface received but component not visible
|
||||
|
||||
**Solutions**:
|
||||
- Verify component is registered in registry
|
||||
- Check console for warnings about unknown component types
|
||||
- Ensure component returns valid JSX
|
||||
|
||||
```typescript
|
||||
// Debug: Check if registered
|
||||
console.log(a2uiRegistry.getRegisteredTypes());
|
||||
// Should include your component type
|
||||
```
|
||||
|
||||
#### 2. State Not Updating
|
||||
|
||||
**Symptoms**: State changes not reflected in UI
|
||||
|
||||
**Solutions**:
|
||||
- Verify binding path matches state key
|
||||
- Check `resolveBinding` implementation
|
||||
- Ensure `updateA2UIState` is called with correct surfaceId
|
||||
|
||||
```typescript
|
||||
// Debug: Log state changes
|
||||
const handleAction = (actionId: string, params: any) => {
|
||||
console.log('Action:', actionId, 'Params:', params);
|
||||
onAction(actionId, params);
|
||||
};
|
||||
```
|
||||
|
||||
#### 3. Actions Not Being Sent
|
||||
|
||||
**Symptoms**: Clicking buttons doesn't trigger backend action
|
||||
|
||||
**Solutions**:
|
||||
- Check WebSocket connection status
|
||||
- Verify action event listener is attached
|
||||
- Ensure actionId matches backend handler
|
||||
|
||||
```typescript
|
||||
// Debug: Monitor a2ui-action events
|
||||
window.addEventListener('a2ui-action', (e) => {
|
||||
console.log('A2UI Action:', (e as CustomEvent).detail);
|
||||
});
|
||||
```
|
||||
|
||||
#### 4. Parse Errors
|
||||
|
||||
**Symptoms**: Surface updates fail validation
|
||||
|
||||
**Solutions**:
|
||||
- Validate JSON structure against A2UI spec
|
||||
- Check Zod error details
|
||||
- Use `safeParse` for debugging
|
||||
|
||||
```typescript
|
||||
// Debug: Safe parse with details
|
||||
const result = a2uiParser.safeParse(jsonString);
|
||||
if (!result.success) {
|
||||
console.error('Parse error:', result.error);
|
||||
// Get detailed Zod errors
|
||||
}
|
||||
```
|
||||
|
||||
### Development Tools
|
||||
|
||||
#### React DevTools
|
||||
|
||||
Inspect A2UI component hierarchy:
|
||||
|
||||
```typescript
|
||||
// Add data attributes for debugging
|
||||
<div data-a2ui-component="Custom" data-surface-id={surfaceId}>
|
||||
{/* component content */}
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Logging
|
||||
|
||||
Enable verbose logging in development:
|
||||
|
||||
```typescript
|
||||
// In development mode
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[A2UI] Rendering component:', componentType);
|
||||
console.log('[A2UI] Surface:', surface);
|
||||
console.log('[A2UI] State:', state);
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
1. **Lazy Load Components**: For complex custom components, use React.lazy
|
||||
|
||||
2. **Debounce Actions**: Prevent rapid action firing
|
||||
|
||||
```typescript
|
||||
import { debounce } from 'lodash-es';
|
||||
|
||||
const debouncedAction = debounce(onAction, 300);
|
||||
```
|
||||
|
||||
3. **Memoize Renders**: Use React.memo for expensive components
|
||||
|
||||
```typescript
|
||||
export const A2UICustom = React.memo<ComponentRenderer>(({ /* ... */ }) => {
|
||||
// component implementation
|
||||
});
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [A2UI Runtime README](./a2ui-runtime/README.md)
|
||||
- [A2UI Protocol Usage](./a2ui-integration.md#protocol-usage)
|
||||
- [Component Examples](./a2ui-integration.md#examples)
|
||||
|
||||
For questions or issues, please refer to the main CCW documentation.
|
||||
775
ccw/docs/a2ui-protocol-guide.md
Normal file
775
ccw/docs/a2ui-protocol-guide.md
Normal file
@@ -0,0 +1,775 @@
|
||||
# A2UI Protocol Usage and Troubleshooting Guide
|
||||
|
||||
This guide provides comprehensive information about the A2UI protocol for AI agent developers using CCW.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Protocol Overview](#protocol-overview)
|
||||
2. [Surface Update Structure](#surface-update-structure)
|
||||
3. [Component Reference](#component-reference)
|
||||
4. [Action Handling](#action-handling)
|
||||
5. [State Management](#state-management)
|
||||
6. [Code Examples](#code-examples)
|
||||
7. [Troubleshooting](#troubleshooting)
|
||||
8. [Best Practices](#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
|
||||
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
|
||||
```json
|
||||
{
|
||||
"literalString": "Hello, World!"
|
||||
}
|
||||
```
|
||||
|
||||
#### Binding
|
||||
|
||||
Reference to state value:
|
||||
|
||||
```json
|
||||
{
|
||||
"path": "user.name"
|
||||
}
|
||||
```
|
||||
|
||||
### Standard Components
|
||||
|
||||
#### Text
|
||||
|
||||
Display text with semantic hints.
|
||||
|
||||
```json
|
||||
{
|
||||
"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.
|
||||
|
||||
```json
|
||||
{
|
||||
"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.
|
||||
|
||||
```json
|
||||
{
|
||||
"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.
|
||||
|
||||
```json
|
||||
{
|
||||
"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.
|
||||
|
||||
```json
|
||||
{
|
||||
"TextArea": {
|
||||
"onChange": { "actionId": "textarea-change" },
|
||||
"placeholder": "Enter description",
|
||||
"rows": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Checkbox
|
||||
|
||||
Boolean checkbox with label.
|
||||
|
||||
```json
|
||||
{
|
||||
"Checkbox": {
|
||||
"checked": { "literalBoolean": true },
|
||||
"onChange": { "actionId": "checkbox-change" },
|
||||
"label": { "literalString": "Accept terms" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Progress
|
||||
|
||||
Progress bar indicator.
|
||||
|
||||
```json
|
||||
{
|
||||
"Progress": {
|
||||
"value": { "literalNumber": 75 },
|
||||
"max": 100
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Card
|
||||
|
||||
Container with title and nested content.
|
||||
|
||||
```json
|
||||
{
|
||||
"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.
|
||||
|
||||
```json
|
||||
{
|
||||
"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.
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
```json
|
||||
{
|
||||
"actionId": "unique-action-id",
|
||||
"parameters": {
|
||||
"key1": "value1",
|
||||
"key2": 42
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Action Flow
|
||||
|
||||
1. User interacts with component (click, type, select)
|
||||
2. Component triggers `onAction` callback
|
||||
3. Action sent via WebSocket to backend
|
||||
4. 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:
|
||||
|
||||
```json
|
||||
{
|
||||
"TextField": {
|
||||
"value": { "path": "form.username" },
|
||||
"onChange": { "actionId": "update-field" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### State Update
|
||||
|
||||
Backend sends state updates:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "a2ui-state-update",
|
||||
"surfaceId": "form-surface",
|
||||
"updates": {
|
||||
"form": {
|
||||
"username": "newvalue",
|
||||
"email": "updated@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Example 1: Simple Form
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
```json
|
||||
{
|
||||
"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**:
|
||||
```typescript
|
||||
// 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**:
|
||||
```typescript
|
||||
// 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**:
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// In development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
(window as any).A2UI_DEBUG = true;
|
||||
}
|
||||
```
|
||||
|
||||
#### Inspect Component Props
|
||||
|
||||
```typescript
|
||||
// Add logging in renderer
|
||||
export const A2UICustom: ComponentRenderer = (props) => {
|
||||
console.log('[A2UICustom] Props:', props);
|
||||
// ... rest of implementation
|
||||
};
|
||||
```
|
||||
|
||||
#### Validate Before Sending
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
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**:
|
||||
```json
|
||||
{
|
||||
"CLIOutput": {
|
||||
"output": { "path": "output" },
|
||||
"maxLines": 1000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Component IDs
|
||||
|
||||
Use unique, descriptive IDs:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "user-form-username",
|
||||
"component": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Surface IDs
|
||||
|
||||
Include timestamp for uniqueness:
|
||||
|
||||
```json
|
||||
{
|
||||
"surfaceId": "form-1704067200000",
|
||||
"components": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. State Structure
|
||||
|
||||
Organize state logically:
|
||||
|
||||
```json
|
||||
{
|
||||
"initialState": {
|
||||
"form": {
|
||||
"username": "",
|
||||
"email": ""
|
||||
},
|
||||
"ui": {
|
||||
"loading": false,
|
||||
"error": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Error Handling
|
||||
|
||||
Always include error states:
|
||||
|
||||
```json
|
||||
{
|
||||
"components": [
|
||||
{
|
||||
"id": "error-display",
|
||||
"component": {
|
||||
"Text": {
|
||||
"text": { "path": "ui.error" },
|
||||
"usageHint": "p"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Progressive Enhancement
|
||||
|
||||
Start simple, add complexity:
|
||||
|
||||
```json
|
||||
// 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-item`
|
||||
- `refresh-data`, `load-more`, `sort-by-date`
|
||||
|
||||
### Binding Paths
|
||||
|
||||
Use dot notation for nested paths:
|
||||
|
||||
- `user.profile.name`
|
||||
- `items.0.title`
|
||||
- `form.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](../../issues)
|
||||
- Review [Integration Guide](./a2ui-integration.md)
|
||||
- See [Component Examples](../../src/packages/a2ui-runtime/renderer/components/)
|
||||
Reference in New Issue
Block a user