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.
- package/README.md +14 -0
- 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 +37 -16
- package/daemon/client/App.tsx +27 -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/canvas-runtime.ts +1 -0
- 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/annotations-runtime.js +1 -0
- package/daemon/dist/client.css +1 -1
- package/daemon/dist/client.js +76 -76
- package/daemon/dist/components.js +59 -59
- package/daemon/dist/index.html +1 -0
- package/daemon/dist/preact-compat.js +1 -1
- package/daemon/dist/runtime.js +1 -1
- package/daemon/src/components.ts +13 -0
- package/daemon/src/handlers/api.ts +13 -9
- package/daemon/src/handlers/files.ts +2 -3
- package/daemon/src/handlers/static.ts +1 -1
- package/daemon/src/handlers/uploads.ts +2 -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 +2 -2
- package/daemon/src/server.ts +11 -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 +11 -4
- 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
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
|
-
|
|
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/daemon-kit";
|
|
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/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) =>
|
|
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
|
@@ -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(
|
|
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(
|
|
56
|
+
entrypoints: [join(KERNEL_CLIENT, "runtime.ts")],
|
|
39
57
|
outdir: DIST,
|
|
40
58
|
format: "esm",
|
|
41
|
-
external:
|
|
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: [...
|
|
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: [...
|
|
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(
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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);
|
package/daemon/client/App.tsx
CHANGED
|
@@ -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 {
|
|
6
|
-
|
|
7
|
-
|
|
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 "
|
|
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/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 "
|
|
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/annotations";
|
|
7
7
|
import { FS_AVAILABLE } from "./clientApi";
|
|
8
8
|
|
|
9
9
|
interface TreeEntry { name: string; type: "file" | "dir"; }
|