rustyclaw_core/canvas/
host.rs1use anyhow::Result;
4use std::collections::HashMap;
5use std::path::PathBuf;
6use std::sync::Arc;
7use tokio::sync::RwLock;
8use tracing::{debug, info};
9
10use super::a2ui::{A2UIMessage, A2UISurface};
11use super::config::CanvasConfig;
12
13pub struct CanvasHost {
17 config: CanvasConfig,
19
20 workspace: PathBuf,
22
23 surfaces: Arc<RwLock<HashMap<String, HashMap<String, A2UISurface>>>>,
25
26 running: Arc<RwLock<bool>>,
28}
29
30impl CanvasHost {
31 pub fn new(config: CanvasConfig, workspace: PathBuf) -> Self {
33 Self {
34 config,
35 workspace,
36 surfaces: Arc::new(RwLock::new(HashMap::new())),
37 running: Arc::new(RwLock::new(false)),
38 }
39 }
40
41 pub async fn start(&self) -> Result<()> {
43 if !self.config.enabled {
44 info!("Canvas is disabled");
45 return Ok(());
46 }
47
48 let root = self.config.canvas_root(&self.workspace);
50 tokio::fs::create_dir_all(&root).await?;
51
52 *self.running.write().await = true;
53 info!(port = self.config.port, root = %root.display(), "Canvas host started");
54
55 Ok(())
59 }
60
61 pub async fn stop(&self) -> Result<()> {
63 *self.running.write().await = false;
64 info!("Canvas host stopped");
65 Ok(())
66 }
67
68 pub async fn is_running(&self) -> bool {
70 *self.running.read().await
71 }
72
73 pub fn canvas_url(&self, session: &str) -> String {
75 format!("http://localhost:{}/canvas/{}/", self.config.port, session)
76 }
77
78 pub fn a2ui_url(&self, session: &str) -> String {
80 format!(
81 "http://localhost:{}/__rustyclaw__/a2ui/{}/",
82 self.config.port, session
83 )
84 }
85
86 pub async fn ensure_session(&self, session: &str) -> Result<PathBuf> {
90 let dir = self.config.session_dir(&self.workspace, session);
91 tokio::fs::create_dir_all(&dir).await?;
92 Ok(dir)
93 }
94
95 pub async fn write_file(&self, session: &str, path: &str, content: &[u8]) -> Result<PathBuf> {
97 let dir = self.ensure_session(session).await?;
98 let file_path = dir.join(path);
99
100 if let Some(parent) = file_path.parent() {
102 tokio::fs::create_dir_all(parent).await?;
103 }
104
105 tokio::fs::write(&file_path, content).await?;
106 debug!(session, path, "Canvas file written");
107
108 Ok(file_path)
109 }
110
111 pub async fn read_file(&self, session: &str, path: &str) -> Result<Vec<u8>> {
113 let dir = self.config.session_dir(&self.workspace, session);
114 let file_path = dir.join(path);
115 let content = tokio::fs::read(&file_path).await?;
116 Ok(content)
117 }
118
119 pub async fn push_a2ui(&self, session: &str, messages: Vec<A2UIMessage>) -> Result<()> {
123 let mut surfaces = self.surfaces.write().await;
124 let session_surfaces = surfaces.entry(session.to_string()).or_default();
125
126 for msg in messages {
127 match &msg {
128 A2UIMessage::BeginRendering { surface_id, .. }
129 | A2UIMessage::SurfaceUpdate { surface_id, .. }
130 | A2UIMessage::DataModelUpdate { surface_id, .. } => {
131 let surface = session_surfaces
132 .entry(surface_id.clone())
133 .or_insert_with(|| A2UISurface::new(surface_id));
134 surface.apply(&msg);
135 }
136 A2UIMessage::DeleteSurface { surface_id } => {
137 session_surfaces.remove(surface_id);
138 }
139 }
140 }
141
142 debug!(
143 session,
144 count = session_surfaces.len(),
145 "A2UI surfaces updated"
146 );
147 Ok(())
148 }
149
150 pub async fn push_text(&self, session: &str, text: &str) -> Result<()> {
152 use super::a2ui::{A2UIChildren, A2UIComponent, A2UIComponentDef, A2UITextValue};
153
154 let messages = vec![
155 A2UIMessage::SurfaceUpdate {
156 surface_id: "main".to_string(),
157 components: vec![
158 A2UIComponentDef {
159 id: "root".to_string(),
160 component: A2UIComponent::Column {
161 children: A2UIChildren::ExplicitList(vec!["content".to_string()]),
162 spacing: None,
163 },
164 },
165 A2UIComponentDef {
166 id: "content".to_string(),
167 component: A2UIComponent::Text {
168 text: A2UITextValue::literal(text),
169 usage_hint: Some("body".to_string()),
170 },
171 },
172 ],
173 },
174 A2UIMessage::BeginRendering {
175 surface_id: "main".to_string(),
176 root: "root".to_string(),
177 },
178 ];
179
180 self.push_a2ui(session, messages).await
181 }
182
183 pub async fn get_surfaces(&self, session: &str) -> HashMap<String, A2UISurface> {
185 self.surfaces
186 .read()
187 .await
188 .get(session)
189 .cloned()
190 .unwrap_or_default()
191 }
192
193 pub async fn reset_a2ui(&self, session: &str) -> Result<()> {
195 self.surfaces.write().await.remove(session);
196 debug!(session, "A2UI state reset");
197 Ok(())
198 }
199
200 pub async fn snapshot(&self, _session: &str) -> Result<Vec<u8>> {
204 anyhow::bail!("Canvas snapshot requires browser automation (not yet implemented)")
207 }
208}