@frebreco/canvas 0.0.1 → 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/LICENSE +21 -0
- package/README.md +50 -1
- package/bin/canvas.js +31 -0
- package/package.json +27 -3
- package/src/cli.ts +72 -0
- package/src/server/agent-config.ts +82 -0
- package/src/server/agents.ts +501 -0
- package/src/server/board-mcp.ts +191 -0
- package/src/server/files.ts +136 -0
- package/src/server/server.ts +194 -0
- package/src/server/shared-set.ts +175 -0
- package/src/server/store.ts +98 -0
- package/src/server/terminals.ts +65 -0
- package/src/server/trim-event.ts +74 -0
- package/src/server/web-url.ts +18 -0
- package/src/shared/board-tools.ts +172 -0
- package/src/shared/layout.ts +256 -0
- package/src/shared/protocol.ts +310 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two wire contracts of canvas, shared by the CLI server and the web app.
|
|
3
|
+
*
|
|
4
|
+
* guest browser ──trystero──▶ host browser ──WebSocket──▶ `canvas serve`
|
|
5
|
+
*
|
|
6
|
+
* Only the host's browser talks to the server (it holds the token from the
|
|
7
|
+
* link the CLI printed). Guests talk to the host over trystero and never reach
|
|
8
|
+
* the machine directly: every action that would touch it is a request the
|
|
9
|
+
* host's browser checks against the room's access policy before relaying.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Agent sessions
|
|
14
|
+
|
|
15
|
+
export type AgentKind = string;
|
|
16
|
+
|
|
17
|
+
export interface AgentInfo {
|
|
18
|
+
readonly kind: AgentKind;
|
|
19
|
+
readonly label: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Author {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly color: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type SessionStatus = "idle" | "running" | "waiting";
|
|
28
|
+
|
|
29
|
+
export interface SessionMeta {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly agent: AgentKind;
|
|
32
|
+
readonly status: SessionStatus;
|
|
33
|
+
/** The agent's own session id, used to resume the conversation next turn. */
|
|
34
|
+
readonly acpSessionId?: string;
|
|
35
|
+
/**
|
|
36
|
+
* The agent's settings as it last reported them. Small enough to persist:
|
|
37
|
+
* shown while the agent is not connected, and re-applied when it reconnects.
|
|
38
|
+
*/
|
|
39
|
+
readonly settings?: ReadonlyArray<AgentSetting>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type AgentConfigValue = string | boolean;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* One setting an agent offers for its session (an ACP session config option):
|
|
46
|
+
* model, reasoning effort, mode, … The agent decides which exist — they can
|
|
47
|
+
* change with the model — so clients render whatever arrives.
|
|
48
|
+
*/
|
|
49
|
+
export interface AgentConfigOption {
|
|
50
|
+
readonly id: string;
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly description?: string;
|
|
53
|
+
/** ACP category, for placement only: `model`, `thought_level`, `model_config`, `mode`, … */
|
|
54
|
+
readonly category?: string;
|
|
55
|
+
readonly type: "select" | "boolean";
|
|
56
|
+
readonly value: AgentConfigValue;
|
|
57
|
+
/** The values of a `select` (empty for `boolean`). */
|
|
58
|
+
readonly choices: ReadonlyArray<AgentConfigChoice>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface AgentConfigChoice {
|
|
62
|
+
readonly value: string;
|
|
63
|
+
readonly name: string;
|
|
64
|
+
readonly description?: string;
|
|
65
|
+
/** The agent's group heading, if it groups its values. */
|
|
66
|
+
readonly group?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** An option's current value, without the list of choices. */
|
|
70
|
+
export interface AgentSetting {
|
|
71
|
+
readonly id: string;
|
|
72
|
+
readonly name: string;
|
|
73
|
+
readonly category?: string;
|
|
74
|
+
readonly value: AgentConfigValue;
|
|
75
|
+
/** Display name of `value`. */
|
|
76
|
+
readonly label: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface PermissionOption {
|
|
80
|
+
readonly optionId: string;
|
|
81
|
+
readonly name: string;
|
|
82
|
+
/** ACP option kind: `allow_once`, `allow_always`, `reject_once`, `reject_always`. */
|
|
83
|
+
readonly kind: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* One entry of a session's append-only log. `chunk` carries an AG-UI chunk
|
|
88
|
+
* verbatim; every client folds the log with the same reducer, so the host and
|
|
89
|
+
* all guests render an identical thread.
|
|
90
|
+
*/
|
|
91
|
+
export type AgentEvent =
|
|
92
|
+
| {
|
|
93
|
+
readonly kind: "turn";
|
|
94
|
+
readonly turnId: string;
|
|
95
|
+
readonly text: string;
|
|
96
|
+
readonly author: Author;
|
|
97
|
+
readonly at: number;
|
|
98
|
+
}
|
|
99
|
+
| { readonly kind: "chunk"; readonly turnId: string; readonly chunk: unknown }
|
|
100
|
+
| {
|
|
101
|
+
readonly kind: "permission";
|
|
102
|
+
readonly turnId: string;
|
|
103
|
+
readonly requestId: string;
|
|
104
|
+
/** The tool call this permission gates, when the agent says. */
|
|
105
|
+
readonly toolCallId?: string;
|
|
106
|
+
readonly title: string;
|
|
107
|
+
readonly options: ReadonlyArray<PermissionOption>;
|
|
108
|
+
}
|
|
109
|
+
| {
|
|
110
|
+
readonly kind: "permission-resolved";
|
|
111
|
+
readonly requestId: string;
|
|
112
|
+
readonly optionId: string | null;
|
|
113
|
+
readonly by: string;
|
|
114
|
+
}
|
|
115
|
+
| {
|
|
116
|
+
readonly kind: "turn-end";
|
|
117
|
+
readonly turnId: string;
|
|
118
|
+
readonly error?: string;
|
|
119
|
+
readonly cancelled?: boolean;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export interface SessionSnapshot {
|
|
123
|
+
readonly meta: SessionMeta;
|
|
124
|
+
readonly events: ReadonlyArray<AgentEvent>;
|
|
125
|
+
/** The settings with their choices; absent until the agent has connected. */
|
|
126
|
+
readonly options?: ReadonlyArray<AgentConfigOption>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A session without its event log: what a guest sees before the history arrives. */
|
|
130
|
+
export type SessionHead = Omit<SessionSnapshot, "events">;
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Files
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A file of the working dir as the server lets it out: only paths in the
|
|
137
|
+
* shared set (ADR 0002), read-only, text only.
|
|
138
|
+
*/
|
|
139
|
+
export type FileContent =
|
|
140
|
+
| { readonly kind: "text"; readonly text: string }
|
|
141
|
+
/** Shared, but not there (yet) — an agent may be about to write it. */
|
|
142
|
+
| { readonly kind: "missing" }
|
|
143
|
+
| { readonly kind: "binary"; readonly size: number }
|
|
144
|
+
| { readonly kind: "too-large"; readonly size: number }
|
|
145
|
+
| { readonly kind: "denied"; readonly reason: string };
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Host browser ⇄ server (JSON over one WebSocket)
|
|
149
|
+
|
|
150
|
+
export interface RoomSecrets {
|
|
151
|
+
readonly roomId: string;
|
|
152
|
+
/** trystero room password — peers without it cannot even decrypt signalling. */
|
|
153
|
+
readonly key: string;
|
|
154
|
+
/** ECDSA P-256 public key (base64url raw point); guests verify the host with it. */
|
|
155
|
+
readonly hostPublicKey: string;
|
|
156
|
+
/** The matching private key (JWK) — only ever sent to the host's browser. */
|
|
157
|
+
readonly hostPrivateKey: JsonWebKey;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type ClientToServer =
|
|
161
|
+
| { readonly t: "board-save"; readonly state: string }
|
|
162
|
+
| { readonly t: "agent-create"; readonly id: string; readonly agent: AgentKind }
|
|
163
|
+
| {
|
|
164
|
+
readonly t: "agent-prompt";
|
|
165
|
+
readonly sessionId: string;
|
|
166
|
+
readonly text: string;
|
|
167
|
+
readonly author: Author;
|
|
168
|
+
}
|
|
169
|
+
| { readonly t: "agent-cancel"; readonly sessionId: string }
|
|
170
|
+
| {
|
|
171
|
+
readonly t: "agent-config";
|
|
172
|
+
readonly sessionId: string;
|
|
173
|
+
readonly configId: string;
|
|
174
|
+
readonly value: AgentConfigValue;
|
|
175
|
+
}
|
|
176
|
+
| {
|
|
177
|
+
readonly t: "agent-permission";
|
|
178
|
+
readonly sessionId: string;
|
|
179
|
+
readonly requestId: string;
|
|
180
|
+
readonly optionId: string | null;
|
|
181
|
+
readonly by: string;
|
|
182
|
+
}
|
|
183
|
+
/** Send a file now and on every change, until closed. */
|
|
184
|
+
| { readonly t: "file-open"; readonly path: string }
|
|
185
|
+
| { readonly t: "file-close"; readonly path: string }
|
|
186
|
+
/** Send the shared set's file list now and whenever it changes. */
|
|
187
|
+
| { readonly t: "tree-watch" }
|
|
188
|
+
| { readonly t: "term-open"; readonly id: string; readonly cols: number; readonly rows: number }
|
|
189
|
+
| { readonly t: "term-input"; readonly id: string; readonly data: string }
|
|
190
|
+
| {
|
|
191
|
+
readonly t: "term-resize";
|
|
192
|
+
readonly id: string;
|
|
193
|
+
readonly cols: number;
|
|
194
|
+
readonly rows: number;
|
|
195
|
+
}
|
|
196
|
+
/** The answer to a `board-call`: the tool's text, for the agent. */
|
|
197
|
+
| {
|
|
198
|
+
readonly t: "board-result";
|
|
199
|
+
readonly callId: string;
|
|
200
|
+
readonly ok: boolean;
|
|
201
|
+
readonly text: string;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export type ServerToClient =
|
|
205
|
+
| {
|
|
206
|
+
readonly t: "welcome";
|
|
207
|
+
readonly room: RoomSecrets;
|
|
208
|
+
readonly cwd: string;
|
|
209
|
+
readonly agents: ReadonlyArray<AgentInfo>;
|
|
210
|
+
/** base64 Yjs update of the persisted board, if any. */
|
|
211
|
+
readonly board: string | null;
|
|
212
|
+
readonly sessions: ReadonlyArray<SessionSnapshot>;
|
|
213
|
+
}
|
|
214
|
+
| { readonly t: "agent-meta"; readonly meta: SessionMeta }
|
|
215
|
+
| { readonly t: "agent-event"; readonly sessionId: string; readonly event: AgentEvent }
|
|
216
|
+
| AgentOptionsMessage
|
|
217
|
+
| FileMessage
|
|
218
|
+
| TreeMessage
|
|
219
|
+
| { readonly t: "term-data"; readonly id: string; readonly data: string }
|
|
220
|
+
| { readonly t: "term-exit"; readonly id: string; readonly code: number | null }
|
|
221
|
+
/**
|
|
222
|
+
* An agent called a board tool (`shared/board-tools.ts`); the board is in
|
|
223
|
+
* the browser, so the browser runs it. `sessionId` is the agent's frame.
|
|
224
|
+
*/
|
|
225
|
+
| {
|
|
226
|
+
readonly t: "board-call";
|
|
227
|
+
readonly callId: string;
|
|
228
|
+
readonly sessionId: string;
|
|
229
|
+
readonly tool: string;
|
|
230
|
+
readonly args: unknown;
|
|
231
|
+
}
|
|
232
|
+
| { readonly t: "error"; readonly message: string };
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Peer ⇄ peer (trystero actions)
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* What a guest may do, set by the host for the whole room.
|
|
239
|
+
* - `view`: read-only — board edits are dropped, requests refused; sees the
|
|
240
|
+
* files others open, but not the file tree.
|
|
241
|
+
* - `edit`: edit the board and prompt drafts, open shared files and browse the
|
|
242
|
+
* tree (ADR 0002); anything that runs on the host's machine (send a prompt,
|
|
243
|
+
* start an agent, type into a terminal) waits for the host to approve it.
|
|
244
|
+
* - `trusted`: as `edit`, without the approval step. Tool-call permissions the
|
|
245
|
+
* agent asks for still go to the host only.
|
|
246
|
+
*/
|
|
247
|
+
export type GuestAccess = "view" | "edit" | "trusted";
|
|
248
|
+
|
|
249
|
+
export interface RoomState {
|
|
250
|
+
readonly hostPeerId: string;
|
|
251
|
+
readonly access: GuestAccess;
|
|
252
|
+
readonly cwd: string;
|
|
253
|
+
readonly agents: ReadonlyArray<AgentInfo>;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Requests a guest sends the host; the host answers `{ok}` or `{ok:false, error}`. */
|
|
257
|
+
export type GuestRequest =
|
|
258
|
+
| { readonly t: "agent-create"; readonly frameId: string; readonly agent: AgentKind }
|
|
259
|
+
| { readonly t: "agent-prompt"; readonly sessionId: string; readonly text: string }
|
|
260
|
+
| { readonly t: "agent-cancel"; readonly sessionId: string }
|
|
261
|
+
| {
|
|
262
|
+
readonly t: "agent-config";
|
|
263
|
+
readonly sessionId: string;
|
|
264
|
+
readonly configId: string;
|
|
265
|
+
readonly value: AgentConfigValue;
|
|
266
|
+
}
|
|
267
|
+
| { readonly t: "term-input"; readonly id: string; readonly data: string };
|
|
268
|
+
|
|
269
|
+
export type GuestReply = { readonly ok: true } | { readonly ok: false; readonly error: string };
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Host → guests: the mirrored agent sessions, terminals and open files, and
|
|
273
|
+
* the file tree (not to `view` guests).
|
|
274
|
+
*
|
|
275
|
+
* A joining guest gets `sessions` first — every session on the board, without
|
|
276
|
+
* its log — then one `session-history` per session, shortest first, so a long
|
|
277
|
+
* thread holds up nobody else's.
|
|
278
|
+
*/
|
|
279
|
+
export type HostBroadcast =
|
|
280
|
+
| { readonly t: "sessions"; readonly sessions: ReadonlyArray<SessionHead> }
|
|
281
|
+
| {
|
|
282
|
+
readonly t: "session-history";
|
|
283
|
+
readonly sessionId: string;
|
|
284
|
+
readonly events: ReadonlyArray<AgentEvent>;
|
|
285
|
+
}
|
|
286
|
+
| { readonly t: "agent-meta"; readonly meta: SessionMeta }
|
|
287
|
+
| { readonly t: "agent-event"; readonly sessionId: string; readonly event: AgentEvent }
|
|
288
|
+
| AgentOptionsMessage
|
|
289
|
+
| { readonly t: "term-data"; readonly id: string; readonly data: string }
|
|
290
|
+
| FileMessage
|
|
291
|
+
| TreeMessage;
|
|
292
|
+
|
|
293
|
+
export interface FileMessage {
|
|
294
|
+
readonly t: "file";
|
|
295
|
+
readonly path: string;
|
|
296
|
+
readonly file: FileContent;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Every file in the shared set, working-dir-relative with `/` separators. */
|
|
300
|
+
export interface TreeMessage {
|
|
301
|
+
readonly t: "tree";
|
|
302
|
+
readonly paths: ReadonlyArray<string>;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** A session's settings changed (or the agent connected and listed them). */
|
|
306
|
+
export interface AgentOptionsMessage {
|
|
307
|
+
readonly t: "agent-options";
|
|
308
|
+
readonly sessionId: string;
|
|
309
|
+
readonly options: ReadonlyArray<AgentConfigOption>;
|
|
310
|
+
}
|