agent-canvas 0.1.8 → 0.1.10
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/client/AnnotationEditor.tsx +307 -0
- package/daemon/client/AnnotationProvider.tsx +15 -3
- package/daemon/client/AnnotationSidebar.tsx +89 -37
- package/daemon/client/App.tsx +144 -13
- package/daemon/client/CompareView.tsx +54 -23
- package/daemon/client/FileBrowser.tsx +2 -1
- package/daemon/client/FileViewer.tsx +41 -161
- package/daemon/client/PlanRenderer.tsx +32 -149
- package/daemon/client/Popover.tsx +168 -108
- package/daemon/client/ResponsePreview.tsx +7 -1
- package/daemon/client/RevisionSelect.tsx +8 -1
- package/daemon/client/annotationContext.ts +1 -1
- package/daemon/client/annotationDom.ts +104 -0
- package/daemon/client/components/Mermaid.tsx +2 -2
- package/daemon/client/generateMarkdown.ts +21 -2
- package/daemon/client/highlightRange.ts +1 -22
- 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/client/utils.ts +3 -0
- package/daemon/dist/client.css +1 -1
- package/daemon/dist/client.js +61 -85
- package/daemon/dist/components.js +54 -54
- package/daemon/dist/index.html +1 -1
- 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 +5 -6
- package/hooks/session-start.sh +0 -13
package/daemon/dist/index.html
CHANGED
|
@@ -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
|
@@ -155,12 +155,11 @@ User wants to make a decision?
|
|
|
155
155
|
1. **Determine flow** from user intent
|
|
156
156
|
2. **Announce** briefly: "I'll start with discovery, then create a detailed plan."
|
|
157
157
|
3. **Write canvas JSX** with the Write tool to `.claude/agent-canvas/${CLAUDE_SESSION_ID}/<name>.jsx`
|
|
158
|
-
4. **Push**:
|
|
159
|
-
5. **
|
|
160
|
-
6. **
|
|
161
|
-
7. **
|
|
162
|
-
8. **
|
|
163
|
-
9. **After implementation**, push a summary canvas
|
|
158
|
+
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, in the same response. Never stop after pushing. The push→watch sequence is atomic: no push without a watch. The watch blocks until the user submits feedback.
|
|
160
|
+
6. **Read feedback** — check for annotations, answers, added context files, approval
|
|
161
|
+
7. **Edit with the Edit tool and re-push + watch**, or advance to next phase
|
|
162
|
+
8. **After implementation**, push a summary canvas
|
|
164
163
|
|
|
165
164
|
## Interview Integration
|
|
166
165
|
|
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
|