castella-a2ui
Render A2UI JSON as native Castella widgets. Parse A2UI messages, handle actions, progressive rendering, data binding, and connect to A2UI-enabled agents.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install i2y-castella-castella-a2ui
Repository
Skill path: skills/castella-a2ui
Render A2UI JSON as native Castella widgets. Parse A2UI messages, handle actions, progressive rendering, data binding, and connect to A2UI-enabled agents.
Open repositoryBest for
Primary workflow: Analyze Data & AI.
Technical facets: Full Stack, Data / AI.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: i2y.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install castella-a2ui into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/i2y/castella before adding castella-a2ui to shared team environments
- Use castella-a2ui for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: castella-a2ui
description: Render A2UI JSON as native Castella widgets. Parse A2UI messages, handle actions, progressive rendering, data binding, and connect to A2UI-enabled agents.
---
# Castella A2UI Integration
A2UI (Agent-to-User Interface) enables AI agents to generate rich, interactive UIs by outputting JSON component descriptions. Castella renders these natively across desktop, web, and terminal platforms.
**When to use**: "render A2UI JSON", "A2UI component", "A2UIRenderer", "A2UI data binding", "A2UI streaming", "connect to A2UI agent", "updateDataModel", "A2UIClient"
## Quick Start
Render A2UI JSON to a Castella widget:
```python
from castella import App
from castella.a2ui import A2UIRenderer, A2UIComponent
from castella.frame import Frame
renderer = A2UIRenderer()
widget = renderer.render_json({
"components": [
{"id": "root", "component": "Column", "children": {"explicitList": ["text1"]}},
{"id": "text1", "component": "Text", "text": {"literalString": "Hello A2UI!"}}
],
"rootId": "root"
})
App(Frame("A2UI Demo", 800, 600), widget).run()
```
## Core Concepts
### A2UIRenderer
The main class for converting A2UI messages to Castella widgets:
```python
from castella.a2ui import A2UIRenderer, UserAction
def on_action(action: UserAction):
print(f"Action: {action.name}")
print(f"Source: {action.source_component_id}")
print(f"Context: {action.context}")
renderer = A2UIRenderer(on_action=on_action)
```
### Value Types
A2UI uses typed values for properties:
```python
# Literal values (static)
{"text": {"literalString": "Hello"}}
{"value": {"literalNumber": 42}}
{"checked": {"literalBoolean": True}}
# Data binding (dynamic, JSON Pointer RFC 6901)
{"text": {"path": "/user/name"}}
{"value": {"path": "/counter"}}
```
Helper functions:
```python
from castella.a2ui import literal, binding
literal("Hello") # {"literalString": "Hello"}
literal(42) # {"literalNumber": 42}
literal(True) # {"literalBoolean": True}
binding("/counter") # {"path": "/counter"}
```
## Supported Components
| A2UI Component | Castella Widget | Notes |
|----------------|-----------------|-------|
| Text | Text | usageHint: h1-h5, body, caption |
| Button | Button | action with context |
| TextField | Input/MultilineInput | usageHint: password, multiline |
| CheckBox | CheckBox | Two-way binding |
| Slider | Slider | min/max range |
| DateTimeInput | DateTimeInput | Date/time picker |
| ChoicePicker | RadioButtons/Column | Single/multiple selection |
| Image | NetImage | URL-based images |
| Icon | Text | Material Icons → emoji |
| Divider | Spacer | Horizontal/vertical |
| Row | Row | Horizontal layout |
| Column | Column | Vertical layout |
| Card | Column | Container with styling |
| List | Column | TemplateChildren support |
| Tabs | Tabs | Tabbed navigation |
| Modal | Modal | Overlay dialog |
| Markdown | Markdown | Rich text (Castella extension) |
See `references/components.md` for detailed component reference.
## Data Binding
Bind widget values to a data model using JSON Pointer paths:
```python
a2ui_json = {
"components": [
{"id": "root", "component": "Column", "children": {"explicitList": ["greeting"]}},
{"id": "greeting", "component": "Text", "text": {"path": "/message"}}
],
"rootId": "root"
}
# Provide initial data
initial_data = {"/message": "Hello, World!"}
widget = renderer.render_json(a2ui_json, initial_data=initial_data)
```
## Actions
Handle user interactions via actions:
```python
{
"id": "submit_btn",
"component": "Button",
"text": {"literalString": "Submit"},
"action": {"name": "submit", "context": ["/formData"]}
}
```
Action handler receives `UserAction`:
```python
def on_action(action: UserAction):
print(action.name) # "submit"
print(action.source_component_id) # "submit_btn"
print(action.context) # ["/formData"]
```
## updateDataModel
Update bound values dynamically:
```python
renderer.handle_message({
"updateDataModel": {
"surfaceId": "default",
"data": {"/message": "Updated message!"}
}
})
```
## A2UIComponent (Reactive)
Wrap surface in A2UIComponent for automatic UI updates:
```python
from castella.a2ui import A2UIComponent
renderer.render_json(a2ui_json, initial_data=initial_data)
surface = renderer.get_surface("default")
component = A2UIComponent(surface) # Auto-updates on data changes
App(Frame("A2UI", 800, 600), component).run()
```
## TemplateChildren (Dynamic Lists)
Render lists from data arrays:
```python
a2ui_json = {
"components": [
{"id": "root", "component": "Column", "children": {"explicitList": ["user_list"]}},
{"id": "user_list", "component": "List", "children": {
"path": "/users", # JSON Pointer to array
"componentId": "user_item" # Template component
}},
{"id": "user_item", "component": "Text", "text": {"path": "name"}} # Relative path
],
"rootId": "root"
}
initial_data = {"/users": [{"name": "Alice"}, {"name": "Bob"}, {"name": "Charlie"}]}
widget = renderer.render_json(a2ui_json, initial_data=initial_data)
```
## ChoicePicker
Single or multiple selection:
```python
# Single selection (renders as RadioButtons)
{
"id": "color_picker",
"component": "ChoicePicker",
"choices": [
{"literalString": "Red"},
{"literalString": "Green"},
{"literalString": "Blue"}
],
"selected": {"literalString": "Red"},
"allowMultiple": False
}
# Multiple selection (renders as CheckBox list)
{
"id": "toppings",
"component": "ChoicePicker",
"choices": [
{"literalString": "Cheese"},
{"literalString": "Pepperoni"},
{"literalString": "Mushrooms"}
],
"selected": {"path": "/selectedToppings"},
"allowMultiple": True
}
```
## Progressive Rendering (Streaming)
Handle JSONL streams for incremental UI updates:
```python
# From JSONL string
jsonl_content = """
{"beginRendering": {"surfaceId": "main", "root": "root"}}
{"updateComponents": {"surfaceId": "main", "components": [...]}}
"""
surface = renderer.handle_jsonl(jsonl_content)
# From file
with open("ui.jsonl") as f:
surface = renderer.handle_stream(f, on_update=lambda s: app.redraw())
# From async SSE stream
from castella.a2ui.transports import sse_stream
surface = await renderer.handle_stream_async(await sse_stream(url))
```
Message sequence:
1. `beginRendering` - Start progressive render
2. `updateComponents` - Add/update components
3. `updateDataModel` - Update data values
See `references/streaming.md` for transport details.
## A2UIClient
Connect to A2A agents with A2UI extension:
```python
from castella.a2ui import A2UIClient, A2UIComponent, UserAction
def on_action(action: UserAction):
print(f"Action: {action.name}")
client = A2UIClient("http://localhost:10002", on_action=on_action)
surface = client.send("Find restaurants in Tokyo")
if surface:
App(Frame("Restaurant Finder", 800, 600), A2UIComponent(surface)).run()
```
Async usage:
```python
async def main():
client = A2UIClient("http://localhost:10002")
surface = await client.send_async("Hello!")
if surface:
# Send action back to agent
await client.send_action_async(action)
```
## A2UI 0.9 Compatibility
Castella auto-normalizes A2UI 0.9 spec format:
```python
# A2UI 0.9 format (plain values) - accepted
{"text": "Hello", "children": ["a", "b"]}
# Castella internal format (wrapped) - also accepted
{"text": {"literalString": "Hello"}, "children": {"explicitList": ["a", "b"]}}
```
Key normalizations:
- `text: "Hello"` → `text: {literalString: "Hello"}`
- `children: ["a", "b"]` → `children: {explicitList: ["a", "b"]}`
- `usageHint: "shortText"` → `usageHint: "text"`
- `usageHint: "obscured"` → `usageHint: "password"`
## TextField usageHint
```python
# Password field (masked ●●●●)
{"id": "password", "component": "TextField", "usageHint": "password"}
# Multiline field
{"id": "comments", "component": "TextField", "usageHint": "multiline"}
```
## Best Practices
1. **Use A2UIComponent wrapper** for reactive data binding
2. **Provide initial_data** for TemplateChildren/List components
3. **Handle actions** to update data model dynamically
4. **Use semantic IDs** - A2UI component IDs become MCP semantic IDs
5. **Test with mock data** before connecting to live agents
## Installation
```bash
uv sync --extra agent # A2UI + A2A support
```
## Reference
- `references/components.md` - Complete A2UI component reference
- `references/messages.md` - A2UI message types
- `references/streaming.md` - Streaming and transports
- `scripts/` - Executable examples (basic_a2ui.py, a2ui_form.py, a2ui_list.py)
- A2UI Specification: https://a2ui.org/specification/v0.9-a2ui/
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/components.md
```markdown
# A2UI Component Reference
Complete reference for all 17 A2UI components supported by Castella.
## Text Components
### Text
Display text content with optional styling.
```python
{
"id": "title",
"component": "Text",
"text": {"literalString": "Hello, World!"},
"usageHint": "h1" # Optional: h1, h2, h3, h4, h5, body, caption
}
```
**Properties:**
- `text` - String value (literal or binding)
- `usageHint` - Styling hint: h1-h5 (headings), body, caption
### Icon
Display icon (Material Icons mapped to emoji).
```python
{
"id": "icon1",
"component": "Icon",
"name": {"literalString": "home"}
}
```
**Icon mappings:**
- `home` → 🏠
- `search` → 🔍
- `settings` → ⚙️
- `person` → 👤
- `email` → 📧
- `phone` → 📱
- `star` → ⭐
- `check` → ✓
- `close` → ✕
- `arrow_back` → ←
- `arrow_forward` → →
### Markdown
Rich markdown rendering (Castella extension).
```python
{
"id": "content",
"component": "Markdown",
"text": {"literalString": "# Title\n\n**Bold** text"}
}
```
## Input Components
### TextField
Single or multi-line text input.
```python
# Single line
{
"id": "name_input",
"component": "TextField",
"text": {"path": "/name"},
"usageHint": "text"
}
# Password (masked)
{
"id": "password",
"component": "TextField",
"text": {"path": "/password"},
"usageHint": "password"
}
# Multiline
{
"id": "comments",
"component": "TextField",
"text": {"path": "/comments"},
"usageHint": "multiline"
}
```
**usageHint values:**
- `text` (default) - Single line
- `password` / `obscured` - Masked input
- `multiline` - Multi-line editor
### CheckBox
Toggle checkbox.
```python
{
"id": "agree",
"component": "CheckBox",
"checked": {"path": "/agreed"},
"label": {"literalString": "I agree to the terms"}
}
```
### Slider
Range slider.
```python
{
"id": "volume",
"component": "Slider",
"value": {"path": "/volume"},
"min": {"literalNumber": 0},
"max": {"literalNumber": 100}
}
```
### DateTimeInput
Date and/or time picker.
```python
{
"id": "appointment",
"component": "DateTimeInput",
"value": {"path": "/datetime"},
"enableDate": True,
"enableTime": True
}
```
### ChoicePicker
Single or multiple selection from options.
```python
# Single selection (RadioButtons)
{
"id": "size",
"component": "ChoicePicker",
"choices": [
{"literalString": "Small"},
{"literalString": "Medium"},
{"literalString": "Large"}
],
"selected": {"literalString": "Medium"},
"allowMultiple": False
}
# Multiple selection (CheckBox list)
{
"id": "toppings",
"component": "ChoicePicker",
"choices": [
{"literalString": "Cheese"},
{"literalString": "Pepperoni"},
{"literalString": "Mushrooms"}
],
"selected": {"path": "/selectedToppings"},
"allowMultiple": True
}
```
## Button Component
### Button
Clickable button with action.
```python
{
"id": "submit_btn",
"component": "Button",
"text": {"literalString": "Submit"},
"action": {
"name": "submit",
"context": ["/formData"]
}
}
```
**Properties:**
- `text` - Button label
- `action` - Action definition
- `name` - Action identifier
- `context` - Array of data paths to include
## Layout Components
### Row
Horizontal layout.
```python
{
"id": "row1",
"component": "Row",
"children": {"explicitList": ["child1", "child2", "child3"]}
}
```
### Column
Vertical layout.
```python
{
"id": "col1",
"component": "Column",
"children": {"explicitList": ["child1", "child2"]}
}
```
### Card
Container with visual styling.
```python
{
"id": "card1",
"component": "Card",
"children": {"explicitList": ["content"]},
"title": {"literalString": "Card Title"}
}
```
### Divider
Visual separator.
```python
{
"id": "divider1",
"component": "Divider",
"orientation": "horizontal" # or "vertical"
}
```
## List Component
### List
Dynamic list with template children.
```python
{
"id": "user_list",
"component": "List",
"children": {
"path": "/users", # Data array path
"componentId": "user_item" # Template component ID
}
}
# Template component (relative paths)
{
"id": "user_item",
"component": "Row",
"children": {"explicitList": ["user_name", "user_email"]}
}
{
"id": "user_name",
"component": "Text",
"text": {"path": "name"} # Relative to list item
}
{
"id": "user_email",
"component": "Text",
"text": {"path": "email"}
}
```
**Initial data:**
```python
{
"/users": [
{"name": "Alice", "email": "[email protected]"},
{"name": "Bob", "email": "[email protected]"}
]
}
```
## Navigation Components
### Tabs
Tabbed navigation.
```python
{
"id": "tabs1",
"component": "Tabs",
"tabs": [
{"id": "home", "label": {"literalString": "Home"}, "content": "home_content"},
{"id": "settings", "label": {"literalString": "Settings"}, "content": "settings_content"}
],
"selected": {"literalString": "home"}
}
```
### Modal
Overlay dialog.
```python
{
"id": "modal1",
"component": "Modal",
"title": {"literalString": "Confirm"},
"content": {"explicitList": ["modal_content"]},
"open": {"path": "/modalOpen"}
}
```
## Media Components
### Image
Display image from URL.
```python
{
"id": "photo",
"component": "Image",
"src": {"literalString": "https://example.com/image.png"},
"alt": {"literalString": "Photo description"}
}
```
## Value Types Summary
### Literal Values
```python
{"literalString": "text"} # String
{"literalNumber": 42} # Number
{"literalBoolean": True} # Boolean
```
### Data Binding
```python
{"path": "/absolute/path"} # Absolute JSON Pointer
{"path": "relative/path"} # Relative (in List templates)
```
### Children
```python
{"explicitList": ["id1", "id2"]} # Static children
{"path": "/items", "componentId": "template"} # TemplateChildren
```
```
### references/streaming.md
```markdown
# A2UI Streaming and Transports
Progressive rendering with JSONL streams and various transport protocols.
## JSONL Format
A2UI messages stream as newline-delimited JSON:
```
{"beginRendering": {"surfaceId": "main", "root": "root"}}
{"updateComponents": {"surfaceId": "main", "components": [...]}}
{"updateComponents": {"surfaceId": "main", "components": [...]}}
{"updateDataModel": {"surfaceId": "main", "data": {...}}}
```
Each line is a complete JSON message.
## JSONLParser
Parse JSONL strings:
```python
from castella.a2ui import parse_jsonl_string
messages = parse_jsonl_string(jsonl_content)
for msg in messages:
print(msg) # Dict
```
## Streaming from File
```python
from castella.a2ui import A2UIRenderer
renderer = A2UIRenderer()
with open("ui.jsonl") as f:
surface = renderer.handle_stream(
f,
on_update=lambda s: app.redraw() # Called after each message
)
widget = surface.root_widget
```
## SSE Transport
Server-Sent Events for HTTP streaming:
```python
from castella.a2ui import A2UIRenderer
from castella.a2ui.transports import sse_stream
renderer = A2UIRenderer()
async def main():
stream = await sse_stream("http://agent.example.com/ui")
surface = await renderer.handle_stream_async(stream)
return surface.root_widget
```
**Requirements:**
```bash
uv sync --extra agent # Includes httpx
```
## WebSocket Transport
Bidirectional WebSocket streaming:
```python
from castella.a2ui.transports import websocket_stream
async def main():
stream = await websocket_stream("ws://agent.example.com/ui")
surface = await renderer.handle_stream_async(stream)
```
**Requirements:**
```bash
pip install websockets
```
## Custom Stream
Create custom async generator:
```python
async def my_stream():
async for chunk in some_source:
yield chunk
surface = await renderer.handle_stream_async(my_stream())
```
## A2UIClient Streaming
Connect to A2A agents with streaming:
```python
from castella.a2ui import A2UIClient
client = A2UIClient("http://localhost:10002")
# Streaming response
async for update in client.send_stream("Tell me a story"):
# update is surface with incremental changes
app.redraw()
```
## Sync Stream Handling
For synchronous code:
```python
def stream_generator():
for line in response.iter_lines():
yield line.decode()
surface = renderer.handle_stream(
stream_generator(),
on_update=lambda s: print("Updated!")
)
```
## Progressive Rendering Pattern
```python
from castella import App
from castella.a2ui import A2UIRenderer, A2UIComponent
from castella.frame import Frame
renderer = A2UIRenderer()
app = None
def on_update(surface):
if app:
app.redraw()
async def start_stream():
with open("stream.jsonl") as f:
surface = renderer.handle_stream(f, on_update=on_update)
return A2UIComponent(surface)
# Initialize with placeholder, update with stream
component = A2UIComponent(renderer.get_surface("default"))
app = App(Frame("Streaming", 800, 600), component)
# Start stream in background
import asyncio
asyncio.create_task(start_stream())
app.run()
```
## Error Handling
```python
from castella.a2ui import A2UIConnectionError, A2UIParseError
try:
surface = await renderer.handle_stream_async(stream)
except A2UIConnectionError as e:
print(f"Connection failed: {e}")
except A2UIParseError as e:
print(f"Invalid JSONL: {e}")
```
## Performance Tips
1. **Batch component updates** - Send multiple components per `updateComponents`
2. **Use data binding** - `updateDataModel` is cheaper than `updateComponents`
3. **Minimize re-renders** - Group related updates in single message
4. **Buffer appropriately** - Don't send too many tiny messages
## SSE Server Example (Python)
Simple SSE server for testing:
```python
from flask import Flask, Response
import json
import time
app = Flask(__name__)
@app.route('/ui')
def stream_ui():
def generate():
yield f"data: {json.dumps({'beginRendering': {'surfaceId': 'main', 'root': 'root'}})}\n\n"
components = [
{"id": "root", "component": "Column", "children": {"explicitList": ["msg"]}},
{"id": "msg", "component": "Text", "text": {"literalString": "Loading..."}}
]
yield f"data: {json.dumps({'updateComponents': {'surfaceId': 'main', 'components': components}})}\n\n"
time.sleep(1)
yield f"data: {json.dumps({'updateDataModel': {'surfaceId': 'main', 'data': {'/status': 'Complete!'}}})}\n\n"
return Response(generate(), mimetype='text/event-stream')
if __name__ == '__main__':
app.run(port=8080)
```
```
### references/messages.md
```markdown
# A2UI Message Types
Reference for all A2UI message types and their usage.
## createSurface
Create a complete UI surface at once.
```python
{
"createSurface": {
"surfaceId": "main",
"components": [
{"id": "root", "component": "Column", ...},
{"id": "text1", "component": "Text", ...}
],
"rootId": "root"
}
}
```
**Fields:**
- `surfaceId` - Unique identifier for the surface
- `components` - Array of component definitions
- `rootId` - ID of the root component
## beginRendering
Signal the start of progressive rendering.
```python
{
"beginRendering": {
"surfaceId": "main",
"root": "root"
}
}
```
**Fields:**
- `surfaceId` - Surface identifier
- `root` - Root component ID
Use with `updateComponents` for incremental UI construction.
## updateComponents
Add or update components (progressive rendering).
```python
{
"updateComponents": {
"surfaceId": "main",
"components": [
{"id": "header", "component": "Text", "text": {"literalString": "Header"}},
{"id": "content", "component": "Column", "children": {"explicitList": ["item1"]}}
]
}
}
```
**Fields:**
- `surfaceId` - Surface identifier
- `components` - Array of component definitions to add/update
Components with existing IDs are replaced.
## updateDataModel
Update data binding values.
```python
{
"updateDataModel": {
"surfaceId": "main",
"data": {
"/counter": 42,
"/user/name": "Alice",
"/items": [{"id": 1}, {"id": 2}]
}
}
}
```
**Fields:**
- `surfaceId` - Surface identifier
- `data` - Object with JSON Pointer paths as keys
All bound widgets update automatically.
## deleteSurface
Remove a surface.
```python
{
"deleteSurface": {
"surfaceId": "main"
}
}
```
## Message Handling
### render_json()
Create surface from single JSON:
```python
renderer = A2UIRenderer()
widget = renderer.render_json({
"components": [...],
"rootId": "root"
}, initial_data={"/counter": 0})
```
### handle_message()
Process individual messages:
```python
renderer.handle_message({
"updateDataModel": {
"surfaceId": "default",
"data": {"/counter": 10}
}
})
```
### handle_jsonl()
Process JSONL string:
```python
jsonl = """
{"beginRendering": {"surfaceId": "main", "root": "root"}}
{"updateComponents": {"surfaceId": "main", "components": [...]}}
"""
surface = renderer.handle_jsonl(jsonl)
```
### handle_stream()
Process stream (file, generator):
```python
with open("ui.jsonl") as f:
surface = renderer.handle_stream(f, on_update=callback)
```
### handle_stream_async()
Process async stream:
```python
surface = await renderer.handle_stream_async(async_stream)
```
## Surface Management
### get_surface()
Retrieve surface by ID:
```python
surface = renderer.get_surface("main")
if surface:
widget = surface.root_widget
data = surface.data_model
```
### Surface Properties
```python
surface.id # Surface ID
surface.root_widget # Root Castella widget
surface.data_model # Current data model dict
surface.components # Component definitions
```
## Action Messages
When user interacts with action-enabled components:
```python
def on_action(action: UserAction):
# action.name - Action name (e.g., "submit")
# action.source_component_id - Component ID that triggered
# action.context - Context data from action definition
# Respond with data update
renderer.handle_message({
"updateDataModel": {
"surfaceId": "default",
"data": {"/status": "Submitted!"}
}
})
```
## Progressive Rendering Sequence
Typical streaming sequence:
```
1. {"beginRendering": {"surfaceId": "chat", "root": "root"}}
2. {"updateComponents": {"surfaceId": "chat", "components": [root, header]}}
3. {"updateComponents": {"surfaceId": "chat", "components": [message1]}}
4. {"updateComponents": {"surfaceId": "chat", "components": [message2]}}
5. {"updateDataModel": {"surfaceId": "chat", "data": {"/status": "Complete"}}}
```
Each `updateComponents` triggers UI refresh with new components.
```