agent-canvas 0.1.13 → 0.1.15

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.
@@ -1,59 +1,71 @@
1
- import { watch, existsSync, readFileSync } from "fs";
1
+ import { watch, existsSync, readFileSync, readdirSync } from "fs";
2
+ import { join } from "path";
2
3
  import { compilePlan } from "./compiler";
3
4
  import type { SessionManager } from "./session";
4
5
 
5
6
  type BroadcastFn = (sessionId: string) => void;
6
7
 
7
- const watchers = new Map<string, ReturnType<typeof watch>>();
8
+ const watchers = new Map<string, ReturnType<typeof watch>[]>();
8
9
 
9
10
  export function watchSession(
10
11
  sessionId: string,
11
12
  sessionManager: SessionManager,
12
13
  broadcast: BroadcastFn,
13
14
  ) {
14
- // Close existing watcher for this session (revision may have changed)
15
+ // Close existing watchers for this session
15
16
  unwatchSession(sessionId);
16
17
 
17
18
  const session = sessionManager.get(sessionId);
18
19
  if (!session) return;
19
20
 
20
- const planPath = sessionManager.getRevisionJsxPath(sessionId, session.currentRevision);
21
- if (!existsSync(planPath)) return;
22
-
23
- let debounceTimer: ReturnType<typeof setTimeout> | null = null;
24
21
  const rev = session.currentRevision;
22
+ const canvasFiles = sessionManager.getRevisionCanvasFiles(sessionId, rev);
23
+ if (canvasFiles.length === 0) return;
24
+
25
+ const fileWatchers: ReturnType<typeof watch>[] = [];
26
+ const debounceTimers = new Map<string, ReturnType<typeof setTimeout>>();
27
+
28
+ for (const filename of canvasFiles) {
29
+ const filePath = sessionManager.getRevisionJsxPath(sessionId, rev, filename);
30
+ if (!existsSync(filePath)) continue;
31
+
32
+ const watcher = watch(filePath, async (event) => {
33
+ if (event !== "change") return;
34
+
35
+ // Debounce per-file
36
+ const existing = debounceTimers.get(filename);
37
+ if (existing) clearTimeout(existing);
38
+
39
+ debounceTimers.set(filename, setTimeout(async () => {
40
+ debounceTimers.delete(filename);
41
+ try {
42
+ const jsx = readFileSync(filePath, "utf-8");
43
+ const result = await compilePlan(jsx, session.projectRoot);
44
+ if (result.ok) {
45
+ sessionManager.saveCompiled(sessionId, filename, result.js, rev);
46
+ broadcast(sessionId);
47
+ }
48
+ } catch {}
49
+ }, 200));
50
+ });
51
+
52
+ fileWatchers.push(watcher);
53
+ }
25
54
 
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);
55
+ watchers.set(sessionId, fileWatchers);
44
56
  }
45
57
 
46
58
  export function unwatchSession(sessionId: string) {
47
- const watcher = watchers.get(sessionId);
48
- if (watcher) {
49
- watcher.close();
59
+ const sessionWatchers = watchers.get(sessionId);
60
+ if (sessionWatchers) {
61
+ for (const w of sessionWatchers) w.close();
50
62
  watchers.delete(sessionId);
51
63
  }
52
64
  }
53
65
 
54
66
  export function unwatchAll() {
55
- for (const [id, watcher] of watchers) {
56
- watcher.close();
67
+ for (const [, sessionWatchers] of watchers) {
68
+ for (const w of sessionWatchers) w.close();
57
69
  }
58
70
  watchers.clear();
59
71
  }
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "agent-canvas",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Interactive visual canvas for Claude Code — rich annotatable documents in the browser",
5
5
  "bin": {
6
6
  "agent-canvas": "bin/agent-canvas.js"
7
7
  },
8
- "scripts": {
8
+ "scripts": {
9
9
  "build": "bun daemon/build.ts",
10
10
  "typecheck": "tsc --noEmit && tsc --noEmit -p daemon/tsconfig.json",
11
11
  "prepublishOnly": "bun daemon/build.ts"
12
12
  },
13
13
  "dependencies": {
14
14
  "marked": "^17.0.4",
15
- "preact": "^10.25.0"
15
+ "preact": "^10.25.0",
16
+ "preact-render-to-string": "^6.6.6"
16
17
  },
17
18
  "devDependencies": {
18
19
  "@tailwindcss/cli": "4",
@@ -47,13 +47,13 @@ Components are auto-available — no imports needed. The file is a JSX fragment
47
47
 
48
48
  ### Pushing
49
49
 
50
- After writing the file, push it to open the canvas in the browser:
50
+ After writing canvas files, push the directory to open all canvases in the browser:
51
51
 
52
52
  ```bash
53
- bunx agent-canvas push .claude/agent-canvas/${CLAUDE_SESSION_ID}/plan.jsx --session ${CLAUDE_SESSION_ID} --label "Implementation Plan"
53
+ bunx agent-canvas push .claude/agent-canvas/${CLAUDE_SESSION_ID}/ --session ${CLAUDE_SESSION_ID} --label "Implementation Plan"
54
54
  ```
55
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. **Always show the `browserUrl` from the push output to the user** so they can open the canvas manually if auto-open didn't work.
56
+ This pushes all `*.jsx` files in the directory as a snapshot (like a git commit). Each file appears as a separate tab in the browser. The canvas opens in the user's browser (first time) and the command exits immediately. **Always show the `browserUrl` from the push output to the user** so they can open the canvas manually if auto-open didn't work.
57
57
 
58
58
  ### Waiting for feedback
59
59
 
@@ -85,20 +85,20 @@ Returns immediately — prints feedback to stdout if available, otherwise produc
85
85
  Use the **Edit** tool to modify the existing JSX based on feedback — targeted edits, not full rewrites. Then push and watch again:
86
86
 
87
87
  ```bash
88
- bunx agent-canvas push .claude/agent-canvas/${CLAUDE_SESSION_ID}/plan.jsx --session ${CLAUDE_SESSION_ID} --label "Implementation Plan (revised)"
88
+ bunx agent-canvas push .claude/agent-canvas/${CLAUDE_SESSION_ID}/ --session ${CLAUDE_SESSION_ID} --label "Implementation Plan (revised)"
89
89
 
90
90
  # Use Bash tool with run_in_background: true
91
91
  bunx agent-canvas watch --session ${CLAUDE_SESSION_ID}
92
92
  ```
93
93
 
94
- Each push creates a new **round** — the user sees previous rounds and their feedback in the UI.
94
+ Each push creates a new **round** (revision) — all canvas files are snapshotted. The user sees which files changed in the revision selector.
95
95
 
96
96
  ### Responding to feedback
97
97
 
98
98
  When pushing a revised canvas after receiving user feedback, use `--response` to tell the user how you addressed their feedback. The response renders as a short banner at the top of the canvas.
99
99
 
100
100
  ```bash
101
- bunx agent-canvas push .claude/agent-canvas/${CLAUDE_SESSION_ID}/plan.jsx --session ${CLAUDE_SESSION_ID} --label "Plan (revised)" --response "Incorporated all feedback. Switched to connection pooling as suggested. I kept the sync approach for writes — see the note in Phase 2 for my reasoning."
101
+ bunx agent-canvas push .claude/agent-canvas/${CLAUDE_SESSION_ID}/ --session ${CLAUDE_SESSION_ID} --label "Plan (revised)" --response "Incorporated all feedback. Switched to connection pooling as suggested. I kept the sync approach for writes — see the note in Phase 2 for my reasoning."
102
102
  ```
103
103
 
104
104
  Use `--response` to:
@@ -110,20 +110,25 @@ Keep it concise (2-4 sentences). The user can see the diff in the canvas UI, so
110
110
 
111
111
  ### Multiple Canvases
112
112
 
113
- Maintain separate files for different phases:
113
+ All `*.jsx` files in the canvas directory are pushed together as a snapshot. Each file appears as a separate tab in the browser. Use different files for different phases or concerns:
114
+
115
+ ```
116
+ .claude/agent-canvas/${CLAUDE_SESSION_ID}/
117
+ discovery.jsx # Discovery interview
118
+ requirements.jsx # Requirements spec
119
+ plan.jsx # Implementation plan
120
+ ```
121
+
122
+ Write new files as phases progress, then push the directory. Previous phase files remain visible as context. The user can annotate any canvas and submit one combined feedback per revision.
114
123
 
115
124
  ```bash
116
- bunx agent-canvas push .claude/agent-canvas/${CLAUDE_SESSION_ID}/discovery.jsx --session ${CLAUDE_SESSION_ID} --label "Discovery"
117
- # watch with run_in_background: true, wait for notification
118
- bunx agent-canvas watch --session ${CLAUDE_SESSION_ID}
119
- # ... process feedback ...
120
- bunx agent-canvas push .claude/agent-canvas/${CLAUDE_SESSION_ID}/requirements.jsx --session ${CLAUDE_SESSION_ID} --label "Requirements"
121
- # watch with run_in_background: true, wait for notification
122
- bunx agent-canvas watch --session ${CLAUDE_SESSION_ID}
123
- # ... process feedback ...
124
- bunx agent-canvas push .claude/agent-canvas/${CLAUDE_SESSION_ID}/plan.jsx --session ${CLAUDE_SESSION_ID} --label "Implementation Plan"
125
- # watch with run_in_background: true, wait for notification
126
- bunx agent-canvas watch --session ${CLAUDE_SESSION_ID}
125
+ # Write discovery.jsx first, push and get feedback
126
+ bunx agent-canvas push .claude/agent-canvas/${CLAUDE_SESSION_ID}/ --session ${CLAUDE_SESSION_ID} --label "Discovery"
127
+ bunx agent-canvas watch --session ${CLAUDE_SESSION_ID} # run_in_background: true
128
+
129
+ # Later, add requirements.jsx, push again — both files visible as tabs
130
+ bunx agent-canvas push .claude/agent-canvas/${CLAUDE_SESSION_ID}/ --session ${CLAUDE_SESSION_ID} --label "Requirements"
131
+ bunx agent-canvas watch --session ${CLAUDE_SESSION_ID} # run_in_background: true
127
132
  ```
128
133
 
129
134
  ### File Location
@@ -163,7 +168,7 @@ User wants to make a decision?
163
168
  1. **Determine flow** from user intent
164
169
  2. **Announce** briefly: "I'll start with discovery, then create a detailed plan."
165
170
  3. **Write canvas JSX** with the Write tool to `.claude/agent-canvas/${CLAUDE_SESSION_ID}/<name>.jsx`
166
- 4. **Push + tell the user**: Push the canvas and show the `browserUrl` from the output so the user can open it
171
+ 4. **Push the directory + tell the user**: Push with `bunx agent-canvas push .claude/agent-canvas/${CLAUDE_SESSION_ID}/ --session ${CLAUDE_SESSION_ID}` and show the `browserUrl` from the output so the user can open it
167
172
  5. **IMMEDIATELY watch in background** — run `bunx agent-canvas watch --session ${CLAUDE_SESSION_ID}` with `run_in_background: true` right after push. Never stop after pushing. The push→watch sequence is atomic: no push without a watch. You will be notified when feedback arrives — do not poll or sleep.
168
173
  6. **Read feedback** — check for annotations, answers, added context files, approval
169
174
  7. **Edit with the Edit tool and re-push + watch**, or advance to next phase
@@ -3,6 +3,20 @@
3
3
  All components are available globally in canvas JSX — no imports needed.
4
4
  For inline styling use `style={{ ... }}`. Tailwind classes are NOT available.
5
5
 
6
+ ## Theming
7
+
8
+ **Never hardcode hex colors** — canvas supports light/dark themes. Use CSS variables in `style={{ }}`:
9
+
10
+ - **Text:** `--color-text-primary`, `--color-text-secondary`, `--color-text-tertiary`, `--color-text-code`, `--color-text-inverse`
11
+ - **Backgrounds:** `--color-bg-base`, `--color-bg-surface`, `--color-bg-elevated`, `--color-bg-code`, `--color-bg-input`
12
+ - **Accents:** `--color-accent-{green,amber,red,blue}`, `--color-accent-{green,amber,red,blue}-muted`
13
+ - **Borders:** `--color-border-subtle`, `--color-border-medium`, `--color-border-hover`
14
+ - **Fonts:** `--font-heading`, `--font-body`, `--font-mono`
15
+
16
+ ```jsx
17
+ <span style={{ color: 'var(--color-text-tertiary)', fontSize: '0.75rem' }}>meta text</span>
18
+ ```
19
+
6
20
  ---
7
21
 
8
22
  ## Layout
@@ -333,10 +347,10 @@ When you use the full module format (`export default function Canvas()`), you ca
333
347
 
334
348
  ```jsx
335
349
  function Metric({ label, value, trend }) {
336
- const color = trend === 'up' ? '#4a9e6d' : trend === 'down' ? '#c45a5a' : '#a09a92';
350
+ const color = trend === 'up' ? 'var(--color-accent-green)' : trend === 'down' ? 'var(--color-accent-red)' : 'var(--color-text-secondary)';
337
351
  return (
338
352
  <div style={{ display: 'flex', gap: 12, alignItems: 'baseline' }}>
339
- <span style={{ color: '#6b6560', fontSize: '0.75rem' }}>{label}</span>
353
+ <span style={{ color: 'var(--color-text-tertiary)', fontSize: '0.75rem' }}>{label}</span>
340
354
  <span style={{ color, fontSize: '1.25rem', fontWeight: 600 }}>{value}</span>
341
355
  </div>
342
356
  );
@@ -360,27 +374,27 @@ Standard components remain available without import even in full module format.
360
374
  ## Diagrams with Raw SVG
361
375
 
362
376
  For custom visuals that Mermaid can't express, write `<svg>` directly.
363
- Use Canvas CSS variables for colors to match the theme:
377
+ Use CSS variables for colors to match the theme automatically:
364
378
 
365
379
  ```jsx
366
380
  <svg viewBox="0 0 400 200" style={{ width: '100%', maxWidth: 400 }}>
367
- <rect x="10" y="10" width="120" height="50" rx="8"
368
- fill="rgba(90, 142, 196, 0.12)" stroke="rgba(90, 142, 196, 0.3)" />
369
- <text x="70" y="40" textAnchor="middle" fill="#e8e4df" fontSize="13"
370
- fontFamily="Inter, sans-serif">API Gateway</text>
371
-
372
- <line x1="130" y1="35" x2="180" y2="35" stroke="#6b6560" strokeWidth="1.5"
381
+ <rect x="10" y="10" width="120" height="50" rx="8"
382
+ fill="var(--color-accent-blue-muted)" stroke="var(--color-accent-blue)" strokeOpacity="0.3" />
383
+ <text x="70" y="40" textAnchor="middle" fill="var(--color-text-primary)" fontSize="13"
384
+ fontFamily="var(--font-body)">API Gateway</text>
385
+
386
+ <line x1="130" y1="35" x2="180" y2="35" stroke="var(--color-text-tertiary)" strokeWidth="1.5"
373
387
  markerEnd="url(#arrow)" />
374
-
388
+
375
389
  <rect x="180" y="10" width="120" height="50" rx="8"
376
- fill="rgba(74, 158, 109, 0.12)" stroke="rgba(74, 158, 109, 0.3)" />
377
- <text x="240" y="40" textAnchor="middle" fill="#e8e4df" fontSize="13"
378
- fontFamily="Inter, sans-serif">Auth Service</text>
379
-
390
+ fill="var(--color-accent-green-muted)" stroke="var(--color-accent-green)" strokeOpacity="0.3" />
391
+ <text x="240" y="40" textAnchor="middle" fill="var(--color-text-primary)" fontSize="13"
392
+ fontFamily="var(--font-body)">Auth Service</text>
393
+
380
394
  <defs>
381
395
  <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6"
382
396
  orient="auto-start-reverse">
383
- <path d="M 0 0 L 10 5 L 0 10 z" fill="#6b6560" />
397
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="var(--color-text-tertiary)" />
384
398
  </marker>
385
399
  </defs>
386
400
  </svg>
@@ -2,7 +2,7 @@
2
2
 
3
3
  Each flow is a sequence of canvas rounds. Phases can be skipped or combined based on context.
4
4
 
5
- **Tooling reminder**: Write all `.jsx` files using the **Write** tool to `.claude/agent-canvas/${CLAUDE_SESSION_ID}/`. Edit them with the **Edit** tool. Push with `bunx agent-canvas push .claude/agent-canvas/${CLAUDE_SESSION_ID}/<file>.jsx --session ${CLAUDE_SESSION_ID} --label "<Label>"`, then run `bunx agent-canvas watch --session ${CLAUDE_SESSION_ID}` with `run_in_background: true` — you'll be notified when the user submits feedback. The JSX blocks below show what to write — they are the file content, not bash commands.
5
+ **Tooling reminder**: Write all `.jsx` files using the **Write** tool to `.claude/agent-canvas/${CLAUDE_SESSION_ID}/`. Edit them with the **Edit** tool. Push the directory with `bunx agent-canvas push .claude/agent-canvas/${CLAUDE_SESSION_ID}/ --session ${CLAUDE_SESSION_ID} --label "<Label>"`, then run `bunx agent-canvas watch --session ${CLAUDE_SESSION_ID}` with `run_in_background: true` — you'll be notified when the user submits feedback. All `.jsx` files in the directory are pushed as a snapshot — each shows as a tab in the browser. The JSX blocks below show what to write — they are the file content, not bash commands.
6
6
 
7
7
  ---
8
8