rustyclaw_core/gateway/
canvas_handler.rs1use std::sync::Arc;
4use tokio::sync::Mutex;
5use tracing::{debug, instrument, warn};
6
7use crate::canvas::CanvasHost;
8
9pub type SharedCanvasHost = Arc<Mutex<CanvasHost>>;
10
11pub fn is_canvas_tool(name: &str) -> bool {
13 name.starts_with("canvas_")
14}
15
16#[instrument(skip(args, canvas, session), fields(%name))]
18pub async fn execute_canvas_tool(
19 name: &str,
20 args: &serde_json::Value,
21 canvas: &SharedCanvasHost,
22 session: &str,
23) -> Result<String, String> {
24 debug!("Executing canvas tool");
25
26 let host = canvas.lock().await;
27
28 match name {
29 "canvas_present" => {
30 let url = host.canvas_url(session);
32 Ok(format!("Canvas available at: {}", url))
33 }
34
35 "canvas_hide" => {
36 Ok("Canvas hidden".to_string())
38 }
39
40 "canvas_navigate" => {
41 let url = args
42 .get("url")
43 .and_then(|v| v.as_str())
44 .ok_or_else(|| "Missing required parameter: url".to_string())?;
45
46 if url.starts_with("http://") || url.starts_with("https://") {
48 Ok(format!("Navigated to external URL: {}", url))
49 } else {
50 let canvas_url = format!(
51 "{}{}",
52 host.canvas_url(session),
53 url.trim_start_matches('/')
54 );
55 Ok(format!("Navigated to: {}", canvas_url))
56 }
57 }
58
59 "canvas_write" => {
60 let path = args
61 .get("path")
62 .and_then(|v| v.as_str())
63 .ok_or_else(|| "Missing required parameter: path".to_string())?;
64 let content = args
65 .get("content")
66 .and_then(|v| v.as_str())
67 .ok_or_else(|| "Missing required parameter: content".to_string())?;
68
69 drop(host); let host = canvas.lock().await;
71
72 match host.write_file(session, path, content.as_bytes()).await {
73 Ok(file_path) => Ok(format!("Wrote canvas file: {}", file_path.display())),
74 Err(e) => Err(format!("Failed to write canvas file: {}", e)),
75 }
76 }
77
78 "canvas_a2ui_push" => {
79 let text = args.get("text").and_then(|v| v.as_str());
80 let jsonl = args.get("jsonl").and_then(|v| v.as_str());
81
82 drop(host); let host = canvas.lock().await;
84
85 if let Some(text) = text {
86 match host.push_text(session, text).await {
88 Ok(()) => Ok("A2UI text pushed".to_string()),
89 Err(e) => Err(format!("Failed to push A2UI: {}", e)),
90 }
91 } else if let Some(jsonl) = jsonl {
92 let messages: Result<Vec<crate::canvas::A2UIMessage>, _> = jsonl
94 .lines()
95 .filter(|l| !l.trim().is_empty())
96 .map(|l| serde_json::from_str(l))
97 .collect();
98
99 match messages {
100 Ok(msgs) => match host.push_a2ui(session, msgs).await {
101 Ok(()) => Ok("A2UI messages pushed".to_string()),
102 Err(e) => Err(format!("Failed to push A2UI: {}", e)),
103 },
104 Err(e) => Err(format!("Invalid A2UI JSONL: {}", e)),
105 }
106 } else {
107 Err("Either 'text' or 'jsonl' parameter required".to_string())
108 }
109 }
110
111 "canvas_a2ui_reset" => {
112 drop(host);
113 let host = canvas.lock().await;
114
115 match host.reset_a2ui(session).await {
116 Ok(()) => Ok("A2UI state reset".to_string()),
117 Err(e) => Err(format!("Failed to reset A2UI: {}", e)),
118 }
119 }
120
121 "canvas_snapshot" => {
122 drop(host);
123 let host = canvas.lock().await;
124
125 match host.snapshot(session).await {
126 Ok(data) => {
127 let b64 =
129 base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &data);
130 Ok(format!("data:image/png;base64,{}", b64))
131 }
132 Err(e) => Err(format!("Snapshot failed: {}", e)),
133 }
134 }
135
136 _ => {
137 warn!(tool = name, "Unknown canvas tool");
138 Err(format!("Unknown canvas tool: {}", name))
139 }
140 }
141}
142
143pub fn generate_canvas_prompt_section() -> String {
145 r#"
146## Canvas Tools
147
148Canvas provides an agent-controlled visual workspace for HTML, CSS, JS, and A2UI content.
149
150- **canvas_present**: Show the canvas panel
151- **canvas_hide**: Hide the canvas panel
152- **canvas_navigate**: Navigate to a URL or local path
153 - Parameters: `url` (string) — path or URL to navigate to
154- **canvas_write**: Write a file to the canvas directory
155 - Parameters: `path` (string), `content` (string)
156- **canvas_a2ui_push**: Push A2UI component updates
157 - Parameters: `text` (string) OR `jsonl` (string — A2UI messages as JSONL)
158- **canvas_a2ui_reset**: Reset A2UI state for the session
159- **canvas_snapshot**: Capture canvas as image (returns base64 PNG)
160
161"#
162 .to_string()
163}