@particle-academy/agent-integrations 0.3.4 → 0.5.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.
Files changed (130) hide show
  1. package/README.md +65 -0
  2. package/dist/bridges/charts.d.cts +4 -4
  3. package/dist/bridges/charts.d.ts +4 -4
  4. package/dist/bridges/code.d.cts +4 -4
  5. package/dist/bridges/code.d.ts +4 -4
  6. package/dist/bridges/flow.d.cts +4 -4
  7. package/dist/bridges/flow.d.ts +4 -4
  8. package/dist/bridges/forms.d.cts +4 -4
  9. package/dist/bridges/forms.d.ts +4 -4
  10. package/dist/bridges/scene.d.cts +4 -4
  11. package/dist/bridges/scene.d.ts +4 -4
  12. package/dist/bridges/screens.d.cts +78 -0
  13. package/dist/bridges/screens.d.ts +78 -0
  14. package/dist/bridges/sheets.d.cts +4 -4
  15. package/dist/bridges/sheets.d.ts +4 -4
  16. package/dist/bridges/whiteboard.d.cts +4 -4
  17. package/dist/bridges/whiteboard.d.ts +4 -4
  18. package/dist/bridges-charts.cjs +2 -2
  19. package/dist/bridges-charts.cjs.map +1 -1
  20. package/dist/bridges-charts.js +2 -2
  21. package/dist/bridges-code.cjs +2 -2
  22. package/dist/bridges-code.cjs.map +1 -1
  23. package/dist/bridges-code.js +2 -2
  24. package/dist/bridges-flow.cjs +2 -2
  25. package/dist/bridges-flow.cjs.map +1 -1
  26. package/dist/bridges-flow.js +340 -3
  27. package/dist/bridges-flow.js.map +1 -1
  28. package/dist/bridges-forms.cjs +2 -2
  29. package/dist/bridges-forms.cjs.map +1 -1
  30. package/dist/bridges-forms.js +2 -2
  31. package/dist/bridges-scene.cjs +2 -2
  32. package/dist/bridges-scene.cjs.map +1 -1
  33. package/dist/bridges-scene.js +2 -2
  34. package/dist/bridges-screens.cjs +227 -0
  35. package/dist/bridges-screens.cjs.map +1 -0
  36. package/dist/bridges-screens.js +6 -0
  37. package/dist/bridges-screens.js.map +1 -0
  38. package/dist/bridges-sheets.cjs +2 -2
  39. package/dist/bridges-sheets.cjs.map +1 -1
  40. package/dist/bridges-sheets.js +2 -2
  41. package/dist/bridges-whiteboard.cjs +12 -12
  42. package/dist/bridges-whiteboard.cjs.map +1 -1
  43. package/dist/bridges-whiteboard.js +3 -3
  44. package/dist/{chunk-TBEITXF4.js → chunk-3KSZNGNW.js} +7 -7
  45. package/dist/chunk-3KSZNGNW.js.map +1 -0
  46. package/dist/{chunk-OEIULP2L.js → chunk-4BL5M3U3.js} +5 -5
  47. package/dist/chunk-4BL5M3U3.js.map +1 -0
  48. package/dist/{chunk-QGCF7YKW.js → chunk-4KAIV6OD.js} +40 -12
  49. package/dist/chunk-4KAIV6OD.js.map +1 -0
  50. package/dist/chunk-57ZDHD53.js +180 -0
  51. package/dist/chunk-57ZDHD53.js.map +1 -0
  52. package/dist/chunk-5XELJIJR.js +83 -0
  53. package/dist/chunk-5XELJIJR.js.map +1 -0
  54. package/dist/{chunk-6LTKCNLF.js → chunk-AFUULW5E.js} +3 -34
  55. package/dist/chunk-AFUULW5E.js.map +1 -0
  56. package/dist/chunk-G6N2TQVO.js +34 -0
  57. package/dist/chunk-G6N2TQVO.js.map +1 -0
  58. package/dist/{chunk-ACBENYYO.js → chunk-GQ7XXK7G.js} +12 -12
  59. package/dist/chunk-GQ7XXK7G.js.map +1 -0
  60. package/dist/{chunk-XYYSTJHW.js → chunk-HSTW7ZNO.js} +5 -5
  61. package/dist/chunk-HSTW7ZNO.js.map +1 -0
  62. package/dist/{chunk-PDBF4W7E.js → chunk-IANI25IT.js} +5 -5
  63. package/dist/chunk-IANI25IT.js.map +1 -0
  64. package/dist/chunk-IJ6JX5VC.js +3 -0
  65. package/dist/chunk-IJ6JX5VC.js.map +1 -0
  66. package/dist/{chunk-JMYPUAFH.js → chunk-LVQXIUJH.js} +2 -2
  67. package/dist/{chunk-JMYPUAFH.js.map → chunk-LVQXIUJH.js.map} +1 -1
  68. package/dist/{chunk-PHPXKSWI.js → chunk-NTDZWGYB.js} +5 -5
  69. package/dist/chunk-NTDZWGYB.js.map +1 -0
  70. package/dist/{chunk-DJOWMF6Q.js → chunk-RGO42EQ6.js} +3 -3
  71. package/dist/{chunk-DJOWMF6Q.js.map → chunk-RGO42EQ6.js.map} +1 -1
  72. package/dist/{chunk-QJUTISFC.js → chunk-XRAJSOPS.js} +5 -5
  73. package/dist/chunk-XRAJSOPS.js.map +1 -0
  74. package/dist/chunk-ZHAK2DQR.js +289 -0
  75. package/dist/chunk-ZHAK2DQR.js.map +1 -0
  76. package/dist/components/SharedWhiteboard/index.d.cts +55 -0
  77. package/dist/components/SharedWhiteboard/index.d.ts +55 -0
  78. package/dist/components-shared-whiteboard.cjs +1533 -0
  79. package/dist/components-shared-whiteboard.cjs.map +1 -0
  80. package/dist/components-shared-whiteboard.js +285 -0
  81. package/dist/components-shared-whiteboard.js.map +1 -0
  82. package/dist/index.cjs +618 -1371
  83. package/dist/index.cjs.map +1 -1
  84. package/dist/index.d.cts +11 -59
  85. package/dist/index.d.ts +11 -59
  86. package/dist/index.js +19 -571
  87. package/dist/index.js.map +1 -1
  88. package/dist/mcp/index.d.cts +5 -4
  89. package/dist/mcp/index.d.ts +5 -4
  90. package/dist/mcp.cjs +37 -9
  91. package/dist/mcp.cjs.map +1 -1
  92. package/dist/mcp.js +3 -2
  93. package/dist/presence/index.d.cts +1 -1
  94. package/dist/presence/index.d.ts +1 -1
  95. package/dist/{server-BJu_AMH3.d.ts → server-BsSwfemr.d.cts} +4 -5
  96. package/dist/{server-si-VvFxI.d.cts → server-Du3-IGqM.d.ts} +4 -5
  97. package/dist/sharing/index.d.cts +5 -36
  98. package/dist/sharing/index.d.ts +5 -36
  99. package/dist/sharing.js +2 -1
  100. package/dist/sheets-adapter.cjs +96 -0
  101. package/dist/sheets-adapter.cjs.map +1 -0
  102. package/dist/sheets-adapter.d.cts +119 -0
  103. package/dist/sheets-adapter.d.ts +119 -0
  104. package/dist/sheets-adapter.js +4 -0
  105. package/dist/sheets-adapter.js.map +1 -0
  106. package/dist/token-CrJF76oH.d.cts +34 -0
  107. package/dist/token-CrJF76oH.d.ts +34 -0
  108. package/dist/tool-host-BQuUygLF.d.cts +60 -0
  109. package/dist/tool-host-C8JMMGYq.d.ts +60 -0
  110. package/dist/{types-DXKpLuia.d.ts → types-CCSBGW9T.d.cts} +2 -2
  111. package/dist/{types-Bf1ZoGmI.d.cts → types-DIVNcIQO.d.ts} +2 -2
  112. package/dist/{types-DksGd5Y7.d.cts → types-aOQLTW0E.d.cts} +1 -1
  113. package/dist/{types-DksGd5Y7.d.ts → types-aOQLTW0E.d.ts} +1 -1
  114. package/dist/undo/index.d.cts +4 -4
  115. package/dist/undo/index.d.ts +4 -4
  116. package/dist/undo.cjs +9 -9
  117. package/dist/undo.cjs.map +1 -1
  118. package/dist/undo.js +3 -3
  119. package/package.json +57 -7
  120. package/dist/chunk-4IAVAFUV.js +0 -342
  121. package/dist/chunk-4IAVAFUV.js.map +0 -1
  122. package/dist/chunk-6LTKCNLF.js.map +0 -1
  123. package/dist/chunk-ACBENYYO.js.map +0 -1
  124. package/dist/chunk-OEIULP2L.js.map +0 -1
  125. package/dist/chunk-PDBF4W7E.js.map +0 -1
  126. package/dist/chunk-PHPXKSWI.js.map +0 -1
  127. package/dist/chunk-QGCF7YKW.js.map +0 -1
  128. package/dist/chunk-QJUTISFC.js.map +0 -1
  129. package/dist/chunk-TBEITXF4.js.map +0 -1
  130. package/dist/chunk-XYYSTJHW.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