agent-canvas 0.1.9 → 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.
@@ -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.25);
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
+ }
@@ -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 });
@@ -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
 
@@ -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
- const revInfo: RevisionInfo = { revision, createdAt: now, hasFeedback: false, feedbackConsumed: false, ...(label ? { label } : {}), ...(sourceFile ? { sourceFile } : {}), ...(response ? { response } : {}) };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-canvas",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
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"
@@ -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