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
package/README.md CHANGED
@@ -82,6 +82,20 @@ Agent Canvas supports light and dark themes, automatically matching your system
82
82
 
83
83
  ## Development
84
84
 
85
+ ### Reusable packages
86
+
87
+ | Package | Use without the Agent Canvas shell |
88
+ | --- | --- |
89
+ | [`@fabrika/daemon-kit`](https://github.com/contember/fabrika-platform/tree/main/packages/daemon-kit) | Bun daemon lifecycle, HTTP routing, WebSocket channels and CLI waiters |
90
+ | [`@fabrika/annotations`](https://github.com/contember/fabrika-platform/tree/main/packages/annotations) | Annotation state, editors and text/block/region locators |
91
+ | [`@fabrika/canvas-kernel`](https://github.com/contember/fabrika-platform/tree/main/packages/canvas-kernel) | JSX compiler, revisions, renderer and interactive canvas components |
92
+
93
+ The kernel depends on the other two; neither depends on the kernel. All ship as
94
+ TypeScript sources from the [`fabrika-platform`](https://github.com/contember/fabrika-platform)
95
+ repository. See the kernel README for the breaking 0.2 import migration.
96
+
97
+ ### Local commands
98
+
85
99
  ```bash
86
100
  bun install
87
101
  bun run build # build client assets
@@ -23,7 +23,7 @@ export async function handleDaemon(args: string[]) {
23
23
  break;
24
24
  }
25
25
  case "stop": {
26
- stopDaemon();
26
+ await stopDaemon();
27
27
  break;
28
28
  }
29
29
  case "start": {
@@ -35,11 +35,8 @@ export async function handleDaemon(args: string[]) {
35
35
  break;
36
36
  }
37
37
  case "restart": {
38
- stopDaemon();
39
- for (let i = 0; i < 10; i++) {
40
- if (!(await isDaemonRunning())) break;
41
- await new Promise((r) => setTimeout(r, 100));
42
- }
38
+ // stopDaemon() already waits for the daemon to go down.
39
+ await stopDaemon();
43
40
  await startDaemon();
44
41
  break;
45
42
  }
@@ -67,7 +67,7 @@ export async function handlePush(args: string[]) {
67
67
  const shouldRestart = !result.ok && result.error === "Canvas compilation failed" && !isUserError;
68
68
  if (shouldRestart) {
69
69
  console.error("Compilation failed, restarting daemon and retrying...");
70
- stopDaemon();
70
+ await stopDaemon();
71
71
  await startDaemon();
72
72
  const retryResponse = await fetch(`${BASE_URL}/api/session/${sessionId}/plan`, {
73
73
  method: "POST",
@@ -1,94 +1,87 @@
1
- import { spawn } from "child_process";
2
- import { openSync, closeSync, readFileSync, existsSync, mkdirSync, unlinkSync } from "fs";
1
+ import { readFileSync } from "fs";
3
2
  import { join } from "path";
3
+ import {
4
+ createDaemonLifecycle,
5
+ DaemonStartError,
6
+ type DaemonEvent,
7
+ } from "@fabrika/daemon-kit";
4
8
  import { PACKAGE_ROOT, TEMP_DIR, BASE_URL, PID_FILE, CLI_AUTH_FILE, VERSION } from "./config.ts";
5
9
 
6
- export async function isDaemonRunning(): Promise<boolean> {
7
- try {
8
- const res = await fetch(`${BASE_URL}/health`, { signal: AbortSignal.timeout(1000) });
9
- const data = await res.json() as any;
10
- return data.ok === true;
11
- } catch {
12
- return false;
13
- }
10
+ interface CanvasHealth {
11
+ pid: number;
12
+ version: string;
13
+ }
14
+
15
+ function isRecord(value: unknown): value is Record<string, unknown> {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14
17
  }
15
18
 
16
- async function getDaemonVersion(): Promise<string | null> {
19
+ /**
20
+ * Transitional: daemons before the pid landed in `/health` only leave a pid file
21
+ * behind. Drop this fallback once no such daemon can still be running.
22
+ */
23
+ function legacyPidFromFile(): number | undefined {
17
24
  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;
25
+ const pid = Number(readFileSync(PID_FILE, "utf-8").trim());
26
+ return Number.isInteger(pid) && pid > 0 ? pid : undefined;
21
27
  } catch {
22
- return null;
28
+ return undefined;
23
29
  }
24
30
  }
25
31
 
26
- export async function startDaemon(): Promise<void> {
27
- console.error("Starting canvas daemon...");
28
- mkdirSync(TEMP_DIR, { recursive: true });
29
-
30
- const daemonScript = join(PACKAGE_ROOT, "daemon", "src", "server.ts");
31
- const stderrPath = join(TEMP_DIR, "daemon-startup.log");
32
- const stderrFd = openSync(stderrPath, "w");
32
+ function parseHealth(body: unknown): CanvasHealth | undefined {
33
+ if (!isRecord(body) || body.ok !== true || typeof body.version !== "string") return undefined;
34
+ const pid = typeof body.pid === "number" ? body.pid : legacyPidFromFile();
35
+ if (pid === undefined) return undefined;
36
+ return { pid, version: body.version };
37
+ }
33
38
 
34
- const child = spawn("bun", ["run", daemonScript], {
35
- detached: true,
36
- stdio: ["ignore", "ignore", stderrFd],
37
- cwd: join(PACKAGE_ROOT, "daemon"),
38
- });
39
+ function report(event: DaemonEvent) {
40
+ if (event.type === "version-mismatch") {
41
+ console.error(`Daemon version mismatch (daemon: ${event.running}, CLI: ${VERSION}), restarting...`);
42
+ }
43
+ if (event.type === "starting") console.error("Starting canvas daemon...");
44
+ if (event.type === "started") console.error("Daemon started.");
45
+ }
39
46
 
40
- child.unref();
41
- closeSync(stderrFd);
47
+ const lifecycle = createDaemonLifecycle<CanvasHealth>({
48
+ baseUrl: BASE_URL,
49
+ parseHealth,
50
+ pidFile: PID_FILE,
51
+ // The running Bun, not a PATH lookup — `Bun.spawn` throws on a missing executable.
52
+ command: [process.execPath, "run", join(PACKAGE_ROOT, "daemon", "src", "server.ts")],
53
+ cwd: join(PACKAGE_ROOT, "daemon"),
54
+ logFile: join(TEMP_DIR, "daemon-startup.log"),
55
+ versionCheck: { expected: VERSION, of: (health) => health.version },
56
+ cleanupFiles: [CLI_AUTH_FILE],
57
+ onEvent: report,
58
+ });
42
59
 
43
- for (let i = 0; i < 30; i++) {
44
- await new Promise((r) => setTimeout(r, 100));
45
- if (await isDaemonRunning()) {
46
- console.error("Daemon started.");
47
- return;
48
- }
49
- }
60
+ export async function isDaemonRunning(): Promise<boolean> {
61
+ return lifecycle.isRunning();
62
+ }
50
63
 
51
- if (existsSync(stderrPath)) {
52
- const stderr = readFileSync(stderrPath, "utf-8").trim();
53
- if (stderr) {
54
- console.error("Daemon stderr:\n" + stderr);
55
- }
64
+ function reportStartFailure(error: unknown): never {
65
+ if (error instanceof DaemonStartError && error.logTail) {
66
+ console.error("Daemon stderr:\n" + error.logTail);
56
67
  }
57
-
58
- console.error("Error: Daemon failed to start within 3 seconds.");
68
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
59
69
  process.exit(1);
60
70
  }
61
71
 
72
+ export async function startDaemon(): Promise<void> {
73
+ await lifecycle.start().catch(reportStartFailure);
74
+ }
75
+
62
76
  export async function ensureDaemon(): Promise<void> {
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
- }
72
- await startDaemon();
77
+ await lifecycle.ensure().catch(reportStartFailure);
73
78
  }
74
79
 
75
- export function stopDaemon(): boolean {
76
- if (existsSync(PID_FILE)) {
77
- const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
78
- try {
79
- process.kill(pid, "SIGTERM");
80
- console.log("Daemon stopped.");
81
- try { unlinkSync(PID_FILE); } catch {}
82
- try { unlinkSync(CLI_AUTH_FILE); } catch {}
83
- return true;
84
- } catch {
85
- console.log("Daemon was not running (stale PID file).");
86
- try { unlinkSync(PID_FILE); } catch {}
87
- try { unlinkSync(CLI_AUTH_FILE); } catch {}
88
- return false;
89
- }
90
- } else {
91
- console.log("No daemon PID file found.");
92
- return false;
93
- }
80
+ export async function stopDaemon(): Promise<boolean> {
81
+ const result = await lifecycle.stop();
82
+ if (result.reason === "stopped") console.log("Daemon stopped.");
83
+ else if (result.reason === "stale-pid") console.log("Daemon was not running (stale PID file).");
84
+ else if (result.reason === "not-running") console.log("No daemon PID file found.");
85
+ else console.log(result.message);
86
+ return result.stopped;
94
87
  }
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
  import { waitForFeedback } from "./helpers";
3
3
 
4
4
  class FakeSocket {
5
+ onopen: ((event: Event) => void) | null = null;
5
6
  onmessage: ((event: MessageEvent) => void) | null = null;
6
7
  onerror: ((event: Event) => void) | null = null;
7
8
  onclose: ((event: CloseEvent) => void) | null = null;
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "child_process";
2
2
  import { randomUUID } from "crypto";
3
+ import { waitForEvent, type WaiterSocket, type WaitVerdict } from "@fabrika/daemon-kit";
3
4
  import { BASE_URL, WS_URL, TIMEOUT_MS } from "./config.ts";
4
5
 
5
6
  export function getSessionId(session?: string): string {
@@ -33,15 +34,8 @@ export function openBrowser(url: string) {
33
34
  console.error(`Open this URL in your browser: ${url}`);
34
35
  }
35
36
 
36
- interface FeedbackSocket {
37
- close(): void;
38
- onmessage: ((event: MessageEvent) => void) | null;
39
- onerror: ((event: Event) => void) | null;
40
- onclose: ((event: CloseEvent) => void) | null;
41
- }
42
-
43
37
  interface FeedbackWaitOptions {
44
- createSocket?: (url: string) => FeedbackSocket;
38
+ createSocket?: (url: string) => WaiterSocket;
45
39
  timeoutMs?: number;
46
40
  reconnectAttempts?: number;
47
41
  reconnectDelayMs?: number;
@@ -55,6 +49,10 @@ interface CanvasRenderError {
55
49
  componentStack?: string;
56
50
  }
57
51
 
52
+ function isRecord(value: unknown): value is Record<string, unknown> {
53
+ return typeof value === "object" && value !== null && !Array.isArray(value);
54
+ }
55
+
58
56
  function isCanvasRenderError(value: unknown): value is CanvasRenderError {
59
57
  return typeof value === "object" && value !== null
60
58
  && "revision" in value && Number.isInteger(value.revision)
@@ -71,78 +69,36 @@ function formatCanvasRenderError(error: CanvasRenderError): string {
71
69
  return details.join("\n");
72
70
  }
73
71
 
74
- class FeedbackWaitError extends Error {
75
- constructor(message: string, readonly retryable: boolean) {
76
- super(message);
72
+ /** A canvas that failed to render is the answer — retrying the socket cannot fix it. */
73
+ function readFeedbackFrame(frame: unknown): WaitVerdict<string> | undefined {
74
+ if (!isRecord(frame)) return undefined;
75
+ if (frame.type === "submit" && typeof frame.feedback === "string") {
76
+ return { type: "settle", value: frame.feedback };
77
77
  }
78
+ if (frame.type === "render-error" && isCanvasRenderError(frame.error)) {
79
+ return { type: "fail", error: new Error(formatCanvasRenderError(frame.error)) };
80
+ }
81
+ return undefined;
78
82
  }
79
83
 
80
- function waitForFeedbackConnection(
81
- sessionId: string,
82
- timeoutMs: number,
83
- createSocket: (url: string) => FeedbackSocket,
84
- ): Promise<string> {
85
- return new Promise((resolve, reject) => {
86
- const ws = createSocket(`${WS_URL}/ws/wait/${sessionId}`);
87
- let settled = false;
88
-
89
- const finish = (result: string | FeedbackWaitError) => {
90
- if (settled) return;
91
- settled = true;
92
- clearTimeout(timeout);
93
- ws.close();
94
- if (result instanceof FeedbackWaitError) reject(result);
95
- else resolve(result);
96
- };
97
-
98
- const timeout = setTimeout(() => {
99
- finish(new FeedbackWaitError("Timeout waiting for feedback.", false));
100
- }, timeoutMs);
101
-
102
- ws.onmessage = (event) => {
103
- try {
104
- const data = JSON.parse(typeof event.data === "string" ? event.data : "");
105
- if (data.type === "submit") {
106
- finish(data.feedback);
107
- }
108
- if (data.type === "render-error" && isCanvasRenderError(data.error)) {
109
- finish(new FeedbackWaitError(formatCanvasRenderError(data.error), false));
110
- }
111
- } catch {}
112
- };
113
-
114
- ws.onerror = () => {
115
- finish(new FeedbackWaitError("WebSocket connection failed.", true));
116
- };
117
-
118
- ws.onclose = () => {
119
- finish(new FeedbackWaitError("WebSocket closed before feedback was submitted.", true));
120
- };
84
+ export function waitForFeedback(sessionId: string, options: FeedbackWaitOptions = {}): Promise<string> {
85
+ return waitForEvent<string>({
86
+ url: `${WS_URL}/ws/wait/${sessionId}`,
87
+ onFrame: readFeedbackFrame,
88
+ ...(options.createSocket ? { createSocket: options.createSocket } : {}),
89
+ timeoutMs: options.timeoutMs ?? TIMEOUT_MS,
90
+ reconnect: {
91
+ attempts: options.reconnectAttempts ?? 3,
92
+ delayMs: options.reconnectDelayMs ?? 1_000,
93
+ },
94
+ messages: {
95
+ timeout: "Timeout waiting for feedback.",
96
+ closed: "WebSocket closed before feedback was submitted.",
97
+ connectionFailed: "WebSocket connection failed.",
98
+ },
121
99
  });
122
100
  }
123
101
 
124
- export async function waitForFeedback(sessionId: string, options: FeedbackWaitOptions = {}): Promise<string> {
125
- const createSocket = options.createSocket ?? ((url: string) => new WebSocket(url));
126
- const timeoutMs = options.timeoutMs ?? TIMEOUT_MS;
127
- const reconnectAttempts = options.reconnectAttempts ?? 3;
128
- const reconnectDelayMs = options.reconnectDelayMs ?? 1_000;
129
- const deadline = Date.now() + timeoutMs;
130
-
131
- for (let attempt = 0; ; attempt++) {
132
- const remainingMs = deadline - Date.now();
133
- if (remainingMs <= 0) throw new FeedbackWaitError("Timeout waiting for feedback.", false);
134
-
135
- try {
136
- return await waitForFeedbackConnection(sessionId, remainingMs, createSocket);
137
- } catch (error) {
138
- if (!(error instanceof FeedbackWaitError) || !error.retryable || attempt >= reconnectAttempts) {
139
- throw error;
140
- }
141
- await Bun.sleep(Math.min(reconnectDelayMs, Math.max(0, deadline - Date.now())));
142
- }
143
- }
144
- }
145
-
146
102
  export async function readLine(): Promise<string> {
147
103
  const buf: number[] = [];
148
104
  for await (const chunk of Bun.stdin.stream()) {
package/daemon/build.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  import { mkdirSync, cpSync, writeFileSync, readFileSync, existsSync, watch as fsWatch } from "fs";
2
2
  import { join, dirname } from "path";
3
+ import { fileURLToPath } from "url";
3
4
  import { $ } from "bun";
4
5
 
5
6
  const ROOT = dirname(import.meta.path);
6
7
  const DIST = join(ROOT, "dist");
8
+ /** The kernel ships the runtime, the component library and the base stylesheet. */
9
+ const KERNEL_CLIENT = dirname(fileURLToPath(import.meta.resolve("@fabrika/canvas-kernel/runtime")));
10
+ const ANNOTATIONS_RUNTIME = fileURLToPath(import.meta.resolve("@fabrika/annotations/runtime"));
7
11
 
8
12
  async function build() {
9
13
  console.log("Building canvas client...");
@@ -11,11 +15,25 @@ async function build() {
11
15
  mkdirSync(DIST, { recursive: true });
12
16
 
13
17
  const REACT_EXTERNALS = ["react", "react-dom", "react-dom/client", "react/jsx-runtime", "react/jsx-dev-runtime"];
18
+ const ANNOTATION_EXTERNALS = [...REACT_EXTERNALS, "@fabrika/annotations/runtime"];
19
+
20
+ const annotationResult = await Bun.build({
21
+ entrypoints: [ANNOTATIONS_RUNTIME],
22
+ outdir: DIST,
23
+ format: "esm",
24
+ external: REACT_EXTERNALS,
25
+ naming: "annotations-runtime.js",
26
+ minify: true,
27
+ });
28
+ if (!annotationResult.success) {
29
+ console.error("Annotation runtime build failed:", annotationResult.logs);
30
+ process.exit(1);
31
+ }
14
32
 
15
33
  // 0. Build preact-compat bundle
16
34
  console.log(" Building preact-compat...");
17
35
  const preactResult = await Bun.build({
18
- entrypoints: [join(ROOT, "client/compat/preact-all.ts")],
36
+ entrypoints: [join(KERNEL_CLIENT, "preact-all.ts")],
19
37
  outdir: DIST,
20
38
  format: "esm",
21
39
  naming: "preact-compat.js",
@@ -35,10 +53,10 @@ async function build() {
35
53
  // 1. Build runtime (shared context between app and components)
36
54
  console.log(" Building #canvas/runtime...");
37
55
  const runtimeResult = await Bun.build({
38
- entrypoints: [join(ROOT, "client/runtime.ts")],
56
+ entrypoints: [join(KERNEL_CLIENT, "runtime.ts")],
39
57
  outdir: DIST,
40
58
  format: "esm",
41
- external: [...REACT_EXTERNALS],
59
+ external: ANNOTATION_EXTERNALS,
42
60
  naming: "runtime.js",
43
61
  minify: true,
44
62
  });
@@ -54,7 +72,7 @@ async function build() {
54
72
  entrypoints: [join(ROOT, "client/components/index.ts")],
55
73
  outdir: DIST,
56
74
  format: "esm",
57
- external: [...REACT_EXTERNALS, "#canvas/runtime"],
75
+ external: [...ANNOTATION_EXTERNALS, "#canvas/runtime"],
58
76
  naming: "components.js",
59
77
  minify: true,
60
78
  });
@@ -70,7 +88,7 @@ async function build() {
70
88
  entrypoints: [join(ROOT, "client/App.tsx")],
71
89
  outdir: DIST,
72
90
  format: "esm",
73
- external: [...REACT_EXTERNALS, "#canvas/components", "#canvas/runtime"],
91
+ external: [...ANNOTATION_EXTERNALS, "#canvas/components", "#canvas/runtime"],
74
92
  naming: "client.js",
75
93
  minify: true,
76
94
  });
@@ -85,7 +103,7 @@ async function build() {
85
103
  await $`cd ${ROOT} && npx @tailwindcss/cli -i client/styles.css -o dist/client.css --minify`.quiet();
86
104
 
87
105
  // 5. Read theme.css to inline into HTML for browser Tailwind runtime
88
- const themeCss = readFileSync(join(ROOT, "client/theme.css"), "utf-8");
106
+ const themeCss = readFileSync(join(KERNEL_CLIENT, "theme.css"), "utf-8");
89
107
 
90
108
  // 6. Create index.html
91
109
  console.log(" Creating index.html...");
@@ -118,6 +136,7 @@ ${themeCss}
118
136
  "react/jsx-runtime": "/assets/jsx-runtime.js",
119
137
  "react/jsx-dev-runtime": "/assets/jsx-runtime.js",
120
138
  "#canvas/components": "/assets/components.js",
139
+ "@fabrika/annotations/runtime": "/assets/annotations-runtime.js",
121
140
  "#canvas/runtime": "/assets/runtime.js"
122
141
  }
123
142
  }
@@ -142,6 +161,7 @@ ${themeCss}
142
161
  "preact-compat.js",
143
162
  "jsx-runtime.js",
144
163
  "runtime.js",
164
+ "annotations-runtime.js",
145
165
  "components.js",
146
166
  "client.js",
147
167
  "client.css",
@@ -155,17 +175,18 @@ ${themeCss}
155
175
 
156
176
  build().then(() => {
157
177
  if (process.argv.includes("--watch")) {
158
- const clientDir = join(ROOT, "client");
159
178
  let timeout: ReturnType<typeof setTimeout> | null = null;
160
- console.log("Watching for changes in client/...");
161
- fsWatch(clientDir, { recursive: true }, (_event: string, filename: string | null) => {
162
- if (!filename || filename.endsWith("~")) return;
163
- if (timeout) clearTimeout(timeout);
164
- timeout = setTimeout(() => {
165
- console.log(`\n Changed: ${filename}`);
166
- build().catch((e) => console.error("Rebuild failed:", e));
167
- }, 200);
168
- });
179
+ console.log("Watching for changes in client/ and the kernel client...");
180
+ for (const dir of [join(ROOT, "client"), KERNEL_CLIENT, dirname(ANNOTATIONS_RUNTIME)]) {
181
+ fsWatch(dir, { recursive: true }, (_event: string, filename: string | null) => {
182
+ if (!filename || filename.endsWith("~")) return;
183
+ if (timeout) clearTimeout(timeout);
184
+ timeout = setTimeout(() => {
185
+ console.log(`\n Changed: ${filename}`);
186
+ build().catch((e) => console.error("Rebuild failed:", e));
187
+ }, 200);
188
+ });
189
+ }
169
190
  }
170
191
  }).catch((e) => {
171
192
  console.error("Build failed:", e);
@@ -2,29 +2,41 @@ import React, { useEffect, useLayoutEffect, useState, useCallback, useRef, creat
2
2
  import { createRoot } from "react-dom/client";
3
3
  import { marked } from "marked";
4
4
  import { SessionContext, ActiveViewCtx } from "#canvas/runtime";
5
- import { AnnotationProvider, useAnnotations } from "./AnnotationProvider";
6
- import { PlanRenderer } from "./PlanRenderer";
7
- import { AnnotationSidebar } from "./AnnotationSidebar";
5
+ import {
6
+ ActiveViewContext,
7
+ AnnotationProvider,
8
+ AnnotationSidebar,
9
+ CanvasHostContext,
10
+ FileIcon,
11
+ PlanRenderer,
12
+ RenderErrorContext,
13
+ RevisionContext,
14
+ carryUnsubmittedDraft,
15
+ clearPersistedDraft,
16
+ useAnnotations,
17
+ type ActiveView,
18
+ type AnnotationDraftPhase,
19
+ type CanvasFileInfo,
20
+ type CanvasRenderError,
21
+ type RevisionInfo,
22
+ } from "@fabrika/canvas-kernel/client";
23
+ import {
24
+ AnnotationCreatePopover, AnnotationEditPopover, RESPONSE_ANNOTATION_PATH,
25
+ extractContext, generateAnnotationId, renameMarkId, restoreMarks,
26
+ unwrapMarks, updateAllMarkStates, wrapRangeWithMark,
27
+ } from "@fabrika/annotations";
8
28
  import { FileBrowser } from "./FileBrowser";
9
- import { FileIcon } from "./FileIcon";
10
29
  import { FileViewer } from "./FileViewer";
11
30
  import { SessionSwitcher } from "./SessionSwitcher";
12
31
  import { exportCanvasToMarkdown } from "./exportMarkdown";
13
32
  import { CompareView } from "./CompareView";
14
33
  import { OverviewView, categorizeChanges, getAffectedFiles } from "./OverviewView";
15
34
  import { RevisionSelect } from "./RevisionSelect";
16
- import { generateAnnotationId, RESPONSE_ANNOTATION_PATH } from "./utils";
17
- import { wrapRangeWithMark, restoreMarks, renameMarkId, unwrapMarks, updateAllMarkStates } from "./highlightRange";
18
- import { extractContext } from "./annotationContext";
19
- import { AnnotationCreatePopover, AnnotationEditPopover } from "./Popover";
20
35
  import { ShareDialog, ShareButton, type ShareEntry } from "./ShareDialog";
21
36
  import { ReviewerIdentityDialog } from "./ReviewerIdentityDialog";
22
37
  import type { Annotation } from "#canvas/runtime";
23
- import { MODE, fetchMeta, FS_AVAILABLE, WS_AVAILABLE, submitSharedFeedback, getReviewerIdentity, setReviewerIdentity } from "./clientApi";
24
- import { carryUnsubmittedDraft, clearPersistedDraft, type AnnotationDraftPhase } from "./annotationDraft";
25
- import { RenderErrorContext, type CanvasRenderError } from "./RenderErrorContext";
38
+ import { MODE, fetchMeta, FS_AVAILABLE, WS_AVAILABLE, submitSharedFeedback, getReviewerIdentity, setReviewerIdentity, canvasHost } from "./clientApi";
26
39
 
27
- export type ActiveView = { type: "overview" } | { type: "canvas"; filename: string } | { type: "file"; path: string };
28
40
 
29
41
  /**
30
42
  * Wire shape of a remote feedback entry as delivered by the daemon
@@ -62,55 +74,6 @@ function remoteFeedbackToAnnotations(entries: RemoteFeedbackEntry[]): Annotation
62
74
  return out;
63
75
  }
64
76
 
65
- export interface CanvasFileInfo {
66
- filename: string;
67
- diffStats?: { added: number; removed: number };
68
- }
69
-
70
- export interface RevisionInfo {
71
- revision: number;
72
- label?: string;
73
- canvasFiles: CanvasFileInfo[];
74
- createdAt: string;
75
- hasFeedback: boolean;
76
- feedbackConsumed: boolean;
77
- response?: string;
78
- }
79
-
80
- export const ActiveViewContext = createContext<{
81
- activeView: ActiveView;
82
- setActiveView: (v: ActiveView) => void;
83
- openFiles: string[];
84
- closeFile: (path: string) => void;
85
- canvasFiles: string[];
86
- }>({
87
- activeView: { type: "overview" },
88
- setActiveView: () => {},
89
- openFiles: [],
90
- closeFile: () => {},
91
- canvasFiles: [],
92
- });
93
-
94
- export const RevisionContext = createContext<{
95
- currentRevision: number;
96
- selectedRevision: number;
97
- revisions: RevisionInfo[];
98
- setSelectedRevision: (rev: number) => void;
99
- isReadOnly: boolean;
100
- compareRevision: { left: number; right: number } | null;
101
- setCompareRevision: (rev: { left: number; right: number } | null) => void;
102
- agentWatching: boolean;
103
- }>({
104
- currentRevision: 1,
105
- selectedRevision: 1,
106
- revisions: [],
107
- setSelectedRevision: () => {},
108
- isReadOnly: false,
109
- compareRevision: null,
110
- setCompareRevision: () => {},
111
- agentWatching: false,
112
- });
113
-
114
77
  function resolveTheme(pref: string): "light" | "dark" {
115
78
  if (pref === "auto") return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
116
79
  return pref as "light" | "dark";
@@ -519,6 +482,7 @@ function App() {
519
482
  }
520
483
 
521
484
  return (
485
+ <CanvasHostContext.Provider value={canvasHost}>
522
486
  <SessionContext.Provider value={sessionId}>
523
487
  <RenderErrorContext.Provider value={reportRenderError}>
524
488
  <RevisionContext.Provider value={{ currentRevision, selectedRevision, revisions, setSelectedRevision, isReadOnly, compareRevision, setCompareRevision, agentWatching }}>
@@ -529,6 +493,7 @@ function App() {
529
493
  isReadOnly={isReadOnly}
530
494
  draftPhase={draftPhase}
531
495
  remoteAnnotations={remoteAnnotationsByRev.get(selectedRevision)}
496
+ canvasFiles={canvasFiles}
532
497
  >
533
498
  <ActiveViewContext.Provider value={{ activeView, setActiveView, openFiles, closeFile, canvasFiles }}>
534
499
  <ActiveViewCtx.Provider value={{ setActiveView }}>
@@ -707,6 +672,7 @@ function App() {
707
672
  </RevisionContext.Provider>
708
673
  </RenderErrorContext.Provider>
709
674
  </SessionContext.Provider>
675
+ </CanvasHostContext.Provider>
710
676
  );
711
677
  }
712
678
 
@@ -1,11 +1,11 @@
1
1
  import React, { useEffect, useState, useRef, useCallback, useMemo, useContext } from "react";
2
2
  import { RevisionSelect } from "./RevisionSelect";
3
- import { RevisionContext, type RevisionInfo, type CanvasFileInfo } from "./App";
3
+ import { RevisionContext, type RevisionInfo, type CanvasFileInfo } from "@fabrika/canvas-kernel/client";
4
4
  import { runDomDiff } from "./domDiff";
5
5
  import { extractBlockTree, matchBlocks, buildUnifiedDom } from "./unifiedDiff";
6
- import { useAnnotations } from "./AnnotationProvider";
7
- import { useTextAnnotation } from "./useTextAnnotation";
8
- import { extractContext } from "./annotationContext";
6
+ import { useAnnotations } from "@fabrika/canvas-kernel/client";
7
+ import { useTextAnnotation } from "@fabrika/annotations";
8
+ import { extractContext } from "@fabrika/annotations";
9
9
 
10
10
  interface CompareViewProps {
11
11
  initialLeft: number;
@@ -1,9 +1,9 @@
1
1
  import React, { useEffect, useState, useContext, useCallback, useMemo } from "react";
2
2
  import { SessionContext } from "#canvas/runtime";
3
- import { useAnnotations } from "./AnnotationProvider";
4
- import { ActiveViewContext } from "./App";
5
- import { FileIcon } from "./FileIcon";
6
- import { RESPONSE_ANNOTATION_PATH } from "./utils";
3
+ import { useAnnotations } from "@fabrika/canvas-kernel/client";
4
+ import { ActiveViewContext } from "@fabrika/canvas-kernel/client";
5
+ import { FileIcon } from "@fabrika/canvas-kernel/client";
6
+ import { RESPONSE_ANNOTATION_PATH } from "@fabrika/annotations";
7
7
  import { FS_AVAILABLE } from "./clientApi";
8
8
 
9
9
  interface TreeEntry { name: string; type: "file" | "dir"; }