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:
catlog22
2026-01-31 16:02:20 +08:00
parent 715ef12c92
commit 345437415f
33 changed files with 7049 additions and 105 deletions

View File

@@ -0,0 +1,467 @@
# 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.
```typescript
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.
```typescript
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.
```typescript
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
```typescript
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:
```typescript
// 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:
```typescript
// Literal string
{ literalString: "Hello, World!" }
// Bound to state
{ path: "user.name" }
```
### Actions
Actions are triggered by user interactions:
```typescript
{
actionId: "save-form",
parameters: {
formId: "contact-form",
validate: true
}
}
```
## Creating Custom Components
### 1. Define the Renderer
```typescript
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
```typescript
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:
```typescript
// 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
```typescript
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
```typescript
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
```typescript
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:
```typescript
// In your component renderer
const value = resolveBinding({ path: 'user.name' });
```
### State Updates
Send state updates through actions:
```typescript
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:
```typescript
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:
```typescript
if (a2uiParser.validate(surfaceUpdate)) {
return <A2UIRenderer surface={surfaceUpdate} onAction={handleAction} />;
}
```
2. **Handle unknown components** gracefully:
```typescript
const renderer = a2uiRegistry.get(componentType);
if (!renderer) {
return <FallbackComponent />;
}
```
3. **Use TypeScript types** for type safety:
```typescript
import type { SurfaceUpdate, A2UIComponent } from '@/packages/a2ui-runtime/core/A2UITypes';
```
4. **Clean up resources** when surfaces are removed:
```typescript
useEffect(() => {
return () => {
// Cleanup timers, subscriptions, etc.
};
}, [surfaceId]);
```
## License
Part of the CCW project.