Files
agcanvas/AGENTS.md
David Ibia ce2079ad95 docs: update README and AGENTS.md with boolean ops, undo tree, new architecture
- Document boolean shape operations feature and boolean_op MCP tool
- Document visual undo tree with Cmd+H shortcut
- Add BooleanOp to WebSocket protocol examples
- Update architecture tree with history.rs, boolean.rs modules
- Add i_overlay and earcutr to dependency table
- Update roadmap: mark boolean ops and undo tree as complete
2026-02-10 00:02:01 +01:00

5.8 KiB

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

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 <test_name>         # 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)

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

// 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<T> 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

#[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<String> },
    Rectangle { rx: Option<f32>, ry: Option<f32> },
}

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:

// src/svg/mod.rs
mod parser;
mod renderer;
pub use parser::parse_svg;
pub use renderer::SvgRenderer;

Async & Logging

// 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

// Arc + RwLock for shared state
tree_handle: Arc<RwLock<Option<ElementTree>>>
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::<AgentRequest>(&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:

// 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"}