# 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.

## 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": []
}
```
