Files
agcanvas/AGENTS.md
David Ibia 5ca1e85209 feat: clickable zoom reset, Pan tool (H), and batch command support
- Clicking the zoom percentage in the menu bar resets zoom to 100%
- New Pan tool (H key) for explicit left-click-drag panning mode
- Batch command support: agents can send multiple operations in a
  single WebSocket message via {"type": "Batch", "requests": [...]}
  with sequential execution and collected results
- New MCP tool 'batch' accepts a JSON array of request objects
- Nested batches rejected with clear error message
- Updated AGENTS.md with .app rebuild requirement
2026-02-10 10:27:06 +01:00

188 lines
6.3 KiB
Markdown

# 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 <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
./scripts/bundle-macos.sh --install # Rebuild + install .app to /Applications
```
**IMPORTANT:** After every release build or code change that requires testing the running app, you MUST run `./scripts/bundle-macos.sh --install` to update the macOS `.app` bundle in `/Applications`. The running `Augmented Canvas.app` uses a copied binary — a bare `cargo build --release` alone does NOT update it. Kill the running app first with `pkill -f "Augmented Canvas"`, then rebuild and relaunch with `open "/Applications/Augmented Canvas.app"`.
## 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<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
```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<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:
```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<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`:
```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"}
```