@particle-academy/agent-integrations 0.2.4

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 (49) hide show
  1. package/README.md +131 -0
  2. package/dist/bridges/flow.d.cts +72 -0
  3. package/dist/bridges/flow.d.ts +72 -0
  4. package/dist/bridges/whiteboard.d.cts +40 -0
  5. package/dist/bridges/whiteboard.d.ts +40 -0
  6. package/dist/bridges-flow.cjs +330 -0
  7. package/dist/bridges-flow.cjs.map +1 -0
  8. package/dist/bridges-flow.js +4 -0
  9. package/dist/bridges-flow.js.map +1 -0
  10. package/dist/bridges-whiteboard.cjs +409 -0
  11. package/dist/bridges-whiteboard.cjs.map +1 -0
  12. package/dist/bridges-whiteboard.js +4 -0
  13. package/dist/bridges-whiteboard.js.map +1 -0
  14. package/dist/chunk-2VOQJKSU.js +320 -0
  15. package/dist/chunk-2VOQJKSU.js.map +1 -0
  16. package/dist/chunk-5ZUHNNLR.js +398 -0
  17. package/dist/chunk-5ZUHNNLR.js.map +1 -0
  18. package/dist/chunk-6LTKCNLF.js +68 -0
  19. package/dist/chunk-6LTKCNLF.js.map +1 -0
  20. package/dist/chunk-FLEOQUKF.js +157 -0
  21. package/dist/chunk-FLEOQUKF.js.map +1 -0
  22. package/dist/chunk-QGCF7YKW.js +130 -0
  23. package/dist/chunk-QGCF7YKW.js.map +1 -0
  24. package/dist/index.cjs +1632 -0
  25. package/dist/index.cjs.map +1 -0
  26. package/dist/index.d.cts +155 -0
  27. package/dist/index.d.ts +155 -0
  28. package/dist/index.js +567 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/mcp/index.d.cts +73 -0
  31. package/dist/mcp/index.d.ts +73 -0
  32. package/dist/mcp.cjs +210 -0
  33. package/dist/mcp.cjs.map +1 -0
  34. package/dist/mcp.js +4 -0
  35. package/dist/mcp.js.map +1 -0
  36. package/dist/server-Bv985us3.d.cts +173 -0
  37. package/dist/server-Bv985us3.d.ts +173 -0
  38. package/dist/sharing/index.d.cts +89 -0
  39. package/dist/sharing/index.d.ts +89 -0
  40. package/dist/sharing.cjs +166 -0
  41. package/dist/sharing.cjs.map +1 -0
  42. package/dist/sharing.js +3 -0
  43. package/dist/sharing.js.map +1 -0
  44. package/dist/styles.css +331 -0
  45. package/dist/styles.css.map +1 -0
  46. package/dist/types-CRPA_D0z.d.ts +18 -0
  47. package/dist/types-DR5AS6Rd.d.cts +18 -0
  48. package/docs/relay-protocol.md +57 -0
  49. package/package.json +61 -0
package/dist/index.js ADDED
@@ -0,0 +1,567 @@
1
+ import { attachInProcess } from './chunk-6LTKCNLF.js';
2
+ export { InProcessTransport, RelayTransport, attachInProcess, attachRelay } from './chunk-6LTKCNLF.js';
3
+ import { registerWhiteboardBridge } from './chunk-5ZUHNNLR.js';
4
+ export { registerWhiteboardBridge } from './chunk-5ZUHNNLR.js';
5
+ export { registerFlowBridge } from './chunk-2VOQJKSU.js';
6
+ import { MicroMcpServer } from './chunk-QGCF7YKW.js';
7
+ export { MCP_PROTOCOL_VERSION, MicroMcpServer, errorResult, rpcError, textResult } from './chunk-QGCF7YKW.js';
8
+ import { buildShareUrl, buildShareConfig, createSessionDescriptor, attachSseRelay } from './chunk-FLEOQUKF.js';
9
+ export { SseRelayTransport, attachSseRelay, buildShareConfig, buildShareUrl, createSessionDescriptor, describeSession, readSessionFromUrl } from './chunk-FLEOQUKF.js';
10
+ import { useRef, useEffect, useState, useCallback, useMemo } from 'react';
11
+ import { jsxs, jsx } from 'react/jsx-runtime';
12
+ import { Board, Connector, Shape, StickyNote, CursorLayer } from '@particle-academy/fancy-whiteboard';
13
+
14
+ function AgentPanel({ agent, activity, onSubmit, busy, actions, className, style }) {
15
+ const scrollRef = useRef(null);
16
+ const inputRef = useRef(null);
17
+ useEffect(() => {
18
+ const el = scrollRef.current;
19
+ if (!el) return;
20
+ el.scrollTop = el.scrollHeight;
21
+ }, [activity.length]);
22
+ const handleSubmit = (e) => {
23
+ e.preventDefault();
24
+ const value = inputRef.current?.value.trim();
25
+ if (!value || !onSubmit) return;
26
+ onSubmit(value);
27
+ if (inputRef.current) inputRef.current.value = "";
28
+ };
29
+ const color = agent?.color ?? "#a855f7";
30
+ const name = agent?.name ?? "Agent";
31
+ return /* @__PURE__ */ jsxs("div", { className: ["fai-panel", className ?? ""].filter(Boolean).join(" "), style, children: [
32
+ /* @__PURE__ */ jsxs("header", { className: "fai-panel__header", children: [
33
+ /* @__PURE__ */ jsx(
34
+ "div",
35
+ {
36
+ className: "fai-panel__avatar",
37
+ style: { background: color },
38
+ "aria-hidden": true,
39
+ children: name.slice(0, 1)
40
+ }
41
+ ),
42
+ /* @__PURE__ */ jsxs("div", { className: "fai-panel__title", children: [
43
+ /* @__PURE__ */ jsx("strong", { children: name }),
44
+ /* @__PURE__ */ jsx("span", { className: "fai-panel__subtitle", children: busy ? "Working\u2026" : `${activity.length} event${activity.length === 1 ? "" : "s"}` })
45
+ ] }),
46
+ actions && /* @__PURE__ */ jsx("div", { className: "fai-panel__actions", children: actions })
47
+ ] }),
48
+ /* @__PURE__ */ jsx("div", { ref: scrollRef, className: "fai-panel__stream", children: activity.length === 0 ? /* @__PURE__ */ jsx("p", { className: "fai-panel__empty", children: "No activity yet." }) : activity.map((a) => /* @__PURE__ */ jsx(ActivityRow, { item: a }, a.id)) }),
49
+ onSubmit && /* @__PURE__ */ jsxs("form", { className: "fai-panel__composer", onSubmit: handleSubmit, children: [
50
+ /* @__PURE__ */ jsx(
51
+ "textarea",
52
+ {
53
+ ref: inputRef,
54
+ className: "fai-panel__input",
55
+ placeholder: busy ? "Working\u2026" : "Ask the agent\u2026",
56
+ disabled: busy,
57
+ rows: 2,
58
+ onKeyDown: (e) => {
59
+ if (e.key === "Enter" && !e.shiftKey) {
60
+ e.preventDefault();
61
+ handleSubmit(e);
62
+ }
63
+ }
64
+ }
65
+ ),
66
+ /* @__PURE__ */ jsx("button", { type: "submit", className: "fai-panel__send", disabled: busy, children: "Send" })
67
+ ] })
68
+ ] });
69
+ }
70
+ function ActivityRow({ item }) {
71
+ const time = formatTime(item.at);
72
+ return /* @__PURE__ */ jsxs("div", { className: `fai-row fai-row--${item.kind}`, children: [
73
+ /* @__PURE__ */ jsxs("div", { className: "fai-row__meta", children: [
74
+ /* @__PURE__ */ jsx("span", { className: "fai-row__source", children: item.source }),
75
+ /* @__PURE__ */ jsx("span", { className: "fai-row__time", children: time })
76
+ ] }),
77
+ /* @__PURE__ */ jsx("div", { className: "fai-row__text", children: item.text }),
78
+ item.detail !== void 0 && /* @__PURE__ */ jsxs("details", { className: "fai-row__detail", children: [
79
+ /* @__PURE__ */ jsx("summary", { children: "details" }),
80
+ /* @__PURE__ */ jsx("pre", { children: safeJson(item.detail) })
81
+ ] })
82
+ ] });
83
+ }
84
+ function formatTime(at) {
85
+ const d = new Date(at);
86
+ const hh = d.getHours().toString().padStart(2, "0");
87
+ const mm = d.getMinutes().toString().padStart(2, "0");
88
+ const ss = d.getSeconds().toString().padStart(2, "0");
89
+ return `${hh}:${mm}:${ss}`;
90
+ }
91
+ function safeJson(v) {
92
+ try {
93
+ return JSON.stringify(v, null, 2);
94
+ } catch {
95
+ return String(v);
96
+ }
97
+ }
98
+ function AgentCursor({ x, y, name, color = "#a855f7", status, className, style }) {
99
+ return /* @__PURE__ */ jsxs(
100
+ "div",
101
+ {
102
+ className: ["fai-cursor", className ?? ""].filter(Boolean).join(" "),
103
+ style: {
104
+ position: "absolute",
105
+ left: x,
106
+ top: y,
107
+ pointerEvents: "none",
108
+ transform: "translate(-2px, -2px)",
109
+ ...style
110
+ },
111
+ children: [
112
+ /* @__PURE__ */ jsx("svg", { width: "22", height: "22", viewBox: "0 0 22 22", "aria-hidden": true, children: /* @__PURE__ */ jsx(
113
+ "path",
114
+ {
115
+ d: "M2 2 L2 17 L7 13 L10 19 L12 18 L9 12 L15 12 Z",
116
+ fill: color,
117
+ stroke: "white",
118
+ strokeWidth: "1.2"
119
+ }
120
+ ) }),
121
+ name && /* @__PURE__ */ jsxs(
122
+ "span",
123
+ {
124
+ className: "fai-cursor__tag",
125
+ style: { background: color },
126
+ children: [
127
+ name,
128
+ status ? /* @__PURE__ */ jsxs("em", { className: "fai-cursor__status", children: [
129
+ " \xB7 ",
130
+ status
131
+ ] }) : null
132
+ ]
133
+ }
134
+ )
135
+ ]
136
+ }
137
+ );
138
+ }
139
+ function AgentActivityHighlight({
140
+ x,
141
+ y,
142
+ width,
143
+ height,
144
+ pulseKey,
145
+ color = "#a855f7",
146
+ duration = 1200,
147
+ className,
148
+ style
149
+ }) {
150
+ const [visible, setVisible] = useState(false);
151
+ useEffect(() => {
152
+ if (pulseKey === void 0) return;
153
+ setVisible(true);
154
+ const t = setTimeout(() => setVisible(false), duration);
155
+ return () => clearTimeout(t);
156
+ }, [pulseKey, duration]);
157
+ if (!visible) return null;
158
+ return /* @__PURE__ */ jsx(
159
+ "div",
160
+ {
161
+ className: ["fai-highlight", className ?? ""].filter(Boolean).join(" "),
162
+ style: {
163
+ position: "absolute",
164
+ left: x - 4,
165
+ top: y - 4,
166
+ width: width + 8,
167
+ height: height + 8,
168
+ borderRadius: 8,
169
+ boxShadow: `0 0 0 2px ${color}, 0 0 16px ${color}66`,
170
+ pointerEvents: "none",
171
+ animation: `fai-pulse ${duration}ms ease-out forwards`,
172
+ ...style
173
+ }
174
+ }
175
+ );
176
+ }
177
+ function ShareControls({
178
+ session,
179
+ onStart,
180
+ onStop,
181
+ status,
182
+ shareBaseUrl,
183
+ className,
184
+ style
185
+ }) {
186
+ const [tab, setTab] = useState("url");
187
+ if (!session) {
188
+ return /* @__PURE__ */ jsxs("div", { className: ["fai-share fai-share--idle", className ?? ""].filter(Boolean).join(" "), style, children: [
189
+ /* @__PURE__ */ jsx("button", { type: "button", className: "fai-share__start", onClick: onStart, children: "Start shared session" }),
190
+ /* @__PURE__ */ 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." })
191
+ ] });
192
+ }
193
+ const url = buildShareUrl(session, shareBaseUrl);
194
+ const config = buildShareConfig(session);
195
+ const curl = buildCurlRecipe(session);
196
+ return /* @__PURE__ */ jsxs("div", { className: ["fai-share fai-share--active", className ?? ""].filter(Boolean).join(" "), style, children: [
197
+ /* @__PURE__ */ jsxs("div", { className: "fai-share__header", children: [
198
+ /* @__PURE__ */ jsxs("div", { children: [
199
+ /* @__PURE__ */ jsx("strong", { children: "Sharing" }),
200
+ /* @__PURE__ */ jsxs("span", { className: "fai-share__id", children: [
201
+ "session ",
202
+ /* @__PURE__ */ jsx("code", { children: session.id }),
203
+ " \xB7 token ",
204
+ /* @__PURE__ */ jsxs("code", { children: [
205
+ session.display,
206
+ "\u2026"
207
+ ] })
208
+ ] })
209
+ ] }),
210
+ /* @__PURE__ */ jsxs("div", { className: "fai-share__header-actions", children: [
211
+ status && /* @__PURE__ */ jsx("span", { className: "fai-share__status", children: status }),
212
+ /* @__PURE__ */ jsx("button", { type: "button", className: "fai-share__stop", onClick: onStop, children: "Stop" })
213
+ ] })
214
+ ] }),
215
+ /* @__PURE__ */ jsxs("div", { className: "fai-share__tabs", role: "tablist", children: [
216
+ /* @__PURE__ */ jsx(TabButton, { tab: "url", active: tab, setTab, children: "URL" }),
217
+ /* @__PURE__ */ jsx(TabButton, { tab: "json", active: tab, setTab, children: "JSON" }),
218
+ /* @__PURE__ */ jsx(TabButton, { tab: "curl", active: tab, setTab, children: "cURL recipe" })
219
+ ] }),
220
+ /* @__PURE__ */ jsxs("div", { className: "fai-share__panel", children: [
221
+ tab === "url" && /* @__PURE__ */ jsx(CopyBox, { label: "Open this URL in another tab to join the session", value: url }),
222
+ tab === "json" && /* @__PURE__ */ jsx(
223
+ CopyBox,
224
+ {
225
+ label: "Paste into Claude Desktop / Cline MCP server config",
226
+ value: JSON.stringify(config, null, 2)
227
+ }
228
+ ),
229
+ tab === "curl" && /* @__PURE__ */ jsx(
230
+ CopyBox,
231
+ {
232
+ label: "Connect from a terminal (verifies the relay is reachable)",
233
+ value: curl,
234
+ multiline: true
235
+ }
236
+ )
237
+ ] })
238
+ ] });
239
+ }
240
+ function TabButton({ tab, active, setTab, children }) {
241
+ return /* @__PURE__ */ jsx(
242
+ "button",
243
+ {
244
+ type: "button",
245
+ role: "tab",
246
+ "aria-selected": tab === active,
247
+ className: `fai-share__tab${tab === active ? " is-active" : ""}`,
248
+ onClick: () => setTab(tab),
249
+ children
250
+ }
251
+ );
252
+ }
253
+ function CopyBox({ label, value, multiline }) {
254
+ const [copied, setCopied] = useState(false);
255
+ const copy = async () => {
256
+ try {
257
+ await navigator.clipboard.writeText(value);
258
+ setCopied(true);
259
+ setTimeout(() => setCopied(false), 1200);
260
+ } catch {
261
+ }
262
+ };
263
+ return /* @__PURE__ */ jsxs("div", { children: [
264
+ /* @__PURE__ */ jsx("div", { className: "fai-share__panel-label", children: label }),
265
+ /* @__PURE__ */ jsxs("div", { className: "fai-share__copy", children: [
266
+ /* @__PURE__ */ jsx("pre", { className: `fai-share__pre${multiline ? " is-multi" : ""}`, children: value }),
267
+ /* @__PURE__ */ jsx("button", { type: "button", className: "fai-share__copy-btn", onClick: copy, children: copied ? "Copied" : "Copy" })
268
+ ] })
269
+ ] });
270
+ }
271
+ function buildCurlRecipe(session) {
272
+ const base = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.host}` : "http://localhost";
273
+ const inbox = `${base}/whiteboard-share/${session.id}/inbox?token=${session.token}`;
274
+ const events = `${base}/whiteboard-share/${session.id}/events?token=${session.token}`;
275
+ return [
276
+ `# 1) In one terminal, subscribe to server-pushed frames (SSE)`,
277
+ `curl -N "${events}"`,
278
+ ``,
279
+ `# 2) In another terminal, send an initialize handshake`,
280
+ `curl -X POST "${inbox}" \\`,
281
+ ` -H 'content-type: application/json' \\`,
282
+ ` -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'`,
283
+ ``,
284
+ `# 3) List the tools the bridge exposes`,
285
+ `curl -X POST "${inbox}" \\`,
286
+ ` -H 'content-type: application/json' \\`,
287
+ ` -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'`,
288
+ ``,
289
+ `# 4) Add a sticky note`,
290
+ `curl -X POST "${inbox}" \\`,
291
+ ` -H 'content-type: application/json' \\`,
292
+ ` -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whiteboard_add_sticky","arguments":{"x":300,"y":300,"text":"hello from curl"}}}'`
293
+ ].join("\n");
294
+ }
295
+ var DEFAULT_AGENT = { id: "agent", name: "Agent", color: "#a855f7" };
296
+ function SharedWhiteboard({
297
+ initialNotes = [],
298
+ initialShapes = [],
299
+ initialConnectors = [],
300
+ initialStrokes = [],
301
+ initialViewport = { x: 0, y: 0, zoom: 1 },
302
+ agent = DEFAULT_AGENT,
303
+ shareBaseUrl = "/whiteboard-share",
304
+ onRegisterSession,
305
+ showAgentPanel = true,
306
+ showShareControls = true,
307
+ broadcastEdits = true,
308
+ height = 640,
309
+ header,
310
+ className,
311
+ style
312
+ }) {
313
+ const [notes, setNotes] = useState(initialNotes);
314
+ const [shapes, setShapes] = useState(initialShapes);
315
+ const [connectors, setConnectors] = useState(initialConnectors);
316
+ const [strokes, setStrokes] = useState(initialStrokes);
317
+ const [viewport, setViewport] = useState(initialViewport);
318
+ const [agentCursor, setAgentCursor] = useState(null);
319
+ const [activity, setActivity] = useState([]);
320
+ const [highlight, setHighlight] = useState(null);
321
+ const stateRefs = useRef({ notes, shapes, connectors, strokes, viewport });
322
+ useEffect(() => {
323
+ stateRefs.current = { notes, shapes, connectors, strokes, viewport };
324
+ }, [notes, shapes, connectors, strokes, viewport]);
325
+ const serverRef = useRef(null);
326
+ const inProcRef = useRef(null);
327
+ const bridgeRef = useRef(null);
328
+ useEffect(() => {
329
+ const server = new MicroMcpServer({
330
+ info: { name: "shared-whiteboard", version: "0.2.0" },
331
+ instructions: "Collaborative whiteboard. Use whiteboard_* tools to read or modify the board."
332
+ });
333
+ bridgeRef.current = registerWhiteboardBridge(server, {
334
+ adapter: {
335
+ getNotes: () => stateRefs.current.notes,
336
+ setNotes: (next) => setNotes(typeof next === "function" ? next : () => next),
337
+ getShapes: () => stateRefs.current.shapes,
338
+ setShapes: (next) => setShapes(typeof next === "function" ? next : () => next),
339
+ getConnectors: () => stateRefs.current.connectors,
340
+ setConnectors: (next) => setConnectors(typeof next === "function" ? next : () => next),
341
+ getStrokes: () => stateRefs.current.strokes,
342
+ setStrokes: (next) => setStrokes(typeof next === "function" ? next : () => next),
343
+ getViewport: () => stateRefs.current.viewport,
344
+ setViewport,
345
+ setAgentCursor
346
+ },
347
+ agent
348
+ });
349
+ inProcRef.current = attachInProcess(server);
350
+ serverRef.current = server;
351
+ const off = inProcRef.current.onServerMessage((msg) => {
352
+ if (msg?.id !== void 0 && "result" in msg && msg.result?.structuredContent?.id) {
353
+ const id = msg.result.structuredContent.id;
354
+ requestAnimationFrame(() => pulseFor(id));
355
+ }
356
+ });
357
+ return () => {
358
+ off();
359
+ bridgeRef.current?.dispose();
360
+ bridgeRef.current = null;
361
+ if (inProcRef.current) server.detach(inProcRef.current);
362
+ };
363
+ }, []);
364
+ const pulseFor = (id) => {
365
+ const n = stateRefs.current.notes.find((x) => x.id === id);
366
+ if (n) return setHighlight({ pulseKey: Date.now(), bounds: { x: n.x, y: n.y, width: n.width, height: n.height } });
367
+ const s = stateRefs.current.shapes.find((x) => x.id === id);
368
+ if (s) return setHighlight({ pulseKey: Date.now(), bounds: { x: s.x, y: s.y, width: s.width, height: s.height } });
369
+ };
370
+ const log = useCallback((entry) => {
371
+ setActivity((all) => [...all.slice(-200), { id: `a_${Date.now()}_${all.length}`, at: Date.now(), ...entry }]);
372
+ }, []);
373
+ const [session, setSession] = useState(null);
374
+ const [relayState, setRelayState] = useState("idle");
375
+ const sseRef = useRef(null);
376
+ const logEsRef = useRef(null);
377
+ const startShare = async () => {
378
+ if (session || !serverRef.current || !shareBaseUrl) return;
379
+ const desc = createSessionDescriptor();
380
+ try {
381
+ if (onRegisterSession) {
382
+ await onRegisterSession(desc);
383
+ } else {
384
+ const csrf = document.querySelector('meta[name="csrf-token"]')?.content ?? "";
385
+ const reg = await fetch(`${shareBaseUrl}/register`, {
386
+ method: "POST",
387
+ headers: { "content-type": "application/json", "x-csrf-token": csrf, accept: "application/json" },
388
+ body: JSON.stringify({ session: desc.id, token: desc.token })
389
+ });
390
+ if (!reg.ok) throw new Error(`registration failed (HTTP ${reg.status})`);
391
+ }
392
+ } catch (e) {
393
+ log({ kind: "error", source: "share", text: e instanceof Error ? e.message : String(e) });
394
+ return;
395
+ }
396
+ const relay = attachSseRelay(serverRef.current, {
397
+ baseUrl: shareBaseUrl,
398
+ sessionId: desc.id,
399
+ token: desc.token
400
+ });
401
+ sseRef.current = relay;
402
+ relay.onStateChange(setRelayState);
403
+ const es = new EventSource(`${shareBaseUrl}/${desc.id}/events?token=${desc.token}&direction=inbound`);
404
+ es.addEventListener("mcp", (ev) => {
405
+ try {
406
+ const frame = JSON.parse(ev.data);
407
+ if (frame.method === "notifications/peer_joined") {
408
+ setAgentCursor((c) => c ?? { userId: agent.id, name: agent.name, color: agent.color, x: 60, y: 60 });
409
+ log({ kind: "info", source: "presence", text: `${agent.name ?? "Agent"} connected` });
410
+ return;
411
+ }
412
+ if (frame.method === "notifications/peer_left") {
413
+ setAgentCursor(null);
414
+ log({ kind: "info", source: "presence", text: `${agent.name ?? "Agent"} disconnected` });
415
+ return;
416
+ }
417
+ if (frame.method === "notifications/agent_message") {
418
+ log({ kind: "message", source: agent.name ?? "Agent", text: String(frame.params?.text ?? "") });
419
+ } else if (frame.method === "notifications/agent_status") {
420
+ log({ kind: "info", source: agent.name ?? "Agent", text: String(frame.params?.text ?? "") });
421
+ } else if (frame.method?.startsWith("notifications/")) {
422
+ } else {
423
+ log({ kind: "tool", source: "remote", text: `\u2190 ${frame.method ?? `id:${frame.id}`}`, detail: frame });
424
+ }
425
+ } catch {
426
+ }
427
+ });
428
+ logEsRef.current = es;
429
+ setSession(desc);
430
+ log({ kind: "info", source: "share", text: `Sharing started \xB7 session ${desc.id}` });
431
+ };
432
+ const stopShare = async () => {
433
+ if (!session) return;
434
+ const desc = session;
435
+ setSession(null);
436
+ logEsRef.current?.close();
437
+ logEsRef.current = null;
438
+ if (sseRef.current && serverRef.current) serverRef.current.detach(sseRef.current);
439
+ sseRef.current = null;
440
+ setRelayState("closed");
441
+ if (shareBaseUrl) {
442
+ const csrf = document.querySelector('meta[name="csrf-token"]')?.content ?? "";
443
+ await fetch(`${shareBaseUrl}/${desc.id}/unregister?token=${encodeURIComponent(desc.token)}`, {
444
+ method: "POST",
445
+ headers: { "x-csrf-token": csrf, accept: "application/json" }
446
+ }).catch(() => {
447
+ });
448
+ }
449
+ log({ kind: "info", source: "share", text: "Sharing stopped." });
450
+ };
451
+ const lastBroadcastRef = useRef(0);
452
+ useEffect(() => {
453
+ if (!broadcastEdits || !sseRef.current || !session) return;
454
+ const now = Date.now();
455
+ if (now - lastBroadcastRef.current < 80) return;
456
+ lastBroadcastRef.current = now;
457
+ sseRef.current.send({
458
+ jsonrpc: "2.0",
459
+ method: "notifications/state_update",
460
+ params: { notes, shapes, connectors, viewport, ts: now }
461
+ });
462
+ }, [notes, shapes, connectors, viewport, session, broadcastEdits]);
463
+ const handleSubmit = (text) => {
464
+ if (!sseRef.current) {
465
+ log({ kind: "error", source: "you", text: "Start a shared session first." });
466
+ return;
467
+ }
468
+ sseRef.current.send({
469
+ jsonrpc: "2.0",
470
+ method: "notifications/user_message",
471
+ params: { text, ts: Date.now() }
472
+ });
473
+ log({ kind: "message", source: "You", text });
474
+ };
475
+ const cursors = useMemo(() => [], []);
476
+ const statusText = (() => {
477
+ switch (relayState) {
478
+ case "open":
479
+ return "live";
480
+ case "connecting":
481
+ return "connecting\u2026";
482
+ case "error":
483
+ return "error";
484
+ case "closed":
485
+ return "closed";
486
+ default:
487
+ return void 0;
488
+ }
489
+ })();
490
+ return /* @__PURE__ */ jsxs("div", { className: ["fai-shared-whiteboard", className ?? ""].filter(Boolean).join(" "), style, children: [
491
+ header,
492
+ showShareControls && shareBaseUrl !== null && /* @__PURE__ */ jsx("div", { className: "fai-shared-whiteboard__controls", children: /* @__PURE__ */ jsx(ShareControls, { session, onStart: startShare, onStop: stopShare, status: statusText }) }),
493
+ /* @__PURE__ */ jsxs(
494
+ "div",
495
+ {
496
+ className: "fai-shared-whiteboard__layout",
497
+ style: {
498
+ display: "grid",
499
+ gap: 16,
500
+ gridTemplateColumns: showAgentPanel ? "1fr 360px" : "1fr"
501
+ },
502
+ children: [
503
+ /* @__PURE__ */ jsx(
504
+ "div",
505
+ {
506
+ className: "fai-shared-whiteboard__board",
507
+ style: {
508
+ position: "relative",
509
+ overflow: "hidden",
510
+ borderRadius: 12,
511
+ border: "1px solid #e4e4e7",
512
+ background: "radial-gradient(circle at 1px 1px, rgba(0,0,0,0.07) 1px, transparent 0)",
513
+ backgroundSize: "20px 20px",
514
+ height
515
+ },
516
+ children: /* @__PURE__ */ jsxs(Board, { viewport, onViewportChange: setViewport, style: { width: "100%", height: "100%" }, children: [
517
+ connectors.map((c) => {
518
+ const a = resolveCenter(c.from, notes, shapes);
519
+ const b = resolveCenter(c.to, notes, shapes);
520
+ if (!a || !b) return null;
521
+ return /* @__PURE__ */ jsx(Connector, { from: a, to: b, color: c.color ?? "#64748b" }, c.id);
522
+ }),
523
+ shapes.map((s) => /* @__PURE__ */ jsx(Shape, { item: s, onChange: (next) => setShapes((all) => all.map((x) => x.id === next.id ? next : x)) }, s.id)),
524
+ notes.map((n) => /* @__PURE__ */ jsx(StickyNote, { item: n, onChange: (next) => setNotes((all) => all.map((x) => x.id === next.id ? next : x)) }, n.id)),
525
+ /* @__PURE__ */ jsx(CursorLayer, { cursors }),
526
+ agentCursor && /* @__PURE__ */ jsx(AgentCursor, { x: agentCursor.x, y: agentCursor.y, name: agentCursor.name, color: agentCursor.color }),
527
+ highlight && /* @__PURE__ */ jsx(
528
+ AgentActivityHighlight,
529
+ {
530
+ x: highlight.bounds.x,
531
+ y: highlight.bounds.y,
532
+ width: highlight.bounds.width,
533
+ height: highlight.bounds.height,
534
+ color: agent.color ?? "#a855f7",
535
+ pulseKey: highlight.pulseKey
536
+ }
537
+ )
538
+ ] })
539
+ }
540
+ ),
541
+ showAgentPanel && /* @__PURE__ */ jsx("div", { style: { height }, children: /* @__PURE__ */ jsx(
542
+ AgentPanel,
543
+ {
544
+ agent,
545
+ activity,
546
+ onSubmit: handleSubmit
547
+ }
548
+ ) })
549
+ ]
550
+ }
551
+ )
552
+ ] });
553
+ }
554
+ function resolveCenter(ref, notes, shapes) {
555
+ if (typeof ref === "string") {
556
+ const n = notes.find((x) => x.id === ref);
557
+ if (n) return { x: n.x + n.width / 2, y: n.y + n.height / 2 };
558
+ const s = shapes.find((x) => x.id === ref);
559
+ if (s) return { x: s.x + s.width / 2, y: s.y + s.height / 2 };
560
+ return null;
561
+ }
562
+ return ref;
563
+ }
564
+
565
+ export { AgentActivityHighlight, AgentCursor, AgentPanel, ShareControls, SharedWhiteboard };
566
+ //# sourceMappingURL=index.js.map
567
+ //# sourceMappingURL=index.js.map