agent-canvas 0.1.37 → 0.1.38

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.
@@ -14,7 +14,6 @@ jobs:
14
14
  uses: ./.github/workflows/deploy-worker.yml
15
15
  with:
16
16
  env: prod
17
- secrets:
18
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
19
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
20
- SHARE_AUTH_TOKEN: ${{ secrets.SHARE_AUTH_TOKEN_PROD }}
17
+ # inherit so the reusable workflow can resolve secrets; the env-scoped
18
+ # values (set on the `prod` environment) win over repo-level ones.
19
+ secrets: inherit
@@ -16,7 +16,6 @@ jobs:
16
16
  uses: ./.github/workflows/deploy-worker.yml
17
17
  with:
18
18
  env: stage
19
- secrets:
20
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
21
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
22
- SHARE_AUTH_TOKEN: ${{ secrets.SHARE_AUTH_TOKEN_STAGE }}
19
+ # inherit so the reusable workflow can resolve secrets; the env-scoped
20
+ # values (set on the `stage` environment) win over repo-level ones.
21
+ secrets: inherit
@@ -7,14 +7,13 @@ on:
7
7
  description: "Target environment (stage or prod)"
8
8
  required: true
9
9
  type: string
10
- secrets:
11
- CLOUDFLARE_API_TOKEN:
12
- required: true
13
- CLOUDFLARE_ACCOUNT_ID:
14
- required: true
15
- SHARE_AUTH_TOKEN:
16
- description: "Bearer token the daemon must send to POST /shares. Optional but strongly recommended in production."
17
- required: false
10
+ # No `secrets:` inputs on purpose. The deploy job below declares
11
+ # `environment: ${{ inputs.env }}`, so referencing `secrets.X` directly
12
+ # resolves the matching *environment* secret (falling back to a
13
+ # repo-level secret of the same name). Callers pass `secrets: inherit`
14
+ # so org/repo secrets remain reachable too. This is what makes the
15
+ # CLOUDFLARE_* / SHARE_AUTH_TOKEN values environment-scoped — a prod
16
+ # token can never be read by a stage deploy and vice versa.
18
17
 
19
18
  jobs:
20
19
  deploy:
@@ -59,7 +59,11 @@ export async function handlePush(args: string[]) {
59
59
  // Skip restart only for clear user errors (syntax errors with line/col,
60
60
  // runtime validation errors) — those won't be fixed by a restart.
61
61
  const isUserError = !result.ok && result.errors && Object.values(result.errors).every((e: any) =>
62
- typeof e === "string" && (/\(line \d+, col \d+\)/.test(e) || e.startsWith("Runtime error:")));
62
+ typeof e === "string" && (
63
+ /\(line \d+, col \d+\)/.test(e) ||
64
+ /Syntax Error|Parse error|Mermaid syntax error/i.test(e) ||
65
+ e.startsWith("Runtime error:")
66
+ ));
63
67
  const shouldRestart = !result.ok && result.error === "Canvas compilation failed" && !isUserError;
64
68
  if (shouldRestart) {
65
69
  console.error("Compilation failed, restarting daemon and retrying...");
@@ -0,0 +1,39 @@
1
+ import { test, expect } from "bun:test";
2
+ import { compilePlan } from "./compiler";
3
+
4
+ // Regression coverage for the error-reporting path. Bun surfaces JSX syntax
5
+ // errors through three shapes (Transpiler throw, build logs, AggregateError
6
+ // from Bun.build); all must reach the CLI as an actionable message carrying a
7
+ // position + code frame — never a bare "Parse error" / "Bundle failed".
8
+
9
+ test("syntax error reports line, column and a code frame", async () => {
10
+ // A straight double-quote inside a double-quoted attribute terminates the
11
+ // attribute early — the exact mistake that previously printed only
12
+ // "Parse error" with no location.
13
+ const result = await compilePlan(`<Section title="a "b" c">x</Section>`);
14
+
15
+ expect(result.ok).toBe(false);
16
+ if (result.ok) return;
17
+ expect(result.error).toMatch(/\(line \d+, col \d+\)/);
18
+ // Code frame: offending source line + caret.
19
+ expect(result.error).toContain("^");
20
+ });
21
+
22
+ test("syntax error line maps back to the authored file (1-based, not offset by injected imports)", async () => {
23
+ // Bad token on the authored 3rd line; the reported line must be small/positive,
24
+ // not shifted by the prepended component-import preamble.
25
+ const result = await compilePlan(`<Section title="ok">\n <Item id="a" label="x">\n <span a="1 "2"></span>\n </Item>\n</Section>`);
26
+
27
+ expect(result.ok).toBe(false);
28
+ if (result.ok) return;
29
+ const m = result.error.match(/\(line (\d+), col \d+\)/);
30
+ expect(m).not.toBeNull();
31
+ const line = Number(m![1]);
32
+ expect(line).toBeGreaterThan(0);
33
+ expect(line).toBeLessThan(10);
34
+ });
35
+
36
+ test("well-formed canvas compiles", async () => {
37
+ const result = await compilePlan(`<Section title="Hello"><Item id="a" label="b">text</Item></Section>`);
38
+ expect(result.ok).toBe(true);
39
+ });
@@ -130,7 +130,59 @@ async function validateCompiledPlan(js: string): Promise<{ ok: true } | { ok: fa
130
130
  return { ok: true };
131
131
  }
132
132
 
133
+ /**
134
+ * Normalize a Bun build/transpile failure into a precise, actionable string.
135
+ *
136
+ * Bun surfaces JSX syntax errors through three different shapes, all handled
137
+ * here so the CLI never again prints a bare "Parse error" / "Bundle failed":
138
+ * - `Bun.Transpiler.transformSync` throws a `BuildMessage` (has `.position`).
139
+ * - `Bun.build()` returns `{ success: false, logs: [BuildMessage] }`.
140
+ * - `Bun.build()` throws an `AggregateError` whose `.errors[0]` is a
141
+ * `BuildMessage` — the case the old catch discarded, returning only the
142
+ * useless top-level "Bundle failed".
143
+ *
144
+ * `lineOffset` is the number of lines Canvas prepended (component imports +
145
+ * wrapper) so the reported line maps back to the user's authored file. When a
146
+ * position is available we append `(line N, col M)` plus a one-line code frame
147
+ * with a caret — that suffix is also what lets the CLI classify the error as a
148
+ * user error and skip a pointless daemon restart (see push.ts).
149
+ */
150
+ function formatBuildError(err: any, lineOffset: number): string {
151
+ // Collect every candidate that might carry a `.position`, most specific first.
152
+ const candidates: any[] = [];
153
+ if (err?.position) candidates.push(err);
154
+ if (Array.isArray(err?.errors)) candidates.push(...err.errors);
155
+ if (!candidates.length && err && typeof err[Symbol.iterator] === "function") {
156
+ try { candidates.push(...err); } catch {}
157
+ }
158
+ const detail = candidates.find((c) => c?.position) ?? candidates[0] ?? err;
159
+ const message = String(detail?.message || err?.message || "Syntax error").split("\n")[0];
160
+
161
+ const pos = detail?.position;
162
+ if (!pos || typeof pos.line !== "number") return message;
163
+
164
+ const loc = ` (line ${pos.line - lineOffset}, col ${pos.column})`;
165
+ let frame = "";
166
+ if (typeof pos.lineText === "string") {
167
+ const caret = " ".repeat(Math.max(0, pos.column ?? 0)) + "^";
168
+ frame = `\n ${pos.lineText}\n ${caret}`;
169
+ }
170
+ return `${message}${loc}${frame}`;
171
+ }
172
+
133
173
  export async function compilePlan(jsx: string, projectRoot?: string): Promise<CompileResult> {
174
+ // Strip leading pragma / block / line comments (e.g. `/** @jsxImportSource ... */`).
175
+ // Authored JSX sometimes carries a JSX-runtime pragma from another toolchain;
176
+ // Canvas injects its own imports and uses the React automatic runtime, so the
177
+ // pragma is unused. Left in place it sits at the top level of the wrapped
178
+ // fragment as plain text (JSX only treats `{/* */}` as a comment) and renders
179
+ // literally on the page.
180
+ let stripped: string;
181
+ do {
182
+ stripped = jsx;
183
+ jsx = jsx.replace(/^\s*(?:\/\*[\s\S]*?\*\/|\/\/[^\n]*)\s*/, "");
184
+ } while (jsx !== stripped);
185
+
134
186
  // Resolve file contents at compile time
135
187
  if (projectRoot) {
136
188
  jsx = resolveFilePreviews(jsx, projectRoot);
@@ -150,18 +202,18 @@ export async function compilePlan(jsx: string, projectRoot?: string): Promise<Co
150
202
  ? `${COMPONENT_IMPORTS}\n${jsx}`
151
203
  : `${COMPONENT_IMPORTS}\nexport default function Plan() {\n return (<>${jsx}</>);\n}\n`;
152
204
 
205
+ // Offset: COMPONENT_IMPORTS lines + wrapper lines (export default function
206
+ // Plan() + return). Used to map Bun's line numbers back to the authored file.
207
+ const wrapperLines = hasDefaultExport ? 1 : 2;
208
+ const lineOffset = COMPONENT_IMPORTS_LINES + wrapperLines;
209
+
153
210
  // Pre-validate syntax with Bun.Transpiler — gives precise error positions,
154
211
  // unlike Bun.build() which throws opaque "Bundle failed" for syntax errors.
155
212
  try {
156
213
  const transpiler = new Bun.Transpiler({ loader: "jsx" });
157
214
  transpiler.transformSync(source);
158
215
  } catch (syntaxError: any) {
159
- const pos = syntaxError?.position;
160
- // Offset: COMPONENT_IMPORTS lines + wrapper lines (export default function Plan() + return)
161
- const wrapperLines = hasDefaultExport ? 1 : 2;
162
- const lineOffset = COMPONENT_IMPORTS_LINES + wrapperLines;
163
- const loc = pos ? ` (line ${pos.line - lineOffset}, col ${pos.column})` : "";
164
- return { ok: false, error: `${syntaxError?.message || "Syntax error"}${loc}` };
216
+ return { ok: false, error: formatBuildError(syntaxError, lineOffset) };
165
217
  }
166
218
 
167
219
  const tmpFile = join(COMPILE_TEMP_DIR, `plan-${randomUUID()}.jsx`);
@@ -178,9 +230,9 @@ export async function compilePlan(jsx: string, projectRoot?: string): Promise<Co
178
230
  });
179
231
 
180
232
  if (!result.success) {
181
- const errors = result.logs
182
- .filter((l) => l.level === "error")
183
- .map((l) => l.message)
233
+ const errorLogs = result.logs.filter((l) => l.level === "error");
234
+ const errors = errorLogs
235
+ .map((l) => formatBuildError(l, lineOffset))
184
236
  .join("\n");
185
237
  return { ok: false, error: errors || "Bundle failed" };
186
238
  }
@@ -192,8 +244,17 @@ export async function compilePlan(jsx: string, projectRoot?: string): Promise<Co
192
244
  }
193
245
  return { ok: true, js };
194
246
  } catch (buildError: any) {
195
- // Bun.build() can throw "Unknown Error, TODO" in long-running processes
196
- // when its internal bundler state gets corrupted. Let the CLI handle restart.
247
+ // A real syntax error surfaces as an AggregateError carrying BuildMessages
248
+ // (with positions) — format it precisely. The opaque "Unknown Error, TODO"
249
+ // corruption case (internal bundler state gets corrupted in long-running
250
+ // processes) has no sub-errors; pass it through so the CLI restarts the
251
+ // daemon and retries.
252
+ const hasDetail =
253
+ buildError?.position ||
254
+ (Array.isArray(buildError?.errors) && buildError.errors.length > 0);
255
+ if (hasDetail) {
256
+ return { ok: false, error: formatBuildError(buildError, lineOffset) };
257
+ }
197
258
  return { ok: false, error: buildError?.message || "Unknown Error" };
198
259
  }
199
260
  } catch (e: any) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-canvas",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
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"