# canvas

> Create, edit and render diagrams in the console — stored as editable source, drawn live in chat and on a canvas page.

| field | value |
|-------|-------|
| version | 0.1.13 |
| type | binary |
| license | Apache-2.0 |
| repo | https://github.com/iii-hq/workers |
| supported_targets | aarch64-apple-darwin, x86_64-pc-windows-msvc, aarch64-pc-windows-msvc, x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-unknown-linux-musl, armv7-unknown-linux-gnueabihf |
| author | iii |

## installation

```sh
iii trigger compose::add worker=canvas@0.1.13
```

## dependencies

- `configuration` @ `latest`
- `state` @ `latest`

## readme

# canvas

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

```bash
iii trigger compose::add worker=canvas worker=state # state stores canvas records
```

`iii 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`](https://github.com/iii-hq/workers/tree/main/state) | Required. Every canvas record lives in its `canvas` scope; the worker holds nothing in process memory, so a restart loses nothing. |
| [`console`](https://github.com/iii-hq/workers/tree/main/ade) | Optional. Renders the `canvas` page and draws `canvas::*` calls as live diagrams in chat. |

## Quickstart

Get the primer, validate, store:

```rust
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:

```rust
// 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 `canvas` page 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.

```yaml
max_source_bytes: 2097152   # largest canvas source accepted, in bytes
max_list: 200               # most records canvas::list returns in one response
```

Both 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`](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`](skills/SKILL.md);
a person finds it through the console page.

## skills

This version ships 1 skill file for agents, also browsable on the worker page:

- [`SKILL.md`](https://workers.iii.dev/workers/canvas?tab=skills&skill=SKILL.md&version=0.1.13)

### SKILL.md

# canvas

The canvas worker stores diagrams as editable source. A canvas is a named
record — mermaid text, or an excalidraw scene JSON for a freeform whiteboard —
kept under a stable 8-character id. The worker never renders anything; the
console does the drawing, so a `canvas::*` call appears in chat as the live
diagram and the canvas page lists and edits everything stored. The source is
the artifact, which is what keeps a diagram editable instead of frozen into
pixels.

Ids are stable across updates on purpose: revise a diagram as the
conversation evolves and every earlier reference to it keeps working. Records
persist in the `state` worker, so nothing is lost across a restart.

Mermaid should not be written from memory. `canvas::syntax` returns the
families the renderer actually supports with a working example each, and
`canvas::validate` parses source without storing it — together they mean a
broken diagram never lands in the store or renders as an error card.

## When to Use

- A diagram would say it better than prose — an architecture, a sequence of
  calls, a state machine, an ER model: create a mermaid canvas and let the
  console draw it.
- About to write mermaid source: call `canvas::syntax` first, narrowed to the
  family being written.
- Generated source in hand: `canvas::validate` before `canvas::create` or
  `canvas::update`.
- A diagram needs revising: `canvas::update` on the existing id, not a new
  canvas — the id is what earlier references point at.
- A spatial sketch a person will edit by hand on a whiteboard: create with
  format `freeform` and an excalidraw scene JSON as the source.
- Drawing while a person watches: create a freeform canvas, then
  `canvas::element::add` one shape or small group per call — the open
  console board shows each step as it lands.
- Finding what is already drawn: `canvas::list`, newest first, with an
  optional format filter; `canvas::element::list` for the shapes on one
  freeform board.

## Boundaries

- Nothing here renders. Headless callers get source back, never SVG or
  pixels; drawing happens in the console.
- Not a file store and not a document editor: the source is one diagram,
  capped by the configured `max_source_bytes`. Files belong to the shell
  worker.
- `canvas::validate` checks that source parses, not that the diagram is any
  good — a valid diagram can still be the wrong diagram.
- A canvas's format is fixed at creation; `canvas::update` changes name and
  source, not format.
- `canvas::delete` is idempotent (`deleted=false` for an unknown id); the
  other id-taking functions error on unknown ids.

## Functions

- `canvas::create` — store a new canvas from mermaid text or an excalidraw
  scene JSON; mints the stable id and derives the mermaid family.
- `canvas::get` — read one canvas by id, editable source included.
- `canvas::list` — every stored canvas, newest first, optionally filtered by
  format; capped by the configured `max_list`.
- `canvas::update` — revise a canvas's name and/or source by id; the id never
  changes and the mermaid family is re-derived.
- `canvas::delete` — remove one canvas by id; unknown ids report
  `deleted=false` rather than erroring.
- `canvas::syntax` — the mermaid syntax reference: every supported diagram
  family with a summary and a working example, optionally one family.
- `canvas::validate` — parse source without storing it; reports validity, the
  derived family, and per-issue messages with line numbers where known.
- `canvas::element::add` — append shapes to a freeform canvas one drawing
  step at a time; the open console whiteboard shows each call as it lands.
- `canvas::element::update` — merge properties into one element of a
  freeform canvas by element id: move, recolor, or relabel a shape.
- `canvas::element::delete` — remove elements of a freeform canvas by
  element id; unknown ids are ignored and the response reports the count.
- `canvas::element::list` — the freeform board map: id, type, position,
  size, and text per element, read before updating or connecting shapes.

The `element` family works only on `format: freeform` canvases (excalidraw
scene source). Mermaid canvases are edited as text through `canvas::update`.

## api reference

```json
{
  "functions": [
    {
      "description": "Create a new canvas from mermaid text or an excalidraw scene JSON. Returns the stored record, including the minted stable id and, for mermaid, the diagram family derived from the source.",
      "metadata": {},
      "name": "canvas::create",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "CanvasFormat": {
            "description": "Diagram format a canvas holds.",
            "oneOf": [
              {
                "description": "Mermaid text — rendered from source by the console.",
                "enum": [
                  "mermaid"
                ],
                "type": "string"
              },
              {
                "description": "A freeform whiteboard — the source is an excalidraw scene JSON.",
                "enum": [
                  "freeform"
                ],
                "type": "string"
              }
            ]
          }
        },
        "properties": {
          "format": {
            "anyOf": [
              {
                "$ref": "#/definitions/CanvasFormat"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Diagram format: `mermaid` or `freeform`. Defaults to `mermaid`."
          },
          "name": {
            "default": null,
            "description": "Human-readable canvas name. Omit for a name derived from the detected diagram family (`Untitled flowchart`, `Untitled whiteboard`, …).",
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "description": "The editable source: mermaid text for `mermaid`, an excalidraw scene JSON string for `freeform`.",
            "type": "string"
          }
        },
        "required": [
          "source"
        ],
        "title": "Request",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "CanvasFormat": {
            "description": "Diagram format a canvas holds.",
            "oneOf": [
              {
                "description": "Mermaid text — rendered from source by the console.",
                "enum": [
                  "mermaid"
                ],
                "type": "string"
              },
              {
                "description": "A freeform whiteboard — the source is an excalidraw scene JSON.",
                "enum": [
                  "freeform"
                ],
                "type": "string"
              }
            ]
          }
        },
        "description": "One stored canvas. The id is a stable 8-character slug that never changes across updates; `source` is always the editable source of truth (mermaid text, or the excalidraw scene JSON for freeform).",
        "properties": {
          "created_at": {
            "description": "Creation time, unix seconds.",
            "format": "int64",
            "type": "integer"
          },
          "family": {
            "description": "Mermaid diagram family (`flowchart`, `sequenceDiagram`, …), derived from the source. `null` for a freeform canvas.",
            "type": [
              "string",
              "null"
            ]
          },
          "format": {
            "allOf": [
              {
                "$ref": "#/definitions/CanvasFormat"
              }
            ],
            "description": "Diagram format: `mermaid` or `freeform`."
          },
          "id": {
            "description": "Stable 8-character slug identifying this canvas. Never changes across updates.",
            "type": "string"
          },
          "name": {
            "description": "Human-readable canvas name.",
            "type": "string"
          },
          "source": {
            "description": "The editable source of truth: mermaid text for `mermaid`, the excalidraw scene JSON for `freeform`.",
            "type": "string"
          },
          "updated_at": {
            "description": "Last update time, unix seconds.",
            "format": "int64",
            "type": "integer"
          }
        },
        "required": [
          "created_at",
          "format",
          "id",
          "name",
          "source",
          "updated_at"
        ],
        "title": "CanvasRecord",
        "type": "object"
      }
    }
  ],
  "triggers": []
}
```
