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.
- package/bin/lib/commands/daemon.ts +3 -6
- package/bin/lib/commands/push.ts +1 -1
- package/bin/lib/daemon-lifecycle.ts +65 -72
- package/bin/lib/helpers.test.ts +1 -0
- package/bin/lib/helpers.ts +30 -74
- package/daemon/build.ts +16 -13
- package/daemon/client/App.tsx +32 -61
- package/daemon/client/CompareView.tsx +4 -4
- package/daemon/client/FileBrowser.tsx +4 -4
- package/daemon/client/FileViewer.tsx +4 -4
- package/daemon/client/OverviewView.tsx +2 -2
- package/daemon/client/RevisionSelect.tsx +1 -1
- package/daemon/client/clientApi.ts +18 -0
- package/daemon/client/components/index.ts +3 -16
- package/daemon/client/styles.css +4 -457
- package/daemon/dist/client.css +1 -1
- package/daemon/dist/client.js +76 -76
- package/daemon/dist/components.js +29 -29
- package/daemon/dist/runtime.js +1 -1
- package/daemon/src/components.ts +13 -0
- package/daemon/src/handlers/api.ts +14 -9
- package/daemon/src/handlers/files.ts +1 -3
- package/daemon/src/handlers/static.ts +1 -1
- package/daemon/src/handlers/uploads.ts +1 -3
- package/daemon/src/paths.ts +10 -24
- package/daemon/src/remote-feedback.test.ts +1 -1
- package/daemon/src/remote-feedback.ts +1 -1
- package/daemon/src/secrets-api.test.ts +1 -2
- package/daemon/src/server.ts +13 -10
- package/daemon/src/share-e2e.test.ts +1 -1
- package/daemon/src/share.test.ts +1 -1
- package/daemon/src/share.ts +1 -1
- package/daemon/tsconfig.json +1 -3
- package/package.json +10 -2
- package/.github/dark.png +0 -0
- package/.github/light.png +0 -0
- package/.github/workflows/ci.yml +0 -48
- package/.github/workflows/deploy-worker-prod.yml +0 -19
- package/.github/workflows/deploy-worker-stage.yml +0 -21
- package/.github/workflows/deploy-worker.yml +0 -72
- package/.github/workflows/release.yml +0 -36
- package/CLAUDE.md +0 -86
- package/bun.lock +0 -377
- package/daemon/client/AnnotationEditor.tsx +0 -315
- package/daemon/client/AnnotationProvider.tsx +0 -195
- package/daemon/client/AnnotationSidebar.tsx +0 -627
- package/daemon/client/FileIcon.tsx +0 -76
- package/daemon/client/PlanRenderer.tsx +0 -614
- package/daemon/client/Popover.tsx +0 -350
- package/daemon/client/RenderErrorContext.ts +0 -11
- package/daemon/client/ResponsePreview.tsx +0 -186
- package/daemon/client/SessionContext.tsx +0 -3
- package/daemon/client/annotationContext.ts +0 -114
- package/daemon/client/annotationDom.ts +0 -104
- package/daemon/client/annotationDraft.test.ts +0 -188
- package/daemon/client/annotationDraft.ts +0 -130
- package/daemon/client/compat/preact-all.ts +0 -4
- package/daemon/client/components/Callout.tsx +0 -26
- package/daemon/client/components/Checklist.tsx +0 -37
- package/daemon/client/components/Choice.tsx +0 -176
- package/daemon/client/components/CodeBlock.tsx +0 -35
- package/daemon/client/components/Diff.tsx +0 -65
- package/daemon/client/components/FilePreview.tsx +0 -88
- package/daemon/client/components/Image.tsx +0 -45
- package/daemon/client/components/Markdown.tsx +0 -44
- package/daemon/client/components/Mermaid.tsx +0 -565
- package/daemon/client/components/Note.tsx +0 -14
- package/daemon/client/components/Priority.tsx +0 -19
- package/daemon/client/components/ResponseNote.tsx +0 -19
- package/daemon/client/components/Section.tsx +0 -33
- package/daemon/client/components/Table.tsx +0 -35
- package/daemon/client/components/Task.tsx +0 -76
- package/daemon/client/components/UserInput.tsx +0 -174
- package/daemon/client/components/renderMarkdownChildren.tsx +0 -97
- package/daemon/client/generateMarkdown.ts +0 -273
- package/daemon/client/highlightRange.ts +0 -224
- package/daemon/client/popoverPosition.ts +0 -31
- package/daemon/client/runtime.ts +0 -123
- package/daemon/client/theme.css +0 -66
- package/daemon/client/useTextAnnotation.tsx +0 -188
- package/daemon/client/utils.ts +0 -13
- package/daemon/src/compiler.test.ts +0 -57
- package/daemon/src/compiler.ts +0 -360
- package/daemon/src/handlers/utils.ts +0 -14
- package/daemon/src/router.ts +0 -16
- package/daemon/src/session-secrets.test.ts +0 -83
- package/daemon/src/session.ts +0 -612
- package/daemon/src/watcher.ts +0 -71
- package/daemon/src/websocket.test.ts +0 -99
- package/daemon/src/websocket.ts +0 -211
- package/tsconfig.json +0 -14
- package/workers/canvas-share/README.md +0 -159
- package/workers/canvas-share/oblaka.ts +0 -56
- package/workers/canvas-share/package.json +0 -25
- package/workers/canvas-share/public/assets/.gitkeep +0 -0
- package/workers/canvas-share/public/assets/client.css +0 -2
- package/workers/canvas-share/public/assets/client.js +0 -91
- package/workers/canvas-share/public/assets/components.js +0 -65
- package/workers/canvas-share/public/assets/jsx-runtime.js +0 -3
- package/workers/canvas-share/public/assets/preact-compat.js +0 -1
- package/workers/canvas-share/public/assets/runtime.js +0 -1
- package/workers/canvas-share/public/index.html +0 -385
- package/workers/canvas-share/public/screenshot-dark.png +0 -0
- package/workers/canvas-share/public/screenshot-light.png +0 -0
- package/workers/canvas-share/src/index.test.ts +0 -209
- package/workers/canvas-share/src/index.ts +0 -518
- package/workers/canvas-share/src/types.ts +0 -119
- package/workers/canvas-share/src/util.ts +0 -156
- package/workers/canvas-share/src/validation.ts +0 -160
- 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
|
-
|
|
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
|
}
|
package/bin/lib/commands/push.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
19
|
-
|
|
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
|
|
28
|
+
return undefined;
|
|
23
29
|
}
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
console.error("Daemon started.");
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
60
|
+
export async function isDaemonRunning(): Promise<boolean> {
|
|
61
|
+
return lifecycle.isRunning();
|
|
62
|
+
}
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
}
|
package/bin/lib/helpers.test.ts
CHANGED
|
@@ -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;
|
package/bin/lib/helpers.ts
CHANGED
|
@@ -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) =>
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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);
|
package/daemon/client/App.tsx
CHANGED
|
@@ -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 {
|
|
6
|
-
|
|
7
|
-
|
|
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 "
|
|
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 "
|
|
7
|
-
import { useTextAnnotation } from "
|
|
8
|
-
import { extractContext } from "
|
|
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 "
|
|
4
|
-
import { ActiveViewContext } from "
|
|
5
|
-
import { FileIcon } from "
|
|
6
|
-
import { RESPONSE_ANNOTATION_PATH } from "
|
|
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 "
|
|
5
|
-
import { wrapRangeWithMark } from "
|
|
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 "
|
|
8
|
-
import { useTextAnnotation } from "
|
|
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 "
|
|
3
|
+
import { PlanRenderer } from "@fabrika/canvas-kernel/client";
|
|
4
4
|
import { ComparePanelRenderer } from "./CompareView";
|
|
5
|
-
import { ActiveViewContext, RevisionContext, type CanvasFileInfo, type RevisionInfo } from "
|
|
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 "
|
|
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
|
+
};
|