# AGENTS.md - agcanvas Guidelines for AI agents working in this Rust codebase. ## Project Overview **agcanvas** is a desktop application for agent-human collaboration via SVG visualization. - GUI framework: egui/eframe - SVG parsing: usvg, rendering: resvg/tiny-skia - Async runtime: tokio - WebSocket: tokio-tungstenite - Serialization: serde/serde_json - Error handling: anyhow/thiserror - Boolean ops: i_overlay, triangulation: earcutr - Logging: tracing ## Build/Lint/Test Commands ```bash cargo build # Debug build cargo build --release # Release build (optimized) cargo run --release # Run app cargo clippy # Run clippy lints cargo fmt # Format all code cargo test # Run all tests cargo test # Run single test by name cargo test -- --nocapture # Show println! output cargo check # Type check only (fast) cargo doc --open # Generate and open docs ``` ## Project Structure ``` src/ ├── main.rs # Entry point, window setup ├── app.rs # Main app state, UI (eframe::App impl) ├── session.rs # Session/tab state with history integration ├── history.rs # Undo tree: branching history, snapshots, checkout, fork ├── persistence.rs # Workspace save/load ├── command_palette.rs # Cmd+K fuzzy command palette ├── element_tree.rs # ElementTree, Element, ElementKind types ├── clipboard.rs # System clipboard integration ├── mermaid.rs # Mermaid -> SVG rendering ├── drawing/ │ ├── element.rs # DrawingElement, Shape (incl. Path), ShapeStyle, hit testing │ ├── boolean.rs # Boolean shape ops (union, intersection, difference, xor) │ ├── tool.rs # Tool enum, DragState, ResizeHandle │ ├── render.rs # Shape rendering via egui Painter + triangulation │ └── mod.rs # Re-exports ├── canvas/ │ ├── state.rs # Pan/zoom transformation state │ └── interaction.rs # Mouse/keyboard input handling ├── svg/ │ ├── parser.rs # SVG -> ElementTree conversion │ └── renderer.rs # SVG -> pixels (resvg/tiny-skia) └── agent/ ├── protocol.rs # JSON message types (incl. BooleanOp) └── server.rs # WebSocket server (ws://127.0.0.1:9876) ``` ## Code Style ### Imports (ordered with blank lines between groups) ```rust use std::collections::HashMap; // 1. std use anyhow::Result; // 2. External crates use crate::element_tree::ElementTree; // 3. crate:: use super::protocol::AgentRequest; // 4. super/self ``` ### Naming | Item | Convention | Example | |------|------------|---------| | Types/Structs | PascalCase | `ElementTree`, `CanvasState` | | Functions/Methods | snake_case | `parse_svg`, `find_by_id` | | Constants | SCREAMING_SNAKE | `AGENT_PORT` | | Enum variants | PascalCase | `ElementKind::Rectangle` | ### Error Handling ```rust // Use anyhow::Result for fallible functions pub fn parse_svg(svg_data: &str) -> Result<(ElementTree, Tree)> { let tree = Tree::from_str(svg_data, &options)?; // ? operator Ok((element_tree, tree)) } // Return Option for recoverable errors pub fn find_by_id(&self, id: &str) -> Option<&Element> { ... } // Use anyhow::anyhow!() for error messages .ok_or_else(|| anyhow::anyhow!("Failed to create pixmap"))? ``` ### Struct/Enum Definitions ```rust #[derive(Debug, Clone, Serialize, Deserialize)] // Always derive common traits pub struct ElementTree { pub root: Element, pub metadata: TreeMetadata } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] // Use tag for enum serialization pub enum ElementKind { Group { name: Option }, Rectangle { rx: Option, ry: Option }, } impl Default for CanvasState { // Implement Default for configurable structs fn default() -> Self { Self { offset: Vec2::ZERO, zoom: 1.0, ... } } } ``` ### Module Organization Each module folder has a `mod.rs` that re-exports public items: ```rust // src/svg/mod.rs mod parser; mod renderer; pub use parser::parse_svg; pub use renderer::SvgRenderer; ``` ### Async & Logging ```rust // Spawn async tasks from runtime runtime.spawn(async move { if let Err(e) = server.run().await { tracing::error!("Agent server error: {}", e); } }); // Use tracing macros tracing::info!("Agent server listening on ws://{}", addr); tracing::debug!("Processing request: {:?}", request); ``` ### Common Patterns ```rust // Arc + RwLock for shared state tree_handle: Arc>> let mut guard = tree_handle.write().await; *guard = Some(tree_clone); // Option chaining let svg_data = self.clipboard.as_mut().and_then(|c| c.get_svg()); name.as_ref().map(|n| format!(" '{}'", n)).unwrap_or_default() // Turbofish for type inference serde_json::from_str::(&text) ``` ## Formatting Run `cargo fmt` before committing: - 4-space indentation - Max line width: 100 characters - Trailing commas in multi-line constructs ## What NOT to Do - **No `unwrap()`** in production paths (use `?` or explicit handling) - **No clippy suppression** without justification - **No blocking the main thread** (use async/spawn) - **No `unsafe`** without clear documentation - **No unnecessary dependencies** (consider binary size) ## Agent Protocol WebSocket server on `ws://127.0.0.1:9876`: ```json // Request examples {"type": "GetTree"} {"type": "GetElementById", "id": "button-1"} {"type": "Describe"} {"type": "GenerateCode", "target": "react", "element_id": null} {"type": "BooleanOp", "operation": "union", "element_ids": ["id1", "id2"], "consume_sources": true} // Response {"type": "Tree", "tree": {...}} {"type": "Error", "message": "No SVG loaded"} ```