agent-canvas 0.1.9 → 0.1.11
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/.github/dark.png +0 -0
- package/.github/light.png +0 -0
- package/README.md +10 -0
- package/daemon/build.ts +1 -1
- package/daemon/client/AnnotationEditor.tsx +307 -0
- package/daemon/client/AnnotationProvider.tsx +15 -3
- package/daemon/client/AnnotationSidebar.tsx +54 -28
- package/daemon/client/App.tsx +64 -24
- package/daemon/client/CompareView.tsx +53 -22
- package/daemon/client/FileBrowser.tsx +2 -1
- package/daemon/client/FileViewer.tsx +41 -161
- package/daemon/client/PlanRenderer.tsx +30 -147
- package/daemon/client/Popover.tsx +168 -108
- package/daemon/client/ResponsePreview.tsx +7 -1
- package/daemon/client/RevisionSelect.tsx +8 -1
- package/daemon/client/annotationDom.ts +104 -0
- package/daemon/client/components/Mermaid.tsx +2 -2
- package/daemon/client/generateMarkdown.ts +8 -0
- package/daemon/client/highlightRange.ts +1 -22
- package/daemon/client/index.html +1 -1
- package/daemon/client/runtime.ts +4 -1
- package/daemon/client/styles.css +28 -2
- package/daemon/client/theme.css +1 -1
- package/daemon/client/useTextAnnotation.tsx +183 -0
- package/daemon/dist/client.css +1 -1
- package/daemon/dist/client.js +63 -87
- package/daemon/dist/components.js +54 -54
- package/daemon/dist/index.html +2 -2
- package/daemon/src/handlers/uploads.ts +62 -0
- package/daemon/src/paths.ts +4 -0
- package/daemon/src/server.ts +2 -0
- package/daemon/src/session.ts +39 -1
- package/package.json +1 -1
- package/skills/canvas/SKILL.md +15 -7
- package/skills/canvas/flows.md +1 -1
- package/hooks/session-start.sh +0 -13
package/daemon/dist/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>Agent Canvas</title>
|
|
7
|
-
<script
|
|
7
|
+
<script>!function(){var p=localStorage.getItem('canvas-theme')||'auto';document.documentElement.dataset.theme=p==='auto'?(matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light'):p}()</script>
|
|
8
8
|
<link rel="stylesheet" href="/assets/client.css" />
|
|
9
9
|
<link rel="stylesheet" href="https://unpkg.com/@highlightjs/cdn-assets@11.11.1/styles/github-dark-dimmed.min.css" />
|
|
10
10
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
--color-highlight-bg: rgba(255, 220, 100, 0.15);
|
|
43
43
|
--color-highlight-border: rgba(255, 220, 100, 0.35);
|
|
44
44
|
--color-highlight-annotation: rgba(255, 220, 100, 0.18);
|
|
45
|
-
--color-highlight-active: rgba(255, 220, 100, 0.
|
|
45
|
+
--color-highlight-active: rgba(255, 220, 100, 0.35);
|
|
46
46
|
--color-highlight-selected: rgba(255, 220, 100, 0.10);
|
|
47
47
|
|
|
48
48
|
--color-badge-bg: rgba(196, 154, 58, 0.20);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { UPLOADS_DIR } from "../paths";
|
|
4
|
+
import type { SessionManager } from "../session";
|
|
5
|
+
import { jsonResponse } from "./utils";
|
|
6
|
+
import type { Route } from "../router";
|
|
7
|
+
|
|
8
|
+
const IMAGE_MIME: Record<string, string> = {
|
|
9
|
+
png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg",
|
|
10
|
+
gif: "image/gif", svg: "image/svg+xml", webp: "image/webp",
|
|
11
|
+
bmp: "image/bmp", avif: "image/avif",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function createUploadHandlers(sessionManager: SessionManager): Route[] {
|
|
15
|
+
async function handleUpload(req: Request, _url: URL, match: URLPatternResult): Promise<Response> {
|
|
16
|
+
const sessionId = match.pathname.groups.id!;
|
|
17
|
+
if (!sessionManager.get(sessionId)) {
|
|
18
|
+
return jsonResponse({ error: "Session not found" }, 404);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const formData = await req.formData();
|
|
22
|
+
const file = formData.get("image") as File | null;
|
|
23
|
+
if (!file) return jsonResponse({ error: "No image provided" }, 400);
|
|
24
|
+
if (!file.type.startsWith("image/")) return jsonResponse({ error: "Not an image" }, 400);
|
|
25
|
+
if (file.size > 10 * 1024 * 1024) return jsonResponse({ error: "Image too large (max 10MB)" }, 413);
|
|
26
|
+
|
|
27
|
+
const ext = file.name.split(".").pop()?.toLowerCase() || "png";
|
|
28
|
+
const filename = `${crypto.randomUUID()}.${ext}`;
|
|
29
|
+
const filepath = join(UPLOADS_DIR, filename);
|
|
30
|
+
|
|
31
|
+
const buffer = await file.arrayBuffer();
|
|
32
|
+
writeFileSync(filepath, Buffer.from(buffer));
|
|
33
|
+
|
|
34
|
+
return jsonResponse({ path: filepath, filename });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function handleUploadServe(_req: Request, _url: URL, match: URLPatternResult): Promise<Response> {
|
|
38
|
+
const filename = match.pathname.groups.filename!;
|
|
39
|
+
if (filename.includes("/") || filename.includes("..")) {
|
|
40
|
+
return jsonResponse({ error: "Invalid filename" }, 400);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const filepath = join(UPLOADS_DIR, filename);
|
|
44
|
+
const file = Bun.file(filepath);
|
|
45
|
+
if (!(await file.exists())) return jsonResponse({ error: "Not found" }, 404);
|
|
46
|
+
|
|
47
|
+
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
|
48
|
+
const mime = IMAGE_MIME[ext] || "application/octet-stream";
|
|
49
|
+
|
|
50
|
+
return new Response(file, {
|
|
51
|
+
headers: {
|
|
52
|
+
"Content-Type": mime,
|
|
53
|
+
"Cache-Control": "public, max-age=3600",
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return [
|
|
59
|
+
{ method: "POST", pattern: new URLPattern({ pathname: "/api/session/:id/upload" }), handler: handleUpload },
|
|
60
|
+
{ method: "GET", pattern: new URLPattern({ pathname: "/api/uploads/:filename" }), handler: handleUploadServe },
|
|
61
|
+
];
|
|
62
|
+
}
|
package/daemon/src/paths.ts
CHANGED
|
@@ -17,6 +17,10 @@ export const PID_FILE = join(TEMP_DIR, "daemon.pid");
|
|
|
17
17
|
/** Temp dir for JSX compilation */
|
|
18
18
|
export const COMPILE_TEMP_DIR = join(TEMP_DIR, "compile");
|
|
19
19
|
|
|
20
|
+
/** Uploaded annotation images */
|
|
21
|
+
export const UPLOADS_DIR = join(TEMP_DIR, "uploads");
|
|
22
|
+
|
|
20
23
|
// Ensure dirs exist
|
|
21
24
|
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
22
25
|
mkdirSync(COMPILE_TEMP_DIR, { recursive: true });
|
|
26
|
+
mkdirSync(UPLOADS_DIR, { recursive: true });
|
package/daemon/src/server.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { jsonResponse, corsHeaders } from "./handlers/utils";
|
|
|
8
8
|
import { createApiHandlers } from "./handlers/api";
|
|
9
9
|
import { createFileHandlers } from "./handlers/files";
|
|
10
10
|
import { createStaticHandlers } from "./handlers/static";
|
|
11
|
+
import { createUploadHandlers } from "./handlers/uploads";
|
|
11
12
|
|
|
12
13
|
const PORT = parseInt(process.env.CANVAS_PORT || "19400", 10);
|
|
13
14
|
|
|
@@ -22,6 +23,7 @@ const wsManager = createWebSocketManager(sessionManager);
|
|
|
22
23
|
const routes = [
|
|
23
24
|
...createApiHandlers({ sessionManager, broadcastPlanUpdate: wsManager.broadcastPlanUpdate, port: PORT }),
|
|
24
25
|
...createFileHandlers(sessionManager),
|
|
26
|
+
...createUploadHandlers(sessionManager),
|
|
25
27
|
...createStaticHandlers(),
|
|
26
28
|
];
|
|
27
29
|
|
package/daemon/src/session.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync, rmSync
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { SESSIONS_DIR } from "./paths";
|
|
4
4
|
|
|
5
|
+
export interface DiffStats {
|
|
6
|
+
added: number;
|
|
7
|
+
removed: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
export interface RevisionInfo {
|
|
6
11
|
revision: number;
|
|
7
12
|
label?: string;
|
|
@@ -10,6 +15,7 @@ export interface RevisionInfo {
|
|
|
10
15
|
hasFeedback: boolean;
|
|
11
16
|
feedbackConsumed: boolean;
|
|
12
17
|
response?: string;
|
|
18
|
+
diffStats?: DiffStats;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
export interface SessionData {
|
|
@@ -40,6 +46,27 @@ interface LegacyMeta {
|
|
|
40
46
|
|
|
41
47
|
const STALE_TIMEOUT_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
42
48
|
|
|
49
|
+
function computeLineDiffStats(oldText: string, newText: string): DiffStats {
|
|
50
|
+
const oldLines = oldText.split("\n");
|
|
51
|
+
const newLines = newText.split("\n");
|
|
52
|
+
const m = oldLines.length, n = newLines.length;
|
|
53
|
+
|
|
54
|
+
// LCS length via two-row DP (O(n) space)
|
|
55
|
+
let prev = new Uint32Array(n + 1);
|
|
56
|
+
let curr = new Uint32Array(n + 1);
|
|
57
|
+
for (let i = 1; i <= m; i++) {
|
|
58
|
+
for (let j = 1; j <= n; j++) {
|
|
59
|
+
curr[j] = oldLines[i - 1] === newLines[j - 1]
|
|
60
|
+
? prev[j - 1] + 1
|
|
61
|
+
: Math.max(prev[j], curr[j - 1]);
|
|
62
|
+
}
|
|
63
|
+
[prev, curr] = [curr, prev];
|
|
64
|
+
curr.fill(0);
|
|
65
|
+
}
|
|
66
|
+
const lcsLen = prev[n];
|
|
67
|
+
return { added: n - lcsLen, removed: m - lcsLen };
|
|
68
|
+
}
|
|
69
|
+
|
|
43
70
|
export class SessionManager {
|
|
44
71
|
private sessions = new Map<string, SessionData>();
|
|
45
72
|
|
|
@@ -162,7 +189,18 @@ export class SessionManager {
|
|
|
162
189
|
const now = new Date().toISOString();
|
|
163
190
|
const revision = existing ? existing.currentRevision + 1 : 1;
|
|
164
191
|
|
|
165
|
-
|
|
192
|
+
let diffStats: DiffStats | undefined;
|
|
193
|
+
if (existing && sourceFile) {
|
|
194
|
+
const prev = [...existing.revisions]
|
|
195
|
+
.reverse()
|
|
196
|
+
.find((r) => r.sourceFile === sourceFile);
|
|
197
|
+
if (prev) {
|
|
198
|
+
const prevJsx = this.readRevisionJsx(id, prev.revision);
|
|
199
|
+
if (prevJsx) diffStats = computeLineDiffStats(prevJsx, jsx);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const revInfo: RevisionInfo = { revision, createdAt: now, hasFeedback: false, feedbackConsumed: false, ...(label ? { label } : {}), ...(sourceFile ? { sourceFile } : {}), ...(response ? { response } : {}), ...(diffStats ? { diffStats } : {}) };
|
|
166
204
|
const revisions = existing ? [...existing.revisions, revInfo] : [revInfo];
|
|
167
205
|
|
|
168
206
|
const session: SessionData = {
|
package/package.json
CHANGED
package/skills/canvas/SKILL.md
CHANGED
|
@@ -57,32 +57,37 @@ This opens the canvas in the user's browser (first time) and exits immediately.
|
|
|
57
57
|
|
|
58
58
|
### Waiting for feedback
|
|
59
59
|
|
|
60
|
-
After pushing,
|
|
60
|
+
After pushing, start the watch command **in the background** using the Bash tool's `run_in_background` parameter:
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
|
+
# Use Bash tool with run_in_background: true
|
|
63
64
|
bunx agent-canvas watch --session ${CLAUDE_SESSION_ID}
|
|
64
65
|
```
|
|
65
66
|
|
|
66
|
-
This **
|
|
67
|
+
This runs the watch process in the background. You will be **automatically notified** when the user submits feedback — the output will contain the feedback markdown. Do NOT poll, sleep, or proactively check on it. Just stop and wait for the notification.
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
If feedback was already submitted but not yet consumed, the command returns immediately with the existing feedback.
|
|
70
|
+
|
|
71
|
+
**Important**: After starting the background watch, do not continue with other work unless instructed. Wait for the notification that feedback has arrived before proceeding.
|
|
69
72
|
|
|
70
73
|
### Checking for feedback without blocking
|
|
71
74
|
|
|
72
|
-
Use `fetch` to
|
|
75
|
+
Use `fetch` to retrieve feedback if the user tells you they've already submitted:
|
|
73
76
|
|
|
74
77
|
```bash
|
|
75
78
|
bunx agent-canvas fetch --session ${CLAUDE_SESSION_ID}
|
|
76
79
|
```
|
|
77
80
|
|
|
78
|
-
Returns immediately — prints feedback to stdout if available, otherwise produces no output. Use this
|
|
81
|
+
Returns immediately — prints feedback to stdout if available, otherwise produces no output. Use this when the user tells you they've submitted feedback (e.g. after a reconnect or if the background watch was lost).
|
|
79
82
|
|
|
80
83
|
### Iterating
|
|
81
84
|
|
|
82
|
-
Use the **Edit** tool to modify the existing JSX based on feedback — targeted edits, not full rewrites. Then push and
|
|
85
|
+
Use the **Edit** tool to modify the existing JSX based on feedback — targeted edits, not full rewrites. Then push and watch again:
|
|
83
86
|
|
|
84
87
|
```bash
|
|
85
88
|
bunx agent-canvas push .claude/agent-canvas/${CLAUDE_SESSION_ID}/plan.jsx --session ${CLAUDE_SESSION_ID} --label "Implementation Plan (revised)"
|
|
89
|
+
|
|
90
|
+
# Use Bash tool with run_in_background: true
|
|
86
91
|
bunx agent-canvas watch --session ${CLAUDE_SESSION_ID}
|
|
87
92
|
```
|
|
88
93
|
|
|
@@ -109,12 +114,15 @@ Maintain separate files for different phases:
|
|
|
109
114
|
|
|
110
115
|
```bash
|
|
111
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
|
|
112
118
|
bunx agent-canvas watch --session ${CLAUDE_SESSION_ID}
|
|
113
119
|
# ... process feedback ...
|
|
114
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
|
|
115
122
|
bunx agent-canvas watch --session ${CLAUDE_SESSION_ID}
|
|
116
123
|
# ... process feedback ...
|
|
117
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
|
|
118
126
|
bunx agent-canvas watch --session ${CLAUDE_SESSION_ID}
|
|
119
127
|
```
|
|
120
128
|
|
|
@@ -156,7 +164,7 @@ User wants to make a decision?
|
|
|
156
164
|
2. **Announce** briefly: "I'll start with discovery, then create a detailed plan."
|
|
157
165
|
3. **Write canvas JSX** with the Write tool to `.claude/agent-canvas/${CLAUDE_SESSION_ID}/<name>.jsx`
|
|
158
166
|
4. **Push + tell the user**: Push the canvas and show the `browserUrl` from the output so the user can open it
|
|
159
|
-
5. **IMMEDIATELY watch** — run `bunx agent-canvas watch --session ${CLAUDE_SESSION_ID}` right after push
|
|
167
|
+
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.
|
|
160
168
|
6. **Read feedback** — check for annotations, answers, added context files, approval
|
|
161
169
|
7. **Edit with the Edit tool and re-push + watch**, or advance to next phase
|
|
162
170
|
8. **After implementation**, push a summary canvas
|
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}/<file>.jsx --session ${CLAUDE_SESSION_ID} --label "<Label>"`, then `bunx agent-canvas watch --session ${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 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.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
package/hooks/session-start.sh
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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
|
-
|
|
5
|
-
if [ -n "$CLAUDE_ENV_FILE" ]; then
|
|
6
|
-
# Extract session ID from CLAUDE_ENV_FILE path:
|
|
7
|
-
# /home/.../.claude/session-env/<SESSION_ID>/sessionstart-hook-0.sh
|
|
8
|
-
SESSION_ID=$(basename "$(dirname "$CLAUDE_ENV_FILE")")
|
|
9
|
-
CWD="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
10
|
-
|
|
11
|
-
echo "export CANVAS_SESSION_ID=$SESSION_ID" >> "$CLAUDE_ENV_FILE"
|
|
12
|
-
echo "export CANVAS_PROJECT_ROOT=$CWD" >> "$CLAUDE_ENV_FILE"
|
|
13
|
-
fi
|