5.5 KiB
Canvases
Canvases are interactive artifacts rendered next to the terminal in the session detail view. Instead of scrolling through terminal scrollback to find a diff, a test summary, or PR status, agents and plugins emit structured artifacts that the dashboard renders as standalone panels.
This is AO's answer to Cursor's canvases, shaped for an open-source plugin ecosystem.
Design principles
- Expressive data, constrained UI. Producers supply data in one of a fixed set of renderer types. They do not ship React components. This keeps the dashboard install one-step, avoids running third-party JS in the supervisor, and keeps the rendered surface consistent across every AO instance.
- No new plugin slot. Canvases are session output, not infrastructure. Existing plugins (agent, SCM, tracker) opt in by implementing
CanvasProducer. Core also synthesizes canvases from PR / CI / git data so the feature works without per-agent integration. - Workspace is the source of truth. Agents write canvases to
{workspacePath}/.ao/canvases/{id}.json— same pattern asactivity.jsonl. Core reads, validates, size-caps, and exposes through the dashboard API. The dashboard never reads agent files directly. - Pull, then push. v0.1 polls a REST endpoint on the session detail page. Live updates ride on the existing mux WebSocket later — no new SSE channel.
Renderer types
Defined in packages/core/src/types.ts as CanvasArtifact. Current set:
| Type | Use for | Payload shape |
|---|---|---|
markdown |
Notes, summaries, READMEs, plain text reports | { markdown: string } |
diff |
File changes, patches, reviews | { files: CanvasDiffFile[] } |
table |
Test results, dependency lists, anything tabular | { columns, rows } |
stats |
Cost, token counts, durations, pass/fail counts | { metrics: CanvasStatMetric[] } |
If your data doesn't fit, shape it to fit one of these before reaching for a new type. A "test results" canvas is a table. A "deployment status" canvas is stats plus markdown. Adding a new renderer type requires a core PR — propose it as an issue first with at least two real use cases.
Producing canvases
From an agent (file-based)
Write a JSON file to {workspacePath}/.ao/canvases/{id}.json matching CanvasArtifact. Use a stable id if you want to overwrite the same canvas across runs; use a fresh id to append.
{
"version": 1,
"id": "test-results",
"type": "table",
"title": "Test results",
"createdAt": "2026-05-04T17:00:00Z",
"updatedAt": "2026-05-04T17:00:00Z",
"source": "agent",
"payload": {
"columns": [
{ "key": "name", "label": "Test" },
{ "key": "status", "label": "Status" },
{ "key": "duration_ms", "label": "Duration", "align": "right" }
],
"rows": [
{ "name": "auth.test.ts", "status": "pass", "duration_ms": 142 },
{ "name": "billing.test.ts", "status": "fail", "duration_ms": 89 }
]
}
}
The directory is gitignored and travels with the worktree.
From a plugin (programmatic)
Implement CanvasProducer on any existing plugin (agent, SCM, tracker). Core calls listCanvases when the session detail view loads.
import type { CanvasProducer, CanvasArtifact, Session, ProjectConfig } from "@aoagents/ao-core";
const producer: CanvasProducer = {
async listCanvases(session: Session, project: ProjectConfig): Promise<CanvasArtifact[]> {
return [
{
version: 1,
id: "pr-status",
type: "stats",
title: "PR status",
createdAt: session.createdAt,
updatedAt: new Date().toISOString(),
source: "scm-github",
payload: {
metrics: [
{ label: "CI", value: "passing", tone: "good" },
{ label: "Reviews", value: 2 },
{ label: "Mergeable", value: "yes", tone: "good" },
],
},
},
];
},
};
Returned artifacts are validated and size-capped by core before reaching the dashboard.
Validation rules
Canvases that fail validation are dropped silently and logged. Rules:
versionmust be1.typemust be a knownCanvasType.idmust be[a-z0-9-]{1,64}.- The
core-id prefix is reserved for canvases synthesized by AO core (e.g.core-git-diff). File canvases using this prefix are dropped — pick a different id. - Total serialized size capped at 256 KB per canvas.
- Per-session count capped at 32 canvases (oldest by
updatedAtevicted). - Payload must structurally match the type.
Storage layout
{workspacePath}/.ao/canvases/
test-results.json
diff-summary.json
Synthesized canvases (PR, CI, cost) are computed on read and not persisted. If a canvas needs to survive workspace cleanup, persist it via the session metadata directory in core — not from the producer.
Roadmap
- v0.1 (shipped) — file reader,
GET /api/sessions/[id]/canvases, four built-in renderers,core.git-diffsynthesized canvas, right-rail inSessionDetail(auto-expands when canvases exist), 5s REST poll with visibility-aware pause. - v0.2 —
CanvasProducerinvoked on agent / SCM / tracker plugins. - v0.3 — mux topic for live updates, replacing poll.
- Later, only if justified — sandboxed iframe escape hatch for custom UI, build-time allowlisted renderer packages.
Out of scope indefinitely: dynamic React imports from third-party plugins, write APIs from the dashboard back into canvases, action buttons that mutate session state.