- 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
188 lines
6.3 KiB
Markdown
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"}
|
|
```
|