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.
- package/bin/lib/commands/push.ts +26 -10
- package/bun.lock +3 -0
- package/daemon/client/AnnotationProvider.tsx +2 -2
- package/daemon/client/App.tsx +125 -47
- package/daemon/client/CompareView.tsx +161 -31
- package/daemon/client/OverviewView.tsx +326 -0
- package/daemon/client/PlanRenderer.tsx +13 -8
- package/daemon/client/RevisionSelect.tsx +16 -7
- package/daemon/client/components/Mermaid.tsx +3 -2
- package/daemon/client/generateMarkdown.ts +24 -5
- package/daemon/client/runtime.ts +9 -2
- package/daemon/client/useTextAnnotation.tsx +6 -2
- package/daemon/dist/client.css +1 -1
- package/daemon/dist/client.js +58 -58
- package/daemon/dist/components.js +23 -23
- package/daemon/dist/runtime.js +1 -1
- package/daemon/src/compiler.ts +100 -0
- package/daemon/src/handlers/api.ts +59 -24
- package/daemon/src/session.ts +140 -41
- package/daemon/src/watcher.ts +42 -30
- package/package.json +4 -3
- package/skills/canvas/SKILL.md +24 -19
- package/skills/canvas/components.md +29 -15
- package/skills/canvas/flows.md +1 -1
package/daemon/src/watcher.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
48
|
-
if (
|
|
49
|
-
|
|
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 [
|
|
56
|
-
|
|
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.
|
|
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",
|
package/skills/canvas/SKILL.md
CHANGED
|
@@ -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
|
|
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}/
|
|
53
|
+
bunx agent-canvas push .claude/agent-canvas/${CLAUDE_SESSION_ID}/ --session ${CLAUDE_SESSION_ID} --label "Implementation Plan"
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
This
|
|
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}/
|
|
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** —
|
|
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}/
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
bunx agent-canvas watch --session ${CLAUDE_SESSION_ID}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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' ? '
|
|
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: '
|
|
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
|
|
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="
|
|
369
|
-
<text x="70" y="40" textAnchor="middle" fill="
|
|
370
|
-
fontFamily="
|
|
371
|
-
|
|
372
|
-
<line x1="130" y1="35" x2="180" y2="35" stroke="
|
|
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="
|
|
377
|
-
<text x="240" y="40" textAnchor="middle" fill="
|
|
378
|
-
fontFamily="
|
|
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="
|
|
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>
|
package/skills/canvas/flows.md
CHANGED
|
@@ -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}
|
|
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
|
|