@frebreco/canvas 0.3.0-next.3 → 0.3.0-next.5
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/server/server.ts +16 -12
- package/src/server/terminals.ts +13 -2
- package/src/shared/protocol.ts +7 -0
package/package.json
CHANGED
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";
|
|
@@ -26,11 +27,9 @@ export async function serve(options: ServeOptions) {
|
|
|
26
27
|
const store = new Store(options.dir);
|
|
27
28
|
excludeFromGit(options.dir);
|
|
28
29
|
const room = await store.room();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
for (const ws of clients) ws.send(text);
|
|
33
|
-
};
|
|
30
|
+
// The host tab: the board, and so every board tool call, lives there.
|
|
31
|
+
let host: ServerWebSocket<unknown> | null = null;
|
|
32
|
+
const broadcast = (message: ServerToClient) => host?.send(JSON.stringify(message));
|
|
34
33
|
|
|
35
34
|
// Scratch files are written by agents' board tools; `files` mirrors them.
|
|
36
35
|
const scratch = new Scratch(options.dir, (path) => files.scratchChanged(path));
|
|
@@ -46,9 +45,8 @@ export async function serve(options: ServeOptions) {
|
|
|
46
45
|
read: (path) => files.read(path),
|
|
47
46
|
scratch,
|
|
48
47
|
relay: (call) => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
ws.send(JSON.stringify({ t: "board-call", ...call } satisfies ServerToClient));
|
|
48
|
+
if (!host) return false;
|
|
49
|
+
host.send(JSON.stringify({ t: "board-call", ...call } satisfies ServerToClient));
|
|
52
50
|
return true;
|
|
53
51
|
},
|
|
54
52
|
});
|
|
@@ -148,7 +146,11 @@ export async function serve(options: ServeOptions) {
|
|
|
148
146
|
websocket: {
|
|
149
147
|
maxPayloadLength: 64 * 1024 * 1024,
|
|
150
148
|
open(ws) {
|
|
151
|
-
|
|
149
|
+
if (host) {
|
|
150
|
+
files.drop(host);
|
|
151
|
+
host.close(HOST_REPLACED, "opened in another tab");
|
|
152
|
+
}
|
|
153
|
+
host = ws;
|
|
152
154
|
const board = store.board();
|
|
153
155
|
const { token: _, ...secrets } = room;
|
|
154
156
|
ws.send(
|
|
@@ -163,6 +165,8 @@ export async function serve(options: ServeOptions) {
|
|
|
163
165
|
);
|
|
164
166
|
},
|
|
165
167
|
message(ws, data) {
|
|
168
|
+
// Still in flight from a tab that was just replaced.
|
|
169
|
+
if (ws !== host) return;
|
|
166
170
|
try {
|
|
167
171
|
handle(ws, JSON.parse(String(data)) as ClientToServer);
|
|
168
172
|
} catch (error) {
|
|
@@ -170,7 +174,7 @@ export async function serve(options: ServeOptions) {
|
|
|
170
174
|
}
|
|
171
175
|
},
|
|
172
176
|
close(ws) {
|
|
173
|
-
|
|
177
|
+
if (ws === host) host = null;
|
|
174
178
|
files.drop(ws);
|
|
175
179
|
},
|
|
176
180
|
},
|
package/src/server/terminals.ts
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
const SCROLLBACK = 200_000;
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* The shell must lead a session of its own, with the PTY as its controlling
|
|
11
|
+
* terminal, as in any terminal emulator. `Bun.spawn` leaves it in the session
|
|
12
|
+
* of `canvas serve` (Bun 1.4): Ctrl-C reaches nothing, and whatever opens
|
|
13
|
+
* `/dev/tty` — fzf's Ctrl-R, sudo, ssh prompts — gets the terminal `canvas
|
|
14
|
+
* serve` runs in, where it is stopped as a background job. util-linux
|
|
15
|
+
* `setsid -c` does both; where it is missing (macOS) the shell runs as before.
|
|
16
|
+
*/
|
|
17
|
+
const SETSID = Bun.which("setsid");
|
|
18
|
+
const SESSION = SETSID ? [SETSID, "-c"] : [];
|
|
19
|
+
|
|
9
20
|
interface Term {
|
|
10
21
|
terminal: Bun.Terminal;
|
|
11
22
|
scrollback: string;
|
|
@@ -19,6 +30,7 @@ export class Terminals {
|
|
|
19
30
|
private readonly dir: string,
|
|
20
31
|
private readonly onData: (id: string, data: string) => void,
|
|
21
32
|
private readonly onExit: (id: string, code: number | null) => void,
|
|
33
|
+
private readonly shell = process.env.SHELL ?? "bash",
|
|
22
34
|
) {}
|
|
23
35
|
|
|
24
36
|
/** Open (or re-attach to) a terminal; returns the scrollback to replay. */
|
|
@@ -42,8 +54,7 @@ export class Terminals {
|
|
|
42
54
|
}),
|
|
43
55
|
};
|
|
44
56
|
this.terms.set(id, term);
|
|
45
|
-
const
|
|
46
|
-
const proc = Bun.spawn([shell, "-l"], {
|
|
57
|
+
const proc = Bun.spawn([...SESSION, this.shell, "-l"], {
|
|
47
58
|
cwd: this.dir,
|
|
48
59
|
terminal: term.terminal,
|
|
49
60
|
env: { ...process.env, TERM: "xterm-256color" },
|
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 }
|