@particle-academy/agent-integrations 0.4.0 → 0.6.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 +45 -0
- package/dist/bridges-flow.js +340 -3
- package/dist/bridges-flow.js.map +1 -1
- package/dist/{chunk-E4AICMFZ.js → chunk-5XELJIJR.js} +3 -3
- package/dist/chunk-5XELJIJR.js.map +1 -0
- package/dist/{chunk-6LTKCNLF.js → chunk-AFUULW5E.js} +3 -34
- package/dist/chunk-AFUULW5E.js.map +1 -0
- package/dist/chunk-G6N2TQVO.js +34 -0
- package/dist/chunk-G6N2TQVO.js.map +1 -0
- package/dist/chunk-IJ6JX5VC.js +3 -0
- package/dist/chunk-IJ6JX5VC.js.map +1 -0
- package/dist/{chunk-JMYPUAFH.js → chunk-LVQXIUJH.js} +2 -2
- package/dist/{chunk-JMYPUAFH.js.map → chunk-LVQXIUJH.js.map} +1 -1
- package/dist/chunk-OIX2ANFS.js +386 -0
- package/dist/chunk-OIX2ANFS.js.map +1 -0
- package/dist/chunk-ZHAK2DQR.js +289 -0
- package/dist/chunk-ZHAK2DQR.js.map +1 -0
- package/dist/components/SharedWhiteboard/index.d.cts +55 -0
- package/dist/components/SharedWhiteboard/index.d.ts +55 -0
- package/dist/components-shared-whiteboard.cjs +1533 -0
- package/dist/components-shared-whiteboard.cjs.map +1 -0
- package/dist/components-shared-whiteboard.js +285 -0
- package/dist/components-shared-whiteboard.js.map +1 -0
- package/dist/index.cjs +249 -1287
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -55
- package/dist/index.d.ts +4 -55
- package/dist/index.js +9 -563
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +2 -1
- package/dist/relay-server/index.d.cts +134 -0
- package/dist/relay-server/index.d.ts +134 -0
- package/dist/relay-server-cli.cjs +483 -0
- package/dist/relay-server-cli.cjs.map +1 -0
- package/dist/relay-server-cli.js +98 -0
- package/dist/relay-server-cli.js.map +1 -0
- package/dist/relay-server.cjs +389 -0
- package/dist/relay-server.cjs.map +1 -0
- package/dist/relay-server.js +3 -0
- package/dist/relay-server.js.map +1 -0
- package/dist/sharing/index.d.cts +2 -34
- package/dist/sharing/index.d.ts +2 -34
- package/dist/sharing.js +2 -1
- package/dist/sheets-adapter.cjs +1 -1
- package/dist/sheets-adapter.cjs.map +1 -1
- package/dist/sheets-adapter.d.cts +11 -7
- package/dist/sheets-adapter.d.ts +11 -7
- package/dist/sheets-adapter.js +1 -1
- package/dist/token-CrJF76oH.d.cts +34 -0
- package/dist/token-CrJF76oH.d.ts +34 -0
- package/docs/relay-server.md +126 -0
- package/package.json +66 -7
- package/dist/chunk-6LTKCNLF.js.map +0 -1
- package/dist/chunk-E4AICMFZ.js.map +0 -1
- package/dist/chunk-N3H4DXY5.js +0 -342
- package/dist/chunk-N3H4DXY5.js.map +0 -1
|
@@ -0,0 +1,1533 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var fancyWhiteboard = require('@particle-academy/fancy-whiteboard');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/presence/registry.ts
|
|
18
|
+
var registry_exports = {};
|
|
19
|
+
__export(registry_exports, {
|
|
20
|
+
emitActivity: () => emitActivity,
|
|
21
|
+
onActivity: () => onActivity,
|
|
22
|
+
readActivityHistory: () => readActivityHistory,
|
|
23
|
+
resetActivityRegistry: () => resetActivityRegistry
|
|
24
|
+
});
|
|
25
|
+
function emitActivity(event) {
|
|
26
|
+
history.push(event);
|
|
27
|
+
if (history.length > HISTORY_CAP) history.splice(0, history.length - HISTORY_CAP);
|
|
28
|
+
for (const l of listeners) l(event);
|
|
29
|
+
}
|
|
30
|
+
function onActivity(listener, filter) {
|
|
31
|
+
const wrapped = filter ? (e) => {
|
|
32
|
+
if (matches(e, filter)) listener(e);
|
|
33
|
+
} : listener;
|
|
34
|
+
listeners.add(wrapped);
|
|
35
|
+
return () => listeners.delete(wrapped);
|
|
36
|
+
}
|
|
37
|
+
function readActivityHistory(filter) {
|
|
38
|
+
if (!filter) return history.slice();
|
|
39
|
+
return history.filter((e) => matches(e, filter));
|
|
40
|
+
}
|
|
41
|
+
function resetActivityRegistry() {
|
|
42
|
+
listeners.clear();
|
|
43
|
+
history.length = 0;
|
|
44
|
+
}
|
|
45
|
+
function matches(e, f) {
|
|
46
|
+
if (f.agentId !== void 0 && e.agentId !== f.agentId) return false;
|
|
47
|
+
if (f.screenId !== void 0 && e.target.screenId !== f.screenId) return false;
|
|
48
|
+
if (f.kind !== void 0 && e.target.kind !== f.kind) return false;
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
var HISTORY_CAP, listeners, history;
|
|
52
|
+
var init_registry = __esm({
|
|
53
|
+
"src/presence/registry.ts"() {
|
|
54
|
+
HISTORY_CAP = 200;
|
|
55
|
+
listeners = /* @__PURE__ */ new Set();
|
|
56
|
+
history = [];
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// src/mcp/types.ts
|
|
61
|
+
var JSONRPC_METHOD_NOT_FOUND = -32601;
|
|
62
|
+
var JSONRPC_INVALID_PARAMS = -32602;
|
|
63
|
+
var JSONRPC_INTERNAL_ERROR = -32603;
|
|
64
|
+
var MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
65
|
+
|
|
66
|
+
// src/mcp/tool-host.ts
|
|
67
|
+
var ToolRegistry = class {
|
|
68
|
+
constructor() {
|
|
69
|
+
this.tools = /* @__PURE__ */ new Map();
|
|
70
|
+
}
|
|
71
|
+
registerTool(definition, handler) {
|
|
72
|
+
this.tools.set(definition.name, { definition, handler });
|
|
73
|
+
this.onToolsChanged();
|
|
74
|
+
return () => {
|
|
75
|
+
if (this.tools.delete(definition.name)) this.onToolsChanged();
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
getTool(name) {
|
|
79
|
+
return this.tools.get(name) ?? null;
|
|
80
|
+
}
|
|
81
|
+
listTools() {
|
|
82
|
+
return Array.from(this.tools.values()).map((t) => t.definition);
|
|
83
|
+
}
|
|
84
|
+
async callTool(name, args = {}) {
|
|
85
|
+
const tool = this.tools.get(name);
|
|
86
|
+
if (!tool) {
|
|
87
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
88
|
+
}
|
|
89
|
+
return tool.handler(args);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Hook for subclasses (e.g. MicroMcpServer) to notify subscribers
|
|
93
|
+
* when the tool catalog changes. Default no-op.
|
|
94
|
+
*/
|
|
95
|
+
onToolsChanged() {
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// src/mcp/server.ts
|
|
100
|
+
var MicroMcpServer = class extends ToolRegistry {
|
|
101
|
+
constructor(options) {
|
|
102
|
+
super();
|
|
103
|
+
this.transports = /* @__PURE__ */ new Set();
|
|
104
|
+
this.notifyListChangedScheduled = false;
|
|
105
|
+
this.info = options.info;
|
|
106
|
+
this.capabilities = options.capabilities ?? { tools: { listChanged: true } };
|
|
107
|
+
this.instructions = options.instructions;
|
|
108
|
+
}
|
|
109
|
+
attach(transport) {
|
|
110
|
+
this.transports.add(transport);
|
|
111
|
+
return () => this.detach(transport);
|
|
112
|
+
}
|
|
113
|
+
detach(transport) {
|
|
114
|
+
if (this.transports.delete(transport)) {
|
|
115
|
+
transport.close?.();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
unregisterTool(name) {
|
|
119
|
+
if (this.tools.delete(name)) {
|
|
120
|
+
this.scheduleListChangedNotification();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
onToolsChanged() {
|
|
124
|
+
this.scheduleListChangedNotification();
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Receive a JSON-RPC frame from a client (called by the transport).
|
|
128
|
+
* The transport is responsible for sending the response back.
|
|
129
|
+
*/
|
|
130
|
+
async receive(transport, message) {
|
|
131
|
+
if (!("method" in message)) return;
|
|
132
|
+
const isNotification = !("id" in message);
|
|
133
|
+
if (isNotification) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const request = message;
|
|
137
|
+
try {
|
|
138
|
+
const result = await this.handle(request);
|
|
139
|
+
transport.send({ jsonrpc: "2.0", id: request.id, result });
|
|
140
|
+
} catch (err) {
|
|
141
|
+
transport.send({
|
|
142
|
+
jsonrpc: "2.0",
|
|
143
|
+
id: request.id,
|
|
144
|
+
error: this.toRpcError(err)
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async handle(request) {
|
|
149
|
+
const { method, params } = request;
|
|
150
|
+
switch (method) {
|
|
151
|
+
case "initialize":
|
|
152
|
+
return {
|
|
153
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
154
|
+
capabilities: this.capabilities,
|
|
155
|
+
serverInfo: this.info,
|
|
156
|
+
...this.instructions ? { instructions: this.instructions } : {}
|
|
157
|
+
};
|
|
158
|
+
case "tools/list":
|
|
159
|
+
return { tools: this.listTools() };
|
|
160
|
+
case "tools/call": {
|
|
161
|
+
const name = params?.name;
|
|
162
|
+
const args = params?.arguments ?? {};
|
|
163
|
+
if (typeof name !== "string") {
|
|
164
|
+
throw rpcError(JSONRPC_INVALID_PARAMS, "tools/call requires `name`");
|
|
165
|
+
}
|
|
166
|
+
const tool = this.tools.get(name);
|
|
167
|
+
if (!tool) {
|
|
168
|
+
throw rpcError(JSONRPC_METHOD_NOT_FOUND, `Unknown tool: ${name}`);
|
|
169
|
+
}
|
|
170
|
+
const result = await tool.handler(args);
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
case "ping":
|
|
174
|
+
return {};
|
|
175
|
+
default:
|
|
176
|
+
throw rpcError(JSONRPC_METHOD_NOT_FOUND, `Unsupported method: ${method}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
scheduleListChangedNotification() {
|
|
180
|
+
if (this.notifyListChangedScheduled) return;
|
|
181
|
+
this.notifyListChangedScheduled = true;
|
|
182
|
+
queueMicrotask(() => {
|
|
183
|
+
this.notifyListChangedScheduled = false;
|
|
184
|
+
this.broadcast({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
broadcast(message) {
|
|
188
|
+
for (const t of this.transports) t.send(message);
|
|
189
|
+
}
|
|
190
|
+
toRpcError(err) {
|
|
191
|
+
if (err && typeof err === "object" && "code" in err && "message" in err) {
|
|
192
|
+
return err;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
code: JSONRPC_INTERNAL_ERROR,
|
|
196
|
+
message: err instanceof Error ? err.message : String(err)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
function rpcError(code, message, data) {
|
|
201
|
+
return { code, message, ...{} };
|
|
202
|
+
}
|
|
203
|
+
function textResult(text, structured) {
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: "text", text }],
|
|
206
|
+
...structured !== void 0 ? { structuredContent: structured } : {}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function errorResult(text) {
|
|
210
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/mcp/transports/in-process.ts
|
|
214
|
+
var InProcessTransport = class {
|
|
215
|
+
constructor() {
|
|
216
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
217
|
+
}
|
|
218
|
+
/** Bind to a server. Called from the client's setup, not directly. */
|
|
219
|
+
bindServer(server) {
|
|
220
|
+
this.server = server;
|
|
221
|
+
}
|
|
222
|
+
/** Server → client (delivered to subscribed listeners). */
|
|
223
|
+
send(message) {
|
|
224
|
+
for (const l of this.listeners) l(message);
|
|
225
|
+
}
|
|
226
|
+
/** Client → server. Awaitable so callers can flush. */
|
|
227
|
+
async deliver(message) {
|
|
228
|
+
if (!this.server) throw new Error("InProcessTransport has no bound server");
|
|
229
|
+
await this.server.receive(this, message);
|
|
230
|
+
}
|
|
231
|
+
/** Subscribe to messages the server pushes to this client. */
|
|
232
|
+
onServerMessage(listener) {
|
|
233
|
+
this.listeners.add(listener);
|
|
234
|
+
return () => this.listeners.delete(listener);
|
|
235
|
+
}
|
|
236
|
+
close() {
|
|
237
|
+
this.listeners.clear();
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
function attachInProcess(server) {
|
|
241
|
+
const transport = new InProcessTransport();
|
|
242
|
+
transport.bindServer(server);
|
|
243
|
+
server.attach(transport);
|
|
244
|
+
return transport;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/sharing/token.ts
|
|
248
|
+
var TOKEN_BYTES = 24;
|
|
249
|
+
function createSessionDescriptor() {
|
|
250
|
+
const id = randomId(8);
|
|
251
|
+
const token = randomToken();
|
|
252
|
+
return { id, token, display: token.slice(0, 8) };
|
|
253
|
+
}
|
|
254
|
+
function buildShareUrl(descriptor, baseUrl = typeof window !== "undefined" ? window.location.href.split("?")[0] : "") {
|
|
255
|
+
const u = new URL(baseUrl);
|
|
256
|
+
u.searchParams.set("session", descriptor.id);
|
|
257
|
+
u.searchParams.set("token", descriptor.token);
|
|
258
|
+
return u.toString();
|
|
259
|
+
}
|
|
260
|
+
function buildShareConfig(descriptor, transport = "broadcast-channel") {
|
|
261
|
+
return {
|
|
262
|
+
name: `whiteboard-${descriptor.id}`,
|
|
263
|
+
transport,
|
|
264
|
+
session: descriptor.id,
|
|
265
|
+
token: descriptor.token,
|
|
266
|
+
channel: `fai:share:${descriptor.id}`,
|
|
267
|
+
protocol_version: "2025-06-18"
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function randomToken() {
|
|
271
|
+
const bytes = new Uint8Array(TOKEN_BYTES);
|
|
272
|
+
crypto.getRandomValues(bytes);
|
|
273
|
+
return base64Url(bytes);
|
|
274
|
+
}
|
|
275
|
+
function randomId(len) {
|
|
276
|
+
const bytes = new Uint8Array(Math.ceil(len * 3 / 4));
|
|
277
|
+
crypto.getRandomValues(bytes);
|
|
278
|
+
return base64Url(bytes).slice(0, len);
|
|
279
|
+
}
|
|
280
|
+
function base64Url(bytes) {
|
|
281
|
+
let s = "";
|
|
282
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
283
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
284
|
+
}
|
|
285
|
+
function constantTimeEqual(a, b) {
|
|
286
|
+
if (a.length !== b.length) return false;
|
|
287
|
+
let diff = 0;
|
|
288
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
289
|
+
return diff === 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/sharing/sse-relay.ts
|
|
293
|
+
var SseRelayTransport = class {
|
|
294
|
+
constructor(options) {
|
|
295
|
+
this.sendQueue = [];
|
|
296
|
+
this.connected = false;
|
|
297
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
298
|
+
this.state = "idle";
|
|
299
|
+
this.opts = options;
|
|
300
|
+
this.expectedToken = options.token;
|
|
301
|
+
}
|
|
302
|
+
bindServer(server) {
|
|
303
|
+
this.server = server;
|
|
304
|
+
}
|
|
305
|
+
/** Open the SSE channel. Idempotent. */
|
|
306
|
+
start() {
|
|
307
|
+
if (this.connected || typeof window === "undefined") return;
|
|
308
|
+
const url = `${this.opts.baseUrl}/${encodeURIComponent(this.opts.sessionId)}/events?token=${encodeURIComponent(this.opts.token)}`;
|
|
309
|
+
this.setState("connecting");
|
|
310
|
+
const es = new EventSource(url, { withCredentials: false });
|
|
311
|
+
this.es = es;
|
|
312
|
+
es.addEventListener("open", () => {
|
|
313
|
+
this.connected = true;
|
|
314
|
+
this.setState("open");
|
|
315
|
+
const queued = this.sendQueue.splice(0);
|
|
316
|
+
for (const msg of queued) this.postOut(msg);
|
|
317
|
+
});
|
|
318
|
+
es.addEventListener("mcp", (ev) => {
|
|
319
|
+
const raw = ev.data;
|
|
320
|
+
this.handleInbound(raw);
|
|
321
|
+
});
|
|
322
|
+
es.addEventListener("error", () => {
|
|
323
|
+
this.setState("error");
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
send(message) {
|
|
327
|
+
if (!this.connected) {
|
|
328
|
+
this.sendQueue.push(message);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
this.postOut(message);
|
|
332
|
+
}
|
|
333
|
+
close() {
|
|
334
|
+
this.es?.close();
|
|
335
|
+
this.es = void 0;
|
|
336
|
+
this.connected = false;
|
|
337
|
+
this.setState("closed");
|
|
338
|
+
}
|
|
339
|
+
onStateChange(listener) {
|
|
340
|
+
this.listeners.add(listener);
|
|
341
|
+
listener(this.state);
|
|
342
|
+
return () => this.listeners.delete(listener);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* For relays that wrap each frame with auth metadata: hosts can call this
|
|
346
|
+
* directly when a frame arrives via a non-SSE path. The transport will
|
|
347
|
+
* dispatch it to the bound server.
|
|
348
|
+
*/
|
|
349
|
+
async deliverFromRemote(payload, token) {
|
|
350
|
+
if (token !== void 0 && !constantTimeEqual(token, this.expectedToken)) return;
|
|
351
|
+
if (!this.server) throw new Error("SseRelayTransport has no bound server");
|
|
352
|
+
const message = typeof payload === "string" ? JSON.parse(payload) : payload;
|
|
353
|
+
await this.server.receive(this, message);
|
|
354
|
+
}
|
|
355
|
+
async postOut(message) {
|
|
356
|
+
const url = `${this.opts.baseUrl}/${encodeURIComponent(this.opts.sessionId)}/outbox?token=${encodeURIComponent(this.opts.token)}`;
|
|
357
|
+
const f = this.opts.fetch ?? fetch;
|
|
358
|
+
try {
|
|
359
|
+
await f(url, {
|
|
360
|
+
method: "POST",
|
|
361
|
+
headers: { "content-type": "application/json", "accept": "application/json" },
|
|
362
|
+
body: JSON.stringify(message)
|
|
363
|
+
});
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async handleInbound(raw) {
|
|
368
|
+
if (!this.server) return;
|
|
369
|
+
let message;
|
|
370
|
+
try {
|
|
371
|
+
message = JSON.parse(raw);
|
|
372
|
+
} catch {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
await this.server.receive(this, message);
|
|
376
|
+
}
|
|
377
|
+
setState(state) {
|
|
378
|
+
this.state = state;
|
|
379
|
+
for (const l of this.listeners) l(state);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
function attachSseRelay(server, options) {
|
|
383
|
+
const transport = new SseRelayTransport(options);
|
|
384
|
+
transport.bindServer(server);
|
|
385
|
+
server.attach(transport);
|
|
386
|
+
transport.start();
|
|
387
|
+
Promise.resolve().then(() => (init_registry(), registry_exports)).then(({ onActivity: onActivity2 }) => {
|
|
388
|
+
const off = onActivity2((event) => {
|
|
389
|
+
transport.send({
|
|
390
|
+
jsonrpc: "2.0",
|
|
391
|
+
method: "notifications/agent_activity",
|
|
392
|
+
params: event
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
const origClose = transport.close.bind(transport);
|
|
396
|
+
transport.close = () => {
|
|
397
|
+
off();
|
|
398
|
+
origClose();
|
|
399
|
+
};
|
|
400
|
+
}).catch(() => {
|
|
401
|
+
});
|
|
402
|
+
return transport;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/presence/wrap-tool-with-activity.ts
|
|
406
|
+
init_registry();
|
|
407
|
+
function wrapToolWithActivity(handler, options) {
|
|
408
|
+
return async (args) => {
|
|
409
|
+
const result = await handler(args);
|
|
410
|
+
if (result.isError) return result;
|
|
411
|
+
let target;
|
|
412
|
+
if (options.resolveTarget) {
|
|
413
|
+
target = options.resolveTarget({ toolName: options.toolName, args, result });
|
|
414
|
+
} else {
|
|
415
|
+
target = { kind: options.kind, screenId: options.screenId };
|
|
416
|
+
}
|
|
417
|
+
if (!target) return result;
|
|
418
|
+
emitActivity({
|
|
419
|
+
agentId: options.agent.id,
|
|
420
|
+
agentName: options.agent.name,
|
|
421
|
+
agentColor: options.agent.color,
|
|
422
|
+
target: { ...target, kind: target.kind ?? options.kind, screenId: target.screenId ?? options.screenId },
|
|
423
|
+
action: options.toolName,
|
|
424
|
+
timestamp: Date.now(),
|
|
425
|
+
meta: extractMeta(result),
|
|
426
|
+
ttlMs: options.ttlMs
|
|
427
|
+
});
|
|
428
|
+
return result;
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function extractMeta(result) {
|
|
432
|
+
const sc = result.structuredContent;
|
|
433
|
+
if (sc && typeof sc === "object" && !Array.isArray(sc)) {
|
|
434
|
+
return sc;
|
|
435
|
+
}
|
|
436
|
+
return void 0;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/undo/undo-stack.ts
|
|
440
|
+
var stacks = /* @__PURE__ */ new Map();
|
|
441
|
+
var CAP = 200;
|
|
442
|
+
function getStack(agentId) {
|
|
443
|
+
let s = stacks.get(agentId);
|
|
444
|
+
if (!s) {
|
|
445
|
+
s = { past: [], future: [] };
|
|
446
|
+
stacks.set(agentId, s);
|
|
447
|
+
}
|
|
448
|
+
return s;
|
|
449
|
+
}
|
|
450
|
+
function pushUndoEntry(agentId, entry) {
|
|
451
|
+
const s = getStack(agentId);
|
|
452
|
+
s.past.push(entry);
|
|
453
|
+
if (s.past.length > CAP) s.past.splice(0, s.past.length - CAP);
|
|
454
|
+
s.future.length = 0;
|
|
455
|
+
}
|
|
456
|
+
async function undoOne(agentId) {
|
|
457
|
+
const s = getStack(agentId);
|
|
458
|
+
const entry = s.past.pop();
|
|
459
|
+
if (!entry) return null;
|
|
460
|
+
await entry.undo();
|
|
461
|
+
s.future.push(entry);
|
|
462
|
+
return entry;
|
|
463
|
+
}
|
|
464
|
+
async function redoOne(agentId) {
|
|
465
|
+
const s = getStack(agentId);
|
|
466
|
+
const entry = s.future.pop();
|
|
467
|
+
if (!entry) return null;
|
|
468
|
+
await entry.redo();
|
|
469
|
+
s.past.push(entry);
|
|
470
|
+
return entry;
|
|
471
|
+
}
|
|
472
|
+
function readHistory(agentId) {
|
|
473
|
+
return getStack(agentId).past.slice();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/undo/undo-tools.ts
|
|
477
|
+
var installedHosts = /* @__PURE__ */ new WeakSet();
|
|
478
|
+
function ensureUndoToolsRegistered(host, options = {}) {
|
|
479
|
+
if (installedHosts.has(host)) return;
|
|
480
|
+
installedHosts.add(host);
|
|
481
|
+
registerUndoTools(host, options);
|
|
482
|
+
}
|
|
483
|
+
function registerUndoTools(host, options = {}) {
|
|
484
|
+
const defaultAgent = options.defaultAgentId ?? "agent";
|
|
485
|
+
const disposers = [];
|
|
486
|
+
const agentOf = (args) => typeof args?.agentId === "string" ? args.agentId : defaultAgent;
|
|
487
|
+
disposers.push(
|
|
488
|
+
host.registerTool(
|
|
489
|
+
{
|
|
490
|
+
name: "agent_undo",
|
|
491
|
+
description: "Undo the most recent action on the agent's stack. Optional agentId targets a specific agent.",
|
|
492
|
+
inputSchema: {
|
|
493
|
+
type: "object",
|
|
494
|
+
properties: { agentId: { type: "string" } },
|
|
495
|
+
additionalProperties: false
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
async (args) => {
|
|
499
|
+
const entry = await undoOne(agentOf(args));
|
|
500
|
+
if (!entry) return errorResult("Nothing to undo.");
|
|
501
|
+
return textResult(`Undid: ${entry.label}`, { entry: serialize(entry) });
|
|
502
|
+
}
|
|
503
|
+
)
|
|
504
|
+
);
|
|
505
|
+
disposers.push(
|
|
506
|
+
host.registerTool(
|
|
507
|
+
{
|
|
508
|
+
name: "agent_redo",
|
|
509
|
+
description: "Redo the most recently undone action.",
|
|
510
|
+
inputSchema: {
|
|
511
|
+
type: "object",
|
|
512
|
+
properties: { agentId: { type: "string" } },
|
|
513
|
+
additionalProperties: false
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
async (args) => {
|
|
517
|
+
const entry = await redoOne(agentOf(args));
|
|
518
|
+
if (!entry) return errorResult("Nothing to redo.");
|
|
519
|
+
return textResult(`Redid: ${entry.label}`, { entry: serialize(entry) });
|
|
520
|
+
}
|
|
521
|
+
)
|
|
522
|
+
);
|
|
523
|
+
disposers.push(
|
|
524
|
+
host.registerTool(
|
|
525
|
+
{
|
|
526
|
+
name: "agent_history",
|
|
527
|
+
description: "List the agent's undo stack (oldest first). Useful for understanding what's reversible.",
|
|
528
|
+
inputSchema: {
|
|
529
|
+
type: "object",
|
|
530
|
+
properties: { agentId: { type: "string" } },
|
|
531
|
+
additionalProperties: false
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
async (args) => {
|
|
535
|
+
const history2 = readHistory(agentOf(args)).map(serialize);
|
|
536
|
+
const text = history2.map((e) => `${new Date(e.timestamp).toISOString()} ${e.bridgeId} ${e.action}: ${e.label}`).join("\n");
|
|
537
|
+
return textResult(text || "(empty)", history2);
|
|
538
|
+
}
|
|
539
|
+
)
|
|
540
|
+
);
|
|
541
|
+
return () => disposers.forEach((d) => d());
|
|
542
|
+
}
|
|
543
|
+
function serialize(entry) {
|
|
544
|
+
return {
|
|
545
|
+
timestamp: entry.timestamp,
|
|
546
|
+
bridgeId: entry.bridgeId,
|
|
547
|
+
action: entry.action,
|
|
548
|
+
label: entry.label
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/bridges/whiteboard.ts
|
|
553
|
+
var DEFAULT_AGENT = { id: "agent", name: "Agent", color: "#a855f7" };
|
|
554
|
+
var VALID_SHAPES = ["rect", "rounded-rect", "ellipse", "diamond", "triangle", "line", "arrow", "text"];
|
|
555
|
+
var num = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : fallback ?? 0;
|
|
556
|
+
var str = (v, fallback = "") => typeof v === "string" ? v : fallback;
|
|
557
|
+
var bool = (v, fallback = false) => typeof v === "boolean" ? v : fallback;
|
|
558
|
+
var newId = (prefix) => `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
|
559
|
+
function registerWhiteboardBridge(host, options) {
|
|
560
|
+
const { adapter } = options;
|
|
561
|
+
const agent = { ...DEFAULT_AGENT, ...options.agent ?? {} };
|
|
562
|
+
const disposers = [];
|
|
563
|
+
ensureUndoToolsRegistered(host, { defaultAgentId: agent.id });
|
|
564
|
+
const wbTarget = (args, result) => ({
|
|
565
|
+
kind: "whiteboard",
|
|
566
|
+
elementId: result?.structuredContent?.id ?? args?.id
|
|
567
|
+
});
|
|
568
|
+
const reg = (name, description, inputProperties, required, handler, resolveTarget) => {
|
|
569
|
+
const wrapped = async (args) => {
|
|
570
|
+
try {
|
|
571
|
+
return await handler(args);
|
|
572
|
+
} catch (e) {
|
|
573
|
+
return errorResult(e instanceof Error ? e.message : String(e));
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
const final = resolveTarget ? wrapToolWithActivity(wrapped, {
|
|
577
|
+
toolName: name,
|
|
578
|
+
agent: { id: agent.id, name: agent.name, color: agent.color },
|
|
579
|
+
kind: "whiteboard",
|
|
580
|
+
resolveTarget: ({ args, result }) => resolveTarget(args, result)
|
|
581
|
+
}) : wrapped;
|
|
582
|
+
disposers.push(
|
|
583
|
+
host.registerTool(
|
|
584
|
+
{
|
|
585
|
+
name,
|
|
586
|
+
description,
|
|
587
|
+
inputSchema: {
|
|
588
|
+
type: "object",
|
|
589
|
+
properties: inputProperties,
|
|
590
|
+
required,
|
|
591
|
+
additionalProperties: false
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
final
|
|
595
|
+
)
|
|
596
|
+
);
|
|
597
|
+
};
|
|
598
|
+
reg("whiteboard_get_state", "Get the full board state: viewport, all items, strokes.", {}, [], () => {
|
|
599
|
+
const state = {
|
|
600
|
+
viewport: adapter.getViewport(),
|
|
601
|
+
notes: adapter.getNotes(),
|
|
602
|
+
shapes: adapter.getShapes(),
|
|
603
|
+
connectors: adapter.getConnectors(),
|
|
604
|
+
strokes: adapter.getStrokes()
|
|
605
|
+
};
|
|
606
|
+
return textResult(JSON.stringify(state, null, 2), state);
|
|
607
|
+
});
|
|
608
|
+
reg("whiteboard_list_items", "List notes, shapes, and connectors with id, kind, and bounds.", {}, [], () => {
|
|
609
|
+
const items = [];
|
|
610
|
+
for (const n of adapter.getNotes()) {
|
|
611
|
+
items.push({
|
|
612
|
+
id: n.id,
|
|
613
|
+
kind: "sticky",
|
|
614
|
+
summary: `"${(n.text ?? "").slice(0, 40)}" @(${Math.round(n.x)},${Math.round(n.y)}) ${n.width}\xD7${n.height}`
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
for (const s of adapter.getShapes()) {
|
|
618
|
+
items.push({
|
|
619
|
+
id: s.id,
|
|
620
|
+
kind: `shape:${s.shape}`,
|
|
621
|
+
summary: `${s.text ? `"${s.text}" ` : ""}@(${Math.round(s.x)},${Math.round(s.y)}) ${s.width}\xD7${s.height}`
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
for (const c of adapter.getConnectors()) {
|
|
625
|
+
items.push({ id: c.id, kind: "connector", summary: `from=${JSON.stringify(c.from)} to=${JSON.stringify(c.to)}` });
|
|
626
|
+
}
|
|
627
|
+
return textResult(items.map((i) => `${i.kind} ${i.id}: ${i.summary}`).join("\n") || "(empty board)", items);
|
|
628
|
+
});
|
|
629
|
+
reg(
|
|
630
|
+
"whiteboard_get_item",
|
|
631
|
+
"Get a single item (sticky / shape / connector) by id.",
|
|
632
|
+
{ id: { type: "string" } },
|
|
633
|
+
["id"],
|
|
634
|
+
(args) => {
|
|
635
|
+
const id = str(args.id);
|
|
636
|
+
const all = [...adapter.getNotes(), ...adapter.getShapes(), ...adapter.getConnectors()];
|
|
637
|
+
const found = all.find((x) => x.id === id);
|
|
638
|
+
if (!found) return errorResult(`No item with id ${id}`);
|
|
639
|
+
return textResult(JSON.stringify(found, null, 2), found);
|
|
640
|
+
}
|
|
641
|
+
);
|
|
642
|
+
reg(
|
|
643
|
+
"whiteboard_add_sticky",
|
|
644
|
+
"Add a sticky note. Position is in world coordinates.",
|
|
645
|
+
{
|
|
646
|
+
x: { type: "number" },
|
|
647
|
+
y: { type: "number" },
|
|
648
|
+
text: { type: "string" },
|
|
649
|
+
width: { type: "number" },
|
|
650
|
+
height: { type: "number" },
|
|
651
|
+
color: { type: "string", description: "CSS color, e.g. #fde68a" }
|
|
652
|
+
},
|
|
653
|
+
["x", "y"],
|
|
654
|
+
async (args) => {
|
|
655
|
+
const x = num(args.x);
|
|
656
|
+
const y = num(args.y);
|
|
657
|
+
const width = num(args.width, 180);
|
|
658
|
+
const height = num(args.height, 140);
|
|
659
|
+
const note = {
|
|
660
|
+
id: newId("n"),
|
|
661
|
+
kind: "sticky",
|
|
662
|
+
x,
|
|
663
|
+
y,
|
|
664
|
+
width,
|
|
665
|
+
height,
|
|
666
|
+
text: str(args.text),
|
|
667
|
+
color: typeof args.color === "string" ? args.color : "#fde68a",
|
|
668
|
+
authorId: agent.id
|
|
669
|
+
};
|
|
670
|
+
adapter.setNotes((all) => [...all, note]);
|
|
671
|
+
pushUndoEntry(agent.id, {
|
|
672
|
+
timestamp: Date.now(),
|
|
673
|
+
bridgeId: "whiteboard",
|
|
674
|
+
action: "whiteboard_add_sticky",
|
|
675
|
+
label: `Added sticky ${note.id}`,
|
|
676
|
+
undo: () => adapter.setNotes((all) => all.filter((n) => n.id !== note.id)),
|
|
677
|
+
redo: () => adapter.setNotes((all) => [...all, note])
|
|
678
|
+
});
|
|
679
|
+
return textResult(`Added sticky ${note.id}`, note);
|
|
680
|
+
},
|
|
681
|
+
wbTarget
|
|
682
|
+
);
|
|
683
|
+
reg(
|
|
684
|
+
"whiteboard_stream_text",
|
|
685
|
+
"Type text into a sticky note character-by-character so the human can read it forming. The tool returns once streaming finishes.",
|
|
686
|
+
{
|
|
687
|
+
id: { type: "string" },
|
|
688
|
+
text: { type: "string" },
|
|
689
|
+
cps: { type: "number", description: "Characters per second. Default 25." },
|
|
690
|
+
append: { type: "boolean", description: "Append to existing text instead of replacing. Default false." }
|
|
691
|
+
},
|
|
692
|
+
["id", "text"],
|
|
693
|
+
async (args) => {
|
|
694
|
+
const id = str(args.id);
|
|
695
|
+
const target = str(args.text);
|
|
696
|
+
const cps = Math.max(1, num(args.cps, 25));
|
|
697
|
+
const append = bool(args.append);
|
|
698
|
+
const startNote = adapter.getNotes().find((n) => n.id === id);
|
|
699
|
+
if (!startNote) return errorResult(`No sticky with id ${id}`);
|
|
700
|
+
const base = append ? startNote.text ?? "" : "";
|
|
701
|
+
const interval = Math.max(8, Math.round(1e3 / cps));
|
|
702
|
+
for (let i = 0; i <= target.length; i++) {
|
|
703
|
+
const nextText = base + target.slice(0, i);
|
|
704
|
+
adapter.setNotes((all) => all.map((n) => n.id === id ? { ...n, text: nextText } : n));
|
|
705
|
+
if (i < target.length) await new Promise((r) => setTimeout(r, interval));
|
|
706
|
+
}
|
|
707
|
+
return textResult(`Streamed ${target.length} chars to ${id}`, { id, text: base + target });
|
|
708
|
+
}
|
|
709
|
+
);
|
|
710
|
+
reg(
|
|
711
|
+
"whiteboard_update_sticky",
|
|
712
|
+
"Update fields on a sticky note. Only provided fields are changed.",
|
|
713
|
+
{
|
|
714
|
+
id: { type: "string" },
|
|
715
|
+
x: { type: "number" },
|
|
716
|
+
y: { type: "number" },
|
|
717
|
+
width: { type: "number" },
|
|
718
|
+
height: { type: "number" },
|
|
719
|
+
text: { type: "string" },
|
|
720
|
+
color: { type: "string" }
|
|
721
|
+
},
|
|
722
|
+
["id"],
|
|
723
|
+
async (args) => {
|
|
724
|
+
const id = str(args.id);
|
|
725
|
+
const existing = adapter.getNotes().find((n) => n.id === id);
|
|
726
|
+
if (!existing) return errorResult(`No sticky with id ${id}`);
|
|
727
|
+
const nextX = args.x !== void 0 ? num(args.x) : existing.x;
|
|
728
|
+
const nextY = args.y !== void 0 ? num(args.y) : existing.y;
|
|
729
|
+
const nextW = args.width !== void 0 ? num(args.width) : existing.width;
|
|
730
|
+
const nextH = args.height !== void 0 ? num(args.height) : existing.height;
|
|
731
|
+
let updated = null;
|
|
732
|
+
adapter.setNotes(
|
|
733
|
+
(all) => all.map((n) => {
|
|
734
|
+
if (n.id !== id) return n;
|
|
735
|
+
updated = {
|
|
736
|
+
...n,
|
|
737
|
+
x: nextX,
|
|
738
|
+
y: nextY,
|
|
739
|
+
width: nextW,
|
|
740
|
+
height: nextH,
|
|
741
|
+
...args.text !== void 0 ? { text: str(args.text) } : {},
|
|
742
|
+
...args.color !== void 0 ? { color: str(args.color) } : {}
|
|
743
|
+
};
|
|
744
|
+
return updated;
|
|
745
|
+
})
|
|
746
|
+
);
|
|
747
|
+
return textResult(`Updated sticky ${id}`, updated);
|
|
748
|
+
},
|
|
749
|
+
wbTarget
|
|
750
|
+
);
|
|
751
|
+
reg(
|
|
752
|
+
"whiteboard_add_shape",
|
|
753
|
+
`Add a shape. Kind must be one of: ${VALID_SHAPES.join(", ")}.`,
|
|
754
|
+
{
|
|
755
|
+
shape: { type: "string", enum: VALID_SHAPES },
|
|
756
|
+
x: { type: "number" },
|
|
757
|
+
y: { type: "number" },
|
|
758
|
+
width: { type: "number" },
|
|
759
|
+
height: { type: "number" },
|
|
760
|
+
text: { type: "string" },
|
|
761
|
+
fill: { type: "string" },
|
|
762
|
+
stroke: { type: "string" },
|
|
763
|
+
flipX: { type: "boolean" },
|
|
764
|
+
flipY: { type: "boolean" }
|
|
765
|
+
},
|
|
766
|
+
["shape", "x", "y", "width", "height"],
|
|
767
|
+
async (args) => {
|
|
768
|
+
const kind = str(args.shape);
|
|
769
|
+
if (!VALID_SHAPES.includes(kind)) return errorResult(`Invalid shape kind: ${kind}`);
|
|
770
|
+
const x = num(args.x);
|
|
771
|
+
const y = num(args.y);
|
|
772
|
+
const width = num(args.width);
|
|
773
|
+
const height = num(args.height);
|
|
774
|
+
const shape = {
|
|
775
|
+
id: newId("s"),
|
|
776
|
+
kind: "shape",
|
|
777
|
+
shape: kind,
|
|
778
|
+
x,
|
|
779
|
+
y,
|
|
780
|
+
width,
|
|
781
|
+
height,
|
|
782
|
+
...args.text !== void 0 ? { text: str(args.text) } : {},
|
|
783
|
+
...args.fill !== void 0 ? { fill: str(args.fill) } : {},
|
|
784
|
+
...args.stroke !== void 0 ? { stroke: str(args.stroke) } : {},
|
|
785
|
+
...args.flipX !== void 0 ? { flipX: bool(args.flipX) } : {},
|
|
786
|
+
...args.flipY !== void 0 ? { flipY: bool(args.flipY) } : {}
|
|
787
|
+
};
|
|
788
|
+
adapter.setShapes((all) => [...all, shape]);
|
|
789
|
+
return textResult(`Added ${kind} ${shape.id}`, shape);
|
|
790
|
+
},
|
|
791
|
+
wbTarget
|
|
792
|
+
);
|
|
793
|
+
reg(
|
|
794
|
+
"whiteboard_update_shape",
|
|
795
|
+
"Update fields on a shape.",
|
|
796
|
+
{
|
|
797
|
+
id: { type: "string" },
|
|
798
|
+
x: { type: "number" },
|
|
799
|
+
y: { type: "number" },
|
|
800
|
+
width: { type: "number" },
|
|
801
|
+
height: { type: "number" },
|
|
802
|
+
text: { type: "string" },
|
|
803
|
+
fill: { type: "string" },
|
|
804
|
+
stroke: { type: "string" }
|
|
805
|
+
},
|
|
806
|
+
["id"],
|
|
807
|
+
async (args) => {
|
|
808
|
+
const id = str(args.id);
|
|
809
|
+
const existing = adapter.getShapes().find((s) => s.id === id);
|
|
810
|
+
if (!existing) return errorResult(`No shape with id ${id}`);
|
|
811
|
+
const nextX = args.x !== void 0 ? num(args.x) : existing.x;
|
|
812
|
+
const nextY = args.y !== void 0 ? num(args.y) : existing.y;
|
|
813
|
+
const nextW = args.width !== void 0 ? num(args.width) : existing.width;
|
|
814
|
+
const nextH = args.height !== void 0 ? num(args.height) : existing.height;
|
|
815
|
+
let updated = null;
|
|
816
|
+
adapter.setShapes(
|
|
817
|
+
(all) => all.map((s) => {
|
|
818
|
+
if (s.id !== id) return s;
|
|
819
|
+
updated = {
|
|
820
|
+
...s,
|
|
821
|
+
x: nextX,
|
|
822
|
+
y: nextY,
|
|
823
|
+
width: nextW,
|
|
824
|
+
height: nextH,
|
|
825
|
+
...args.text !== void 0 ? { text: str(args.text) } : {},
|
|
826
|
+
...args.fill !== void 0 ? { fill: str(args.fill) } : {},
|
|
827
|
+
...args.stroke !== void 0 ? { stroke: str(args.stroke) } : {}
|
|
828
|
+
};
|
|
829
|
+
return updated;
|
|
830
|
+
})
|
|
831
|
+
);
|
|
832
|
+
return textResult(`Updated shape ${id}`, updated);
|
|
833
|
+
},
|
|
834
|
+
wbTarget
|
|
835
|
+
);
|
|
836
|
+
reg(
|
|
837
|
+
"whiteboard_add_connector",
|
|
838
|
+
"Connect two items by id, or specify explicit world-space points.",
|
|
839
|
+
{
|
|
840
|
+
from: { description: "Item id (string) or {x,y}" },
|
|
841
|
+
to: { description: "Item id (string) or {x,y}" },
|
|
842
|
+
color: { type: "string" }
|
|
843
|
+
},
|
|
844
|
+
["from", "to"],
|
|
845
|
+
(args) => {
|
|
846
|
+
const c = {
|
|
847
|
+
id: newId("c"),
|
|
848
|
+
kind: "connector",
|
|
849
|
+
from: args.from,
|
|
850
|
+
to: args.to,
|
|
851
|
+
...args.color !== void 0 ? { color: str(args.color) } : {}
|
|
852
|
+
};
|
|
853
|
+
adapter.setConnectors((all) => [...all, c]);
|
|
854
|
+
return textResult(`Added connector ${c.id}`, c);
|
|
855
|
+
},
|
|
856
|
+
wbTarget
|
|
857
|
+
);
|
|
858
|
+
reg(
|
|
859
|
+
"whiteboard_add_stroke",
|
|
860
|
+
"Add a freeform pen stroke. Points are absolute screen coords (matching the Drawing layer).",
|
|
861
|
+
{
|
|
862
|
+
points: {
|
|
863
|
+
type: "array",
|
|
864
|
+
description: "Array of {x,y} points"
|
|
865
|
+
},
|
|
866
|
+
color: { type: "string" },
|
|
867
|
+
size: { type: "number" }
|
|
868
|
+
},
|
|
869
|
+
["points"],
|
|
870
|
+
(args) => {
|
|
871
|
+
const points = (Array.isArray(args.points) ? args.points : []).map((p) => ({
|
|
872
|
+
x: num(p?.x),
|
|
873
|
+
y: num(p?.y)
|
|
874
|
+
}));
|
|
875
|
+
if (!points.length) return errorResult("Stroke requires at least one point");
|
|
876
|
+
const stroke = {
|
|
877
|
+
id: newId("st"),
|
|
878
|
+
points,
|
|
879
|
+
color: typeof args.color === "string" ? args.color : "#0f172a",
|
|
880
|
+
size: typeof args.size === "number" ? args.size : 2,
|
|
881
|
+
authorId: agent.id
|
|
882
|
+
};
|
|
883
|
+
adapter.setStrokes((all) => [...all, stroke]);
|
|
884
|
+
return textResult(`Added stroke ${stroke.id} (${points.length} points)`, stroke);
|
|
885
|
+
},
|
|
886
|
+
wbTarget
|
|
887
|
+
);
|
|
888
|
+
reg(
|
|
889
|
+
"whiteboard_delete_item",
|
|
890
|
+
"Remove any item by id (sticky / shape / connector / stroke).",
|
|
891
|
+
{ id: { type: "string" } },
|
|
892
|
+
["id"],
|
|
893
|
+
(args) => {
|
|
894
|
+
const id = str(args.id);
|
|
895
|
+
const removedNotes = adapter.getNotes().filter((x) => x.id === id);
|
|
896
|
+
const removedShapes = adapter.getShapes().filter((x) => x.id === id);
|
|
897
|
+
const removedConnectors = adapter.getConnectors().filter((x) => x.id === id);
|
|
898
|
+
const removedStrokes = adapter.getStrokes().filter((x) => x.id === id);
|
|
899
|
+
const removed = removedNotes.length + removedShapes.length + removedConnectors.length + removedStrokes.length > 0;
|
|
900
|
+
if (!removed) return errorResult(`No item with id ${id}`);
|
|
901
|
+
adapter.setNotes((all) => all.filter((x) => x.id !== id));
|
|
902
|
+
adapter.setShapes((all) => all.filter((x) => x.id !== id));
|
|
903
|
+
adapter.setConnectors((all) => all.filter((x) => x.id !== id));
|
|
904
|
+
adapter.setStrokes((all) => all.filter((x) => x.id !== id));
|
|
905
|
+
pushUndoEntry(agent.id, {
|
|
906
|
+
timestamp: Date.now(),
|
|
907
|
+
bridgeId: "whiteboard",
|
|
908
|
+
action: "whiteboard_delete_item",
|
|
909
|
+
label: `Deleted ${id}`,
|
|
910
|
+
undo: () => {
|
|
911
|
+
if (removedNotes.length) adapter.setNotes((all) => [...all, ...removedNotes]);
|
|
912
|
+
if (removedShapes.length) adapter.setShapes((all) => [...all, ...removedShapes]);
|
|
913
|
+
if (removedConnectors.length) adapter.setConnectors((all) => [...all, ...removedConnectors]);
|
|
914
|
+
if (removedStrokes.length) adapter.setStrokes((all) => [...all, ...removedStrokes]);
|
|
915
|
+
},
|
|
916
|
+
redo: () => {
|
|
917
|
+
adapter.setNotes((all) => all.filter((x) => x.id !== id));
|
|
918
|
+
adapter.setShapes((all) => all.filter((x) => x.id !== id));
|
|
919
|
+
adapter.setConnectors((all) => all.filter((x) => x.id !== id));
|
|
920
|
+
adapter.setStrokes((all) => all.filter((x) => x.id !== id));
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
return textResult(`Deleted ${id}`);
|
|
924
|
+
},
|
|
925
|
+
wbTarget
|
|
926
|
+
);
|
|
927
|
+
reg(
|
|
928
|
+
"whiteboard_set_viewport",
|
|
929
|
+
"Pan / zoom the viewport.",
|
|
930
|
+
{ x: { type: "number" }, y: { type: "number" }, zoom: { type: "number" } },
|
|
931
|
+
[],
|
|
932
|
+
(args) => {
|
|
933
|
+
const v = adapter.getViewport();
|
|
934
|
+
const next = {
|
|
935
|
+
x: args.x !== void 0 ? num(args.x) : v.x,
|
|
936
|
+
y: args.y !== void 0 ? num(args.y) : v.y,
|
|
937
|
+
zoom: args.zoom !== void 0 ? num(args.zoom) : v.zoom
|
|
938
|
+
};
|
|
939
|
+
adapter.setViewport(next);
|
|
940
|
+
return textResult(`Viewport \u2192 ${JSON.stringify(next)}`, next);
|
|
941
|
+
},
|
|
942
|
+
wbTarget
|
|
943
|
+
);
|
|
944
|
+
reg(
|
|
945
|
+
"whiteboard_set_agent_cursor",
|
|
946
|
+
"Move the agent's presence cursor (or pass null to hide it).",
|
|
947
|
+
{
|
|
948
|
+
x: { type: "number" },
|
|
949
|
+
y: { type: "number" },
|
|
950
|
+
hide: { type: "boolean" }
|
|
951
|
+
},
|
|
952
|
+
[],
|
|
953
|
+
(args) => {
|
|
954
|
+
if (!adapter.setAgentCursor) return errorResult("Host did not provide setAgentCursor");
|
|
955
|
+
if (bool(args.hide)) {
|
|
956
|
+
adapter.setAgentCursor(null);
|
|
957
|
+
return textResult("Agent cursor hidden");
|
|
958
|
+
}
|
|
959
|
+
const cursor = {
|
|
960
|
+
userId: agent.id,
|
|
961
|
+
name: agent.name,
|
|
962
|
+
color: agent.color,
|
|
963
|
+
x: num(args.x),
|
|
964
|
+
y: num(args.y)
|
|
965
|
+
};
|
|
966
|
+
adapter.setAgentCursor(cursor);
|
|
967
|
+
return textResult(`Cursor \u2192 (${cursor.x}, ${cursor.y})`, cursor);
|
|
968
|
+
},
|
|
969
|
+
wbTarget
|
|
970
|
+
);
|
|
971
|
+
return {
|
|
972
|
+
id: "whiteboard",
|
|
973
|
+
title: "Whiteboard",
|
|
974
|
+
dispose: () => {
|
|
975
|
+
for (const d of disposers) d();
|
|
976
|
+
adapter.setAgentCursor?.(null);
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function ShareControls({
|
|
981
|
+
session,
|
|
982
|
+
onStart,
|
|
983
|
+
onStop,
|
|
984
|
+
status,
|
|
985
|
+
shareBaseUrl,
|
|
986
|
+
className,
|
|
987
|
+
style
|
|
988
|
+
}) {
|
|
989
|
+
const [tab, setTab] = react.useState("url");
|
|
990
|
+
if (!session) {
|
|
991
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["fai-share fai-share--idle", className ?? ""].filter(Boolean).join(" "), style, children: [
|
|
992
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "fai-share__start", onClick: onStart, children: "Start shared session" }),
|
|
993
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "fai-share__hint", children: "Generates a session id + secret token. Share the URL with humans, or hand the JSON config to an MCP-capable agent." })
|
|
994
|
+
] });
|
|
995
|
+
}
|
|
996
|
+
const url = buildShareUrl(session, shareBaseUrl);
|
|
997
|
+
const config = buildShareConfig(session);
|
|
998
|
+
const curl = buildCurlRecipe(session);
|
|
999
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["fai-share fai-share--active", className ?? ""].filter(Boolean).join(" "), style, children: [
|
|
1000
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fai-share__header", children: [
|
|
1001
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1002
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Sharing" }),
|
|
1003
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "fai-share__id", children: [
|
|
1004
|
+
"session ",
|
|
1005
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { children: session.id }),
|
|
1006
|
+
" \xB7 token ",
|
|
1007
|
+
/* @__PURE__ */ jsxRuntime.jsxs("code", { children: [
|
|
1008
|
+
session.display,
|
|
1009
|
+
"\u2026"
|
|
1010
|
+
] })
|
|
1011
|
+
] })
|
|
1012
|
+
] }),
|
|
1013
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fai-share__header-actions", children: [
|
|
1014
|
+
status && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fai-share__status", children: status }),
|
|
1015
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "fai-share__stop", onClick: onStop, children: "Stop" })
|
|
1016
|
+
] })
|
|
1017
|
+
] }),
|
|
1018
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fai-share__tabs", role: "tablist", children: [
|
|
1019
|
+
/* @__PURE__ */ jsxRuntime.jsx(TabButton, { tab: "url", active: tab, setTab, children: "URL" }),
|
|
1020
|
+
/* @__PURE__ */ jsxRuntime.jsx(TabButton, { tab: "json", active: tab, setTab, children: "JSON" }),
|
|
1021
|
+
/* @__PURE__ */ jsxRuntime.jsx(TabButton, { tab: "curl", active: tab, setTab, children: "cURL recipe" })
|
|
1022
|
+
] }),
|
|
1023
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fai-share__panel", children: [
|
|
1024
|
+
tab === "url" && /* @__PURE__ */ jsxRuntime.jsx(CopyBox, { label: "Open this URL in another tab to join the session", value: url }),
|
|
1025
|
+
tab === "json" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1026
|
+
CopyBox,
|
|
1027
|
+
{
|
|
1028
|
+
label: "Paste into Claude Desktop / Cline MCP server config",
|
|
1029
|
+
value: JSON.stringify(config, null, 2)
|
|
1030
|
+
}
|
|
1031
|
+
),
|
|
1032
|
+
tab === "curl" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1033
|
+
CopyBox,
|
|
1034
|
+
{
|
|
1035
|
+
label: "Connect from a terminal (verifies the relay is reachable)",
|
|
1036
|
+
value: curl,
|
|
1037
|
+
multiline: true
|
|
1038
|
+
}
|
|
1039
|
+
)
|
|
1040
|
+
] })
|
|
1041
|
+
] });
|
|
1042
|
+
}
|
|
1043
|
+
function TabButton({ tab, active, setTab, children }) {
|
|
1044
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1045
|
+
"button",
|
|
1046
|
+
{
|
|
1047
|
+
type: "button",
|
|
1048
|
+
role: "tab",
|
|
1049
|
+
"aria-selected": tab === active,
|
|
1050
|
+
className: `fai-share__tab${tab === active ? " is-active" : ""}`,
|
|
1051
|
+
onClick: () => setTab(tab),
|
|
1052
|
+
children
|
|
1053
|
+
}
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
function CopyBox({ label, value, multiline }) {
|
|
1057
|
+
const [copied, setCopied] = react.useState(false);
|
|
1058
|
+
const copy = async () => {
|
|
1059
|
+
try {
|
|
1060
|
+
await navigator.clipboard.writeText(value);
|
|
1061
|
+
setCopied(true);
|
|
1062
|
+
setTimeout(() => setCopied(false), 1200);
|
|
1063
|
+
} catch {
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1067
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fai-share__panel-label", children: label }),
|
|
1068
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fai-share__copy", children: [
|
|
1069
|
+
/* @__PURE__ */ jsxRuntime.jsx("pre", { className: `fai-share__pre${multiline ? " is-multi" : ""}`, children: value }),
|
|
1070
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "fai-share__copy-btn", onClick: copy, children: copied ? "Copied" : "Copy" })
|
|
1071
|
+
] })
|
|
1072
|
+
] });
|
|
1073
|
+
}
|
|
1074
|
+
function buildCurlRecipe(session) {
|
|
1075
|
+
const base = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.host}` : "http://localhost";
|
|
1076
|
+
const inbox = `${base}/whiteboard-share/${session.id}/inbox?token=${session.token}`;
|
|
1077
|
+
const events = `${base}/whiteboard-share/${session.id}/events?token=${session.token}`;
|
|
1078
|
+
return [
|
|
1079
|
+
`# 1) In one terminal, subscribe to server-pushed frames (SSE)`,
|
|
1080
|
+
`curl -N "${events}"`,
|
|
1081
|
+
``,
|
|
1082
|
+
`# 2) In another terminal, send an initialize handshake`,
|
|
1083
|
+
`curl -X POST "${inbox}" \\`,
|
|
1084
|
+
` -H 'content-type: application/json' \\`,
|
|
1085
|
+
` -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'`,
|
|
1086
|
+
``,
|
|
1087
|
+
`# 3) List the tools the bridge exposes`,
|
|
1088
|
+
`curl -X POST "${inbox}" \\`,
|
|
1089
|
+
` -H 'content-type: application/json' \\`,
|
|
1090
|
+
` -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'`,
|
|
1091
|
+
``,
|
|
1092
|
+
`# 4) Add a sticky note`,
|
|
1093
|
+
`curl -X POST "${inbox}" \\`,
|
|
1094
|
+
` -H 'content-type: application/json' \\`,
|
|
1095
|
+
` -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whiteboard_add_sticky","arguments":{"x":300,"y":300,"text":"hello from curl"}}}'`
|
|
1096
|
+
].join("\n");
|
|
1097
|
+
}
|
|
1098
|
+
function AgentPanel({ agent, activity, onSubmit, busy, actions, className, style }) {
|
|
1099
|
+
const scrollRef = react.useRef(null);
|
|
1100
|
+
const inputRef = react.useRef(null);
|
|
1101
|
+
react.useEffect(() => {
|
|
1102
|
+
const el = scrollRef.current;
|
|
1103
|
+
if (!el) return;
|
|
1104
|
+
el.scrollTop = el.scrollHeight;
|
|
1105
|
+
}, [activity.length]);
|
|
1106
|
+
const handleSubmit = (e) => {
|
|
1107
|
+
e.preventDefault();
|
|
1108
|
+
const value = inputRef.current?.value.trim();
|
|
1109
|
+
if (!value || !onSubmit) return;
|
|
1110
|
+
onSubmit(value);
|
|
1111
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
1112
|
+
};
|
|
1113
|
+
const color = agent?.color ?? "#a855f7";
|
|
1114
|
+
const name = agent?.name ?? "Agent";
|
|
1115
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["fai-panel", className ?? ""].filter(Boolean).join(" "), style, children: [
|
|
1116
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "fai-panel__header", children: [
|
|
1117
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1118
|
+
"div",
|
|
1119
|
+
{
|
|
1120
|
+
className: "fai-panel__avatar",
|
|
1121
|
+
style: { background: color },
|
|
1122
|
+
"aria-hidden": true,
|
|
1123
|
+
children: name.slice(0, 1)
|
|
1124
|
+
}
|
|
1125
|
+
),
|
|
1126
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fai-panel__title", children: [
|
|
1127
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: name }),
|
|
1128
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fai-panel__subtitle", children: busy ? "Working\u2026" : `${activity.length} event${activity.length === 1 ? "" : "s"}` })
|
|
1129
|
+
] }),
|
|
1130
|
+
actions && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fai-panel__actions", children: actions })
|
|
1131
|
+
] }),
|
|
1132
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { ref: scrollRef, className: "fai-panel__stream", children: activity.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "fai-panel__empty", children: "No activity yet." }) : activity.map((a) => /* @__PURE__ */ jsxRuntime.jsx(ActivityRow, { item: a }, a.id)) }),
|
|
1133
|
+
onSubmit && /* @__PURE__ */ jsxRuntime.jsxs("form", { className: "fai-panel__composer", onSubmit: handleSubmit, children: [
|
|
1134
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1135
|
+
"textarea",
|
|
1136
|
+
{
|
|
1137
|
+
ref: inputRef,
|
|
1138
|
+
className: "fai-panel__input",
|
|
1139
|
+
placeholder: busy ? "Working\u2026" : "Ask the agent\u2026",
|
|
1140
|
+
disabled: busy,
|
|
1141
|
+
rows: 2,
|
|
1142
|
+
onKeyDown: (e) => {
|
|
1143
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
1144
|
+
e.preventDefault();
|
|
1145
|
+
handleSubmit(e);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
),
|
|
1150
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: "fai-panel__send", disabled: busy, children: "Send" })
|
|
1151
|
+
] })
|
|
1152
|
+
] });
|
|
1153
|
+
}
|
|
1154
|
+
function ActivityRow({ item }) {
|
|
1155
|
+
const time = formatTime(item.at);
|
|
1156
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `fai-row fai-row--${item.kind}`, children: [
|
|
1157
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fai-row__meta", children: [
|
|
1158
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fai-row__source", children: item.source }),
|
|
1159
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fai-row__time", children: time })
|
|
1160
|
+
] }),
|
|
1161
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fai-row__text", children: item.text }),
|
|
1162
|
+
item.detail !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs("details", { className: "fai-row__detail", children: [
|
|
1163
|
+
/* @__PURE__ */ jsxRuntime.jsx("summary", { children: "details" }),
|
|
1164
|
+
/* @__PURE__ */ jsxRuntime.jsx("pre", { children: safeJson(item.detail) })
|
|
1165
|
+
] })
|
|
1166
|
+
] });
|
|
1167
|
+
}
|
|
1168
|
+
function formatTime(at) {
|
|
1169
|
+
const d = new Date(at);
|
|
1170
|
+
const hh = d.getHours().toString().padStart(2, "0");
|
|
1171
|
+
const mm = d.getMinutes().toString().padStart(2, "0");
|
|
1172
|
+
const ss = d.getSeconds().toString().padStart(2, "0");
|
|
1173
|
+
return `${hh}:${mm}:${ss}`;
|
|
1174
|
+
}
|
|
1175
|
+
function safeJson(v) {
|
|
1176
|
+
try {
|
|
1177
|
+
return JSON.stringify(v, null, 2);
|
|
1178
|
+
} catch {
|
|
1179
|
+
return String(v);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
function AgentCursor({ x, y, name, color = "#a855f7", status, className, style }) {
|
|
1183
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1184
|
+
"div",
|
|
1185
|
+
{
|
|
1186
|
+
className: ["fai-cursor", className ?? ""].filter(Boolean).join(" "),
|
|
1187
|
+
style: {
|
|
1188
|
+
position: "absolute",
|
|
1189
|
+
left: x,
|
|
1190
|
+
top: y,
|
|
1191
|
+
pointerEvents: "none",
|
|
1192
|
+
transform: "translate(-2px, -2px)",
|
|
1193
|
+
...style
|
|
1194
|
+
},
|
|
1195
|
+
children: [
|
|
1196
|
+
/* @__PURE__ */ jsxRuntime.jsx("svg", { width: "22", height: "22", viewBox: "0 0 22 22", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1197
|
+
"path",
|
|
1198
|
+
{
|
|
1199
|
+
d: "M2 2 L2 17 L7 13 L10 19 L12 18 L9 12 L15 12 Z",
|
|
1200
|
+
fill: color,
|
|
1201
|
+
stroke: "white",
|
|
1202
|
+
strokeWidth: "1.2"
|
|
1203
|
+
}
|
|
1204
|
+
) }),
|
|
1205
|
+
name && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1206
|
+
"span",
|
|
1207
|
+
{
|
|
1208
|
+
className: "fai-cursor__tag",
|
|
1209
|
+
style: { background: color },
|
|
1210
|
+
children: [
|
|
1211
|
+
name,
|
|
1212
|
+
status ? /* @__PURE__ */ jsxRuntime.jsxs("em", { className: "fai-cursor__status", children: [
|
|
1213
|
+
" \xB7 ",
|
|
1214
|
+
status
|
|
1215
|
+
] }) : null
|
|
1216
|
+
]
|
|
1217
|
+
}
|
|
1218
|
+
)
|
|
1219
|
+
]
|
|
1220
|
+
}
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
function AgentActivityHighlight({
|
|
1224
|
+
x,
|
|
1225
|
+
y,
|
|
1226
|
+
width,
|
|
1227
|
+
height,
|
|
1228
|
+
pulseKey,
|
|
1229
|
+
color = "#a855f7",
|
|
1230
|
+
duration = 1200,
|
|
1231
|
+
className,
|
|
1232
|
+
style
|
|
1233
|
+
}) {
|
|
1234
|
+
const [visible, setVisible] = react.useState(false);
|
|
1235
|
+
react.useEffect(() => {
|
|
1236
|
+
if (pulseKey === void 0) return;
|
|
1237
|
+
setVisible(true);
|
|
1238
|
+
const t = setTimeout(() => setVisible(false), duration);
|
|
1239
|
+
return () => clearTimeout(t);
|
|
1240
|
+
}, [pulseKey, duration]);
|
|
1241
|
+
if (!visible) return null;
|
|
1242
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1243
|
+
"div",
|
|
1244
|
+
{
|
|
1245
|
+
className: ["fai-highlight", className ?? ""].filter(Boolean).join(" "),
|
|
1246
|
+
style: {
|
|
1247
|
+
position: "absolute",
|
|
1248
|
+
left: x - 4,
|
|
1249
|
+
top: y - 4,
|
|
1250
|
+
width: width + 8,
|
|
1251
|
+
height: height + 8,
|
|
1252
|
+
borderRadius: 8,
|
|
1253
|
+
boxShadow: `0 0 0 2px ${color}, 0 0 16px ${color}66`,
|
|
1254
|
+
pointerEvents: "none",
|
|
1255
|
+
animation: `fai-pulse ${duration}ms ease-out forwards`,
|
|
1256
|
+
...style
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
var DEFAULT_AGENT2 = { id: "agent", name: "Agent", color: "#a855f7" };
|
|
1262
|
+
function SharedWhiteboard({
|
|
1263
|
+
initialNotes = [],
|
|
1264
|
+
initialShapes = [],
|
|
1265
|
+
initialConnectors = [],
|
|
1266
|
+
initialStrokes = [],
|
|
1267
|
+
initialViewport = { x: 0, y: 0, zoom: 1 },
|
|
1268
|
+
agent = DEFAULT_AGENT2,
|
|
1269
|
+
shareBaseUrl = "/whiteboard-share",
|
|
1270
|
+
onRegisterSession,
|
|
1271
|
+
showAgentPanel = true,
|
|
1272
|
+
showShareControls = true,
|
|
1273
|
+
broadcastEdits = true,
|
|
1274
|
+
height = 640,
|
|
1275
|
+
header,
|
|
1276
|
+
className,
|
|
1277
|
+
style
|
|
1278
|
+
}) {
|
|
1279
|
+
const [notes, setNotes] = react.useState(initialNotes);
|
|
1280
|
+
const [shapes, setShapes] = react.useState(initialShapes);
|
|
1281
|
+
const [connectors, setConnectors] = react.useState(initialConnectors);
|
|
1282
|
+
const [strokes, setStrokes] = react.useState(initialStrokes);
|
|
1283
|
+
const [viewport, setViewport] = react.useState(initialViewport);
|
|
1284
|
+
const [agentCursor, setAgentCursor] = react.useState(null);
|
|
1285
|
+
const [activity, setActivity] = react.useState([]);
|
|
1286
|
+
const [highlight, setHighlight] = react.useState(null);
|
|
1287
|
+
const stateRefs = react.useRef({ notes, shapes, connectors, strokes, viewport });
|
|
1288
|
+
react.useEffect(() => {
|
|
1289
|
+
stateRefs.current = { notes, shapes, connectors, strokes, viewport };
|
|
1290
|
+
}, [notes, shapes, connectors, strokes, viewport]);
|
|
1291
|
+
const serverRef = react.useRef(null);
|
|
1292
|
+
const inProcRef = react.useRef(null);
|
|
1293
|
+
const bridgeRef = react.useRef(null);
|
|
1294
|
+
react.useEffect(() => {
|
|
1295
|
+
const server = new MicroMcpServer({
|
|
1296
|
+
info: { name: "shared-whiteboard", version: "0.2.0" },
|
|
1297
|
+
instructions: "Collaborative whiteboard. Use whiteboard_* tools to read or modify the board."
|
|
1298
|
+
});
|
|
1299
|
+
bridgeRef.current = registerWhiteboardBridge(server, {
|
|
1300
|
+
adapter: {
|
|
1301
|
+
getNotes: () => stateRefs.current.notes,
|
|
1302
|
+
setNotes: (next) => setNotes(typeof next === "function" ? next : () => next),
|
|
1303
|
+
getShapes: () => stateRefs.current.shapes,
|
|
1304
|
+
setShapes: (next) => setShapes(typeof next === "function" ? next : () => next),
|
|
1305
|
+
getConnectors: () => stateRefs.current.connectors,
|
|
1306
|
+
setConnectors: (next) => setConnectors(typeof next === "function" ? next : () => next),
|
|
1307
|
+
getStrokes: () => stateRefs.current.strokes,
|
|
1308
|
+
setStrokes: (next) => setStrokes(typeof next === "function" ? next : () => next),
|
|
1309
|
+
getViewport: () => stateRefs.current.viewport,
|
|
1310
|
+
setViewport,
|
|
1311
|
+
setAgentCursor
|
|
1312
|
+
},
|
|
1313
|
+
agent
|
|
1314
|
+
});
|
|
1315
|
+
inProcRef.current = attachInProcess(server);
|
|
1316
|
+
serverRef.current = server;
|
|
1317
|
+
const off = inProcRef.current.onServerMessage((msg) => {
|
|
1318
|
+
if (msg?.id !== void 0 && "result" in msg && msg.result?.structuredContent?.id) {
|
|
1319
|
+
const id = msg.result.structuredContent.id;
|
|
1320
|
+
requestAnimationFrame(() => pulseFor(id));
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
return () => {
|
|
1324
|
+
off();
|
|
1325
|
+
bridgeRef.current?.dispose();
|
|
1326
|
+
bridgeRef.current = null;
|
|
1327
|
+
if (inProcRef.current) server.detach(inProcRef.current);
|
|
1328
|
+
};
|
|
1329
|
+
}, []);
|
|
1330
|
+
const pulseFor = (id) => {
|
|
1331
|
+
const n = stateRefs.current.notes.find((x) => x.id === id);
|
|
1332
|
+
if (n) return setHighlight({ pulseKey: Date.now(), bounds: { x: n.x, y: n.y, width: n.width, height: n.height } });
|
|
1333
|
+
const s = stateRefs.current.shapes.find((x) => x.id === id);
|
|
1334
|
+
if (s) return setHighlight({ pulseKey: Date.now(), bounds: { x: s.x, y: s.y, width: s.width, height: s.height } });
|
|
1335
|
+
};
|
|
1336
|
+
const log = react.useCallback((entry) => {
|
|
1337
|
+
setActivity((all) => [...all.slice(-200), { id: `a_${Date.now()}_${all.length}`, at: Date.now(), ...entry }]);
|
|
1338
|
+
}, []);
|
|
1339
|
+
const [session, setSession] = react.useState(null);
|
|
1340
|
+
const [relayState, setRelayState] = react.useState("idle");
|
|
1341
|
+
const sseRef = react.useRef(null);
|
|
1342
|
+
const logEsRef = react.useRef(null);
|
|
1343
|
+
const startShare = async () => {
|
|
1344
|
+
if (session || !serverRef.current || !shareBaseUrl) return;
|
|
1345
|
+
const desc = createSessionDescriptor();
|
|
1346
|
+
try {
|
|
1347
|
+
if (onRegisterSession) {
|
|
1348
|
+
await onRegisterSession(desc);
|
|
1349
|
+
} else {
|
|
1350
|
+
const csrf = document.querySelector('meta[name="csrf-token"]')?.content ?? "";
|
|
1351
|
+
const reg = await fetch(`${shareBaseUrl}/register`, {
|
|
1352
|
+
method: "POST",
|
|
1353
|
+
headers: { "content-type": "application/json", "x-csrf-token": csrf, accept: "application/json" },
|
|
1354
|
+
body: JSON.stringify({ session: desc.id, token: desc.token })
|
|
1355
|
+
});
|
|
1356
|
+
if (!reg.ok) throw new Error(`registration failed (HTTP ${reg.status})`);
|
|
1357
|
+
}
|
|
1358
|
+
} catch (e) {
|
|
1359
|
+
log({ kind: "error", source: "share", text: e instanceof Error ? e.message : String(e) });
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
const relay = attachSseRelay(serverRef.current, {
|
|
1363
|
+
baseUrl: shareBaseUrl,
|
|
1364
|
+
sessionId: desc.id,
|
|
1365
|
+
token: desc.token
|
|
1366
|
+
});
|
|
1367
|
+
sseRef.current = relay;
|
|
1368
|
+
relay.onStateChange(setRelayState);
|
|
1369
|
+
const es = new EventSource(`${shareBaseUrl}/${desc.id}/events?token=${desc.token}&direction=inbound`);
|
|
1370
|
+
es.addEventListener("mcp", (ev) => {
|
|
1371
|
+
try {
|
|
1372
|
+
const frame = JSON.parse(ev.data);
|
|
1373
|
+
if (frame.method === "notifications/peer_joined") {
|
|
1374
|
+
setAgentCursor((c) => c ?? { userId: agent.id, name: agent.name, color: agent.color, x: 60, y: 60 });
|
|
1375
|
+
log({ kind: "info", source: "presence", text: `${agent.name ?? "Agent"} connected` });
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
if (frame.method === "notifications/peer_left") {
|
|
1379
|
+
setAgentCursor(null);
|
|
1380
|
+
log({ kind: "info", source: "presence", text: `${agent.name ?? "Agent"} disconnected` });
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
if (frame.method === "notifications/agent_message") {
|
|
1384
|
+
log({ kind: "message", source: agent.name ?? "Agent", text: String(frame.params?.text ?? "") });
|
|
1385
|
+
} else if (frame.method === "notifications/agent_status") {
|
|
1386
|
+
log({ kind: "info", source: agent.name ?? "Agent", text: String(frame.params?.text ?? "") });
|
|
1387
|
+
} else if (frame.method?.startsWith("notifications/")) {
|
|
1388
|
+
} else {
|
|
1389
|
+
log({ kind: "tool", source: "remote", text: `\u2190 ${frame.method ?? `id:${frame.id}`}`, detail: frame });
|
|
1390
|
+
}
|
|
1391
|
+
} catch {
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
logEsRef.current = es;
|
|
1395
|
+
setSession(desc);
|
|
1396
|
+
log({ kind: "info", source: "share", text: `Sharing started \xB7 session ${desc.id}` });
|
|
1397
|
+
};
|
|
1398
|
+
const stopShare = async () => {
|
|
1399
|
+
if (!session) return;
|
|
1400
|
+
const desc = session;
|
|
1401
|
+
setSession(null);
|
|
1402
|
+
logEsRef.current?.close();
|
|
1403
|
+
logEsRef.current = null;
|
|
1404
|
+
if (sseRef.current && serverRef.current) serverRef.current.detach(sseRef.current);
|
|
1405
|
+
sseRef.current = null;
|
|
1406
|
+
setRelayState("closed");
|
|
1407
|
+
if (shareBaseUrl) {
|
|
1408
|
+
const csrf = document.querySelector('meta[name="csrf-token"]')?.content ?? "";
|
|
1409
|
+
await fetch(`${shareBaseUrl}/${desc.id}/unregister?token=${encodeURIComponent(desc.token)}`, {
|
|
1410
|
+
method: "POST",
|
|
1411
|
+
headers: { "x-csrf-token": csrf, accept: "application/json" }
|
|
1412
|
+
}).catch(() => {
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
log({ kind: "info", source: "share", text: "Sharing stopped." });
|
|
1416
|
+
};
|
|
1417
|
+
const lastBroadcastRef = react.useRef(0);
|
|
1418
|
+
react.useEffect(() => {
|
|
1419
|
+
if (!broadcastEdits || !sseRef.current || !session) return;
|
|
1420
|
+
const now = Date.now();
|
|
1421
|
+
if (now - lastBroadcastRef.current < 80) return;
|
|
1422
|
+
lastBroadcastRef.current = now;
|
|
1423
|
+
sseRef.current.send({
|
|
1424
|
+
jsonrpc: "2.0",
|
|
1425
|
+
method: "notifications/state_update",
|
|
1426
|
+
params: { notes, shapes, connectors, viewport, ts: now }
|
|
1427
|
+
});
|
|
1428
|
+
}, [notes, shapes, connectors, viewport, session, broadcastEdits]);
|
|
1429
|
+
const handleSubmit = (text) => {
|
|
1430
|
+
if (!sseRef.current) {
|
|
1431
|
+
log({ kind: "error", source: "you", text: "Start a shared session first." });
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
sseRef.current.send({
|
|
1435
|
+
jsonrpc: "2.0",
|
|
1436
|
+
method: "notifications/user_message",
|
|
1437
|
+
params: { text, ts: Date.now() }
|
|
1438
|
+
});
|
|
1439
|
+
log({ kind: "message", source: "You", text });
|
|
1440
|
+
};
|
|
1441
|
+
const cursors = react.useMemo(() => [], []);
|
|
1442
|
+
const statusText = (() => {
|
|
1443
|
+
switch (relayState) {
|
|
1444
|
+
case "open":
|
|
1445
|
+
return "live";
|
|
1446
|
+
case "connecting":
|
|
1447
|
+
return "connecting\u2026";
|
|
1448
|
+
case "error":
|
|
1449
|
+
return "error";
|
|
1450
|
+
case "closed":
|
|
1451
|
+
return "closed";
|
|
1452
|
+
default:
|
|
1453
|
+
return void 0;
|
|
1454
|
+
}
|
|
1455
|
+
})();
|
|
1456
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["fai-shared-whiteboard", className ?? ""].filter(Boolean).join(" "), style, children: [
|
|
1457
|
+
header,
|
|
1458
|
+
showShareControls && shareBaseUrl !== null && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fai-shared-whiteboard__controls", children: /* @__PURE__ */ jsxRuntime.jsx(ShareControls, { session, onStart: startShare, onStop: stopShare, status: statusText }) }),
|
|
1459
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1460
|
+
"div",
|
|
1461
|
+
{
|
|
1462
|
+
className: "fai-shared-whiteboard__layout",
|
|
1463
|
+
style: {
|
|
1464
|
+
display: "grid",
|
|
1465
|
+
gap: 16,
|
|
1466
|
+
gridTemplateColumns: showAgentPanel ? "1fr 360px" : "1fr"
|
|
1467
|
+
},
|
|
1468
|
+
children: [
|
|
1469
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1470
|
+
"div",
|
|
1471
|
+
{
|
|
1472
|
+
className: "fai-shared-whiteboard__board",
|
|
1473
|
+
style: {
|
|
1474
|
+
position: "relative",
|
|
1475
|
+
overflow: "hidden",
|
|
1476
|
+
borderRadius: 12,
|
|
1477
|
+
border: "1px solid #e4e4e7",
|
|
1478
|
+
background: "radial-gradient(circle at 1px 1px, rgba(0,0,0,0.07) 1px, transparent 0)",
|
|
1479
|
+
backgroundSize: "20px 20px",
|
|
1480
|
+
height
|
|
1481
|
+
},
|
|
1482
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(fancyWhiteboard.Board, { viewport, onViewportChange: setViewport, style: { width: "100%", height: "100%" }, children: [
|
|
1483
|
+
connectors.map((c) => {
|
|
1484
|
+
const a = resolveCenter(c.from, notes, shapes);
|
|
1485
|
+
const b = resolveCenter(c.to, notes, shapes);
|
|
1486
|
+
if (!a || !b) return null;
|
|
1487
|
+
return /* @__PURE__ */ jsxRuntime.jsx(fancyWhiteboard.Connector, { from: a, to: b, color: c.color ?? "#64748b" }, c.id);
|
|
1488
|
+
}),
|
|
1489
|
+
shapes.map((s) => /* @__PURE__ */ jsxRuntime.jsx(fancyWhiteboard.Shape, { item: s, onChange: (next) => setShapes((all) => all.map((x) => x.id === next.id ? next : x)) }, s.id)),
|
|
1490
|
+
notes.map((n) => /* @__PURE__ */ jsxRuntime.jsx(fancyWhiteboard.StickyNote, { item: n, onChange: (next) => setNotes((all) => all.map((x) => x.id === next.id ? next : x)) }, n.id)),
|
|
1491
|
+
/* @__PURE__ */ jsxRuntime.jsx(fancyWhiteboard.CursorLayer, { cursors }),
|
|
1492
|
+
agentCursor && /* @__PURE__ */ jsxRuntime.jsx(AgentCursor, { x: agentCursor.x, y: agentCursor.y, name: agentCursor.name, color: agentCursor.color }),
|
|
1493
|
+
highlight && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1494
|
+
AgentActivityHighlight,
|
|
1495
|
+
{
|
|
1496
|
+
x: highlight.bounds.x,
|
|
1497
|
+
y: highlight.bounds.y,
|
|
1498
|
+
width: highlight.bounds.width,
|
|
1499
|
+
height: highlight.bounds.height,
|
|
1500
|
+
color: agent.color ?? "#a855f7",
|
|
1501
|
+
pulseKey: highlight.pulseKey
|
|
1502
|
+
}
|
|
1503
|
+
)
|
|
1504
|
+
] })
|
|
1505
|
+
}
|
|
1506
|
+
),
|
|
1507
|
+
showAgentPanel && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1508
|
+
AgentPanel,
|
|
1509
|
+
{
|
|
1510
|
+
agent,
|
|
1511
|
+
activity,
|
|
1512
|
+
onSubmit: handleSubmit
|
|
1513
|
+
}
|
|
1514
|
+
) })
|
|
1515
|
+
]
|
|
1516
|
+
}
|
|
1517
|
+
)
|
|
1518
|
+
] });
|
|
1519
|
+
}
|
|
1520
|
+
function resolveCenter(ref, notes, shapes) {
|
|
1521
|
+
if (typeof ref === "string") {
|
|
1522
|
+
const n = notes.find((x) => x.id === ref);
|
|
1523
|
+
if (n) return { x: n.x + n.width / 2, y: n.y + n.height / 2 };
|
|
1524
|
+
const s = shapes.find((x) => x.id === ref);
|
|
1525
|
+
if (s) return { x: s.x + s.width / 2, y: s.y + s.height / 2 };
|
|
1526
|
+
return null;
|
|
1527
|
+
}
|
|
1528
|
+
return ref;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
exports.SharedWhiteboard = SharedWhiteboard;
|
|
1532
|
+
//# sourceMappingURL=components-shared-whiteboard.cjs.map
|
|
1533
|
+
//# sourceMappingURL=components-shared-whiteboard.cjs.map
|