agent-canvas 0.1.44 → 0.2.0

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.
Files changed (115) hide show
  1. package/README.md +14 -0
  2. package/bin/lib/commands/daemon.ts +3 -6
  3. package/bin/lib/commands/push.ts +1 -1
  4. package/bin/lib/daemon-lifecycle.ts +65 -72
  5. package/bin/lib/helpers.test.ts +1 -0
  6. package/bin/lib/helpers.ts +30 -74
  7. package/daemon/build.ts +37 -16
  8. package/daemon/client/App.tsx +27 -61
  9. package/daemon/client/CompareView.tsx +4 -4
  10. package/daemon/client/FileBrowser.tsx +4 -4
  11. package/daemon/client/FileViewer.tsx +4 -4
  12. package/daemon/client/OverviewView.tsx +2 -2
  13. package/daemon/client/RevisionSelect.tsx +1 -1
  14. package/daemon/client/canvas-runtime.ts +1 -0
  15. package/daemon/client/clientApi.ts +18 -0
  16. package/daemon/client/components/index.ts +3 -16
  17. package/daemon/client/styles.css +4 -457
  18. package/daemon/dist/annotations-runtime.js +1 -0
  19. package/daemon/dist/client.css +1 -1
  20. package/daemon/dist/client.js +76 -76
  21. package/daemon/dist/components.js +59 -59
  22. package/daemon/dist/index.html +1 -0
  23. package/daemon/dist/preact-compat.js +1 -1
  24. package/daemon/dist/runtime.js +1 -1
  25. package/daemon/src/components.ts +13 -0
  26. package/daemon/src/handlers/api.ts +13 -9
  27. package/daemon/src/handlers/files.ts +2 -3
  28. package/daemon/src/handlers/static.ts +1 -1
  29. package/daemon/src/handlers/uploads.ts +2 -3
  30. package/daemon/src/paths.ts +10 -24
  31. package/daemon/src/remote-feedback.test.ts +1 -1
  32. package/daemon/src/remote-feedback.ts +1 -1
  33. package/daemon/src/secrets-api.test.ts +2 -2
  34. package/daemon/src/server.ts +11 -10
  35. package/daemon/src/share-e2e.test.ts +1 -1
  36. package/daemon/src/share.test.ts +1 -1
  37. package/daemon/src/share.ts +1 -1
  38. package/daemon/tsconfig.json +1 -3
  39. package/package.json +11 -4
  40. package/.github/dark.png +0 -0
  41. package/.github/light.png +0 -0
  42. package/.github/workflows/ci.yml +0 -48
  43. package/.github/workflows/deploy-worker-prod.yml +0 -19
  44. package/.github/workflows/deploy-worker-stage.yml +0 -21
  45. package/.github/workflows/deploy-worker.yml +0 -72
  46. package/.github/workflows/release.yml +0 -36
  47. package/CLAUDE.md +0 -86
  48. package/bun.lock +0 -377
  49. package/daemon/client/AnnotationEditor.tsx +0 -315
  50. package/daemon/client/AnnotationProvider.tsx +0 -195
  51. package/daemon/client/AnnotationSidebar.tsx +0 -627
  52. package/daemon/client/FileIcon.tsx +0 -76
  53. package/daemon/client/PlanRenderer.tsx +0 -614
  54. package/daemon/client/Popover.tsx +0 -350
  55. package/daemon/client/RenderErrorContext.ts +0 -11
  56. package/daemon/client/ResponsePreview.tsx +0 -186
  57. package/daemon/client/SessionContext.tsx +0 -3
  58. package/daemon/client/annotationContext.ts +0 -114
  59. package/daemon/client/annotationDom.ts +0 -104
  60. package/daemon/client/annotationDraft.test.ts +0 -188
  61. package/daemon/client/annotationDraft.ts +0 -130
  62. package/daemon/client/compat/preact-all.ts +0 -4
  63. package/daemon/client/components/Callout.tsx +0 -26
  64. package/daemon/client/components/Checklist.tsx +0 -37
  65. package/daemon/client/components/Choice.tsx +0 -176
  66. package/daemon/client/components/CodeBlock.tsx +0 -35
  67. package/daemon/client/components/Diff.tsx +0 -65
  68. package/daemon/client/components/FilePreview.tsx +0 -88
  69. package/daemon/client/components/Image.tsx +0 -45
  70. package/daemon/client/components/Markdown.tsx +0 -44
  71. package/daemon/client/components/Mermaid.tsx +0 -565
  72. package/daemon/client/components/Note.tsx +0 -14
  73. package/daemon/client/components/Priority.tsx +0 -19
  74. package/daemon/client/components/ResponseNote.tsx +0 -19
  75. package/daemon/client/components/Section.tsx +0 -33
  76. package/daemon/client/components/Table.tsx +0 -35
  77. package/daemon/client/components/Task.tsx +0 -76
  78. package/daemon/client/components/UserInput.tsx +0 -174
  79. package/daemon/client/components/renderMarkdownChildren.tsx +0 -97
  80. package/daemon/client/generateMarkdown.ts +0 -273
  81. package/daemon/client/highlightRange.ts +0 -224
  82. package/daemon/client/popoverPosition.ts +0 -31
  83. package/daemon/client/runtime.ts +0 -123
  84. package/daemon/client/theme.css +0 -66
  85. package/daemon/client/useTextAnnotation.tsx +0 -188
  86. package/daemon/client/utils.ts +0 -13
  87. package/daemon/src/compiler.test.ts +0 -57
  88. package/daemon/src/compiler.ts +0 -360
  89. package/daemon/src/handlers/utils.ts +0 -14
  90. package/daemon/src/router.ts +0 -16
  91. package/daemon/src/session-secrets.test.ts +0 -83
  92. package/daemon/src/session.ts +0 -612
  93. package/daemon/src/watcher.ts +0 -71
  94. package/daemon/src/websocket.test.ts +0 -99
  95. package/daemon/src/websocket.ts +0 -211
  96. package/tsconfig.json +0 -14
  97. package/workers/canvas-share/README.md +0 -159
  98. package/workers/canvas-share/oblaka.ts +0 -56
  99. package/workers/canvas-share/package.json +0 -25
  100. package/workers/canvas-share/public/assets/.gitkeep +0 -0
  101. package/workers/canvas-share/public/assets/client.css +0 -2
  102. package/workers/canvas-share/public/assets/client.js +0 -91
  103. package/workers/canvas-share/public/assets/components.js +0 -65
  104. package/workers/canvas-share/public/assets/jsx-runtime.js +0 -3
  105. package/workers/canvas-share/public/assets/preact-compat.js +0 -1
  106. package/workers/canvas-share/public/assets/runtime.js +0 -1
  107. package/workers/canvas-share/public/index.html +0 -385
  108. package/workers/canvas-share/public/screenshot-dark.png +0 -0
  109. package/workers/canvas-share/public/screenshot-light.png +0 -0
  110. package/workers/canvas-share/src/index.test.ts +0 -209
  111. package/workers/canvas-share/src/index.ts +0 -518
  112. package/workers/canvas-share/src/types.ts +0 -119
  113. package/workers/canvas-share/src/util.ts +0 -156
  114. package/workers/canvas-share/src/validation.ts +0 -160
  115. package/workers/canvas-share/tsconfig.json +0 -16
@@ -1,57 +0,0 @@
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
- });
40
-
41
- test("runbook secret input compiles", async () => {
42
- const result = await compilePlan(`<SecretInput id="service-token" label="Service token" env="SERVICE_TOKEN" required />`);
43
- expect(result.ok).toBe(true);
44
- });
45
-
46
- test("missing required component prop fails before the browser renders", async () => {
47
- const result = await compilePlan(`<Table columns={["Name"]} rows={[["Canvas"]]} />`);
48
-
49
- expect(result.ok).toBe(false);
50
- if (result.ok) return;
51
- expect(result.error).toBe("Runtime error: Table: required prop `headers` must be an array");
52
- });
53
-
54
- test("component prop validation accepts valid table props", async () => {
55
- const result = await compilePlan(`<Table headers={["Name"]} rows={[["Canvas"]]} />`);
56
- expect(result.ok).toBe(true);
57
- });
@@ -1,360 +0,0 @@
1
- import { writeFileSync, unlinkSync, readFileSync } from "fs";
2
- import { join } from "path";
3
- import { randomUUID } from "crypto";
4
- import { h, Fragment, type ComponentChildren } from "preact";
5
- import renderToString from "preact-render-to-string";
6
- import { parse as parseMermaid } from "mermaid-parser-bundle";
7
- import { COMPILE_TEMP_DIR } from "./paths";
8
-
9
- type CompileResult =
10
- | { ok: true; js: string }
11
- | { ok: false; error: string };
12
-
13
- const COMPONENT_IMPORTS = `import React from 'react';
14
- import * as C from '#canvas/components';
15
- const { Section, Item, Task, FilePreview, CodeBlock, Callout,
16
- Mermaid, Table, Priority, Checklist, Note, Diff,
17
- Choice, MultiChoice, UserInput, RangeInput, SecretInput, ImageView,
18
- Markdown, useFeedback, useAnnotations } = C;
19
- `;
20
-
21
- /** Number of lines in COMPONENT_IMPORTS — used to adjust error line numbers */
22
- const COMPONENT_IMPORTS_LINES = COMPONENT_IMPORTS.split("\n").length - 1;
23
-
24
- // --- Validation via Preact --------------------------------------------------
25
- // After Bun.build() succeeds we strip ESM syntax from the compiled output,
26
- // provide Preact as the React implementation with lightweight stub components,
27
- // and actually render the component tree. Any runtime error (undefined vars,
28
- // broken template literals, …) is caught and reported back to the CLI.
29
-
30
- /** jsxDEV(type, props, key, isStaticChildren, source, self) → VNode */
31
- function mockJsxDEV(type: any, props: any, key?: any) {
32
- const { children, ...rest } = props || {};
33
- return h(type, { ...rest, key }, children);
34
- }
35
-
36
- interface StubProps {
37
- children?: ComponentChildren;
38
- [key: string]: unknown;
39
- }
40
-
41
- /** Passthrough component — just renders its children */
42
- const Stub = ({ children }: StubProps) => h(Fragment, null, children);
43
-
44
- type RequiredPropKind = "array" | "string";
45
-
46
- function validatingStub(component: string, requiredProps: Record<string, RequiredPropKind>) {
47
- return (props: StubProps) => {
48
- for (const [prop, kind] of Object.entries(requiredProps)) {
49
- const value = props[prop];
50
- const valid = kind === "array" ? Array.isArray(value) : typeof value === kind;
51
- if (!valid) {
52
- throw new Error(`${component}: required prop \`${prop}\` must be ${kind === "array" ? "an array" : `a ${kind}`}`);
53
- }
54
- }
55
- return h(Fragment, null, props.children);
56
- };
57
- }
58
-
59
- const STUB_COMPONENTS: Record<string, any> = {};
60
- for (const name of [
61
- "CodeBlock", "Callout", "Note", "Markdown",
62
- ]) {
63
- STUB_COMPONENTS[name] = Stub;
64
- }
65
- Object.assign(STUB_COMPONENTS, {
66
- Section: validatingStub("Section", { title: "string" }),
67
- Item: validatingStub("Item", { id: "string", label: "string" }),
68
- Task: validatingStub("Task", { id: "string", label: "string" }),
69
- FilePreview: validatingStub("FilePreview", { path: "string" }),
70
- Table: validatingStub("Table", { headers: "array", rows: "array" }),
71
- Priority: validatingStub("Priority", { level: "string" }),
72
- Checklist: validatingStub("Checklist", { items: "array" }),
73
- Diff: validatingStub("Diff", { before: "string", after: "string" }),
74
- Choice: validatingStub("Choice", { id: "string", label: "string", options: "array" }),
75
- MultiChoice: validatingStub("MultiChoice", { id: "string", label: "string", options: "array" }),
76
- UserInput: validatingStub("UserInput", { id: "string", label: "string" }),
77
- RangeInput: validatingStub("RangeInput", { id: "string", label: "string" }),
78
- SecretInput: validatingStub("SecretInput", { id: "string", label: "string", env: "string" }),
79
- ImageView: validatingStub("ImageView", { src: "string" }),
80
- });
81
- // Mermaid stub collects diagram sources for post-render validation
82
- let collectedMermaidSources: string[] = [];
83
- STUB_COMPONENTS.Mermaid = ({ children }: any) => {
84
- const source = typeof children === "string" ? children : String(children ?? "");
85
- if (source.trim()) collectedMermaidSources.push(source.trim());
86
- return null;
87
- };
88
- // Hook stubs — return shapes that won't blow up when destructured
89
- STUB_COMPONENTS.useFeedback = () => ({ submit: () => {}, value: null });
90
- STUB_COMPONENTS.useAnnotations = () => ({ annotations: [], addAnnotation: () => {} });
91
-
92
- const MOCK_REACT = {
93
- createElement: h,
94
- Fragment,
95
- useState: (init: any) => [init, () => {}],
96
- useEffect: () => {},
97
- useRef: (init: any) => ({ current: init }),
98
- useMemo: (fn: any) => fn(),
99
- useCallback: (fn: any) => fn,
100
- useReducer: (r: any, init: any) => [init, () => {}],
101
- useContext: () => ({}),
102
- createContext: () => ({ Provider: Stub, Consumer: Stub }),
103
- useLayoutEffect: () => {},
104
- useImperativeHandle: () => {},
105
- useDebugValue: () => {},
106
- useDeferredValue: (v: any) => v,
107
- useTransition: () => [false, (fn: any) => fn()],
108
- useId: () => "mock",
109
- useSyncExternalStore: (sub: any, get: any) => get(),
110
- memo: (c: any) => c,
111
- forwardRef: (c: any) => c,
112
- lazy: (c: any) => c,
113
- Children: { map: (c: any, fn: any) => (Array.isArray(c) ? c : [c]).map(fn), toArray: (c: any) => (Array.isArray(c) ? c : [c]) },
114
- isValidElement: () => true,
115
- cloneElement: h,
116
- Suspense: Stub,
117
- StrictMode: Stub,
118
- };
119
-
120
- async function validateCompiledPlan(js: string): Promise<{ ok: true } | { ok: false; error: string }> {
121
- let code = js;
122
-
123
- // Strip ESM import statements
124
- code = code.replace(/import\s+\w+\s+from\s+"[^"]+";?/g, "");
125
- code = code.replace(/import\s+\*\s+as\s+\w+\s+from\s+"[^"]+";?/g, "");
126
- code = code.replace(/import\s*\{[^}]*\}\s*from\s+"[^"]+";?/g, "");
127
-
128
- // Strip export, extract default name
129
- let defaultName: string | null = null;
130
- code = code.replace(/export\s*\{\s*(\w+)\s+as\s+default\s*\};?/g, (_, name) => {
131
- defaultName = name;
132
- return "";
133
- });
134
- if (!defaultName) {
135
- code = code.replace(/export\s+default\s+(\w+);?/g, (_, name) => {
136
- defaultName = name;
137
- return "";
138
- });
139
- }
140
-
141
- const callDefault = defaultName
142
- ? `\nreturn typeof ${defaultName} === "function" ? ${defaultName}() : null;`
143
- : "\nreturn null;";
144
-
145
- try {
146
- collectedMermaidSources = [];
147
- const fn = new Function("React", "C", "jsxDEV", "jsx", "Fragment", code + callDefault);
148
- const vnode = fn(MOCK_REACT, STUB_COMPONENTS, mockJsxDEV, mockJsxDEV, Fragment);
149
- if (vnode) renderToString(vnode);
150
- } catch (e: any) {
151
- return { ok: false, error: e?.message || String(e) };
152
- }
153
-
154
- // Validate collected Mermaid diagram sources
155
- for (const source of collectedMermaidSources) {
156
- try {
157
- await parseMermaid(source);
158
- } catch (e: any) {
159
- const msg = (e?.message || "Invalid diagram").split("\n")[0];
160
- return { ok: false, error: `Mermaid syntax error: ${msg}` };
161
- }
162
- }
163
-
164
- return { ok: true };
165
- }
166
-
167
- /**
168
- * Normalize a Bun build/transpile failure into a precise, actionable string.
169
- *
170
- * Bun surfaces JSX syntax errors through three different shapes, all handled
171
- * here so the CLI never again prints a bare "Parse error" / "Bundle failed":
172
- * - `Bun.Transpiler.transformSync` throws a `BuildMessage` (has `.position`).
173
- * - `Bun.build()` returns `{ success: false, logs: [BuildMessage] }`.
174
- * - `Bun.build()` throws an `AggregateError` whose `.errors[0]` is a
175
- * `BuildMessage` — the case the old catch discarded, returning only the
176
- * useless top-level "Bundle failed".
177
- *
178
- * `lineOffset` is the number of lines Canvas prepended (component imports +
179
- * wrapper) so the reported line maps back to the user's authored file. When a
180
- * position is available we append `(line N, col M)` plus a one-line code frame
181
- * with a caret — that suffix is also what lets the CLI classify the error as a
182
- * user error and skip a pointless daemon restart (see push.ts).
183
- */
184
- function formatBuildError(err: any, lineOffset: number): string {
185
- // Collect every candidate that might carry a `.position`, most specific first.
186
- const candidates: any[] = [];
187
- if (err?.position) candidates.push(err);
188
- if (Array.isArray(err?.errors)) candidates.push(...err.errors);
189
- if (!candidates.length && err && typeof err[Symbol.iterator] === "function") {
190
- try { candidates.push(...err); } catch {}
191
- }
192
- const detail = candidates.find((c) => c?.position) ?? candidates[0] ?? err;
193
- const message = String(detail?.message || err?.message || "Syntax error").split("\n")[0];
194
-
195
- const pos = detail?.position;
196
- if (!pos || typeof pos.line !== "number") return message;
197
-
198
- const loc = ` (line ${pos.line - lineOffset}, col ${pos.column})`;
199
- let frame = "";
200
- if (typeof pos.lineText === "string") {
201
- const caret = " ".repeat(Math.max(0, pos.column ?? 0)) + "^";
202
- frame = `\n ${pos.lineText}\n ${caret}`;
203
- }
204
- return `${message}${loc}${frame}`;
205
- }
206
-
207
- export async function compilePlan(jsx: string, projectRoot?: string): Promise<CompileResult> {
208
- // Strip leading pragma / block / line comments (e.g. `/** @jsxImportSource ... */`).
209
- // Authored JSX sometimes carries a JSX-runtime pragma from another toolchain;
210
- // Canvas injects its own imports and uses the React automatic runtime, so the
211
- // pragma is unused. Left in place it sits at the top level of the wrapped
212
- // fragment as plain text (JSX only treats `{/* */}` as a comment) and renders
213
- // literally on the page.
214
- let stripped: string;
215
- do {
216
- stripped = jsx;
217
- jsx = jsx.replace(/^\s*(?:\/\*[\s\S]*?\*\/|\/\/[^\n]*)\s*/, "");
218
- } while (jsx !== stripped);
219
-
220
- // Resolve file contents at compile time
221
- if (projectRoot) {
222
- jsx = resolveFilePreviews(jsx, projectRoot);
223
- jsx = resolveMarkdownFiles(jsx, projectRoot);
224
- }
225
-
226
- // Strip template literals and strings so `export default` inside CodeBlock
227
- // content (e.g. {`export default defineConfig(...)`}) doesn't false-positive.
228
- const strippedJsx = jsx
229
- .replace(/\{`[^`]*`\}/gs, "") // {`...`} template expressions
230
- .replace(/`[^`]*`/gs, "") // standalone template literals
231
- .replace(/"[^"]*"/g, "") // double-quoted strings
232
- .replace(/'[^']*'/g, ""); // single-quoted strings
233
- const hasDefaultExport = /export\s+default\b/.test(strippedJsx);
234
-
235
- const source = hasDefaultExport
236
- ? `${COMPONENT_IMPORTS}\n${jsx}`
237
- : `${COMPONENT_IMPORTS}\nexport default function Plan() {\n return (<>${jsx}</>);\n}\n`;
238
-
239
- // Offset: COMPONENT_IMPORTS lines + wrapper lines (export default function
240
- // Plan() + return). Used to map Bun's line numbers back to the authored file.
241
- const wrapperLines = hasDefaultExport ? 1 : 2;
242
- const lineOffset = COMPONENT_IMPORTS_LINES + wrapperLines;
243
-
244
- // Pre-validate syntax with Bun.Transpiler — gives precise error positions,
245
- // unlike Bun.build() which throws opaque "Bundle failed" for syntax errors.
246
- try {
247
- const transpiler = new Bun.Transpiler({ loader: "jsx" });
248
- transpiler.transformSync(source);
249
- } catch (syntaxError: any) {
250
- return { ok: false, error: formatBuildError(syntaxError, lineOffset) };
251
- }
252
-
253
- const tmpFile = join(COMPILE_TEMP_DIR, `plan-${randomUUID()}.jsx`);
254
-
255
- try {
256
- writeFileSync(tmpFile, source);
257
-
258
- try {
259
- const result = await Bun.build({
260
- entrypoints: [tmpFile],
261
- format: "esm",
262
- external: ["react", "react-dom", "#canvas/components", "#canvas/runtime"],
263
- jsx: { runtime: "automatic", importSource: "react" },
264
- });
265
-
266
- if (!result.success) {
267
- const errorLogs = result.logs.filter((l) => l.level === "error");
268
- const errors = errorLogs
269
- .map((l) => formatBuildError(l, lineOffset))
270
- .join("\n");
271
- return { ok: false, error: errors || "Bundle failed" };
272
- }
273
-
274
- const js = await result.outputs[0].text();
275
- const validation = await validateCompiledPlan(js);
276
- if (!validation.ok) {
277
- return { ok: false, error: `Runtime error: ${validation.error}` };
278
- }
279
- return { ok: true, js };
280
- } catch (buildError: any) {
281
- // A real syntax error surfaces as an AggregateError carrying BuildMessages
282
- // (with positions) — format it precisely. The opaque "Unknown Error, TODO"
283
- // corruption case (internal bundler state gets corrupted in long-running
284
- // processes) has no sub-errors; pass it through so the CLI restarts the
285
- // daemon and retries.
286
- const hasDetail =
287
- buildError?.position ||
288
- (Array.isArray(buildError?.errors) && buildError.errors.length > 0);
289
- if (hasDetail) {
290
- return { ok: false, error: formatBuildError(buildError, lineOffset) };
291
- }
292
- return { ok: false, error: buildError?.message || "Unknown Error" };
293
- }
294
- } catch (e: any) {
295
- return { ok: false, error: e.message };
296
- } finally {
297
- try { unlinkSync(tmpFile); } catch {}
298
- }
299
- }
300
-
301
- /**
302
- * Find <Markdown file="..." /> tags and inject __content with the file contents
303
- * resolved at compile time.
304
- */
305
- function resolveMarkdownFiles(jsx: string, projectRoot: string): string {
306
- return jsx.replace(
307
- /<Markdown\b([^>]*?)\/>/g,
308
- (match, attrs: string) => {
309
- const fileMatch = attrs.match(/file=["']([^"']+)["']/);
310
- if (!fileMatch) return match;
311
-
312
- const filePath = fileMatch[1];
313
- const absPath = join(projectRoot, filePath);
314
-
315
- try {
316
- const content = readFileSync(absPath, "utf-8");
317
- const escaped = JSON.stringify(content);
318
- return `<Markdown${attrs} __content={${escaped}} />`;
319
- } catch {
320
- return match;
321
- }
322
- }
323
- );
324
- }
325
-
326
- /**
327
- * Find <FilePreview path="..." /> tags and inject __content with the file contents
328
- * resolved at compile time so the browser doesn't need to fetch.
329
- */
330
- function resolveFilePreviews(jsx: string, projectRoot: string): string {
331
- // Match <FilePreview ... /> (self-closing)
332
- return jsx.replace(
333
- /<FilePreview\b([^>]*?)\/>/g,
334
- (match, attrs: string) => {
335
- const pathMatch = attrs.match(/path=["']([^"']+)["']/);
336
- if (!pathMatch) return match;
337
-
338
- const filePath = pathMatch[1];
339
- const absPath = join(projectRoot, filePath);
340
-
341
- try {
342
- let content = readFileSync(absPath, "utf-8");
343
-
344
- // Apply lines filter if present
345
- const linesMatch = attrs.match(/lines=\{?\[(\d+)\s*,\s*(\d+)\]\}?/);
346
- if (linesMatch) {
347
- const start = parseInt(linesMatch[1], 10);
348
- const end = parseInt(linesMatch[2], 10);
349
- content = content.split("\n").slice(start - 1, end).join("\n");
350
- }
351
-
352
- // Escape for embedding in JSX string
353
- const escaped = JSON.stringify(content);
354
- return `<FilePreview${attrs} __content={${escaped}} />`;
355
- } catch {
356
- return match;
357
- }
358
- }
359
- );
360
- }
@@ -1,14 +0,0 @@
1
- export function jsonResponse(data: unknown, status = 200): Response {
2
- return new Response(JSON.stringify(data), {
3
- status,
4
- headers: { "Content-Type": "application/json" },
5
- });
6
- }
7
-
8
- export function corsHeaders(port: number): Record<string, string> {
9
- return {
10
- "Access-Control-Allow-Origin": "http://localhost:" + port,
11
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
12
- "Access-Control-Allow-Headers": "Content-Type",
13
- };
14
- }
@@ -1,16 +0,0 @@
1
- export type RouteHandler = (req: Request, url: URL, match: URLPatternResult) => Response | Promise<Response>;
2
-
3
- export interface Route {
4
- method: string;
5
- pattern: URLPattern;
6
- handler: RouteHandler;
7
- }
8
-
9
- export function dispatch(routes: Route[], req: Request, url: URL): Response | Promise<Response> | null {
10
- for (const route of routes) {
11
- if (route.method !== req.method) continue;
12
- const match = route.pattern.exec(url);
13
- if (match) return route.handler(req, url, match);
14
- }
15
- return null;
16
- }
@@ -1,83 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from "fs";
3
- import { tmpdir } from "os";
4
- import { join } from "path";
5
- import { SessionManager } from "./session";
6
-
7
- const testDirectories: string[] = [];
8
-
9
- function createManager(): SessionManager {
10
- const directory = mkdtempSync(join(tmpdir(), "agent-canvas-secrets-"));
11
- testDirectories.push(directory);
12
- return new SessionManager(directory);
13
- }
14
-
15
- function createSession(manager: SessionManager, id = "runbook"): void {
16
- manager.upsert(id, new Map([["runbook.jsx", "<Section title=\"Runbook\" />"]]), process.cwd());
17
- }
18
-
19
- function readTextFiles(directory: string): string {
20
- const parts: string[] = [];
21
- for (const entry of readdirSync(directory)) {
22
- const path = join(directory, entry);
23
- if (statSync(path).isDirectory()) {
24
- parts.push(readTextFiles(path));
25
- } else {
26
- parts.push(readFileSync(path, "utf-8"));
27
- }
28
- }
29
- return parts.join("\n");
30
- }
31
-
32
- afterEach(() => {
33
- for (const directory of testDirectories.splice(0)) {
34
- rmSync(directory, { recursive: true, force: true });
35
- }
36
- });
37
-
38
- describe("session secret vault", () => {
39
- test("resolves a deposited secret without persisting it", () => {
40
- const manager = createManager();
41
- createSession(manager);
42
-
43
- expect(manager.setSecret("runbook", "service-token", "test-secret-value")).toBe(true);
44
- expect(manager.hasSecret("runbook", "service-token")).toBe(true);
45
- expect(manager.resolveSecrets("runbook", ["service-token"])).toEqual({
46
- ok: true,
47
- values: new Map([["service-token", "test-secret-value"]]),
48
- });
49
- expect(readTextFiles(testDirectories[0])).not.toContain("test-secret-value");
50
- });
51
-
52
- test("reports every missing field without returning partial values", () => {
53
- const manager = createManager();
54
- createSession(manager);
55
- manager.setSecret("runbook", "ready-token", "available");
56
-
57
- expect(manager.resolveSecrets("runbook", ["ready-token", "missing-token"])).toEqual({
58
- ok: false,
59
- missing: ["missing-token"],
60
- });
61
- });
62
-
63
- test("clears secrets when a new revision is pushed", () => {
64
- const manager = createManager();
65
- createSession(manager);
66
- manager.setSecret("runbook", "service-token", "test-secret-value");
67
-
68
- manager.upsert("runbook", new Map([["runbook.jsx", "<Section title=\"Updated\" />"]]), process.cwd());
69
-
70
- expect(manager.hasSecret("runbook", "service-token")).toBe(false);
71
- });
72
-
73
- test("clears one field without affecting the rest", () => {
74
- const manager = createManager();
75
- createSession(manager);
76
- manager.setSecret("runbook", "first-token", "first");
77
- manager.setSecret("runbook", "second-token", "second");
78
-
79
- expect(manager.clearSecret("runbook", "first-token")).toBe(true);
80
- expect(manager.hasSecret("runbook", "first-token")).toBe(false);
81
- expect(manager.hasSecret("runbook", "second-token")).toBe(true);
82
- });
83
- });