agent-canvas 0.1.44 → 0.1.45

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 (110) hide show
  1. package/bin/lib/commands/daemon.ts +3 -6
  2. package/bin/lib/commands/push.ts +1 -1
  3. package/bin/lib/daemon-lifecycle.ts +65 -72
  4. package/bin/lib/helpers.test.ts +1 -0
  5. package/bin/lib/helpers.ts +30 -74
  6. package/daemon/build.ts +16 -13
  7. package/daemon/client/App.tsx +32 -61
  8. package/daemon/client/CompareView.tsx +4 -4
  9. package/daemon/client/FileBrowser.tsx +4 -4
  10. package/daemon/client/FileViewer.tsx +4 -4
  11. package/daemon/client/OverviewView.tsx +2 -2
  12. package/daemon/client/RevisionSelect.tsx +1 -1
  13. package/daemon/client/clientApi.ts +18 -0
  14. package/daemon/client/components/index.ts +3 -16
  15. package/daemon/client/styles.css +4 -457
  16. package/daemon/dist/client.css +1 -1
  17. package/daemon/dist/client.js +76 -76
  18. package/daemon/dist/components.js +29 -29
  19. package/daemon/dist/runtime.js +1 -1
  20. package/daemon/src/components.ts +13 -0
  21. package/daemon/src/handlers/api.ts +14 -9
  22. package/daemon/src/handlers/files.ts +1 -3
  23. package/daemon/src/handlers/static.ts +1 -1
  24. package/daemon/src/handlers/uploads.ts +1 -3
  25. package/daemon/src/paths.ts +10 -24
  26. package/daemon/src/remote-feedback.test.ts +1 -1
  27. package/daemon/src/remote-feedback.ts +1 -1
  28. package/daemon/src/secrets-api.test.ts +1 -2
  29. package/daemon/src/server.ts +13 -10
  30. package/daemon/src/share-e2e.test.ts +1 -1
  31. package/daemon/src/share.test.ts +1 -1
  32. package/daemon/src/share.ts +1 -1
  33. package/daemon/tsconfig.json +1 -3
  34. package/package.json +10 -2
  35. package/.github/dark.png +0 -0
  36. package/.github/light.png +0 -0
  37. package/.github/workflows/ci.yml +0 -48
  38. package/.github/workflows/deploy-worker-prod.yml +0 -19
  39. package/.github/workflows/deploy-worker-stage.yml +0 -21
  40. package/.github/workflows/deploy-worker.yml +0 -72
  41. package/.github/workflows/release.yml +0 -36
  42. package/CLAUDE.md +0 -86
  43. package/bun.lock +0 -377
  44. package/daemon/client/AnnotationEditor.tsx +0 -315
  45. package/daemon/client/AnnotationProvider.tsx +0 -195
  46. package/daemon/client/AnnotationSidebar.tsx +0 -627
  47. package/daemon/client/FileIcon.tsx +0 -76
  48. package/daemon/client/PlanRenderer.tsx +0 -614
  49. package/daemon/client/Popover.tsx +0 -350
  50. package/daemon/client/RenderErrorContext.ts +0 -11
  51. package/daemon/client/ResponsePreview.tsx +0 -186
  52. package/daemon/client/SessionContext.tsx +0 -3
  53. package/daemon/client/annotationContext.ts +0 -114
  54. package/daemon/client/annotationDom.ts +0 -104
  55. package/daemon/client/annotationDraft.test.ts +0 -188
  56. package/daemon/client/annotationDraft.ts +0 -130
  57. package/daemon/client/compat/preact-all.ts +0 -4
  58. package/daemon/client/components/Callout.tsx +0 -26
  59. package/daemon/client/components/Checklist.tsx +0 -37
  60. package/daemon/client/components/Choice.tsx +0 -176
  61. package/daemon/client/components/CodeBlock.tsx +0 -35
  62. package/daemon/client/components/Diff.tsx +0 -65
  63. package/daemon/client/components/FilePreview.tsx +0 -88
  64. package/daemon/client/components/Image.tsx +0 -45
  65. package/daemon/client/components/Markdown.tsx +0 -44
  66. package/daemon/client/components/Mermaid.tsx +0 -565
  67. package/daemon/client/components/Note.tsx +0 -14
  68. package/daemon/client/components/Priority.tsx +0 -19
  69. package/daemon/client/components/ResponseNote.tsx +0 -19
  70. package/daemon/client/components/Section.tsx +0 -33
  71. package/daemon/client/components/Table.tsx +0 -35
  72. package/daemon/client/components/Task.tsx +0 -76
  73. package/daemon/client/components/UserInput.tsx +0 -174
  74. package/daemon/client/components/renderMarkdownChildren.tsx +0 -97
  75. package/daemon/client/generateMarkdown.ts +0 -273
  76. package/daemon/client/highlightRange.ts +0 -224
  77. package/daemon/client/popoverPosition.ts +0 -31
  78. package/daemon/client/runtime.ts +0 -123
  79. package/daemon/client/theme.css +0 -66
  80. package/daemon/client/useTextAnnotation.tsx +0 -188
  81. package/daemon/client/utils.ts +0 -13
  82. package/daemon/src/compiler.test.ts +0 -57
  83. package/daemon/src/compiler.ts +0 -360
  84. package/daemon/src/handlers/utils.ts +0 -14
  85. package/daemon/src/router.ts +0 -16
  86. package/daemon/src/session-secrets.test.ts +0 -83
  87. package/daemon/src/session.ts +0 -612
  88. package/daemon/src/watcher.ts +0 -71
  89. package/daemon/src/websocket.test.ts +0 -99
  90. package/daemon/src/websocket.ts +0 -211
  91. package/tsconfig.json +0 -14
  92. package/workers/canvas-share/README.md +0 -159
  93. package/workers/canvas-share/oblaka.ts +0 -56
  94. package/workers/canvas-share/package.json +0 -25
  95. package/workers/canvas-share/public/assets/.gitkeep +0 -0
  96. package/workers/canvas-share/public/assets/client.css +0 -2
  97. package/workers/canvas-share/public/assets/client.js +0 -91
  98. package/workers/canvas-share/public/assets/components.js +0 -65
  99. package/workers/canvas-share/public/assets/jsx-runtime.js +0 -3
  100. package/workers/canvas-share/public/assets/preact-compat.js +0 -1
  101. package/workers/canvas-share/public/assets/runtime.js +0 -1
  102. package/workers/canvas-share/public/index.html +0 -385
  103. package/workers/canvas-share/public/screenshot-dark.png +0 -0
  104. package/workers/canvas-share/public/screenshot-light.png +0 -0
  105. package/workers/canvas-share/src/index.test.ts +0 -209
  106. package/workers/canvas-share/src/index.ts +0 -518
  107. package/workers/canvas-share/src/types.ts +0 -119
  108. package/workers/canvas-share/src/util.ts +0 -156
  109. package/workers/canvas-share/src/validation.ts +0 -160
  110. package/workers/canvas-share/tsconfig.json +0 -16
@@ -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/canvas-kernel/server";
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/canvas-kernel/server";
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
@@ -4,6 +4,8 @@ import { $ } from "bun";
4
4
 
5
5
  const ROOT = dirname(import.meta.path);
6
6
  const DIST = join(ROOT, "dist");
7
+ /** The kernel ships the runtime, the component library and the base stylesheet. */
8
+ const KERNEL_CLIENT = join(ROOT, "..", "packages", "canvas-kernel", "src", "client");
7
9
 
8
10
  async function build() {
9
11
  console.log("Building canvas client...");
@@ -15,7 +17,7 @@ async function build() {
15
17
  // 0. Build preact-compat bundle
16
18
  console.log(" Building preact-compat...");
17
19
  const preactResult = await Bun.build({
18
- entrypoints: [join(ROOT, "client/compat/preact-all.ts")],
20
+ entrypoints: [join(KERNEL_CLIENT, "preact-all.ts")],
19
21
  outdir: DIST,
20
22
  format: "esm",
21
23
  naming: "preact-compat.js",
@@ -35,7 +37,7 @@ async function build() {
35
37
  // 1. Build runtime (shared context between app and components)
36
38
  console.log(" Building #canvas/runtime...");
37
39
  const runtimeResult = await Bun.build({
38
- entrypoints: [join(ROOT, "client/runtime.ts")],
40
+ entrypoints: [join(KERNEL_CLIENT, "runtime.ts")],
39
41
  outdir: DIST,
40
42
  format: "esm",
41
43
  external: [...REACT_EXTERNALS],
@@ -85,7 +87,7 @@ async function build() {
85
87
  await $`cd ${ROOT} && npx @tailwindcss/cli -i client/styles.css -o dist/client.css --minify`.quiet();
86
88
 
87
89
  // 5. Read theme.css to inline into HTML for browser Tailwind runtime
88
- const themeCss = readFileSync(join(ROOT, "client/theme.css"), "utf-8");
90
+ const themeCss = readFileSync(join(KERNEL_CLIENT, "theme.css"), "utf-8");
89
91
 
90
92
  // 6. Create index.html
91
93
  console.log(" Creating index.html...");
@@ -155,17 +157,18 @@ ${themeCss}
155
157
 
156
158
  build().then(() => {
157
159
  if (process.argv.includes("--watch")) {
158
- const clientDir = join(ROOT, "client");
159
160
  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
- });
161
+ console.log("Watching for changes in client/ and the kernel client...");
162
+ for (const dir of [join(ROOT, "client"), KERNEL_CLIENT]) {
163
+ fsWatch(dir, { recursive: true }, (_event: string, filename: string | null) => {
164
+ if (!filename || filename.endsWith("~")) return;
165
+ if (timeout) clearTimeout(timeout);
166
+ timeout = setTimeout(() => {
167
+ console.log(`\n Changed: ${filename}`);
168
+ build().catch((e) => console.error("Rebuild failed:", e));
169
+ }, 200);
170
+ });
171
+ }
169
172
  }
170
173
  }).catch((e) => {
171
174
  console.error("Build failed:", e);
@@ -2,29 +2,46 @@ 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
+ AnnotationCreatePopover,
8
+ AnnotationEditPopover,
9
+ AnnotationProvider,
10
+ AnnotationSidebar,
11
+ CanvasHostContext,
12
+ FileIcon,
13
+ PlanRenderer,
14
+ RenderErrorContext,
15
+ RevisionContext,
16
+ RESPONSE_ANNOTATION_PATH,
17
+ carryUnsubmittedDraft,
18
+ clearPersistedDraft,
19
+ extractContext,
20
+ generateAnnotationId,
21
+ renameMarkId,
22
+ restoreMarks,
23
+ unwrapMarks,
24
+ updateAllMarkStates,
25
+ useAnnotations,
26
+ wrapRangeWithMark,
27
+ type ActiveView,
28
+ type AnnotationDraftPhase,
29
+ type CanvasFileInfo,
30
+ type CanvasRenderError,
31
+ type RevisionInfo,
32
+ } from "@fabrika/canvas-kernel/client";
8
33
  import { FileBrowser } from "./FileBrowser";
9
- import { FileIcon } from "./FileIcon";
10
34
  import { FileViewer } from "./FileViewer";
11
35
  import { SessionSwitcher } from "./SessionSwitcher";
12
36
  import { exportCanvasToMarkdown } from "./exportMarkdown";
13
37
  import { CompareView } from "./CompareView";
14
38
  import { OverviewView, categorizeChanges, getAffectedFiles } from "./OverviewView";
15
39
  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
40
  import { ShareDialog, ShareButton, type ShareEntry } from "./ShareDialog";
21
41
  import { ReviewerIdentityDialog } from "./ReviewerIdentityDialog";
22
42
  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";
43
+ import { MODE, fetchMeta, FS_AVAILABLE, WS_AVAILABLE, submitSharedFeedback, getReviewerIdentity, setReviewerIdentity, canvasHost } from "./clientApi";
26
44
 
27
- export type ActiveView = { type: "overview" } | { type: "canvas"; filename: string } | { type: "file"; path: string };
28
45
 
29
46
  /**
30
47
  * Wire shape of a remote feedback entry as delivered by the daemon
@@ -62,55 +79,6 @@ function remoteFeedbackToAnnotations(entries: RemoteFeedbackEntry[]): Annotation
62
79
  return out;
63
80
  }
64
81
 
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
82
  function resolveTheme(pref: string): "light" | "dark" {
115
83
  if (pref === "auto") return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
116
84
  return pref as "light" | "dark";
@@ -519,6 +487,7 @@ function App() {
519
487
  }
520
488
 
521
489
  return (
490
+ <CanvasHostContext.Provider value={canvasHost}>
522
491
  <SessionContext.Provider value={sessionId}>
523
492
  <RenderErrorContext.Provider value={reportRenderError}>
524
493
  <RevisionContext.Provider value={{ currentRevision, selectedRevision, revisions, setSelectedRevision, isReadOnly, compareRevision, setCompareRevision, agentWatching }}>
@@ -529,6 +498,7 @@ function App() {
529
498
  isReadOnly={isReadOnly}
530
499
  draftPhase={draftPhase}
531
500
  remoteAnnotations={remoteAnnotationsByRev.get(selectedRevision)}
501
+ canvasFiles={canvasFiles}
532
502
  >
533
503
  <ActiveViewContext.Provider value={{ activeView, setActiveView, openFiles, closeFile, canvasFiles }}>
534
504
  <ActiveViewCtx.Provider value={{ setActiveView }}>
@@ -707,6 +677,7 @@ function App() {
707
677
  </RevisionContext.Provider>
708
678
  </RenderErrorContext.Provider>
709
679
  </SessionContext.Provider>
680
+ </CanvasHostContext.Provider>
710
681
  );
711
682
  }
712
683
 
@@ -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/canvas-kernel/client";
8
+ import { extractContext } from "@fabrika/canvas-kernel/client";
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/canvas-kernel/client";
7
7
  import { FS_AVAILABLE } from "./clientApi";
8
8
 
9
9
  interface TreeEntry { name: string; type: "file" | "dir"; }
@@ -1,11 +1,11 @@
1
1
  import React, { useEffect, useState, useContext, useCallback, useRef, useMemo } from "react";
2
2
  import { SessionContext } from "#canvas/runtime";
3
3
  import type { AnnotationContext } from "#canvas/runtime";
4
- import { useAnnotations } from "./AnnotationProvider";
5
- import { wrapRangeWithMark } from "./highlightRange";
4
+ import { useAnnotations } from "@fabrika/canvas-kernel/client";
5
+ import { wrapRangeWithMark } from "@fabrika/canvas-kernel/client";
6
6
  import { LANG_MAP } from "../langMap";
7
- import { generateAnnotationId } from "./utils";
8
- import { useTextAnnotation } from "./useTextAnnotation";
7
+ import { generateAnnotationId } from "@fabrika/canvas-kernel/client";
8
+ import { useTextAnnotation } from "@fabrika/canvas-kernel/client";
9
9
  import { FS_AVAILABLE } from "./clientApi";
10
10
 
11
11
  /** Walk up from a node to find the parent line div with data-line-num */
@@ -1,8 +1,8 @@
1
1
  import React, { useState, useContext, useMemo, useRef, useEffect, useCallback } from "react";
2
2
  import { SessionContext } from "#canvas/runtime";
3
- import { PlanRenderer } from "./PlanRenderer";
3
+ import { PlanRenderer } from "@fabrika/canvas-kernel/client";
4
4
  import { ComparePanelRenderer } from "./CompareView";
5
- import { ActiveViewContext, RevisionContext, type CanvasFileInfo, type RevisionInfo } from "./App";
5
+ import { ActiveViewContext, RevisionContext, type CanvasFileInfo, type RevisionInfo } from "@fabrika/canvas-kernel/client";
6
6
  import { extractBlockTree, matchBlocks, buildUnifiedDom } from "./unifiedDiff";
7
7
  import { runDomDiff } from "./domDiff";
8
8
 
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useState, useRef, useContext } from "react";
2
- import { RevisionContext, RevisionInfo } from "./App";
2
+ import { RevisionContext, RevisionInfo } from "@fabrika/canvas-kernel/client";
3
3
 
4
4
  /** Format a revision for the trigger button: label or "Round N" */
5
5
  function revisionButtonLabel(r: RevisionInfo) {
@@ -28,6 +28,7 @@ import {
28
28
  ENCRYPTION_META,
29
29
  type EncryptionMeta,
30
30
  } from "./shareCrypto";
31
+ import type { CanvasHost } from "@fabrika/canvas-kernel/client";
31
32
 
32
33
  declare global {
33
34
  interface Window {
@@ -334,3 +335,20 @@ export const FS_AVAILABLE: boolean = !MODE.isShared;
334
335
 
335
336
  /** Is live WebSocket session updates available? */
336
337
  export const WS_AVAILABLE: boolean = !MODE.isShared;
338
+
339
+ // --- Kernel host binding ----------------------------------------------------
340
+
341
+ /**
342
+ * What the kernel's annotation surface needs from this host. agent-canvas
343
+ * serves canvases in two modes, so these cannot use the kernel's local-only
344
+ * defaults: shared canvases come from the worker (and may be encrypted).
345
+ */
346
+ export const canvasHost: CanvasHost = {
347
+ get sessionId() {
348
+ return getIdentifier();
349
+ },
350
+ isShared: MODE.isShared,
351
+ fsAvailable: FS_AVAILABLE,
352
+ uploadUrl,
353
+ loadCanvasModule,
354
+ };