@frebreco/canvas 0.3.0-next.4 → 0.3.0-next.6
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/package.json +1 -1
- package/src/cli.ts +1 -0
- package/src/server/server.ts +19 -12
- package/src/shared/protocol.ts +11 -0
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -54,6 +54,7 @@ const { server, room } = await serve({
|
|
|
54
54
|
port: Number(values.port),
|
|
55
55
|
hostname: tlsHost ? "0.0.0.0" : "127.0.0.1",
|
|
56
56
|
...(tlsHost && { tls: { cert: values.cert, key: values.key } }),
|
|
57
|
+
...(manifest.version && { version: manifest.version }),
|
|
57
58
|
});
|
|
58
59
|
|
|
59
60
|
const serverUrl = tlsHost ? `wss://${tlsHost}:${server.port}` : `ws://127.0.0.1:${server.port}`;
|
package/src/server/server.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `canvas serve`: the local half of canvas. One WebSocket endpoint that only
|
|
3
|
-
* the host's browser may use (it presents the token from the printed link)
|
|
3
|
+
* the host's browser may use (it presents the token from the printed link),
|
|
4
|
+
* in one tab at a time: a newer tab takes over from an older one.
|
|
4
5
|
* Nothing here knows about guests — the host's browser is the relay.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import type { ServerWebSocket } from "bun";
|
|
8
9
|
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
9
10
|
import { join } from "node:path";
|
|
10
|
-
import type
|
|
11
|
+
import { HOST_REPLACED, type ClientToServer, type ServerToClient } from "../shared/protocol";
|
|
11
12
|
import { AgentManager, detectAgents } from "./agents";
|
|
12
13
|
import { BoardMcp } from "./board-mcp";
|
|
13
14
|
import { Files } from "./files";
|
|
@@ -20,17 +21,17 @@ export interface ServeOptions {
|
|
|
20
21
|
readonly port: number;
|
|
21
22
|
readonly hostname: string;
|
|
22
23
|
readonly tls?: { readonly cert: string; readonly key: string };
|
|
24
|
+
/** The published version this runs as; none for a checkout. */
|
|
25
|
+
readonly version?: string;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
export async function serve(options: ServeOptions) {
|
|
26
29
|
const store = new Store(options.dir);
|
|
27
30
|
excludeFromGit(options.dir);
|
|
28
31
|
const room = await store.room();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
for (const ws of clients) ws.send(text);
|
|
33
|
-
};
|
|
32
|
+
// The host tab: the board, and so every board tool call, lives there.
|
|
33
|
+
let host: ServerWebSocket<unknown> | null = null;
|
|
34
|
+
const broadcast = (message: ServerToClient) => host?.send(JSON.stringify(message));
|
|
34
35
|
|
|
35
36
|
// Scratch files are written by agents' board tools; `files` mirrors them.
|
|
36
37
|
const scratch = new Scratch(options.dir, (path) => files.scratchChanged(path));
|
|
@@ -46,9 +47,8 @@ export async function serve(options: ServeOptions) {
|
|
|
46
47
|
read: (path) => files.read(path),
|
|
47
48
|
scratch,
|
|
48
49
|
relay: (call) => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
ws.send(JSON.stringify({ t: "board-call", ...call } satisfies ServerToClient));
|
|
50
|
+
if (!host) return false;
|
|
51
|
+
host.send(JSON.stringify({ t: "board-call", ...call } satisfies ServerToClient));
|
|
52
52
|
return true;
|
|
53
53
|
},
|
|
54
54
|
});
|
|
@@ -148,12 +148,17 @@ export async function serve(options: ServeOptions) {
|
|
|
148
148
|
websocket: {
|
|
149
149
|
maxPayloadLength: 64 * 1024 * 1024,
|
|
150
150
|
open(ws) {
|
|
151
|
-
|
|
151
|
+
if (host) {
|
|
152
|
+
files.drop(host);
|
|
153
|
+
host.close(HOST_REPLACED, "opened in another tab");
|
|
154
|
+
}
|
|
155
|
+
host = ws;
|
|
152
156
|
const board = store.board();
|
|
153
157
|
const { token: _, ...secrets } = room;
|
|
154
158
|
ws.send(
|
|
155
159
|
JSON.stringify({
|
|
156
160
|
t: "welcome",
|
|
161
|
+
version: options.version ?? null,
|
|
157
162
|
room: secrets,
|
|
158
163
|
cwd: options.dir,
|
|
159
164
|
agents: agentDefinitions.map(({ kind, label }) => ({ kind, label })),
|
|
@@ -163,6 +168,8 @@ export async function serve(options: ServeOptions) {
|
|
|
163
168
|
);
|
|
164
169
|
},
|
|
165
170
|
message(ws, data) {
|
|
171
|
+
// Still in flight from a tab that was just replaced.
|
|
172
|
+
if (ws !== host) return;
|
|
166
173
|
try {
|
|
167
174
|
handle(ws, JSON.parse(String(data)) as ClientToServer);
|
|
168
175
|
} catch (error) {
|
|
@@ -170,7 +177,7 @@ export async function serve(options: ServeOptions) {
|
|
|
170
177
|
}
|
|
171
178
|
},
|
|
172
179
|
close(ws) {
|
|
173
|
-
|
|
180
|
+
if (ws === host) host = null;
|
|
174
181
|
files.drop(ws);
|
|
175
182
|
},
|
|
176
183
|
},
|
package/src/shared/protocol.ts
CHANGED
|
@@ -157,6 +157,13 @@ export interface RoomSecrets {
|
|
|
157
157
|
readonly hostPrivateKey: JsonWebKey;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Close code of a host browser's WebSocket when another one connected: one
|
|
162
|
+
* host tab at a time, and the replaced one must not reconnect by itself, or
|
|
163
|
+
* two tabs would take the connection from each other forever.
|
|
164
|
+
*/
|
|
165
|
+
export const HOST_REPLACED = 4001;
|
|
166
|
+
|
|
160
167
|
export type ClientToServer =
|
|
161
168
|
| { readonly t: "board-save"; readonly state: string }
|
|
162
169
|
| { readonly t: "agent-create"; readonly id: string; readonly agent: AgentKind }
|
|
@@ -204,6 +211,8 @@ export type ClientToServer =
|
|
|
204
211
|
export type ServerToClient =
|
|
205
212
|
| {
|
|
206
213
|
readonly t: "welcome";
|
|
214
|
+
/** The version `canvas serve` runs as; null for a checkout. */
|
|
215
|
+
readonly version: string | null;
|
|
207
216
|
readonly room: RoomSecrets;
|
|
208
217
|
readonly cwd: string;
|
|
209
218
|
readonly agents: ReadonlyArray<AgentInfo>;
|
|
@@ -251,6 +260,8 @@ export interface RoomState {
|
|
|
251
260
|
readonly access: GuestAccess;
|
|
252
261
|
readonly cwd: string;
|
|
253
262
|
readonly agents: ReadonlyArray<AgentInfo>;
|
|
263
|
+
/** `canvas serve`'s version, null for a checkout; absent from hosts older than it. */
|
|
264
|
+
readonly version?: string | null;
|
|
254
265
|
}
|
|
255
266
|
|
|
256
267
|
/** Requests a guest sends the host; the host answers `{ok}` or `{ok:false, error}`. */
|