agent-canvas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/bin/agent-canvas.js +14 -0
  2. package/bin/agent-canvas.ts +377 -0
  3. package/daemon/build.ts +185 -0
  4. package/daemon/client/AnnotationProvider.tsx +185 -0
  5. package/daemon/client/AnnotationSidebar.tsx +314 -0
  6. package/daemon/client/App.tsx +611 -0
  7. package/daemon/client/CompareView.tsx +333 -0
  8. package/daemon/client/FileBrowser.tsx +188 -0
  9. package/daemon/client/FileIcon.tsx +76 -0
  10. package/daemon/client/FileViewer.tsx +207 -0
  11. package/daemon/client/PlanRenderer.tsx +665 -0
  12. package/daemon/client/Popover.tsx +285 -0
  13. package/daemon/client/ResponsePreview.tsx +260 -0
  14. package/daemon/client/RevisionSelect.tsx +97 -0
  15. package/daemon/client/SessionContext.tsx +3 -0
  16. package/daemon/client/SessionSwitcher.tsx +72 -0
  17. package/daemon/client/annotationContext.ts +114 -0
  18. package/daemon/client/components/Callout.tsx +25 -0
  19. package/daemon/client/components/Checklist.tsx +37 -0
  20. package/daemon/client/components/Choice.tsx +164 -0
  21. package/daemon/client/components/CodeBlock.tsx +35 -0
  22. package/daemon/client/components/Diff.tsx +65 -0
  23. package/daemon/client/components/FilePreview.tsx +88 -0
  24. package/daemon/client/components/Image.tsx +45 -0
  25. package/daemon/client/components/Mermaid.tsx +277 -0
  26. package/daemon/client/components/Note.tsx +13 -0
  27. package/daemon/client/components/Priority.tsx +19 -0
  28. package/daemon/client/components/ResponseNote.tsx +24 -0
  29. package/daemon/client/components/Section.tsx +32 -0
  30. package/daemon/client/components/Table.tsx +35 -0
  31. package/daemon/client/components/Task.tsx +75 -0
  32. package/daemon/client/components/UserInput.tsx +180 -0
  33. package/daemon/client/components/index.ts +15 -0
  34. package/daemon/client/domDiff.ts +336 -0
  35. package/daemon/client/exportMarkdown.ts +267 -0
  36. package/daemon/client/generateMarkdown.ts +194 -0
  37. package/daemon/client/highlightRange.ts +233 -0
  38. package/daemon/client/index.html +26 -0
  39. package/daemon/client/lcs.ts +19 -0
  40. package/daemon/client/popoverPosition.ts +31 -0
  41. package/daemon/client/runtime.ts +89 -0
  42. package/daemon/client/styles.css +306 -0
  43. package/daemon/client/theme.css +66 -0
  44. package/daemon/client/unifiedDiff.ts +604 -0
  45. package/daemon/dist/client.css +2 -0
  46. package/daemon/dist/client.js +68 -0
  47. package/daemon/dist/components.js +5 -0
  48. package/daemon/dist/index.html +108 -0
  49. package/daemon/dist/jsx-dev-runtime-shim.js +17 -0
  50. package/daemon/dist/jsx-runtime-shim.js +17 -0
  51. package/daemon/dist/react-dom-client-shim.js +4 -0
  52. package/daemon/dist/react-dom-shim.js +4 -0
  53. package/daemon/dist/react-shim.js +4 -0
  54. package/daemon/dist/runtime.js +1 -0
  55. package/daemon/dist/theme.css +66 -0
  56. package/daemon/src/compiler.ts +92 -0
  57. package/daemon/src/paths.ts +22 -0
  58. package/daemon/src/server.ts +448 -0
  59. package/daemon/src/session.ts +274 -0
  60. package/daemon/src/watcher.ts +59 -0
  61. package/hooks/session-start.sh +11 -0
  62. package/package.json +39 -0
  63. package/skills/canvas/SKILL.md +174 -0
  64. package/skills/canvas/components.md +349 -0
  65. package/skills/canvas/flows.md +406 -0
@@ -0,0 +1,274 @@
1
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync, rmSync, renameSync } from "fs";
2
+ import { join } from "path";
3
+ import { SESSIONS_DIR } from "./paths";
4
+
5
+ export interface RevisionInfo {
6
+ revision: number;
7
+ label?: string;
8
+ sourceFile?: string;
9
+ createdAt: string;
10
+ hasFeedback: boolean;
11
+ }
12
+
13
+ export interface SessionData {
14
+ id: string;
15
+ projectRoot: string;
16
+ jsx: string;
17
+ currentRevision: number;
18
+ revisions: RevisionInfo[];
19
+ createdAt: string;
20
+ updatedAt: string;
21
+ }
22
+
23
+ interface SessionMeta {
24
+ projectRoot: string;
25
+ createdAt: string;
26
+ updatedAt: string;
27
+ currentRevision: number;
28
+ revisions: RevisionInfo[];
29
+ }
30
+
31
+ // Legacy format
32
+ interface LegacyMeta {
33
+ projectRoot: string;
34
+ createdAt: string;
35
+ updatedAt: string;
36
+ version: number;
37
+ }
38
+
39
+ const STALE_TIMEOUT_MS = 24 * 60 * 60 * 1000; // 24 hours
40
+
41
+ export class SessionManager {
42
+ private sessions = new Map<string, SessionData>();
43
+
44
+ constructor() {
45
+ mkdirSync(SESSIONS_DIR, { recursive: true });
46
+ this.loadFromDisk();
47
+ }
48
+
49
+ private sessionDir(id: string): string {
50
+ return join(SESSIONS_DIR, id);
51
+ }
52
+
53
+ private revisionDir(id: string, rev: number): string {
54
+ return join(this.sessionDir(id), "revisions", String(rev));
55
+ }
56
+
57
+ private loadFromDisk() {
58
+ if (!existsSync(SESSIONS_DIR)) return;
59
+ for (const name of readdirSync(SESSIONS_DIR, { withFileTypes: true })) {
60
+ if (!name.isDirectory()) continue;
61
+ const dir = join(SESSIONS_DIR, name.name);
62
+ const metaPath = join(dir, "meta.json");
63
+ if (!existsSync(metaPath)) continue;
64
+
65
+ try {
66
+ const raw = JSON.parse(readFileSync(metaPath, "utf-8"));
67
+
68
+ // Detect and migrate legacy format
69
+ if ("version" in raw && !("currentRevision" in raw)) {
70
+ this.migrateLegacy(name.name, raw as LegacyMeta);
71
+ // Re-read after migration
72
+ const migrated: SessionMeta = JSON.parse(readFileSync(metaPath, "utf-8"));
73
+ const jsx = this.readRevisionJsx(name.name, migrated.currentRevision);
74
+ if (!jsx) continue;
75
+ this.sessions.set(name.name, {
76
+ id: name.name,
77
+ projectRoot: migrated.projectRoot,
78
+ jsx,
79
+ currentRevision: migrated.currentRevision,
80
+ revisions: migrated.revisions,
81
+ createdAt: migrated.createdAt,
82
+ updatedAt: migrated.updatedAt,
83
+ });
84
+ continue;
85
+ }
86
+
87
+ const meta = raw as SessionMeta;
88
+ const jsx = this.readRevisionJsx(name.name, meta.currentRevision);
89
+ if (!jsx) continue;
90
+
91
+ this.sessions.set(name.name, {
92
+ id: name.name,
93
+ projectRoot: meta.projectRoot,
94
+ jsx,
95
+ currentRevision: meta.currentRevision,
96
+ revisions: meta.revisions,
97
+ createdAt: meta.createdAt,
98
+ updatedAt: meta.updatedAt,
99
+ });
100
+ } catch {}
101
+ }
102
+ }
103
+
104
+ private migrateLegacy(id: string, legacy: LegacyMeta) {
105
+ const dir = this.sessionDir(id);
106
+ const flatJsx = join(dir, "plan.jsx");
107
+ const flatCompiled = join(dir, "plan.compiled.js");
108
+ const historyDir = join(dir, "history");
109
+
110
+ // Collect all historical revisions + current
111
+ const revisions: RevisionInfo[] = [];
112
+
113
+ // Migrate history files
114
+ if (existsSync(historyDir)) {
115
+ const files = readdirSync(historyDir).filter((f) => f.endsWith(".jsx")).sort();
116
+ for (const file of files) {
117
+ const num = parseInt(file.replace(".jsx", ""), 10);
118
+ if (isNaN(num)) continue;
119
+ const revDir = this.revisionDir(id, num);
120
+ mkdirSync(revDir, { recursive: true });
121
+ renameSync(join(historyDir, file), join(revDir, "plan.jsx"));
122
+ revisions.push({ revision: num, createdAt: legacy.createdAt, hasFeedback: false });
123
+ }
124
+ rmSync(historyDir, { recursive: true, force: true });
125
+ }
126
+
127
+ // Migrate current version
128
+ const currentRev = legacy.version;
129
+ const revDir = this.revisionDir(id, currentRev);
130
+ mkdirSync(revDir, { recursive: true });
131
+ if (existsSync(flatJsx)) {
132
+ renameSync(flatJsx, join(revDir, "plan.jsx"));
133
+ }
134
+ if (existsSync(flatCompiled)) {
135
+ renameSync(flatCompiled, join(revDir, "plan.compiled.js"));
136
+ }
137
+ revisions.push({ revision: currentRev, createdAt: legacy.updatedAt, hasFeedback: false });
138
+
139
+ // Write new meta
140
+ const meta: SessionMeta = {
141
+ projectRoot: legacy.projectRoot,
142
+ createdAt: legacy.createdAt,
143
+ updatedAt: legacy.updatedAt,
144
+ currentRevision: currentRev,
145
+ revisions,
146
+ };
147
+ writeFileSync(join(dir, "meta.json"), JSON.stringify(meta, null, 2));
148
+ }
149
+
150
+ private readRevisionJsx(id: string, rev: number): string | null {
151
+ try {
152
+ return readFileSync(join(this.revisionDir(id, rev), "plan.jsx"), "utf-8");
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+
158
+ upsert(id: string, jsx: string, projectRoot: string, label?: string, sourceFile?: string): SessionData {
159
+ const existing = this.sessions.get(id);
160
+ const now = new Date().toISOString();
161
+ const revision = existing ? existing.currentRevision + 1 : 1;
162
+
163
+ const revInfo: RevisionInfo = { revision, createdAt: now, hasFeedback: false, ...(label ? { label } : {}), ...(sourceFile ? { sourceFile } : {}) };
164
+ const revisions = existing ? [...existing.revisions, revInfo] : [revInfo];
165
+
166
+ const session: SessionData = {
167
+ id,
168
+ projectRoot,
169
+ jsx,
170
+ currentRevision: revision,
171
+ revisions,
172
+ createdAt: existing?.createdAt ?? now,
173
+ updatedAt: now,
174
+ };
175
+
176
+ this.sessions.set(id, session);
177
+ this.persistToDisk(session);
178
+ return session;
179
+ }
180
+
181
+ private persistToDisk(session: SessionData) {
182
+ const dir = this.sessionDir(session.id);
183
+ const revDir = this.revisionDir(session.id, session.currentRevision);
184
+ mkdirSync(revDir, { recursive: true });
185
+
186
+ // Write revision JSX
187
+ writeFileSync(join(revDir, "plan.jsx"), session.jsx);
188
+
189
+ // Write meta
190
+ const meta: SessionMeta = {
191
+ projectRoot: session.projectRoot,
192
+ createdAt: session.createdAt,
193
+ updatedAt: session.updatedAt,
194
+ currentRevision: session.currentRevision,
195
+ revisions: session.revisions,
196
+ };
197
+ writeFileSync(join(dir, "meta.json"), JSON.stringify(meta, null, 2));
198
+ }
199
+
200
+ get(id: string): SessionData | undefined {
201
+ return this.sessions.get(id);
202
+ }
203
+
204
+ list(): SessionData[] {
205
+ return Array.from(this.sessions.values());
206
+ }
207
+
208
+ remove(id: string) {
209
+ this.sessions.delete(id);
210
+ rmSync(this.sessionDir(id), { recursive: true, force: true });
211
+ }
212
+
213
+ saveCompiled(id: string, js: string, rev?: number) {
214
+ const session = this.sessions.get(id);
215
+ const revision = rev ?? session?.currentRevision;
216
+ if (!revision) return;
217
+ const revDir = this.revisionDir(id, revision);
218
+ mkdirSync(revDir, { recursive: true });
219
+ writeFileSync(join(revDir, "plan.compiled.js"), js);
220
+ }
221
+
222
+ getCompiled(id: string, rev?: number): string | null {
223
+ const session = this.sessions.get(id);
224
+ const revision = rev ?? session?.currentRevision;
225
+ if (!revision) return null;
226
+ try {
227
+ return readFileSync(join(this.revisionDir(id, revision), "plan.compiled.js"), "utf-8");
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
232
+
233
+ saveFeedback(id: string, rev: number, markdown: string) {
234
+ const revDir = this.revisionDir(id, rev);
235
+ mkdirSync(revDir, { recursive: true });
236
+ writeFileSync(join(revDir, "feedback.md"), markdown);
237
+
238
+ // Update in-memory and on-disk meta
239
+ const session = this.sessions.get(id);
240
+ if (session) {
241
+ const ri = session.revisions.find((r) => r.revision === rev);
242
+ if (ri) ri.hasFeedback = true;
243
+ const meta: SessionMeta = {
244
+ projectRoot: session.projectRoot,
245
+ createdAt: session.createdAt,
246
+ updatedAt: session.updatedAt,
247
+ currentRevision: session.currentRevision,
248
+ revisions: session.revisions,
249
+ };
250
+ writeFileSync(join(this.sessionDir(id), "meta.json"), JSON.stringify(meta, null, 2));
251
+ }
252
+ }
253
+
254
+ getFeedback(id: string, rev: number): string | null {
255
+ try {
256
+ return readFileSync(join(this.revisionDir(id, rev), "feedback.md"), "utf-8");
257
+ } catch {
258
+ return null;
259
+ }
260
+ }
261
+
262
+ getRevisionJsxPath(id: string, rev: number): string {
263
+ return join(this.revisionDir(id, rev), "plan.jsx");
264
+ }
265
+
266
+ cleanupStale(maxAge = STALE_TIMEOUT_MS) {
267
+ const now = Date.now();
268
+ for (const [id, session] of this.sessions) {
269
+ if (now - new Date(session.updatedAt).getTime() > maxAge) {
270
+ this.remove(id);
271
+ }
272
+ }
273
+ }
274
+ }
@@ -0,0 +1,59 @@
1
+ import { watch, existsSync, readFileSync } from "fs";
2
+ import { compilePlan } from "./compiler";
3
+ import type { SessionManager } from "./session";
4
+
5
+ type BroadcastFn = (sessionId: string) => void;
6
+
7
+ const watchers = new Map<string, ReturnType<typeof watch>>();
8
+
9
+ export function watchSession(
10
+ sessionId: string,
11
+ sessionManager: SessionManager,
12
+ broadcast: BroadcastFn,
13
+ ) {
14
+ // Close existing watcher for this session (revision may have changed)
15
+ unwatchSession(sessionId);
16
+
17
+ const session = sessionManager.get(sessionId);
18
+ if (!session) return;
19
+
20
+ const planPath = sessionManager.getRevisionJsxPath(sessionId, session.currentRevision);
21
+ if (!existsSync(planPath)) return;
22
+
23
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
24
+ const rev = session.currentRevision;
25
+
26
+ const watcher = watch(planPath, async (event) => {
27
+ if (event !== "change") return;
28
+
29
+ // Debounce — Claude Code may do multiple rapid edits
30
+ if (debounceTimer) clearTimeout(debounceTimer);
31
+ debounceTimer = setTimeout(async () => {
32
+ try {
33
+ const jsx = readFileSync(planPath, "utf-8");
34
+ const result = await compilePlan(jsx, session.projectRoot);
35
+ if (result.ok) {
36
+ sessionManager.saveCompiled(sessionId, result.js, rev);
37
+ broadcast(sessionId);
38
+ }
39
+ } catch {}
40
+ }, 200);
41
+ });
42
+
43
+ watchers.set(sessionId, watcher);
44
+ }
45
+
46
+ export function unwatchSession(sessionId: string) {
47
+ const watcher = watchers.get(sessionId);
48
+ if (watcher) {
49
+ watcher.close();
50
+ watchers.delete(sessionId);
51
+ }
52
+ }
53
+
54
+ export function unwatchAll() {
55
+ for (const [id, watcher] of watchers) {
56
+ watcher.close();
57
+ }
58
+ watchers.clear();
59
+ }
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+ # Injects CANVAS_SESSION_ID and CANVAS_PROJECT_ROOT into Claude Code environment.
3
+ # Called by Claude Code's SessionStart hook.
4
+ INPUT=$(cat /dev/stdin)
5
+ SESSION_ID=$(echo "$INPUT" | grep -o '"session_id":"[^"]*"' | cut -d'"' -f4)
6
+ CWD=$(echo "$INPUT" | grep -o '"cwd":"[^"]*"' | cut -d'"' -f4)
7
+
8
+ if [ -n "$SESSION_ID" ] && [ -n "$CLAUDE_ENV_FILE" ]; then
9
+ echo "export CANVAS_SESSION_ID=$SESSION_ID" >> "$CLAUDE_ENV_FILE"
10
+ echo "export CANVAS_PROJECT_ROOT=$CWD" >> "$CLAUDE_ENV_FILE"
11
+ fi
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "agent-canvas",
3
+ "version": "0.1.0",
4
+ "description": "Interactive visual canvas for Claude Code — rich annotatable documents in the browser",
5
+ "bin": {
6
+ "agent-canvas": "bin/agent-canvas.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "daemon/src/",
11
+ "daemon/client/",
12
+ "daemon/dist/",
13
+ "daemon/build.ts",
14
+ "daemon/package.json",
15
+ "skills/",
16
+ "hooks/"
17
+ ],
18
+ "scripts": {
19
+ "build": "bun daemon/build.ts",
20
+ "prepublishOnly": "bun daemon/build.ts"
21
+ },
22
+ "dependencies": {
23
+ "react": "^18.3.1",
24
+ "react-dom": "^18.3.1"
25
+ },
26
+ "devDependencies": {
27
+ "@tailwindcss/cli": "4",
28
+ "@types/react": "^18.3.12",
29
+ "@types/react-dom": "^18.3.1",
30
+ "tailwindcss": "4"
31
+ },
32
+ "license": "MIT",
33
+ "keywords": [
34
+ "claude-code",
35
+ "canvas",
36
+ "interactive",
37
+ "annotations"
38
+ ]
39
+ }
@@ -0,0 +1,174 @@
1
+ ---
2
+ name: canvas
3
+ description: >
4
+ Interactive visual canvas for structured communication between agent and user.
5
+ Opens a rich annotatable document in the user's browser where the user reviews,
6
+ comments, answers questions, and submits feedback.
7
+
8
+ Supports planning, architecture reviews, code reviews, discovery interviews,
9
+ implementation summaries, proposals, decision documents, and explanations.
10
+ disable-model-invocation: true
11
+
12
+ ---
13
+
14
+ # Canvas — Interactive Visual Documents
15
+
16
+ Canvas opens a rich, annotatable document in the user's browser. You write JSX, the user reviews and annotates it like Google Docs, and feedback comes back as markdown. Use it whenever structured visual communication beats inline chat.
17
+
18
+ **Read the flow reference before starting**: Check `flows.md` to pick the right flow for your task. Each flow has a specific sequence of canvases with templates.
19
+
20
+ **Read the component reference as needed**: Check `components.md` for the full component API.
21
+
22
+ ## Core Mechanics
23
+
24
+ ### Writing a Canvas
25
+
26
+ Use the **Write** tool to create a `.jsx` file in `.claude/canvas/`:
27
+
28
+ ```jsx
29
+ // Write to: .claude/canvas/plan.jsx
30
+
31
+ <Section title="Authentication Redesign">
32
+ A proposal to replace session-based auth with JWT tokens.
33
+
34
+ <Item label="Current state" badge="context" badgeVariant="info">
35
+ The app uses express-session with Redis store.
36
+ <FilePreview path="src/auth/session.ts" lines={[1, 45]} />
37
+ </Item>
38
+
39
+ <Item label="Proposed change" badge="proposal">
40
+ Switch to stateless JWT with refresh token rotation.
41
+ <Callout type="warning">This will invalidate all existing sessions.</Callout>
42
+ </Item>
43
+ </Section>
44
+ ```
45
+
46
+ Components are auto-available — no imports needed. The file is a JSX fragment (just tags) or a full module with `export default`.
47
+
48
+ ### Pushing
49
+
50
+ After writing the file, push it to open the canvas in the browser:
51
+
52
+ ```bash
53
+ bunx agent-canvas push .claude/canvas/plan.jsx --label "Implementation Plan"
54
+ ```
55
+
56
+ This opens the canvas in the user's browser (first time) and exits immediately. The round label defaults to the filename if `--label` is omitted.
57
+
58
+ ### Waiting for feedback
59
+
60
+ After pushing, wait for the user to review and submit:
61
+
62
+ ```bash
63
+ bunx agent-canvas wait
64
+ ```
65
+
66
+ This **blocks until the user submits feedback**, then prints feedback markdown to stdout. If feedback was already submitted (e.g. retrying after a disconnect), it returns immediately with the existing feedback.
67
+
68
+ ### Iterating
69
+
70
+ Use the **Edit** tool to modify the existing JSX based on feedback — targeted edits, not full rewrites. Then push and wait again:
71
+
72
+ ```bash
73
+ bunx agent-canvas push .claude/canvas/plan.jsx --label "Implementation Plan (revised)"
74
+ bunx agent-canvas wait
75
+ ```
76
+
77
+ Each push creates a new **round** — the user sees previous rounds and their feedback in the UI.
78
+
79
+ ### Multiple Canvases
80
+
81
+ Maintain separate files for different phases:
82
+
83
+ ```bash
84
+ bunx agent-canvas push .claude/canvas/discovery.jsx --label "Discovery"
85
+ bunx agent-canvas wait
86
+ # ... process feedback ...
87
+ bunx agent-canvas push .claude/canvas/requirements.jsx --label "Requirements"
88
+ bunx agent-canvas wait
89
+ # ... process feedback ...
90
+ bunx agent-canvas push .claude/canvas/plan.jsx --label "Implementation Plan"
91
+ bunx agent-canvas wait
92
+ ```
93
+
94
+ ### File Location
95
+
96
+ All canvas files go in `.claude/canvas/` within the project root. Add `.claude/canvas/` to `.gitignore` — these are ephemeral working files.
97
+
98
+ ## Choosing a Flow
99
+
100
+ Determine which flow fits before starting. See `flows.md` for full details with templates.
101
+
102
+ ```
103
+ User wants something built/changed?
104
+ ├─ Vague request ("add auth", "refactor the API")
105
+ │ └─ FEATURE flow: discovery → requirements → plan → [implement] → summary
106
+ ├─ Clear request with specifics
107
+ │ └─ PLAN flow: plan → [implement] → summary
108
+ ├─ Complete instructions, just wants execution
109
+ │ └─ Skip canvas, implement directly, optionally push summary
110
+
111
+ User wants to understand something?
112
+ ├─ "How does X work?", "Explain the auth system"
113
+ │ └─ EXPLAIN flow: single explanatory canvas
114
+
115
+ User wants review/audit?
116
+ ├─ "Review this code", "Check for issues"
117
+ │ └─ REVIEW flow: findings canvas with categorized issues
118
+
119
+ User wants to make a decision?
120
+ ├─ "Should we use X or Y?", "What database?"
121
+ │ └─ DECISION flow: options with tradeoffs, recommendation
122
+ ```
123
+
124
+ **Adapt dynamically**: flows are guidelines, not rigid pipelines. Pivot if needed, skip if the user says so.
125
+
126
+ ## Flow Execution Pattern
127
+
128
+ 1. **Determine flow** from user intent
129
+ 2. **Announce** briefly: "I'll start with discovery, then create a detailed plan."
130
+ 3. **Write canvas JSX** with the Write tool to `.claude/canvas/<name>.jsx`
131
+ 4. **Push**: `bunx agent-canvas push .claude/canvas/<name>.jsx --label "<Label>"`
132
+ 5. **Tell the user** the canvas is ready for review (they see it in their browser)
133
+ 6. **Wait**: `bunx agent-canvas wait` — this blocks until the user submits feedback
134
+ 7. **Read feedback** — check for annotations, answers, added context files, approval
135
+ 8. **Edit with the Edit tool and re-push + wait**, or advance to next phase
136
+ 9. **After implementation**, push a summary canvas
137
+
138
+ ## Interview Integration
139
+
140
+ For discovery phases, use iterative canvases or offer a checklist:
141
+
142
+ ```jsx
143
+ <Section title="Before we start">
144
+ Which areas should we discuss in depth?
145
+
146
+ <MultiChoice id="interview-areas" label="Select areas to cover" required
147
+ options={[
148
+ "Backend architecture & data model",
149
+ "API design & contracts",
150
+ "Frontend / UI behavior",
151
+ "Edge cases & error handling",
152
+ "Performance & scaling",
153
+ "Security considerations",
154
+ "Testing strategy",
155
+ "Migration / deployment"
156
+ ]} />
157
+
158
+ <UserInput id="anything-else" label="Anything else I should know upfront?" />
159
+ </Section>
160
+ ```
161
+
162
+ Then interview deeply in each selected area across subsequent rounds. Or ask in chat first: "Would you prefer a thorough interview before I plan, or should I make assumptions and you correct in review?"
163
+
164
+ ## Important Rules
165
+
166
+ - **Write** canvas files to `.claude/canvas/` using the Write tool. Never use bash heredocs or cat.
167
+ - **Edit** canvas files using the Edit tool. Never rewrite entire files — make targeted edits.
168
+ - JSX can be a fragment (just tags) or a full module with `export default function Canvas()`.
169
+ - Every `<Item>` and interactive component needs a unique `id`.
170
+ - `<FilePreview path="...">` paths are relative to project root.
171
+ - For inline styling use `style={{ ... }}` — Tailwind is NOT available in canvas JSX.
172
+ - Read any files the user added to context (listed under "Added context" in feedback) with the Read tool before the next iteration.
173
+ - Always push a summary canvas after implementation, even if brief.
174
+ - When the user approves, confirm what you'll do next before proceeding.