Skip to main content

rustyclaw_core/canvas/
a2ui.rs

1//! A2UI (Agent-to-UI) protocol types.
2//!
3//! A2UI is a declarative UI protocol that allows agents to push
4//! UI updates to a canvas without writing HTML/JS directly.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// A2UI message types (v0.8 compatible).
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub enum A2UIMessage {
13    /// Start rendering a surface
14    BeginRendering { surface_id: String, root: String },
15
16    /// Update surface components
17    SurfaceUpdate {
18        surface_id: String,
19        components: Vec<A2UIComponentDef>,
20    },
21
22    /// Update data model
23    DataModelUpdate {
24        surface_id: String,
25        data: serde_json::Value,
26    },
27
28    /// Delete a surface
29    DeleteSurface { surface_id: String },
30}
31
32/// A2UI component definition.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct A2UIComponentDef {
35    /// Component ID
36    pub id: String,
37
38    /// Component type and properties
39    pub component: A2UIComponent,
40}
41
42/// A2UI component types.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(rename_all = "PascalCase")]
45pub enum A2UIComponent {
46    /// Container column
47    Column {
48        children: A2UIChildren,
49        #[serde(default)]
50        spacing: Option<String>,
51    },
52
53    /// Container row
54    Row {
55        children: A2UIChildren,
56        #[serde(default)]
57        spacing: Option<String>,
58    },
59
60    /// Text element
61    Text {
62        text: A2UITextValue,
63        #[serde(default)]
64        usage_hint: Option<String>,
65    },
66
67    /// Button element
68    Button {
69        label: A2UITextValue,
70        #[serde(default)]
71        action: Option<String>,
72    },
73
74    /// Input field
75    Input {
76        #[serde(default)]
77        placeholder: Option<String>,
78        #[serde(default)]
79        value: Option<A2UITextValue>,
80        #[serde(default)]
81        on_change: Option<String>,
82    },
83
84    /// Image element
85    Image {
86        src: String,
87        #[serde(default)]
88        alt: Option<String>,
89    },
90
91    /// Markdown content
92    Markdown { content: A2UITextValue },
93
94    /// Code block
95    Code {
96        content: A2UITextValue,
97        #[serde(default)]
98        language: Option<String>,
99    },
100
101    /// Progress indicator
102    Progress {
103        value: f64,
104        #[serde(default)]
105        max: Option<f64>,
106    },
107
108    /// Spacer
109    Spacer {
110        #[serde(default)]
111        size: Option<String>,
112    },
113
114    /// Divider
115    Divider,
116}
117
118/// A2UI children specification.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub enum A2UIChildren {
122    /// Explicit list of child IDs
123    ExplicitList(Vec<String>),
124
125    /// Dynamic children from data binding
126    DataBinding { source: String, template: String },
127}
128
129/// A2UI text value (literal or data binding).
130#[derive(Debug, Clone, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub enum A2UITextValue {
133    /// Literal string
134    LiteralString(String),
135
136    /// Data binding expression
137    DataBinding(String),
138}
139
140impl A2UITextValue {
141    /// Create a literal text value
142    pub fn literal(s: impl Into<String>) -> Self {
143        Self::LiteralString(s.into())
144    }
145
146    /// Create a data binding
147    pub fn binding(expr: impl Into<String>) -> Self {
148        Self::DataBinding(expr.into())
149    }
150}
151
152/// A rendered A2UI surface.
153#[derive(Debug, Clone, Default)]
154pub struct A2UISurface {
155    /// Surface ID
156    pub id: String,
157
158    /// Root component ID
159    pub root: Option<String>,
160
161    /// Components by ID
162    pub components: HashMap<String, A2UIComponent>,
163
164    /// Data model
165    pub data: serde_json::Value,
166}
167
168impl A2UISurface {
169    /// Create a new surface
170    pub fn new(id: impl Into<String>) -> Self {
171        Self {
172            id: id.into(),
173            root: None,
174            components: HashMap::new(),
175            data: serde_json::Value::Null,
176        }
177    }
178
179    /// Apply an A2UI message to update this surface
180    pub fn apply(&mut self, msg: &A2UIMessage) {
181        match msg {
182            A2UIMessage::BeginRendering { surface_id, root } => {
183                if surface_id == &self.id {
184                    self.root = Some(root.clone());
185                }
186            }
187            A2UIMessage::SurfaceUpdate {
188                surface_id,
189                components,
190            } => {
191                if surface_id == &self.id {
192                    for comp_def in components {
193                        self.components
194                            .insert(comp_def.id.clone(), comp_def.component.clone());
195                    }
196                }
197            }
198            A2UIMessage::DataModelUpdate { surface_id, data } => {
199                if surface_id == &self.id {
200                    self.data = data.clone();
201                }
202            }
203            A2UIMessage::DeleteSurface { .. } => {
204                // Handled at manager level
205            }
206        }
207    }
208}