canvas
v0.1.7Create, edit and render diagrams in the console — stored as editable source, drawn live in chat and on a canvas page.
- macOS: arm64
- Linux: arm64 · armv7 · x64
- Windows: arm64 · x64
exact versions are immutable; binary and bundle artifacts are digest-pinned.
readme
open as markdowncanvas
Diagrams an agent can come back to. A canvas stores its editable source —
mermaid text, or an excalidraw scene for a freeform whiteboard — under a
stable 8-character id, so the architecture sketch drawn in one turn can be
revised ten turns later and every earlier link to it keeps working. The
console does the drawing: a canvas::* call renders in chat as the live
diagram rather than a wall of source, and a canvas page lists, edits and
redraws everything stored. Freeform whiteboards go further: the
canvas::element::* family adds, moves and removes individual shapes one
call at a time, and because every mutation is a state write the open page
streams, the board draws itself while the agent works. For generated
mermaid there is a primer on the bus (canvas::syntax) and a parse check
(canvas::validate), so source validates on the first try instead of
guessing dialect details.
Install
iii trigger compose::add worker=canvas worker=state # state stores canvas recordsiii trigger compose::add resolves the workers and their dependencies, writes
exact declarations to worker-compose.yaml, and reconciles the Compose project.
Companion workers
| Worker | Why |
|---|---|
state |
Required. Every canvas record lives in its canvas scope; the worker holds nothing in process memory, so a restart loses nothing. |
console |
Optional. Renders the #/ext/canvas page and draws canvas::* calls as live diagrams in chat. |
Quickstart
Get the primer, validate, store:
use iii_sdk::protocol::TriggerRequest;
use iii_sdk::{register_worker, InitOptions};
use serde_json::json;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let iii = register_worker("ws://localhost:49134", InitOptions::default());
let call = |id: &str, payload| iii.trigger(TriggerRequest {
function_id: id.into(), payload, action: None, timeout_ms: Some(10_000),
});
// The dialect the renderer actually supports, a working example per family.
let primer = call("canvas::syntax", json!({ "family": "sequence" })).await?;
println!("{}", primer["families"][0]["example"]);
let source = "sequenceDiagram\n Client->>Engine: trigger\n Engine-->>Client: result";
// Parse before storing, so a broken diagram never lands in the store.
let verdict = call("canvas::validate", json!({
"format": "mermaid", "source": source,
})).await?;
// { "valid": true, "family": "sequence", "issues": [] }
assert_eq!(verdict["valid"], true);
let record = call("canvas::create", json!({
"name": "Handshake", "format": "mermaid", "source": source,
})).await?;
// { "id": "a1b2c3d4", "name": "Handshake", "format": "mermaid",
// "family": "sequence", "source": "…", "created_at": …, "updated_at": … }
println!("stored canvas {}", record["id"]);
Ok(())
}Revisions go through canvas::update with the id: the id never changes,
updated_at is stamped, and for mermaid the diagram family is re-derived
from the new source. canvas::get reads one back, canvas::list returns
everything newest first with an optional format filter, and canvas::delete
reports deleted: false on an unknown id rather than erroring.
A freeform canvas takes an excalidraw scene JSON string as its source;
canvas::validate checks the scene's shape the same way it parses mermaid.
Drawing element by element
Freeform whiteboards are also editable one shape at a time — the flow an agent uses to draw something live while a person watches the page:
// Same `call` helper as above. Skeleton shorthand is enough: position,
// size, and a label; the console converts to full shapes at render time.
let board = call("canvas::create", json!({
"name": "Request path", "format": "freeform",
"source": r#"{"type":"excalidraw","version":2,"elements":[]}"#,
})).await?;
let added = call("canvas::element::add", json!({
"id": board["id"],
"elements": [
{ "type": "rectangle", "x": 100, "y": 100, "width": 200, "height": 80,
"label": { "text": "engine" } },
{ "type": "ellipse", "x": 400, "y": 100, "width": 180, "height": 80,
"label": { "text": "worker" } },
{ "type": "arrow", "x": 302, "y": 140, "width": 96, "height": 0 }
],
})).await?;
// { "id": "…", "element_ids": ["…", "…", "…"], "element_count": 3 }canvas::element::list returns the board map (id, type, position, text)
before connecting or moving shapes, canvas::element::update merges
properties into one element by id, and canvas::element::delete removes by
id. The family works on freeform canvases only; mermaid canvases are edited
as text through canvas::update.
Console page
The page at #/ext/canvas lists every stored canvas and opens each one for
editing: mermaid source beside its live rendering, a freeform scene on a
drawable whiteboard. The page streams: records live in the state worker's
canvas scope, so an agent-side create pops into the sidebar (and opens,
when nothing else is), an update redraws the open diagram in place, and
element calls land on the open whiteboard as they happen — no reload,
no polling. Renders sketch themselves in with a stroke animation, mermaid
ships the hand-drawn look, and both panes export SVG and PNG with a
background-or-transparent and light-or-dark choice independent of the
console theme. In chat, a canvas::* call renders as the diagram it
touched, not as JSON.
Configuration
Configuration lives in the configuration worker under the id canvas and
every field hot-reloads — handlers read the live snapshot per call, so
nothing needs a restart.
max_source_bytes: 2097152 # largest canvas source accepted, in bytes
max_list: 200 # most records canvas::list returns in one responseBoth fields are bounds: the first keeps an oversized excalidraw scene or
generated mermaid blob off the state bus, the second caps a list response.
Defaults live in src/config.rs.
Called on demand
This worker registers no harness hook and injects nothing into any prompt. A
conversation that never draws pays nothing for having it installed. An agent
finds it through the function registry and skills/SKILL.md;
a person finds it through the console page.