agent-canvas 0.1.18 → 0.1.19
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 +21 -2
- package/bin/lib/config.ts +2 -0
- package/bin/lib/daemon-lifecycle.ts +20 -2
- package/bin/lib/instructions/overview.md +11 -1
- package/daemon/src/compiler.ts +3 -29
- package/daemon/src/handlers/api.ts +2 -1
- package/daemon/src/server.ts +4 -3
- package/package.json +1 -1
package/bin/lib/commands/push.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { parseArgs } from "util";
|
|
|
2
2
|
import { existsSync, statSync } from "fs";
|
|
3
3
|
import { resolve, basename } from "path";
|
|
4
4
|
import { BASE_URL } from "../config.ts";
|
|
5
|
-
import { ensureDaemon } from "../daemon-lifecycle.ts";
|
|
5
|
+
import { ensureDaemon, stopDaemon, startDaemon } from "../daemon-lifecycle.ts";
|
|
6
6
|
import { getSessionId, openBrowser } from "../helpers.ts";
|
|
7
7
|
|
|
8
8
|
export async function handlePush(args: string[]) {
|
|
@@ -52,7 +52,26 @@ export async function handlePush(args: string[]) {
|
|
|
52
52
|
}),
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
let result = await response.json() as any;
|
|
56
|
+
|
|
57
|
+
// Bun.build() can corrupt internal state in long-running daemons,
|
|
58
|
+
// returning "Unknown Error, TODO". Restart daemon and retry once.
|
|
59
|
+
if (!result.ok && typeof result.error === "string" && result.error.includes("Unknown Error")) {
|
|
60
|
+
console.error("Daemon bundler crashed, restarting...");
|
|
61
|
+
stopDaemon();
|
|
62
|
+
await startDaemon();
|
|
63
|
+
const retryResponse = await fetch(`${BASE_URL}/api/session/${sessionId}/plan`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/json" },
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
directory: resolvedPath,
|
|
68
|
+
projectRoot,
|
|
69
|
+
label: label || autoLabel,
|
|
70
|
+
...(values.response ? { response: values.response } : {}),
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
result = await retryResponse.json() as any;
|
|
74
|
+
}
|
|
56
75
|
|
|
57
76
|
if (!result.ok) {
|
|
58
77
|
if (result.unconsumedFeedback) {
|
package/bin/lib/config.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { resolve, join, dirname } from "path";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
2
3
|
import { tmpdir } from "os";
|
|
3
4
|
|
|
4
5
|
export const PACKAGE_ROOT = resolve(join(dirname(import.meta.path), "../.."));
|
|
6
|
+
export const VERSION = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8")).version as string;
|
|
5
7
|
export const TEMP_DIR = join(tmpdir(), "agent-canvas");
|
|
6
8
|
export const DAEMON_PORT = parseInt(process.env.CANVAS_PORT || "19400", 10);
|
|
7
9
|
export const BASE_URL = `http://localhost:${DAEMON_PORT}`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { openSync, closeSync, readFileSync, existsSync, mkdirSync, unlinkSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { PACKAGE_ROOT, TEMP_DIR, BASE_URL, PID_FILE } from "./config.ts";
|
|
4
|
+
import { PACKAGE_ROOT, TEMP_DIR, BASE_URL, PID_FILE, VERSION } from "./config.ts";
|
|
5
5
|
|
|
6
6
|
export async function isDaemonRunning(): Promise<boolean> {
|
|
7
7
|
try {
|
|
@@ -13,6 +13,16 @@ export async function isDaemonRunning(): Promise<boolean> {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
async function getDaemonVersion(): Promise<string | null> {
|
|
17
|
+
try {
|
|
18
|
+
const res = await fetch(`${BASE_URL}/health`, { signal: AbortSignal.timeout(1000) });
|
|
19
|
+
const data = await res.json() as any;
|
|
20
|
+
return data.version || null;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
export async function startDaemon(): Promise<void> {
|
|
17
27
|
console.error("Starting canvas daemon...");
|
|
18
28
|
mkdirSync(TEMP_DIR, { recursive: true });
|
|
@@ -50,7 +60,15 @@ export async function startDaemon(): Promise<void> {
|
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
export async function ensureDaemon(): Promise<void> {
|
|
53
|
-
if (await isDaemonRunning())
|
|
63
|
+
if (await isDaemonRunning()) {
|
|
64
|
+
const daemonVersion = await getDaemonVersion();
|
|
65
|
+
if (daemonVersion && daemonVersion !== VERSION) {
|
|
66
|
+
console.error(`Daemon version mismatch (daemon: ${daemonVersion}, CLI: ${VERSION}), restarting...`);
|
|
67
|
+
stopDaemon();
|
|
68
|
+
await startDaemon();
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
54
72
|
await startDaemon();
|
|
55
73
|
}
|
|
56
74
|
|
|
@@ -32,6 +32,10 @@ Use the **Write** tool to create `.jsx` files in `.claude/agent-canvas/<session-
|
|
|
32
32
|
|
|
33
33
|
Components are auto-available — no imports needed. The file can be a JSX fragment (just tags) or a full module with `export default`.
|
|
34
34
|
|
|
35
|
+
**Content is JSX, not markdown.** Children of components are plain JSX text and nested components — NOT markdown. If you want to render markdown (lists, bold, headings, etc.), use the `<Markdown>` component explicitly. Raw `**bold**` or `- list items` in JSX children will render as literal text.
|
|
36
|
+
|
|
37
|
+
**JSX escaping rules.** Content is in a JSX context. You MUST escape: `<` as `{'<'}` or `<`, `>` as `{'>'}` or `>`, `{` as `{'{'}` and `}` as `{'}'}`. This is critical when writing code snippets, type signatures like `Option<String>`, or any text containing angle brackets. Alternatively, put such content inside `<CodeBlock>` or `<Markdown>` which handle escaping for you.
|
|
38
|
+
|
|
35
39
|
### 2. Push
|
|
36
40
|
|
|
37
41
|
Push the directory to open all canvases in the browser:
|
|
@@ -123,7 +127,13 @@ All components are globally available in canvas JSX — no imports needed. For i
|
|
|
123
127
|
- **useFeedback** — hook for custom components to contribute computed data to feedback
|
|
124
128
|
- **Custom components** — use `export default function Canvas()` module format to define helpers
|
|
125
129
|
|
|
126
|
-
|
|
130
|
+
**Before using any component or flow, you MUST read its detailed instructions** by running:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
bunx agent-canvas instructions <topic>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
For example: `bunx agent-canvas instructions component-item`, `bunx agent-canvas instructions flow-feature`. Run `bunx agent-canvas instructions --list` to see all available topics.
|
|
127
137
|
|
|
128
138
|
## Choosing a Flow
|
|
129
139
|
|
package/daemon/src/compiler.ts
CHANGED
|
@@ -147,10 +147,10 @@ export async function compilePlan(jsx: string, projectRoot?: string): Promise<Co
|
|
|
147
147
|
return { ok: false, error: `Runtime error: ${validation.error}` };
|
|
148
148
|
}
|
|
149
149
|
return { ok: true, js };
|
|
150
|
-
} catch {
|
|
150
|
+
} catch (buildError: any) {
|
|
151
151
|
// Bun.build() can throw "Unknown Error, TODO" in long-running processes
|
|
152
|
-
// when its internal bundler state gets corrupted.
|
|
153
|
-
return
|
|
152
|
+
// when its internal bundler state gets corrupted. Let the CLI handle restart.
|
|
153
|
+
return { ok: false, error: buildError?.message || "Unknown Error" };
|
|
154
154
|
}
|
|
155
155
|
} catch (e: any) {
|
|
156
156
|
return { ok: false, error: e.message };
|
|
@@ -159,32 +159,6 @@ export async function compilePlan(jsx: string, projectRoot?: string): Promise<Co
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
async function compilePlanSubprocess(tmpFile: string): Promise<CompileResult> {
|
|
163
|
-
const outFile = `${tmpFile}.out.js`;
|
|
164
|
-
const proc = Bun.spawn(
|
|
165
|
-
["bun", "build", tmpFile, "--format=esm",
|
|
166
|
-
"--external=react", "--external=react-dom",
|
|
167
|
-
"--external=#canvas/components", "--external=#canvas/runtime",
|
|
168
|
-
"--outfile", outFile],
|
|
169
|
-
{ stdout: "pipe", stderr: "pipe" },
|
|
170
|
-
);
|
|
171
|
-
const exitCode = await proc.exited;
|
|
172
|
-
if (exitCode !== 0) {
|
|
173
|
-
const stderr = await new Response(proc.stderr).text();
|
|
174
|
-
return { ok: false, error: stderr || "Compilation failed" };
|
|
175
|
-
}
|
|
176
|
-
try {
|
|
177
|
-
const js = readFileSync(outFile, "utf-8");
|
|
178
|
-
const validation = validateCompiledPlan(js);
|
|
179
|
-
if (!validation.ok) {
|
|
180
|
-
return { ok: false, error: `Runtime error: ${validation.error}` };
|
|
181
|
-
}
|
|
182
|
-
return { ok: true, js };
|
|
183
|
-
} finally {
|
|
184
|
-
try { unlinkSync(outFile); } catch {}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
162
|
/**
|
|
189
163
|
* Find <Markdown file="..." /> tags and inject __content with the file contents
|
|
190
164
|
* resolved at compile time.
|
|
@@ -10,6 +10,7 @@ export interface ApiContext {
|
|
|
10
10
|
broadcastPlanUpdate: (id: string) => void;
|
|
11
11
|
broadcastRevisionUpdate: (id: string) => void;
|
|
12
12
|
port: number;
|
|
13
|
+
version: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export function createApiHandlers(ctx: ApiContext): Route[] {
|
|
@@ -132,7 +133,7 @@ export function createApiHandlers(ctx: ApiContext): Route[] {
|
|
|
132
133
|
|
|
133
134
|
function handleHealth(): Response {
|
|
134
135
|
const sessions = sessionManager.list().map((s) => s.id);
|
|
135
|
-
return jsonResponse({ ok: true, sessions });
|
|
136
|
+
return jsonResponse({ ok: true, sessions, version: ctx.version });
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
function handleMeta(_req: Request, _url: URL, match: URLPatternResult): Response {
|
package/daemon/src/server.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { writeFileSync, mkdirSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
1
|
+
import { writeFileSync, mkdirSync, readFileSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
3
|
import { tmpdir } from "os";
|
|
4
4
|
import { SessionManager } from "./session";
|
|
5
5
|
import { createWebSocketManager, type WSData } from "./websocket";
|
|
@@ -11,6 +11,7 @@ import { createStaticHandlers } from "./handlers/static";
|
|
|
11
11
|
import { createUploadHandlers } from "./handlers/uploads";
|
|
12
12
|
|
|
13
13
|
const PORT = parseInt(process.env.CANVAS_PORT || "19400", 10);
|
|
14
|
+
const VERSION = JSON.parse(readFileSync(join(dirname(import.meta.dir), "..", "package.json"), "utf-8")).version as string;
|
|
14
15
|
|
|
15
16
|
// Write PID file so CLI can find and stop us
|
|
16
17
|
const pidDir = join(tmpdir(), "agent-canvas");
|
|
@@ -21,7 +22,7 @@ const sessionManager = new SessionManager();
|
|
|
21
22
|
const wsManager = createWebSocketManager(sessionManager);
|
|
22
23
|
|
|
23
24
|
const routes = [
|
|
24
|
-
...createApiHandlers({ sessionManager, broadcastPlanUpdate: wsManager.broadcastPlanUpdate, broadcastRevisionUpdate: wsManager.broadcastRevisionUpdate, port: PORT }),
|
|
25
|
+
...createApiHandlers({ sessionManager, broadcastPlanUpdate: wsManager.broadcastPlanUpdate, broadcastRevisionUpdate: wsManager.broadcastRevisionUpdate, port: PORT, version: VERSION }),
|
|
25
26
|
...createFileHandlers(sessionManager),
|
|
26
27
|
...createUploadHandlers(sessionManager),
|
|
27
28
|
...createStaticHandlers(),
|