@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
package/dist/index.cjs CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  var react = require('react');
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
- var fancyWhiteboard = require('@particle-academy/fancy-whiteboard');
6
5
 
7
6
  var __defProp = Object.defineProperty;
8
7
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -63,10 +62,43 @@ var JSONRPC_INVALID_PARAMS = -32602;
63
62
  var JSONRPC_INTERNAL_ERROR = -32603;
64
63
  var MCP_PROTOCOL_VERSION = "2025-06-18";
65
64
 
65
+ // src/mcp/tool-host.ts
66
+ var ToolRegistry = class {
67
+ constructor() {
68
+ this.tools = /* @__PURE__ */ new Map();
69
+ }
70
+ registerTool(definition, handler) {
71
+ this.tools.set(definition.name, { definition, handler });
72
+ this.onToolsChanged();
73
+ return () => {
74
+ if (this.tools.delete(definition.name)) this.onToolsChanged();
75
+ };
76
+ }
77
+ getTool(name) {
78
+ return this.tools.get(name) ?? null;
79
+ }
80
+ listTools() {
81
+ return Array.from(this.tools.values()).map((t) => t.definition);
82
+ }
83
+ async callTool(name, args = {}) {
84
+ const tool = this.tools.get(name);
85
+ if (!tool) {
86
+ throw new Error(`Unknown tool: ${name}`);
87
+ }
88
+ return tool.handler(args);
89
+ }
90
+ /**
91
+ * Hook for subclasses (e.g. MicroMcpServer) to notify subscribers
92
+ * when the tool catalog changes. Default no-op.
93
+ */
94
+ onToolsChanged() {
95
+ }
96
+ };
97
+
66
98
  // src/mcp/server.ts
67
- var MicroMcpServer = class {
99
+ var MicroMcpServer = class extends ToolRegistry {
68
100
  constructor(options) {
69
- this.tools = /* @__PURE__ */ new Map();
101
+ super();
70
102
  this.transports = /* @__PURE__ */ new Set();
71
103
  this.notifyListChangedScheduled = false;
72
104
  this.info = options.info;
@@ -82,18 +114,13 @@ var MicroMcpServer = class {
82
114
  transport.close?.();
83
115
  }
84
116
  }
85
- registerTool(definition, handler) {
86
- this.tools.set(definition.name, { definition, handler });
87
- this.scheduleListChangedNotification();
88
- return () => this.unregisterTool(definition.name);
89
- }
90
117
  unregisterTool(name) {
91
118
  if (this.tools.delete(name)) {
92
119
  this.scheduleListChangedNotification();
93
120
  }
94
121
  }
95
- listTools() {
96
- return Array.from(this.tools.values()).map((t) => t.definition);
122
+ onToolsChanged() {
123
+ this.scheduleListChangedNotification();
97
124
  }
98
125
  /**
99
126
  * Receive a JSON-RPC frame from a client (called by the transport).
@@ -281,142 +308,20 @@ function extractMeta(result) {
281
308
  return void 0;
282
309
  }
283
310
 
284
- // src/undo/undo-stack.ts
285
- var stacks = /* @__PURE__ */ new Map();
286
- var CAP = 200;
287
- function getStack(agentId) {
288
- let s = stacks.get(agentId);
289
- if (!s) {
290
- s = { past: [], future: [] };
291
- stacks.set(agentId, s);
292
- }
293
- return s;
294
- }
295
- function pushUndoEntry(agentId, entry) {
296
- const s = getStack(agentId);
297
- s.past.push(entry);
298
- if (s.past.length > CAP) s.past.splice(0, s.past.length - CAP);
299
- s.future.length = 0;
300
- }
301
- async function undoOne(agentId) {
302
- const s = getStack(agentId);
303
- const entry = s.past.pop();
304
- if (!entry) return null;
305
- await entry.undo();
306
- s.future.push(entry);
307
- return entry;
308
- }
309
- async function redoOne(agentId) {
310
- const s = getStack(agentId);
311
- const entry = s.future.pop();
312
- if (!entry) return null;
313
- await entry.redo();
314
- s.past.push(entry);
315
- return entry;
316
- }
317
- function readHistory(agentId) {
318
- return getStack(agentId).past.slice();
319
- }
320
- function clearStack(agentId) {
321
- stacks.delete(agentId);
322
- }
323
- function resetAllUndoStacks() {
324
- stacks.clear();
325
- }
326
-
327
- // src/undo/undo-tools.ts
328
- var installedServers = /* @__PURE__ */ new WeakSet();
329
- function ensureUndoToolsRegistered(server, options = {}) {
330
- if (installedServers.has(server)) return;
331
- installedServers.add(server);
332
- registerUndoTools(server, options);
333
- }
334
- function registerUndoTools(server, options = {}) {
335
- const defaultAgent = options.defaultAgentId ?? "agent";
336
- const disposers = [];
337
- const agentOf = (args) => typeof args?.agentId === "string" ? args.agentId : defaultAgent;
338
- disposers.push(
339
- server.registerTool(
340
- {
341
- name: "agent_undo",
342
- description: "Undo the most recent action on the agent's stack. Optional agentId targets a specific agent.",
343
- inputSchema: {
344
- type: "object",
345
- properties: { agentId: { type: "string" } },
346
- additionalProperties: false
347
- }
348
- },
349
- async (args) => {
350
- const entry = await undoOne(agentOf(args));
351
- if (!entry) return errorResult("Nothing to undo.");
352
- return textResult(`Undid: ${entry.label}`, { entry: serialize(entry) });
353
- }
354
- )
355
- );
356
- disposers.push(
357
- server.registerTool(
358
- {
359
- name: "agent_redo",
360
- description: "Redo the most recently undone action.",
361
- inputSchema: {
362
- type: "object",
363
- properties: { agentId: { type: "string" } },
364
- additionalProperties: false
365
- }
366
- },
367
- async (args) => {
368
- const entry = await redoOne(agentOf(args));
369
- if (!entry) return errorResult("Nothing to redo.");
370
- return textResult(`Redid: ${entry.label}`, { entry: serialize(entry) });
371
- }
372
- )
373
- );
374
- disposers.push(
375
- server.registerTool(
376
- {
377
- name: "agent_history",
378
- description: "List the agent's undo stack (oldest first). Useful for understanding what's reversible.",
379
- inputSchema: {
380
- type: "object",
381
- properties: { agentId: { type: "string" } },
382
- additionalProperties: false
383
- }
384
- },
385
- async (args) => {
386
- const history2 = readHistory(agentOf(args)).map(serialize);
387
- const text = history2.map((e) => `${new Date(e.timestamp).toISOString()} ${e.bridgeId} ${e.action}: ${e.label}`).join("\n");
388
- return textResult(text || "(empty)", history2);
389
- }
390
- )
391
- );
392
- return () => disposers.forEach((d) => d());
393
- }
394
- function serialize(entry) {
395
- return {
396
- timestamp: entry.timestamp,
397
- bridgeId: entry.bridgeId,
398
- action: entry.action,
399
- label: entry.label
400
- };
401
- }
402
-
403
- // src/bridges/whiteboard.ts
311
+ // src/bridges/forms.ts
404
312
  var DEFAULT_AGENT = { id: "agent", name: "Agent", color: "#a855f7" };
405
- var VALID_SHAPES = ["rect", "rounded-rect", "ellipse", "diamond", "triangle", "line", "arrow", "text"];
406
- var num = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : fallback ?? 0;
407
- var str = (v, fallback = "") => typeof v === "string" ? v : fallback;
408
- var bool = (v, fallback = false) => typeof v === "boolean" ? v : fallback;
409
- var newId = (prefix) => `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
410
- function registerWhiteboardBridge(server, options) {
313
+ function registerFormBridge(host, options) {
411
314
  const { adapter } = options;
412
315
  const agent = { ...DEFAULT_AGENT, ...options.agent ?? {} };
413
316
  const disposers = [];
414
- ensureUndoToolsRegistered(server, { defaultAgentId: agent.id });
415
- const wbTarget = (args, result) => ({
416
- kind: "whiteboard",
417
- elementId: result?.structuredContent?.id ?? args?.id
317
+ const formId = adapter.id;
318
+ const target = (args) => ({
319
+ kind: "form",
320
+ screenId: adapter.screenId,
321
+ elementId: typeof args?.field === "string" ? `${formId}:${args.field}` : formId,
322
+ label: typeof args?.field === "string" ? `${adapter.title ?? formId} \u2192 ${args.field}` : adapter.title ?? formId
418
323
  });
419
- const reg = (name, description, inputProperties, required, handler, resolveTarget) => {
324
+ const reg = (name, description, properties, required, handler, isMutation) => {
420
325
  const wrapped = async (args) => {
421
326
  try {
422
327
  return await handler(args);
@@ -424,860 +329,98 @@ function registerWhiteboardBridge(server, options) {
424
329
  return errorResult(e instanceof Error ? e.message : String(e));
425
330
  }
426
331
  };
427
- const final = resolveTarget ? wrapToolWithActivity(wrapped, {
332
+ const final = isMutation ? wrapToolWithActivity(wrapped, {
428
333
  toolName: name,
429
334
  agent: { id: agent.id, name: agent.name, color: agent.color },
430
- kind: "whiteboard",
431
- resolveTarget: ({ args, result }) => resolveTarget(args, result)
335
+ kind: "form",
336
+ screenId: adapter.screenId,
337
+ resolveTarget: ({ args }) => target(args)
432
338
  }) : wrapped;
433
339
  disposers.push(
434
- server.registerTool(
340
+ host.registerTool(
435
341
  {
436
342
  name,
437
343
  description,
438
- inputSchema: {
439
- type: "object",
440
- properties: inputProperties,
441
- required,
442
- additionalProperties: false
443
- }
344
+ inputSchema: { type: "object", properties, required, additionalProperties: false }
444
345
  },
445
346
  final
446
347
  )
447
348
  );
448
349
  };
449
- reg("whiteboard_get_state", "Get the full board state: viewport, all items, strokes.", {}, [], () => {
450
- const state = {
451
- viewport: adapter.getViewport(),
452
- notes: adapter.getNotes(),
453
- shapes: adapter.getShapes(),
454
- connectors: adapter.getConnectors(),
455
- strokes: adapter.getStrokes()
456
- };
457
- return textResult(JSON.stringify(state, null, 2), state);
458
- });
459
- reg("whiteboard_list_items", "List notes, shapes, and connectors with id, kind, and bounds.", {}, [], () => {
460
- const items = [];
461
- for (const n of adapter.getNotes()) {
462
- items.push({
463
- id: n.id,
464
- kind: "sticky",
465
- summary: `"${(n.text ?? "").slice(0, 40)}" @(${Math.round(n.x)},${Math.round(n.y)}) ${n.width}\xD7${n.height}`
466
- });
467
- }
468
- for (const s of adapter.getShapes()) {
469
- items.push({
470
- id: s.id,
471
- kind: `shape:${s.shape}`,
472
- summary: `${s.text ? `"${s.text}" ` : ""}@(${Math.round(s.x)},${Math.round(s.y)}) ${s.width}\xD7${s.height}`
473
- });
474
- }
475
- for (const c of adapter.getConnectors()) {
476
- items.push({ id: c.id, kind: "connector", summary: `from=${JSON.stringify(c.from)} to=${JSON.stringify(c.to)}` });
477
- }
478
- return textResult(items.map((i) => `${i.kind} ${i.id}: ${i.summary}`).join("\n") || "(empty board)", items);
479
- });
480
- reg(
481
- "whiteboard_get_item",
482
- "Get a single item (sticky / shape / connector) by id.",
483
- { id: { type: "string" } },
484
- ["id"],
485
- (args) => {
486
- const id = str(args.id);
487
- const all = [...adapter.getNotes(), ...adapter.getShapes(), ...adapter.getConnectors()];
488
- const found = all.find((x) => x.id === id);
489
- if (!found) return errorResult(`No item with id ${id}`);
490
- return textResult(JSON.stringify(found, null, 2), found);
491
- }
492
- );
493
350
  reg(
494
- "whiteboard_add_sticky",
495
- "Add a sticky note. Position is in world coordinates.",
496
- {
497
- x: { type: "number" },
498
- y: { type: "number" },
499
- text: { type: "string" },
500
- width: { type: "number" },
501
- height: { type: "number" },
502
- color: { type: "string", description: "CSS color, e.g. #fde68a" }
503
- },
504
- ["x", "y"],
505
- async (args) => {
506
- const x = num(args.x);
507
- const y = num(args.y);
508
- const width = num(args.width, 180);
509
- const height = num(args.height, 140);
510
- const note = {
511
- id: newId("n"),
512
- kind: "sticky",
513
- x,
514
- y,
515
- width,
516
- height,
517
- text: str(args.text),
518
- color: typeof args.color === "string" ? args.color : "#fde68a",
519
- authorId: agent.id
520
- };
521
- adapter.setNotes((all) => [...all, note]);
522
- pushUndoEntry(agent.id, {
523
- timestamp: Date.now(),
524
- bridgeId: "whiteboard",
525
- action: "whiteboard_add_sticky",
526
- label: `Added sticky ${note.id}`,
527
- undo: () => adapter.setNotes((all) => all.filter((n) => n.id !== note.id)),
528
- redo: () => adapter.setNotes((all) => [...all, note])
529
- });
530
- return textResult(`Added sticky ${note.id}`, note);
351
+ "form_describe",
352
+ `Describe the form "${formId}" \u2014 fields, types, options, required flags. Call this first to know what's fillable.`,
353
+ {},
354
+ [],
355
+ () => {
356
+ const fields = adapter.getFields();
357
+ const text = fields.map((f) => `${f.name}${f.required ? "*" : ""} (${f.type})${f.label ? ` \u2014 ${f.label}` : ""}`).join("\n");
358
+ return textResult(text || "(no fields)", { id: formId, title: adapter.title, fields });
531
359
  },
532
- wbTarget
360
+ false
533
361
  );
534
362
  reg(
535
- "whiteboard_stream_text",
536
- "Type text into a sticky note character-by-character so the human can read it forming. The tool returns once streaming finishes.",
537
- {
538
- id: { type: "string" },
539
- text: { type: "string" },
540
- cps: { type: "number", description: "Characters per second. Default 25." },
541
- append: { type: "boolean", description: "Append to existing text instead of replacing. Default false." }
542
- },
543
- ["id", "text"],
544
- async (args) => {
545
- const id = str(args.id);
546
- const target = str(args.text);
547
- const cps = Math.max(1, num(args.cps, 25));
548
- const append = bool(args.append);
549
- const startNote = adapter.getNotes().find((n) => n.id === id);
550
- if (!startNote) return errorResult(`No sticky with id ${id}`);
551
- const base = append ? startNote.text ?? "" : "";
552
- const interval = Math.max(8, Math.round(1e3 / cps));
553
- for (let i = 0; i <= target.length; i++) {
554
- const nextText = base + target.slice(0, i);
555
- adapter.setNotes((all) => all.map((n) => n.id === id ? { ...n, text: nextText } : n));
556
- if (i < target.length) await new Promise((r) => setTimeout(r, interval));
363
+ "form_get_value",
364
+ "Read a single field's current value.",
365
+ { field: { type: "string" } },
366
+ ["field"],
367
+ (args) => {
368
+ const name = String(args.field ?? "");
369
+ if (!adapter.getFields().find((f) => f.name === name)) {
370
+ return errorResult(`Unknown field: ${name}`);
557
371
  }
558
- return textResult(`Streamed ${target.length} chars to ${id}`, { id, text: base + target });
559
- }
560
- );
561
- reg(
562
- "whiteboard_update_sticky",
563
- "Update fields on a sticky note. Only provided fields are changed.",
564
- {
565
- id: { type: "string" },
566
- x: { type: "number" },
567
- y: { type: "number" },
568
- width: { type: "number" },
569
- height: { type: "number" },
570
- text: { type: "string" },
571
- color: { type: "string" }
572
- },
573
- ["id"],
574
- async (args) => {
575
- const id = str(args.id);
576
- const existing = adapter.getNotes().find((n) => n.id === id);
577
- if (!existing) return errorResult(`No sticky with id ${id}`);
578
- const nextX = args.x !== void 0 ? num(args.x) : existing.x;
579
- const nextY = args.y !== void 0 ? num(args.y) : existing.y;
580
- const nextW = args.width !== void 0 ? num(args.width) : existing.width;
581
- const nextH = args.height !== void 0 ? num(args.height) : existing.height;
582
- let updated = null;
583
- adapter.setNotes(
584
- (all) => all.map((n) => {
585
- if (n.id !== id) return n;
586
- updated = {
587
- ...n,
588
- x: nextX,
589
- y: nextY,
590
- width: nextW,
591
- height: nextH,
592
- ...args.text !== void 0 ? { text: str(args.text) } : {},
593
- ...args.color !== void 0 ? { color: str(args.color) } : {}
594
- };
595
- return updated;
596
- })
597
- );
598
- return textResult(`Updated sticky ${id}`, updated);
372
+ const value = adapter.getValue(name);
373
+ return textResult(JSON.stringify(value), { field: name, value });
599
374
  },
600
- wbTarget
375
+ false
601
376
  );
602
377
  reg(
603
- "whiteboard_add_shape",
604
- `Add a shape. Kind must be one of: ${VALID_SHAPES.join(", ")}.`,
605
- {
606
- shape: { type: "string", enum: VALID_SHAPES },
607
- x: { type: "number" },
608
- y: { type: "number" },
609
- width: { type: "number" },
610
- height: { type: "number" },
611
- text: { type: "string" },
612
- fill: { type: "string" },
613
- stroke: { type: "string" },
614
- flipX: { type: "boolean" },
615
- flipY: { type: "boolean" }
616
- },
617
- ["shape", "x", "y", "width", "height"],
618
- async (args) => {
619
- const kind = str(args.shape);
620
- if (!VALID_SHAPES.includes(kind)) return errorResult(`Invalid shape kind: ${kind}`);
621
- const x = num(args.x);
622
- const y = num(args.y);
623
- const width = num(args.width);
624
- const height = num(args.height);
625
- const shape = {
626
- id: newId("s"),
627
- kind: "shape",
628
- shape: kind,
629
- x,
630
- y,
631
- width,
632
- height,
633
- ...args.text !== void 0 ? { text: str(args.text) } : {},
634
- ...args.fill !== void 0 ? { fill: str(args.fill) } : {},
635
- ...args.stroke !== void 0 ? { stroke: str(args.stroke) } : {},
636
- ...args.flipX !== void 0 ? { flipX: bool(args.flipX) } : {},
637
- ...args.flipY !== void 0 ? { flipY: bool(args.flipY) } : {}
638
- };
639
- adapter.setShapes((all) => [...all, shape]);
640
- return textResult(`Added ${kind} ${shape.id}`, shape);
378
+ "form_get_values",
379
+ "Read every field's current value as a JSON object.",
380
+ {},
381
+ [],
382
+ () => {
383
+ const values = adapter.getValues();
384
+ return textResult(JSON.stringify(values, null, 2), values);
641
385
  },
642
- wbTarget
386
+ false
643
387
  );
644
388
  reg(
645
- "whiteboard_update_shape",
646
- "Update fields on a shape.",
389
+ "form_set_value",
390
+ "Set one field's value. The host's controlled state updates and the human sees the field change.",
647
391
  {
648
- id: { type: "string" },
649
- x: { type: "number" },
650
- y: { type: "number" },
651
- width: { type: "number" },
652
- height: { type: "number" },
653
- text: { type: "string" },
654
- fill: { type: "string" },
655
- stroke: { type: "string" }
392
+ field: { type: "string" },
393
+ value: { description: "Value to set. Type depends on the field's `type`." }
656
394
  },
657
- ["id"],
658
- async (args) => {
659
- const id = str(args.id);
660
- const existing = adapter.getShapes().find((s) => s.id === id);
661
- if (!existing) return errorResult(`No shape with id ${id}`);
662
- const nextX = args.x !== void 0 ? num(args.x) : existing.x;
663
- const nextY = args.y !== void 0 ? num(args.y) : existing.y;
664
- const nextW = args.width !== void 0 ? num(args.width) : existing.width;
665
- const nextH = args.height !== void 0 ? num(args.height) : existing.height;
666
- let updated = null;
667
- adapter.setShapes(
668
- (all) => all.map((s) => {
669
- if (s.id !== id) return s;
670
- updated = {
671
- ...s,
672
- x: nextX,
673
- y: nextY,
674
- width: nextW,
675
- height: nextH,
676
- ...args.text !== void 0 ? { text: str(args.text) } : {},
677
- ...args.fill !== void 0 ? { fill: str(args.fill) } : {},
678
- ...args.stroke !== void 0 ? { stroke: str(args.stroke) } : {}
679
- };
680
- return updated;
681
- })
682
- );
683
- return textResult(`Updated shape ${id}`, updated);
395
+ ["field", "value"],
396
+ (args) => {
397
+ const name = String(args.field ?? "");
398
+ const fieldDef = adapter.getFields().find((f) => f.name === name);
399
+ if (!fieldDef) return errorResult(`Unknown field: ${name}`);
400
+ adapter.setValue(name, args.value);
401
+ return textResult(`${name} \u2190 ${JSON.stringify(args.value)}`, { field: name, value: args.value });
684
402
  },
685
- wbTarget
403
+ true
686
404
  );
687
405
  reg(
688
- "whiteboard_add_connector",
689
- "Connect two items by id, or specify explicit world-space points.",
690
- {
691
- from: { description: "Item id (string) or {x,y}" },
692
- to: { description: "Item id (string) or {x,y}" },
693
- color: { type: "string" }
694
- },
695
- ["from", "to"],
406
+ "form_set_values",
407
+ "Set multiple fields atomically. Pass a `values` object keyed by field name.",
408
+ { values: { type: "object" } },
409
+ ["values"],
696
410
  (args) => {
697
- const c = {
698
- id: newId("c"),
699
- kind: "connector",
700
- from: args.from,
701
- to: args.to,
702
- ...args.color !== void 0 ? { color: str(args.color) } : {}
703
- };
704
- adapter.setConnectors((all) => [...all, c]);
705
- return textResult(`Added connector ${c.id}`, c);
411
+ const values = args.values && typeof args.values === "object" ? args.values : {};
412
+ const fields = adapter.getFields();
413
+ const known = new Set(fields.map((f) => f.name));
414
+ const unknownKeys = Object.keys(values).filter((k) => !known.has(k));
415
+ if (unknownKeys.length) return errorResult(`Unknown fields: ${unknownKeys.join(", ")}`);
416
+ if (adapter.setValues) {
417
+ adapter.setValues(values);
418
+ } else {
419
+ for (const [k, v] of Object.entries(values)) adapter.setValue(k, v);
420
+ }
421
+ return textResult(`Set ${Object.keys(values).length} field(s)`, { values });
706
422
  },
707
- wbTarget
708
- );
709
- reg(
710
- "whiteboard_add_stroke",
711
- "Add a freeform pen stroke. Points are absolute screen coords (matching the Drawing layer).",
712
- {
713
- points: {
714
- type: "array",
715
- description: "Array of {x,y} points"
716
- },
717
- color: { type: "string" },
718
- size: { type: "number" }
719
- },
720
- ["points"],
721
- (args) => {
722
- const points = (Array.isArray(args.points) ? args.points : []).map((p) => ({
723
- x: num(p?.x),
724
- y: num(p?.y)
725
- }));
726
- if (!points.length) return errorResult("Stroke requires at least one point");
727
- const stroke = {
728
- id: newId("st"),
729
- points,
730
- color: typeof args.color === "string" ? args.color : "#0f172a",
731
- size: typeof args.size === "number" ? args.size : 2,
732
- authorId: agent.id
733
- };
734
- adapter.setStrokes((all) => [...all, stroke]);
735
- return textResult(`Added stroke ${stroke.id} (${points.length} points)`, stroke);
736
- },
737
- wbTarget
738
- );
739
- reg(
740
- "whiteboard_delete_item",
741
- "Remove any item by id (sticky / shape / connector / stroke).",
742
- { id: { type: "string" } },
743
- ["id"],
744
- (args) => {
745
- const id = str(args.id);
746
- const removedNotes = adapter.getNotes().filter((x) => x.id === id);
747
- const removedShapes = adapter.getShapes().filter((x) => x.id === id);
748
- const removedConnectors = adapter.getConnectors().filter((x) => x.id === id);
749
- const removedStrokes = adapter.getStrokes().filter((x) => x.id === id);
750
- const removed = removedNotes.length + removedShapes.length + removedConnectors.length + removedStrokes.length > 0;
751
- if (!removed) return errorResult(`No item with id ${id}`);
752
- adapter.setNotes((all) => all.filter((x) => x.id !== id));
753
- adapter.setShapes((all) => all.filter((x) => x.id !== id));
754
- adapter.setConnectors((all) => all.filter((x) => x.id !== id));
755
- adapter.setStrokes((all) => all.filter((x) => x.id !== id));
756
- pushUndoEntry(agent.id, {
757
- timestamp: Date.now(),
758
- bridgeId: "whiteboard",
759
- action: "whiteboard_delete_item",
760
- label: `Deleted ${id}`,
761
- undo: () => {
762
- if (removedNotes.length) adapter.setNotes((all) => [...all, ...removedNotes]);
763
- if (removedShapes.length) adapter.setShapes((all) => [...all, ...removedShapes]);
764
- if (removedConnectors.length) adapter.setConnectors((all) => [...all, ...removedConnectors]);
765
- if (removedStrokes.length) adapter.setStrokes((all) => [...all, ...removedStrokes]);
766
- },
767
- redo: () => {
768
- adapter.setNotes((all) => all.filter((x) => x.id !== id));
769
- adapter.setShapes((all) => all.filter((x) => x.id !== id));
770
- adapter.setConnectors((all) => all.filter((x) => x.id !== id));
771
- adapter.setStrokes((all) => all.filter((x) => x.id !== id));
772
- }
773
- });
774
- return textResult(`Deleted ${id}`);
775
- },
776
- wbTarget
777
- );
778
- reg(
779
- "whiteboard_set_viewport",
780
- "Pan / zoom the viewport.",
781
- { x: { type: "number" }, y: { type: "number" }, zoom: { type: "number" } },
782
- [],
783
- (args) => {
784
- const v = adapter.getViewport();
785
- const next = {
786
- x: args.x !== void 0 ? num(args.x) : v.x,
787
- y: args.y !== void 0 ? num(args.y) : v.y,
788
- zoom: args.zoom !== void 0 ? num(args.zoom) : v.zoom
789
- };
790
- adapter.setViewport(next);
791
- return textResult(`Viewport \u2192 ${JSON.stringify(next)}`, next);
792
- },
793
- wbTarget
794
- );
795
- reg(
796
- "whiteboard_set_agent_cursor",
797
- "Move the agent's presence cursor (or pass null to hide it).",
798
- {
799
- x: { type: "number" },
800
- y: { type: "number" },
801
- hide: { type: "boolean" }
802
- },
803
- [],
804
- (args) => {
805
- if (!adapter.setAgentCursor) return errorResult("Host did not provide setAgentCursor");
806
- if (bool(args.hide)) {
807
- adapter.setAgentCursor(null);
808
- return textResult("Agent cursor hidden");
809
- }
810
- const cursor = {
811
- userId: agent.id,
812
- name: agent.name,
813
- color: agent.color,
814
- x: num(args.x),
815
- y: num(args.y)
816
- };
817
- adapter.setAgentCursor(cursor);
818
- return textResult(`Cursor \u2192 (${cursor.x}, ${cursor.y})`, cursor);
819
- },
820
- wbTarget
821
- );
822
- return {
823
- id: "whiteboard",
824
- title: "Whiteboard",
825
- dispose: () => {
826
- for (const d of disposers) d();
827
- adapter.setAgentCursor?.(null);
828
- }
829
- };
830
- }
831
-
832
- // src/bridges/flow.ts
833
- var DEFAULT_AGENT2 = { id: "agent", name: "Agent", color: "#a855f7" };
834
- var num2 = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : 0;
835
- var str2 = (v, fallback = "") => typeof v === "string" ? v : fallback;
836
- var newId2 = (prefix) => `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
837
- function registerFlowBridge(server, options) {
838
- const { adapter } = options;
839
- const agent = { ...DEFAULT_AGENT2, ...options.agent ?? {} };
840
- const disposers = [];
841
- const flTarget = (args, result) => ({
842
- kind: "flow",
843
- elementId: result?.structuredContent?.id ?? args?.id
844
- });
845
- const reg = (name, description, properties, required, handler, resolveTarget) => {
846
- const wrapped = async (args) => {
847
- try {
848
- return await handler(args);
849
- } catch (e) {
850
- return errorResult(e instanceof Error ? e.message : String(e));
851
- }
852
- };
853
- const final = resolveTarget ? wrapToolWithActivity(wrapped, {
854
- toolName: name,
855
- agent: { id: agent.id, name: agent.name, color: agent.color },
856
- kind: "flow",
857
- resolveTarget: ({ args, result }) => resolveTarget(args, result)
858
- }) : wrapped;
859
- disposers.push(
860
- server.registerTool(
861
- {
862
- name,
863
- description,
864
- inputSchema: { type: "object", properties, required, additionalProperties: false }
865
- },
866
- final
867
- )
868
- );
869
- };
870
- reg("flow_get_state", "Get the full graph: nodes + edges.", {}, [], () => {
871
- const state = { nodes: adapter.getNodes(), edges: adapter.getEdges() };
872
- return textResult(JSON.stringify(state, null, 2), state);
873
- });
874
- reg("flow_list_nodes", "Summarise every node: id, kind, label, position, status.", {}, [], () => {
875
- const items = adapter.getNodes().map((n) => ({
876
- id: n.id,
877
- kind: n.type,
878
- label: n.data?.label,
879
- x: Math.round(n.position.x),
880
- y: Math.round(n.position.y),
881
- status: n.data?.status ?? "idle"
882
- }));
883
- const text = items.map((i) => `${i.kind} ${i.id}: "${i.label}" @(${i.x},${i.y}) [${i.status}]`).join("\n") || "(empty graph)";
884
- return textResult(text, items);
885
- });
886
- reg(
887
- "flow_get_node",
888
- "Get a single node's full record by id.",
889
- { id: { type: "string" } },
890
- ["id"],
891
- (args) => {
892
- const id = str2(args.id);
893
- const node = adapter.getNodes().find((n) => n.id === id);
894
- if (!node) return errorResult(`No node with id ${id}`);
895
- return textResult(JSON.stringify(node, null, 2), node);
896
- }
897
- );
898
- reg(
899
- "flow_list_node_kinds",
900
- "List every node kind registered in fancy-flow's registry. Use this to discover what's authorable before adding nodes.",
901
- { category: { type: "string", description: "Optional category filter: trigger | logic | data | ai | io | human | output | custom." } },
902
- [],
903
- async () => {
904
- try {
905
- const { listNodeKinds } = await import('@particle-academy/fancy-flow');
906
- const cat = adapter ? void 0 : void 0;
907
- const all = (cat ? listNodeKinds(cat) : listNodeKinds()).map((k) => ({
908
- name: k.name,
909
- category: k.category,
910
- label: k.label,
911
- description: k.description,
912
- icon: k.icon,
913
- accent: k.accent,
914
- inputs: k.inputs ?? [],
915
- outputs: k.outputs ?? [],
916
- configFields: (k.configSchema ?? []).map((f) => ({ key: f.key, type: f.type, label: f.label, required: !!f.required }))
917
- }));
918
- const text = all.map((k) => `${k.category}/${k.name}: ${k.label}${k.description ? " \u2014 " + k.description : ""}`).join("\n");
919
- return textResult(text || "(no kinds registered)", all);
920
- } catch (e) {
921
- return errorResult(`fancy-flow registry not available: ${e instanceof Error ? e.message : String(e)}`);
922
- }
923
- }
924
- );
925
- reg(
926
- "flow_get_node_schema",
927
- "Get the full configSchema + ports for a node kind. Use to know exactly what fields a kind accepts before calling flow_add_node.",
928
- { name: { type: "string" } },
929
- ["name"],
930
- async (args) => {
931
- try {
932
- const { getNodeKind } = await import('@particle-academy/fancy-flow');
933
- const k = getNodeKind(str2(args.name));
934
- if (!k) return errorResult(`No kind registered: ${args.name}`);
935
- const summary = {
936
- name: k.name,
937
- category: k.category,
938
- label: k.label,
939
- description: k.description,
940
- inputs: k.inputs ?? [],
941
- outputs: k.outputs ?? [],
942
- configSchema: k.configSchema ?? [],
943
- defaultConfig: k.defaultConfig ?? null
944
- };
945
- return textResult(JSON.stringify(summary, null, 2), summary);
946
- } catch (e) {
947
- return errorResult(`fancy-flow registry not available: ${e instanceof Error ? e.message : String(e)}`);
948
- }
949
- }
950
- );
951
- reg("flow_list_edges", "Summarise every edge.", {}, [], () => {
952
- const items = adapter.getEdges().map((e) => ({
953
- id: e.id,
954
- from: `${e.source}${e.sourceHandle ? `:${e.sourceHandle}` : ""}`,
955
- to: `${e.target}${e.targetHandle ? `:${e.targetHandle}` : ""}`
956
- }));
957
- return textResult(items.map((i) => `${i.id}: ${i.from} \u2192 ${i.to}`).join("\n") || "(no edges)", items);
958
- });
959
- reg(
960
- "flow_add_node",
961
- "Add a node of any kind registered in fancy-flow's registry. Call flow_list_node_kinds first to discover what's available.",
962
- {
963
- kind: { type: "string", description: "Registry kind name (e.g. memory_store, llm_call, branch)." },
964
- label: { type: "string" },
965
- x: { type: "number" },
966
- y: { type: "number" },
967
- description: { type: "string" },
968
- config: { type: "object", description: "Config fields per the kind's configSchema." },
969
- body: { type: "string", description: "Note kinds only \u2014 body text." }
970
- },
971
- ["kind", "label", "x", "y"],
972
- async (args) => {
973
- const kindName = str2(args.kind);
974
- let kindDef = null;
975
- try {
976
- const { getNodeKind, defaultConfigFor } = await import('@particle-academy/fancy-flow');
977
- kindDef = getNodeKind(kindName);
978
- var defaults = kindDef ? defaultConfigFor(kindDef) : {};
979
- } catch {
980
- var defaults = {};
981
- }
982
- const isLegacy = ["trigger", "action", "decision", "output", "note", "subgraph"].includes(kindName);
983
- if (!kindDef && !isLegacy) {
984
- return errorResult(`Unknown kind: ${kindName} \u2014 call flow_list_node_kinds for the registry.`);
985
- }
986
- const id = newId2("n");
987
- const config = { ...defaults, ...args.config && typeof args.config === "object" ? args.config : {} };
988
- const node = {
989
- id,
990
- type: kindName,
991
- position: { x: num2(args.x), y: num2(args.y) },
992
- data: {
993
- kind: kindName,
994
- label: str2(args.label),
995
- ...args.description ? { description: str2(args.description) } : {},
996
- config,
997
- ...kindName === "note" && args.body ? { body: str2(args.body) } : {}
998
- }
999
- };
1000
- adapter.setNodes((all) => [...all, node]);
1001
- return textResult(`Added ${kindName} ${id} ("${str2(args.label)}")`, node);
1002
- },
1003
- flTarget
1004
- );
1005
- reg(
1006
- "flow_update_node",
1007
- "Update fields on a node. Only provided fields change.",
1008
- {
1009
- id: { type: "string" },
1010
- label: { type: "string" },
1011
- x: { type: "number" },
1012
- y: { type: "number" },
1013
- description: { type: "string" },
1014
- config: { type: "object" }
1015
- },
1016
- ["id"],
1017
- (args) => {
1018
- const id = str2(args.id);
1019
- let updated = null;
1020
- adapter.setNodes(
1021
- (all) => all.map((n) => {
1022
- if (n.id !== id) return n;
1023
- updated = {
1024
- ...n,
1025
- position: {
1026
- x: args.x !== void 0 ? num2(args.x) : n.position.x,
1027
- y: args.y !== void 0 ? num2(args.y) : n.position.y
1028
- },
1029
- data: {
1030
- ...n.data,
1031
- ...args.label !== void 0 ? { label: str2(args.label) } : {},
1032
- ...args.description !== void 0 ? { description: str2(args.description) } : {},
1033
- ...args.config && typeof args.config === "object" ? { config: { ...n.data.config ?? {}, ...args.config } } : {}
1034
- }
1035
- };
1036
- return updated;
1037
- })
1038
- );
1039
- if (!updated) return errorResult(`No node with id ${id}`);
1040
- return textResult(`Updated node ${id}`, updated);
1041
- },
1042
- flTarget
1043
- );
1044
- reg(
1045
- "flow_delete_node",
1046
- "Remove a node by id (also removes any connected edges).",
1047
- { id: { type: "string" } },
1048
- ["id"],
1049
- (args) => {
1050
- const id = str2(args.id);
1051
- if (!adapter.getNodes().some((n) => n.id === id)) {
1052
- return errorResult(`No node with id ${id}`);
1053
- }
1054
- adapter.setNodes((all) => all.filter((n) => n.id !== id));
1055
- adapter.setEdges((all) => all.filter((e) => e.source !== id && e.target !== id));
1056
- return textResult(`Deleted node ${id}`);
1057
- },
1058
- flTarget
1059
- );
1060
- reg(
1061
- "flow_connect",
1062
- "Create an edge between two nodes (optionally specifying handle ids).",
1063
- {
1064
- source: { type: "string" },
1065
- target: { type: "string" },
1066
- sourceHandle: { type: "string" },
1067
- targetHandle: { type: "string" },
1068
- label: { type: "string" }
1069
- },
1070
- ["source", "target"],
1071
- (args) => {
1072
- const source = str2(args.source);
1073
- const target = str2(args.target);
1074
- const all = adapter.getNodes();
1075
- if (!all.find((n) => n.id === source)) return errorResult(`No source node ${source}`);
1076
- if (!all.find((n) => n.id === target)) return errorResult(`No target node ${target}`);
1077
- const edge = {
1078
- id: newId2("e"),
1079
- source,
1080
- target,
1081
- ...args.sourceHandle ? { sourceHandle: str2(args.sourceHandle) } : {},
1082
- ...args.targetHandle ? { targetHandle: str2(args.targetHandle) } : {},
1083
- ...args.label ? { label: str2(args.label) } : {}
1084
- };
1085
- adapter.setEdges((existing) => [...existing, edge]);
1086
- return textResult(`Connected ${source}${edge.sourceHandle ? `:${edge.sourceHandle}` : ""} \u2192 ${target}${edge.targetHandle ? `:${edge.targetHandle}` : ""}`, edge);
1087
- },
1088
- flTarget
1089
- );
1090
- reg(
1091
- "flow_disconnect",
1092
- "Remove an edge by id.",
1093
- { id: { type: "string" } },
1094
- ["id"],
1095
- (args) => {
1096
- const id = str2(args.id);
1097
- if (!adapter.getEdges().some((e) => e.id === id)) {
1098
- return errorResult(`No edge ${id}`);
1099
- }
1100
- adapter.setEdges((all) => all.filter((e) => e.id !== id));
1101
- return textResult(`Disconnected ${id}`);
1102
- },
1103
- flTarget
1104
- );
1105
- reg(
1106
- "flow_set_node_status",
1107
- "Manually set a node's status badge (idle | queued | running | done | error) and optional text. Useful for narration outside a run.",
1108
- {
1109
- id: { type: "string" },
1110
- status: { type: "string", enum: ["idle", "queued", "running", "done", "error"] },
1111
- text: { type: "string" }
1112
- },
1113
- ["id", "status"],
1114
- (args) => {
1115
- const id = str2(args.id);
1116
- const status = str2(args.status);
1117
- const text = args.text !== void 0 ? str2(args.text) : void 0;
1118
- if (adapter.setNodeStatus) {
1119
- adapter.setNodeStatus(id, status, text);
1120
- } else {
1121
- let found = false;
1122
- adapter.setNodes(
1123
- (all) => all.map((n) => {
1124
- if (n.id !== id) return n;
1125
- found = true;
1126
- return { ...n, data: { ...n.data, status, statusText: text } };
1127
- })
1128
- );
1129
- if (!found) return errorResult(`No node with id ${id}`);
1130
- }
1131
- return textResult(`${id} \u2192 ${status}${text ? ` (${text})` : ""}`);
1132
- },
1133
- flTarget
1134
- );
1135
- reg(
1136
- "flow_run",
1137
- "Trigger a run of the current graph. Returns the topological result. Requires the host to have wired `run` into the adapter.",
1138
- {},
1139
- [],
1140
- async () => {
1141
- if (!adapter.run) return errorResult("Host did not provide a run handler.");
1142
- const result = await adapter.run();
1143
- return textResult(result.ok ? "Run complete" : `Run failed: ${result.error ?? "unknown"}`, result);
1144
- },
1145
- flTarget
1146
- );
1147
- reg(
1148
- "flow_cancel",
1149
- "Cancel an in-flight run.",
1150
- {},
1151
- [],
1152
- () => {
1153
- if (!adapter.cancel) return errorResult("Host did not provide a cancel handler.");
1154
- adapter.cancel();
1155
- return textResult("Run cancelled");
1156
- },
1157
- flTarget
1158
- );
1159
- return {
1160
- id: "flow",
1161
- title: "Flow",
1162
- dispose: () => {
1163
- for (const d of disposers) d();
1164
- }
1165
- };
1166
- }
1167
-
1168
- // src/bridges/forms.ts
1169
- var DEFAULT_AGENT3 = { id: "agent", name: "Agent", color: "#a855f7" };
1170
- function registerFormBridge(server, options) {
1171
- const { adapter } = options;
1172
- const agent = { ...DEFAULT_AGENT3, ...options.agent ?? {} };
1173
- const disposers = [];
1174
- const formId = adapter.id;
1175
- const target = (args) => ({
1176
- kind: "form",
1177
- screenId: adapter.screenId,
1178
- elementId: typeof args?.field === "string" ? `${formId}:${args.field}` : formId,
1179
- label: typeof args?.field === "string" ? `${adapter.title ?? formId} \u2192 ${args.field}` : adapter.title ?? formId
1180
- });
1181
- const reg = (name, description, properties, required, handler, isMutation) => {
1182
- const wrapped = async (args) => {
1183
- try {
1184
- return await handler(args);
1185
- } catch (e) {
1186
- return errorResult(e instanceof Error ? e.message : String(e));
1187
- }
1188
- };
1189
- const final = isMutation ? wrapToolWithActivity(wrapped, {
1190
- toolName: name,
1191
- agent: { id: agent.id, name: agent.name, color: agent.color },
1192
- kind: "form",
1193
- screenId: adapter.screenId,
1194
- resolveTarget: ({ args }) => target(args)
1195
- }) : wrapped;
1196
- disposers.push(
1197
- server.registerTool(
1198
- {
1199
- name,
1200
- description,
1201
- inputSchema: { type: "object", properties, required, additionalProperties: false }
1202
- },
1203
- final
1204
- )
1205
- );
1206
- };
1207
- reg(
1208
- "form_describe",
1209
- `Describe the form "${formId}" \u2014 fields, types, options, required flags. Call this first to know what's fillable.`,
1210
- {},
1211
- [],
1212
- () => {
1213
- const fields = adapter.getFields();
1214
- const text = fields.map((f) => `${f.name}${f.required ? "*" : ""} (${f.type})${f.label ? ` \u2014 ${f.label}` : ""}`).join("\n");
1215
- return textResult(text || "(no fields)", { id: formId, title: adapter.title, fields });
1216
- },
1217
- false
1218
- );
1219
- reg(
1220
- "form_get_value",
1221
- "Read a single field's current value.",
1222
- { field: { type: "string" } },
1223
- ["field"],
1224
- (args) => {
1225
- const name = String(args.field ?? "");
1226
- if (!adapter.getFields().find((f) => f.name === name)) {
1227
- return errorResult(`Unknown field: ${name}`);
1228
- }
1229
- const value = adapter.getValue(name);
1230
- return textResult(JSON.stringify(value), { field: name, value });
1231
- },
1232
- false
1233
- );
1234
- reg(
1235
- "form_get_values",
1236
- "Read every field's current value as a JSON object.",
1237
- {},
1238
- [],
1239
- () => {
1240
- const values = adapter.getValues();
1241
- return textResult(JSON.stringify(values, null, 2), values);
1242
- },
1243
- false
1244
- );
1245
- reg(
1246
- "form_set_value",
1247
- "Set one field's value. The host's controlled state updates and the human sees the field change.",
1248
- {
1249
- field: { type: "string" },
1250
- value: { description: "Value to set. Type depends on the field's `type`." }
1251
- },
1252
- ["field", "value"],
1253
- (args) => {
1254
- const name = String(args.field ?? "");
1255
- const fieldDef = adapter.getFields().find((f) => f.name === name);
1256
- if (!fieldDef) return errorResult(`Unknown field: ${name}`);
1257
- adapter.setValue(name, args.value);
1258
- return textResult(`${name} \u2190 ${JSON.stringify(args.value)}`, { field: name, value: args.value });
1259
- },
1260
- true
1261
- );
1262
- reg(
1263
- "form_set_values",
1264
- "Set multiple fields atomically. Pass a `values` object keyed by field name.",
1265
- { values: { type: "object" } },
1266
- ["values"],
1267
- (args) => {
1268
- const values = args.values && typeof args.values === "object" ? args.values : {};
1269
- const fields = adapter.getFields();
1270
- const known = new Set(fields.map((f) => f.name));
1271
- const unknownKeys = Object.keys(values).filter((k) => !known.has(k));
1272
- if (unknownKeys.length) return errorResult(`Unknown fields: ${unknownKeys.join(", ")}`);
1273
- if (adapter.setValues) {
1274
- adapter.setValues(values);
1275
- } else {
1276
- for (const [k, v] of Object.entries(values)) adapter.setValue(k, v);
1277
- }
1278
- return textResult(`Set ${Object.keys(values).length} field(s)`, { values });
1279
- },
1280
- true
423
+ true
1281
424
  );
1282
425
  reg(
1283
426
  "form_focus",
@@ -1318,10 +461,10 @@ function registerFormBridge(server, options) {
1318
461
  }
1319
462
 
1320
463
  // src/bridges/sheets.ts
1321
- var DEFAULT_AGENT4 = { id: "agent", name: "Agent", color: "#a855f7" };
1322
- function registerSheetsBridge(server, options) {
464
+ var DEFAULT_AGENT2 = { id: "agent", name: "Agent", color: "#a855f7" };
465
+ function registerSheetsBridge(host, options) {
1323
466
  const { adapter } = options;
1324
- const agent = { ...DEFAULT_AGENT4, ...options.agent ?? {} };
467
+ const agent = { ...DEFAULT_AGENT2, ...options.agent ?? {} };
1325
468
  const disposers = [];
1326
469
  const target = (sheetId, address) => ({
1327
470
  kind: "sheet",
@@ -1345,7 +488,7 @@ function registerSheetsBridge(server, options) {
1345
488
  resolveTarget: ({ args, result }) => resolveTarget?.(args, result) ?? target(getSheetId(args))
1346
489
  }) : wrapped;
1347
490
  disposers.push(
1348
- server.registerTool(
491
+ host.registerTool(
1349
492
  {
1350
493
  name,
1351
494
  description,
@@ -1588,14 +731,93 @@ function readRange(sheet, startAddr, endAddr) {
1588
731
  }
1589
732
  grid.push(row);
1590
733
  }
1591
- return grid;
734
+ return grid;
735
+ }
736
+
737
+ // src/sheets-adapter.ts
738
+ init_registry();
739
+ function useSheetsAdapter(initial, options = {}) {
740
+ const [workbook, setWorkbook] = react.useState(initial);
741
+ const [activeCell, setActiveCellState] = react.useState(null);
742
+ const workbookRef = react.useRef(workbook);
743
+ workbookRef.current = workbook;
744
+ const setActiveCell = react.useCallback((sheetId, address) => {
745
+ setWorkbook((cur) => cur.activeSheetId === sheetId ? cur : { ...cur, activeSheetId: sheetId });
746
+ setActiveCellState(address);
747
+ }, []);
748
+ const onActiveCellChange = react.useCallback((address) => {
749
+ setActiveCellState(address);
750
+ }, []);
751
+ const setWorkbookRef = react.useRef(setWorkbook);
752
+ setWorkbookRef.current = setWorkbook;
753
+ const adapter = react.useMemo(
754
+ () => ({
755
+ screenId: options.screenId,
756
+ getWorkbook: () => workbookRef.current,
757
+ setWorkbook: (next) => setWorkbookRef.current(next),
758
+ setActiveCell
759
+ }),
760
+ [options.screenId, setActiveCell]
761
+ );
762
+ return {
763
+ workbook,
764
+ setWorkbook,
765
+ onActiveCellChange,
766
+ adapter,
767
+ setActiveCell,
768
+ activeCell
769
+ };
770
+ }
771
+ function useSheetsActivityHighlights(options = {}) {
772
+ const ttlMs = options.ttlMs ?? 2200;
773
+ const screenId = options.screenId;
774
+ const [, force] = react.useState(0);
775
+ const hitsRef = react.useRef(/* @__PURE__ */ new Map());
776
+ react.useEffect(() => {
777
+ const off = onActivity((event) => {
778
+ if (event.target?.kind !== "sheet") return;
779
+ if (screenId && event.target.screenId && event.target.screenId !== screenId) return;
780
+ const elementId = event.target.elementId;
781
+ if (!elementId || !elementId.includes("!")) return;
782
+ hitsRef.current.set(elementId, { event, expiresAt: Date.now() + ttlMs });
783
+ force((n) => n + 1);
784
+ });
785
+ return off;
786
+ }, [screenId, ttlMs]);
787
+ react.useEffect(() => {
788
+ const t = window.setInterval(() => {
789
+ const now = Date.now();
790
+ let dirty = false;
791
+ for (const [k, v] of hitsRef.current) {
792
+ if (v.expiresAt < now) {
793
+ hitsRef.current.delete(k);
794
+ dirty = true;
795
+ }
796
+ }
797
+ if (dirty) force((n) => n + 1);
798
+ }, 500);
799
+ return () => window.clearInterval(t);
800
+ }, []);
801
+ const out = {};
802
+ for (const [elementId, { event }] of hitsRef.current) {
803
+ const idx = elementId.indexOf("!");
804
+ const address = elementId.slice(idx + 1);
805
+ if (!address) continue;
806
+ const color = event.agentColor ?? "#a855f7";
807
+ out[address] = {
808
+ color,
809
+ backgroundColor: color + "33",
810
+ label: event.agentName ?? event.agentId ?? "agent"
811
+ };
812
+ }
813
+ return out;
1592
814
  }
1593
815
 
1594
816
  // src/bridges/code.ts
1595
- var DEFAULT_AGENT5 = { id: "agent", name: "Agent", color: "#a855f7" };
1596
- function registerCodeBridge(server, options) {
817
+ var DEFAULT_AGENT3 = { id: "agent", name: "Agent", color: "#a855f7" };
818
+ function registerCodeBridge(host, options) {
1597
819
  const { adapter } = options;
1598
- const agent = { ...DEFAULT_AGENT5, ...options.agent ?? {} };
820
+ const agent = { ...DEFAULT_AGENT3, ...options.agent ?? {} };
1599
821
  const disposers = [];
1600
822
  const target = {
1601
823
  kind: "code",
@@ -1619,7 +841,7 @@ function registerCodeBridge(server, options) {
1619
841
  resolveTarget: () => target
1620
842
  }) : wrapped;
1621
843
  disposers.push(
1622
- server.registerTool(
844
+ host.registerTool(
1623
845
  {
1624
846
  name,
1625
847
  description,
@@ -1758,10 +980,10 @@ function registerCodeBridge(server, options) {
1758
980
  }
1759
981
 
1760
982
  // src/bridges/charts.ts
1761
- var DEFAULT_AGENT6 = { id: "agent", name: "Agent", color: "#a855f7" };
1762
- function registerChartsBridge(server, options) {
983
+ var DEFAULT_AGENT4 = { id: "agent", name: "Agent", color: "#a855f7" };
984
+ function registerChartsBridge(host, options) {
1763
985
  const { adapter } = options;
1764
- const agent = { ...DEFAULT_AGENT6, ...options.agent ?? {} };
986
+ const agent = { ...DEFAULT_AGENT4, ...options.agent ?? {} };
1765
987
  const disposers = [];
1766
988
  const target = {
1767
989
  kind: "chart",
@@ -1785,7 +1007,7 @@ function registerChartsBridge(server, options) {
1785
1007
  resolveTarget: () => target
1786
1008
  }) : wrapped;
1787
1009
  disposers.push(
1788
- server.registerTool(
1010
+ host.registerTool(
1789
1011
  { name, description, inputSchema: { type: "object", properties, required, additionalProperties: false } },
1790
1012
  final
1791
1013
  )
@@ -1872,10 +1094,10 @@ function registerChartsBridge(server, options) {
1872
1094
  }
1873
1095
 
1874
1096
  // src/bridges/scene.ts
1875
- var DEFAULT_AGENT7 = { id: "agent", name: "Agent", color: "#a855f7" };
1876
- function registerSceneBridge(server, options) {
1097
+ var DEFAULT_AGENT5 = { id: "agent", name: "Agent", color: "#a855f7" };
1098
+ function registerSceneBridge(host, options) {
1877
1099
  const { adapter } = options;
1878
- const agent = { ...DEFAULT_AGENT7, ...options.agent ?? {} };
1100
+ const agent = { ...DEFAULT_AGENT5, ...options.agent ?? {} };
1879
1101
  const disposers = [];
1880
1102
  const target = (objectId) => ({
1881
1103
  kind: "scene",
@@ -1899,13 +1121,13 @@ function registerSceneBridge(server, options) {
1899
1121
  resolveTarget: ({ args }) => target(objectIdFromArgs?.(args))
1900
1122
  }) : wrapped;
1901
1123
  disposers.push(
1902
- server.registerTool(
1124
+ host.registerTool(
1903
1125
  { name, description, inputSchema: { type: "object", properties, required, additionalProperties: false } },
1904
1126
  final
1905
1127
  )
1906
1128
  );
1907
1129
  };
1908
- const newId3 = (kind) => `${kind}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
1130
+ const newId = (kind) => `${kind}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
1909
1131
  reg(
1910
1132
  "scene_describe",
1911
1133
  "Describe the scene \u2014 object count, kinds, camera position.",
@@ -1949,7 +1171,7 @@ function registerSceneBridge(server, options) {
1949
1171
  ["kind"],
1950
1172
  (args) => {
1951
1173
  const obj = {
1952
- id: newId3(String(args.kind)),
1174
+ id: newId(String(args.kind)),
1953
1175
  kind: String(args.kind),
1954
1176
  position: parseTriple(args.position),
1955
1177
  rotation: parseTriple(args.rotation),
@@ -2067,6 +1289,180 @@ function parseTriple(v) {
2067
1289
  if (out.some((x) => !Number.isFinite(x))) return void 0;
2068
1290
  return out;
2069
1291
  }
1292
+
1293
+ // src/bridges/screens.ts
1294
+ var DEFAULT_AGENT6 = { id: "agent", name: "Agent", color: "#a855f7" };
1295
+ function registerScreensBridge(host, options) {
1296
+ const { adapter } = options;
1297
+ const agent = { ...DEFAULT_AGENT6, ...options.agent ?? {} };
1298
+ const disposers = [];
1299
+ const target = (screenId) => ({
1300
+ kind: "screens",
1301
+ screenId,
1302
+ label: `Screen ${screenId}`
1303
+ });
1304
+ const reg = (name, description, properties, required, handler, isMutation, targetFromArgs) => {
1305
+ const wrapped = async (args) => {
1306
+ try {
1307
+ return await handler(args);
1308
+ } catch (e) {
1309
+ return errorResult(e instanceof Error ? e.message : String(e));
1310
+ }
1311
+ };
1312
+ const final = isMutation ? wrapToolWithActivity(wrapped, {
1313
+ toolName: name,
1314
+ agent,
1315
+ kind: "screens",
1316
+ resolveTarget: ({ args }) => targetFromArgs?.(args) ?? null
1317
+ }) : wrapped;
1318
+ disposers.push(
1319
+ host.registerTool(
1320
+ { name, description, inputSchema: { type: "object", properties, required, additionalProperties: false } },
1321
+ final
1322
+ )
1323
+ );
1324
+ };
1325
+ reg(
1326
+ "screens_list",
1327
+ "List every screen the host has registered. Returns id, title, active flag, and optional kind.",
1328
+ {},
1329
+ [],
1330
+ () => {
1331
+ const screens = adapter.listScreens();
1332
+ const text = screens.map((s) => `${s.active ? "\u25B8" : " "} ${s.id}${s.title ? ` \u2014 ${s.title}` : ""}${s.kind ? ` [${s.kind}]` : ""}`).join("\n");
1333
+ return textResult(text || "(no screens)", { screens, active: adapter.getActive() });
1334
+ },
1335
+ false
1336
+ );
1337
+ reg(
1338
+ "screens_describe_active",
1339
+ "Get the currently-active screen id (or null).",
1340
+ {},
1341
+ [],
1342
+ () => {
1343
+ const active = adapter.getActive();
1344
+ return textResult(active ?? "(none)", { active });
1345
+ },
1346
+ false
1347
+ );
1348
+ reg(
1349
+ "screens_list_kinds",
1350
+ "List the screen kinds (templates) the host knows how to instantiate. Use this before screens_create to know what's available.",
1351
+ {},
1352
+ [],
1353
+ () => {
1354
+ if (!adapter.listKinds) return errorResult("Host did not register a kind catalog. Cannot create screens dynamically.");
1355
+ const kinds = adapter.listKinds();
1356
+ const text = kinds.map((k) => `${k.kind}${k.label ? ` \u2014 ${k.label}` : ""}${k.description ? ` (${k.description})` : ""}`).join("\n");
1357
+ return textResult(text || "(no kinds registered)", kinds);
1358
+ },
1359
+ false
1360
+ );
1361
+ reg(
1362
+ "screens_create",
1363
+ "Instantiate a new screen from a template kind + config. Switches the active view to the new screen.",
1364
+ {
1365
+ id: { type: "string", description: "Stable screen id. Must be unique." },
1366
+ title: { type: "string" },
1367
+ kind: { type: "string", description: "Template kind \u2014 call screens_list_kinds for the catalog." },
1368
+ config: { type: "object", description: "Template-specific config (e.g. { fields: [...] } for a form)." }
1369
+ },
1370
+ ["id", "kind"],
1371
+ (args) => {
1372
+ if (!adapter.createScreen) return errorResult("Host did not provide createScreen.");
1373
+ const id = String(args.id);
1374
+ const kind = String(args.kind);
1375
+ if (adapter.listScreens().find((s) => s.id === id)) {
1376
+ return errorResult(`Screen ${id} already exists. Use screens_destroy first or pick a fresh id.`);
1377
+ }
1378
+ adapter.createScreen({
1379
+ id,
1380
+ title: typeof args.title === "string" ? args.title : void 0,
1381
+ kind,
1382
+ config: args.config && typeof args.config === "object" ? args.config : void 0
1383
+ });
1384
+ adapter.setActive(id);
1385
+ return textResult(`Created ${kind} screen "${id}"`, { id, kind });
1386
+ },
1387
+ true,
1388
+ (args) => target(String(args.id ?? ""))
1389
+ );
1390
+ reg(
1391
+ "screens_destroy",
1392
+ "Remove a previously-created screen. Active screen falls back to the first remaining one (or null).",
1393
+ { id: { type: "string" } },
1394
+ ["id"],
1395
+ (args) => {
1396
+ if (!adapter.destroyScreen) return errorResult("Host did not provide destroyScreen.");
1397
+ const id = String(args.id);
1398
+ if (!adapter.listScreens().find((s) => s.id === id)) {
1399
+ return errorResult(`No screen with id ${id}`);
1400
+ }
1401
+ adapter.destroyScreen(id);
1402
+ return textResult(`Destroyed screen ${id}`, { id });
1403
+ },
1404
+ true,
1405
+ (args) => target(String(args.id ?? ""))
1406
+ );
1407
+ reg(
1408
+ "screens_set_layout",
1409
+ "Change the layout of an existing composite screen. Layouts: 'single', 'split-h' (left/right), 'split-v' (top/bottom), 'grid-2x2', 'stack' (tabs).",
1410
+ {
1411
+ id: { type: "string" },
1412
+ layout: { type: "string", enum: ["single", "split-h", "split-v", "grid-2x2", "stack"] }
1413
+ },
1414
+ ["id", "layout"],
1415
+ (args) => {
1416
+ if (!adapter.updateScreenContent) return errorResult("Host did not provide updateScreenContent.");
1417
+ adapter.updateScreenContent(String(args.id), { layout: String(args.layout) });
1418
+ return textResult(`Layout of ${args.id} \u2192 ${args.layout}`, { id: args.id, layout: args.layout });
1419
+ },
1420
+ true,
1421
+ (args) => target(String(args.id ?? ""))
1422
+ );
1423
+ reg(
1424
+ "screens_update_content",
1425
+ "Merge new config into an existing screen (e.g. add a field to a form, append a sheet column, change chart series).",
1426
+ {
1427
+ id: { type: "string" },
1428
+ partial: { type: "object", description: "Shallow-merged into the screen's config." }
1429
+ },
1430
+ ["id", "partial"],
1431
+ (args) => {
1432
+ if (!adapter.updateScreenContent) return errorResult("Host did not provide updateScreenContent.");
1433
+ const id = String(args.id);
1434
+ const partial = args.partial && typeof args.partial === "object" ? args.partial : {};
1435
+ adapter.updateScreenContent(id, partial);
1436
+ return textResult(`Updated content of ${id}`, { id });
1437
+ },
1438
+ true,
1439
+ (args) => target(String(args.id ?? ""))
1440
+ );
1441
+ reg(
1442
+ "screens_navigate",
1443
+ "Switch the human's view to a different screen. The host updates its router / tab state and re-renders.",
1444
+ { screen: { type: "string", description: "Screen id to activate." } },
1445
+ ["screen"],
1446
+ (args) => {
1447
+ const screenId = String(args.screen ?? "");
1448
+ const screens = adapter.listScreens();
1449
+ if (!screens.find((s) => s.id === screenId)) {
1450
+ return errorResult(`No screen registered with id "${screenId}". Call screens_list first.`);
1451
+ }
1452
+ adapter.setActive(screenId);
1453
+ return textResult(`Navigated to ${screenId}`, { screen: screenId });
1454
+ },
1455
+ true,
1456
+ (args) => target(String(args.screen ?? ""))
1457
+ );
1458
+ return {
1459
+ id: "screens",
1460
+ title: "Screens",
1461
+ dispose: () => {
1462
+ for (const d of disposers) d();
1463
+ }
1464
+ };
1465
+ }
2070
1466
  function AgentPanel({ agent, activity, onSubmit, busy, actions, className, style }) {
2071
1467
  const scrollRef = react.useRef(null);
2072
1468
  const inputRef = react.useRef(null);
@@ -2432,72 +1828,245 @@ function ShareControls({
2432
1828
  label: "Paste into Claude Desktop / Cline MCP server config",
2433
1829
  value: JSON.stringify(config, null, 2)
2434
1830
  }
2435
- ),
2436
- tab === "curl" && /* @__PURE__ */ jsxRuntime.jsx(
2437
- CopyBox,
2438
- {
2439
- label: "Connect from a terminal (verifies the relay is reachable)",
2440
- value: curl,
2441
- multiline: true
1831
+ ),
1832
+ tab === "curl" && /* @__PURE__ */ jsxRuntime.jsx(
1833
+ CopyBox,
1834
+ {
1835
+ label: "Connect from a terminal (verifies the relay is reachable)",
1836
+ value: curl,
1837
+ multiline: true
1838
+ }
1839
+ )
1840
+ ] })
1841
+ ] });
1842
+ }
1843
+ function TabButton({ tab, active, setTab, children }) {
1844
+ return /* @__PURE__ */ jsxRuntime.jsx(
1845
+ "button",
1846
+ {
1847
+ type: "button",
1848
+ role: "tab",
1849
+ "aria-selected": tab === active,
1850
+ className: `fai-share__tab${tab === active ? " is-active" : ""}`,
1851
+ onClick: () => setTab(tab),
1852
+ children
1853
+ }
1854
+ );
1855
+ }
1856
+ function CopyBox({ label, value, multiline }) {
1857
+ const [copied, setCopied] = react.useState(false);
1858
+ const copy = async () => {
1859
+ try {
1860
+ await navigator.clipboard.writeText(value);
1861
+ setCopied(true);
1862
+ setTimeout(() => setCopied(false), 1200);
1863
+ } catch {
1864
+ }
1865
+ };
1866
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1867
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fai-share__panel-label", children: label }),
1868
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fai-share__copy", children: [
1869
+ /* @__PURE__ */ jsxRuntime.jsx("pre", { className: `fai-share__pre${multiline ? " is-multi" : ""}`, children: value }),
1870
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "fai-share__copy-btn", onClick: copy, children: copied ? "Copied" : "Copy" })
1871
+ ] })
1872
+ ] });
1873
+ }
1874
+ function buildCurlRecipe(session) {
1875
+ const base = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.host}` : "http://localhost";
1876
+ const inbox = `${base}/whiteboard-share/${session.id}/inbox?token=${session.token}`;
1877
+ const events = `${base}/whiteboard-share/${session.id}/events?token=${session.token}`;
1878
+ return [
1879
+ `# 1) In one terminal, subscribe to server-pushed frames (SSE)`,
1880
+ `curl -N "${events}"`,
1881
+ ``,
1882
+ `# 2) In another terminal, send an initialize handshake`,
1883
+ `curl -X POST "${inbox}" \\`,
1884
+ ` -H 'content-type: application/json' \\`,
1885
+ ` -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'`,
1886
+ ``,
1887
+ `# 3) List the tools the bridge exposes`,
1888
+ `curl -X POST "${inbox}" \\`,
1889
+ ` -H 'content-type: application/json' \\`,
1890
+ ` -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'`,
1891
+ ``,
1892
+ `# 4) Add a sticky note`,
1893
+ `curl -X POST "${inbox}" \\`,
1894
+ ` -H 'content-type: application/json' \\`,
1895
+ ` -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whiteboard_add_sticky","arguments":{"x":300,"y":300,"text":"hello from curl"}}}'`
1896
+ ].join("\n");
1897
+ }
1898
+
1899
+ // src/presence/index.ts
1900
+ init_registry();
1901
+
1902
+ // src/presence/use-agent-activity.ts
1903
+ init_registry();
1904
+ function useAgentActivity(filter, options = {}) {
1905
+ const cap = options.capacity ?? 50;
1906
+ const [events, setEvents] = react.useState(() => readActivityHistory(filter).slice(-cap));
1907
+ react.useEffect(() => {
1908
+ setEvents(readActivityHistory(filter).slice(-cap));
1909
+ return onActivity((event) => {
1910
+ setEvents((prev) => {
1911
+ const next = prev.length >= cap ? prev.slice(prev.length - cap + 1) : prev.slice();
1912
+ next.push(event);
1913
+ return next;
1914
+ });
1915
+ }, filter);
1916
+ }, [filter?.agentId, filter?.screenId, filter?.kind, cap]);
1917
+ return { events, latest: events.length > 0 ? events[events.length - 1] : null };
1918
+ }
1919
+ function useAgentActivityForScreen(screenId, options = {}) {
1920
+ const { events, latest } = useAgentActivity({ screenId }, options);
1921
+ const fadeAfter = latest?.ttlMs ?? 1500;
1922
+ const [isAgentActive, setActive] = react.useState(false);
1923
+ react.useEffect(() => {
1924
+ if (!latest) {
1925
+ setActive(false);
1926
+ return;
1927
+ }
1928
+ setActive(true);
1929
+ const timer = setTimeout(() => setActive(false), fadeAfter);
1930
+ return () => clearTimeout(timer);
1931
+ }, [latest, fadeAfter]);
1932
+ return { events, latest, isAgentActive };
1933
+ }
1934
+
1935
+ // src/undo/undo-stack.ts
1936
+ var stacks = /* @__PURE__ */ new Map();
1937
+ var CAP = 200;
1938
+ function getStack(agentId) {
1939
+ let s = stacks.get(agentId);
1940
+ if (!s) {
1941
+ s = { past: [], future: [] };
1942
+ stacks.set(agentId, s);
1943
+ }
1944
+ return s;
1945
+ }
1946
+ function pushUndoEntry(agentId, entry) {
1947
+ const s = getStack(agentId);
1948
+ s.past.push(entry);
1949
+ if (s.past.length > CAP) s.past.splice(0, s.past.length - CAP);
1950
+ s.future.length = 0;
1951
+ }
1952
+ async function undoOne(agentId) {
1953
+ const s = getStack(agentId);
1954
+ const entry = s.past.pop();
1955
+ if (!entry) return null;
1956
+ await entry.undo();
1957
+ s.future.push(entry);
1958
+ return entry;
1959
+ }
1960
+ async function redoOne(agentId) {
1961
+ const s = getStack(agentId);
1962
+ const entry = s.future.pop();
1963
+ if (!entry) return null;
1964
+ await entry.redo();
1965
+ s.past.push(entry);
1966
+ return entry;
1967
+ }
1968
+ function readHistory(agentId) {
1969
+ return getStack(agentId).past.slice();
1970
+ }
1971
+ function clearStack(agentId) {
1972
+ stacks.delete(agentId);
1973
+ }
1974
+ function resetAllUndoStacks() {
1975
+ stacks.clear();
1976
+ }
1977
+
1978
+ // src/undo/undo-tools.ts
1979
+ var installedHosts = /* @__PURE__ */ new WeakSet();
1980
+ function ensureUndoToolsRegistered(host, options = {}) {
1981
+ if (installedHosts.has(host)) return;
1982
+ installedHosts.add(host);
1983
+ registerUndoTools(host, options);
1984
+ }
1985
+ function registerUndoTools(host, options = {}) {
1986
+ const defaultAgent = options.defaultAgentId ?? "agent";
1987
+ const disposers = [];
1988
+ const agentOf = (args) => typeof args?.agentId === "string" ? args.agentId : defaultAgent;
1989
+ disposers.push(
1990
+ host.registerTool(
1991
+ {
1992
+ name: "agent_undo",
1993
+ description: "Undo the most recent action on the agent's stack. Optional agentId targets a specific agent.",
1994
+ inputSchema: {
1995
+ type: "object",
1996
+ properties: { agentId: { type: "string" } },
1997
+ additionalProperties: false
1998
+ }
1999
+ },
2000
+ async (args) => {
2001
+ const entry = await undoOne(agentOf(args));
2002
+ if (!entry) return errorResult("Nothing to undo.");
2003
+ return textResult(`Undid: ${entry.label}`, { entry: serialize(entry) });
2004
+ }
2005
+ )
2006
+ );
2007
+ disposers.push(
2008
+ host.registerTool(
2009
+ {
2010
+ name: "agent_redo",
2011
+ description: "Redo the most recently undone action.",
2012
+ inputSchema: {
2013
+ type: "object",
2014
+ properties: { agentId: { type: "string" } },
2015
+ additionalProperties: false
2442
2016
  }
2443
- )
2444
- ] })
2445
- ] });
2446
- }
2447
- function TabButton({ tab, active, setTab, children }) {
2448
- return /* @__PURE__ */ jsxRuntime.jsx(
2449
- "button",
2450
- {
2451
- type: "button",
2452
- role: "tab",
2453
- "aria-selected": tab === active,
2454
- className: `fai-share__tab${tab === active ? " is-active" : ""}`,
2455
- onClick: () => setTab(tab),
2456
- children
2457
- }
2017
+ },
2018
+ async (args) => {
2019
+ const entry = await redoOne(agentOf(args));
2020
+ if (!entry) return errorResult("Nothing to redo.");
2021
+ return textResult(`Redid: ${entry.label}`, { entry: serialize(entry) });
2022
+ }
2023
+ )
2024
+ );
2025
+ disposers.push(
2026
+ host.registerTool(
2027
+ {
2028
+ name: "agent_history",
2029
+ description: "List the agent's undo stack (oldest first). Useful for understanding what's reversible.",
2030
+ inputSchema: {
2031
+ type: "object",
2032
+ properties: { agentId: { type: "string" } },
2033
+ additionalProperties: false
2034
+ }
2035
+ },
2036
+ async (args) => {
2037
+ const history2 = readHistory(agentOf(args)).map(serialize);
2038
+ const text = history2.map((e) => `${new Date(e.timestamp).toISOString()} ${e.bridgeId} ${e.action}: ${e.label}`).join("\n");
2039
+ return textResult(text || "(empty)", history2);
2040
+ }
2041
+ )
2458
2042
  );
2043
+ return () => disposers.forEach((d) => d());
2459
2044
  }
2460
- function CopyBox({ label, value, multiline }) {
2461
- const [copied, setCopied] = react.useState(false);
2462
- const copy = async () => {
2463
- try {
2464
- await navigator.clipboard.writeText(value);
2465
- setCopied(true);
2466
- setTimeout(() => setCopied(false), 1200);
2467
- } catch {
2468
- }
2045
+ function serialize(entry) {
2046
+ return {
2047
+ timestamp: entry.timestamp,
2048
+ bridgeId: entry.bridgeId,
2049
+ action: entry.action,
2050
+ label: entry.label
2469
2051
  };
2470
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2471
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fai-share__panel-label", children: label }),
2472
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fai-share__copy", children: [
2473
- /* @__PURE__ */ jsxRuntime.jsx("pre", { className: `fai-share__pre${multiline ? " is-multi" : ""}`, children: value }),
2474
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "fai-share__copy-btn", onClick: copy, children: copied ? "Copied" : "Copy" })
2475
- ] })
2476
- ] });
2477
2052
  }
2478
- function buildCurlRecipe(session) {
2479
- const base = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.host}` : "http://localhost";
2480
- const inbox = `${base}/whiteboard-share/${session.id}/inbox?token=${session.token}`;
2481
- const events = `${base}/whiteboard-share/${session.id}/events?token=${session.token}`;
2482
- return [
2483
- `# 1) In one terminal, subscribe to server-pushed frames (SSE)`,
2484
- `curl -N "${events}"`,
2485
- ``,
2486
- `# 2) In another terminal, send an initialize handshake`,
2487
- `curl -X POST "${inbox}" \\`,
2488
- ` -H 'content-type: application/json' \\`,
2489
- ` -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'`,
2490
- ``,
2491
- `# 3) List the tools the bridge exposes`,
2492
- `curl -X POST "${inbox}" \\`,
2493
- ` -H 'content-type: application/json' \\`,
2494
- ` -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'`,
2495
- ``,
2496
- `# 4) Add a sticky note`,
2497
- `curl -X POST "${inbox}" \\`,
2498
- ` -H 'content-type: application/json' \\`,
2499
- ` -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whiteboard_add_sticky","arguments":{"x":300,"y":300,"text":"hello from curl"}}}'`
2500
- ].join("\n");
2053
+ function useUndoStack(agentId, intervalMs = 500) {
2054
+ const [history2, setHistory] = react.useState(() => readHistory(agentId));
2055
+ react.useEffect(() => {
2056
+ let cancelled = false;
2057
+ const tick = () => {
2058
+ if (cancelled) return;
2059
+ setHistory(readHistory(agentId));
2060
+ };
2061
+ const id = setInterval(tick, intervalMs);
2062
+ tick();
2063
+ return () => {
2064
+ cancelled = true;
2065
+ clearInterval(id);
2066
+ };
2067
+ }, [agentId, intervalMs]);
2068
+ const refresh = react.useCallback(() => setHistory(readHistory(agentId)), [agentId]);
2069
+ return { history: history2, refresh };
2501
2070
  }
2502
2071
 
2503
2072
  // src/sharing/sse-relay.ts
@@ -2612,329 +2181,6 @@ function attachSseRelay(server, options) {
2612
2181
  });
2613
2182
  return transport;
2614
2183
  }
2615
- var DEFAULT_AGENT8 = { id: "agent", name: "Agent", color: "#a855f7" };
2616
- function SharedWhiteboard({
2617
- initialNotes = [],
2618
- initialShapes = [],
2619
- initialConnectors = [],
2620
- initialStrokes = [],
2621
- initialViewport = { x: 0, y: 0, zoom: 1 },
2622
- agent = DEFAULT_AGENT8,
2623
- shareBaseUrl = "/whiteboard-share",
2624
- onRegisterSession,
2625
- showAgentPanel = true,
2626
- showShareControls = true,
2627
- broadcastEdits = true,
2628
- height = 640,
2629
- header,
2630
- className,
2631
- style
2632
- }) {
2633
- const [notes, setNotes] = react.useState(initialNotes);
2634
- const [shapes, setShapes] = react.useState(initialShapes);
2635
- const [connectors, setConnectors] = react.useState(initialConnectors);
2636
- const [strokes, setStrokes] = react.useState(initialStrokes);
2637
- const [viewport, setViewport] = react.useState(initialViewport);
2638
- const [agentCursor, setAgentCursor] = react.useState(null);
2639
- const [activity, setActivity] = react.useState([]);
2640
- const [highlight, setHighlight] = react.useState(null);
2641
- const stateRefs = react.useRef({ notes, shapes, connectors, strokes, viewport });
2642
- react.useEffect(() => {
2643
- stateRefs.current = { notes, shapes, connectors, strokes, viewport };
2644
- }, [notes, shapes, connectors, strokes, viewport]);
2645
- const serverRef = react.useRef(null);
2646
- const inProcRef = react.useRef(null);
2647
- const bridgeRef = react.useRef(null);
2648
- react.useEffect(() => {
2649
- const server = new MicroMcpServer({
2650
- info: { name: "shared-whiteboard", version: "0.2.0" },
2651
- instructions: "Collaborative whiteboard. Use whiteboard_* tools to read or modify the board."
2652
- });
2653
- bridgeRef.current = registerWhiteboardBridge(server, {
2654
- adapter: {
2655
- getNotes: () => stateRefs.current.notes,
2656
- setNotes: (next) => setNotes(typeof next === "function" ? next : () => next),
2657
- getShapes: () => stateRefs.current.shapes,
2658
- setShapes: (next) => setShapes(typeof next === "function" ? next : () => next),
2659
- getConnectors: () => stateRefs.current.connectors,
2660
- setConnectors: (next) => setConnectors(typeof next === "function" ? next : () => next),
2661
- getStrokes: () => stateRefs.current.strokes,
2662
- setStrokes: (next) => setStrokes(typeof next === "function" ? next : () => next),
2663
- getViewport: () => stateRefs.current.viewport,
2664
- setViewport,
2665
- setAgentCursor
2666
- },
2667
- agent
2668
- });
2669
- inProcRef.current = attachInProcess(server);
2670
- serverRef.current = server;
2671
- const off = inProcRef.current.onServerMessage((msg) => {
2672
- if (msg?.id !== void 0 && "result" in msg && msg.result?.structuredContent?.id) {
2673
- const id = msg.result.structuredContent.id;
2674
- requestAnimationFrame(() => pulseFor(id));
2675
- }
2676
- });
2677
- return () => {
2678
- off();
2679
- bridgeRef.current?.dispose();
2680
- bridgeRef.current = null;
2681
- if (inProcRef.current) server.detach(inProcRef.current);
2682
- };
2683
- }, []);
2684
- const pulseFor = (id) => {
2685
- const n = stateRefs.current.notes.find((x) => x.id === id);
2686
- if (n) return setHighlight({ pulseKey: Date.now(), bounds: { x: n.x, y: n.y, width: n.width, height: n.height } });
2687
- const s = stateRefs.current.shapes.find((x) => x.id === id);
2688
- if (s) return setHighlight({ pulseKey: Date.now(), bounds: { x: s.x, y: s.y, width: s.width, height: s.height } });
2689
- };
2690
- const log = react.useCallback((entry) => {
2691
- setActivity((all) => [...all.slice(-200), { id: `a_${Date.now()}_${all.length}`, at: Date.now(), ...entry }]);
2692
- }, []);
2693
- const [session, setSession] = react.useState(null);
2694
- const [relayState, setRelayState] = react.useState("idle");
2695
- const sseRef = react.useRef(null);
2696
- const logEsRef = react.useRef(null);
2697
- const startShare = async () => {
2698
- if (session || !serverRef.current || !shareBaseUrl) return;
2699
- const desc = createSessionDescriptor();
2700
- try {
2701
- if (onRegisterSession) {
2702
- await onRegisterSession(desc);
2703
- } else {
2704
- const csrf = document.querySelector('meta[name="csrf-token"]')?.content ?? "";
2705
- const reg = await fetch(`${shareBaseUrl}/register`, {
2706
- method: "POST",
2707
- headers: { "content-type": "application/json", "x-csrf-token": csrf, accept: "application/json" },
2708
- body: JSON.stringify({ session: desc.id, token: desc.token })
2709
- });
2710
- if (!reg.ok) throw new Error(`registration failed (HTTP ${reg.status})`);
2711
- }
2712
- } catch (e) {
2713
- log({ kind: "error", source: "share", text: e instanceof Error ? e.message : String(e) });
2714
- return;
2715
- }
2716
- const relay = attachSseRelay(serverRef.current, {
2717
- baseUrl: shareBaseUrl,
2718
- sessionId: desc.id,
2719
- token: desc.token
2720
- });
2721
- sseRef.current = relay;
2722
- relay.onStateChange(setRelayState);
2723
- const es = new EventSource(`${shareBaseUrl}/${desc.id}/events?token=${desc.token}&direction=inbound`);
2724
- es.addEventListener("mcp", (ev) => {
2725
- try {
2726
- const frame = JSON.parse(ev.data);
2727
- if (frame.method === "notifications/peer_joined") {
2728
- setAgentCursor((c) => c ?? { userId: agent.id, name: agent.name, color: agent.color, x: 60, y: 60 });
2729
- log({ kind: "info", source: "presence", text: `${agent.name ?? "Agent"} connected` });
2730
- return;
2731
- }
2732
- if (frame.method === "notifications/peer_left") {
2733
- setAgentCursor(null);
2734
- log({ kind: "info", source: "presence", text: `${agent.name ?? "Agent"} disconnected` });
2735
- return;
2736
- }
2737
- if (frame.method === "notifications/agent_message") {
2738
- log({ kind: "message", source: agent.name ?? "Agent", text: String(frame.params?.text ?? "") });
2739
- } else if (frame.method === "notifications/agent_status") {
2740
- log({ kind: "info", source: agent.name ?? "Agent", text: String(frame.params?.text ?? "") });
2741
- } else if (frame.method?.startsWith("notifications/")) {
2742
- } else {
2743
- log({ kind: "tool", source: "remote", text: `\u2190 ${frame.method ?? `id:${frame.id}`}`, detail: frame });
2744
- }
2745
- } catch {
2746
- }
2747
- });
2748
- logEsRef.current = es;
2749
- setSession(desc);
2750
- log({ kind: "info", source: "share", text: `Sharing started \xB7 session ${desc.id}` });
2751
- };
2752
- const stopShare = async () => {
2753
- if (!session) return;
2754
- const desc = session;
2755
- setSession(null);
2756
- logEsRef.current?.close();
2757
- logEsRef.current = null;
2758
- if (sseRef.current && serverRef.current) serverRef.current.detach(sseRef.current);
2759
- sseRef.current = null;
2760
- setRelayState("closed");
2761
- if (shareBaseUrl) {
2762
- const csrf = document.querySelector('meta[name="csrf-token"]')?.content ?? "";
2763
- await fetch(`${shareBaseUrl}/${desc.id}/unregister?token=${encodeURIComponent(desc.token)}`, {
2764
- method: "POST",
2765
- headers: { "x-csrf-token": csrf, accept: "application/json" }
2766
- }).catch(() => {
2767
- });
2768
- }
2769
- log({ kind: "info", source: "share", text: "Sharing stopped." });
2770
- };
2771
- const lastBroadcastRef = react.useRef(0);
2772
- react.useEffect(() => {
2773
- if (!broadcastEdits || !sseRef.current || !session) return;
2774
- const now = Date.now();
2775
- if (now - lastBroadcastRef.current < 80) return;
2776
- lastBroadcastRef.current = now;
2777
- sseRef.current.send({
2778
- jsonrpc: "2.0",
2779
- method: "notifications/state_update",
2780
- params: { notes, shapes, connectors, viewport, ts: now }
2781
- });
2782
- }, [notes, shapes, connectors, viewport, session, broadcastEdits]);
2783
- const handleSubmit = (text) => {
2784
- if (!sseRef.current) {
2785
- log({ kind: "error", source: "you", text: "Start a shared session first." });
2786
- return;
2787
- }
2788
- sseRef.current.send({
2789
- jsonrpc: "2.0",
2790
- method: "notifications/user_message",
2791
- params: { text, ts: Date.now() }
2792
- });
2793
- log({ kind: "message", source: "You", text });
2794
- };
2795
- const cursors = react.useMemo(() => [], []);
2796
- const statusText = (() => {
2797
- switch (relayState) {
2798
- case "open":
2799
- return "live";
2800
- case "connecting":
2801
- return "connecting\u2026";
2802
- case "error":
2803
- return "error";
2804
- case "closed":
2805
- return "closed";
2806
- default:
2807
- return void 0;
2808
- }
2809
- })();
2810
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["fai-shared-whiteboard", className ?? ""].filter(Boolean).join(" "), style, children: [
2811
- header,
2812
- showShareControls && shareBaseUrl !== null && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fai-shared-whiteboard__controls", children: /* @__PURE__ */ jsxRuntime.jsx(ShareControls, { session, onStart: startShare, onStop: stopShare, status: statusText }) }),
2813
- /* @__PURE__ */ jsxRuntime.jsxs(
2814
- "div",
2815
- {
2816
- className: "fai-shared-whiteboard__layout",
2817
- style: {
2818
- display: "grid",
2819
- gap: 16,
2820
- gridTemplateColumns: showAgentPanel ? "1fr 360px" : "1fr"
2821
- },
2822
- children: [
2823
- /* @__PURE__ */ jsxRuntime.jsx(
2824
- "div",
2825
- {
2826
- className: "fai-shared-whiteboard__board",
2827
- style: {
2828
- position: "relative",
2829
- overflow: "hidden",
2830
- borderRadius: 12,
2831
- border: "1px solid #e4e4e7",
2832
- background: "radial-gradient(circle at 1px 1px, rgba(0,0,0,0.07) 1px, transparent 0)",
2833
- backgroundSize: "20px 20px",
2834
- height
2835
- },
2836
- children: /* @__PURE__ */ jsxRuntime.jsxs(fancyWhiteboard.Board, { viewport, onViewportChange: setViewport, style: { width: "100%", height: "100%" }, children: [
2837
- connectors.map((c) => {
2838
- const a = resolveCenter(c.from, notes, shapes);
2839
- const b = resolveCenter(c.to, notes, shapes);
2840
- if (!a || !b) return null;
2841
- return /* @__PURE__ */ jsxRuntime.jsx(fancyWhiteboard.Connector, { from: a, to: b, color: c.color ?? "#64748b" }, c.id);
2842
- }),
2843
- 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)),
2844
- 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)),
2845
- /* @__PURE__ */ jsxRuntime.jsx(fancyWhiteboard.CursorLayer, { cursors }),
2846
- agentCursor && /* @__PURE__ */ jsxRuntime.jsx(AgentCursor, { x: agentCursor.x, y: agentCursor.y, name: agentCursor.name, color: agentCursor.color }),
2847
- highlight && /* @__PURE__ */ jsxRuntime.jsx(
2848
- AgentActivityHighlight,
2849
- {
2850
- x: highlight.bounds.x,
2851
- y: highlight.bounds.y,
2852
- width: highlight.bounds.width,
2853
- height: highlight.bounds.height,
2854
- color: agent.color ?? "#a855f7",
2855
- pulseKey: highlight.pulseKey
2856
- }
2857
- )
2858
- ] })
2859
- }
2860
- ),
2861
- showAgentPanel && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height }, children: /* @__PURE__ */ jsxRuntime.jsx(
2862
- AgentPanel,
2863
- {
2864
- agent,
2865
- activity,
2866
- onSubmit: handleSubmit
2867
- }
2868
- ) })
2869
- ]
2870
- }
2871
- )
2872
- ] });
2873
- }
2874
- function resolveCenter(ref, notes, shapes) {
2875
- if (typeof ref === "string") {
2876
- const n = notes.find((x) => x.id === ref);
2877
- if (n) return { x: n.x + n.width / 2, y: n.y + n.height / 2 };
2878
- const s = shapes.find((x) => x.id === ref);
2879
- if (s) return { x: s.x + s.width / 2, y: s.y + s.height / 2 };
2880
- return null;
2881
- }
2882
- return ref;
2883
- }
2884
-
2885
- // src/presence/index.ts
2886
- init_registry();
2887
-
2888
- // src/presence/use-agent-activity.ts
2889
- init_registry();
2890
- function useAgentActivity(filter, options = {}) {
2891
- const cap = options.capacity ?? 50;
2892
- const [events, setEvents] = react.useState(() => readActivityHistory(filter).slice(-cap));
2893
- react.useEffect(() => {
2894
- setEvents(readActivityHistory(filter).slice(-cap));
2895
- return onActivity((event) => {
2896
- setEvents((prev) => {
2897
- const next = prev.length >= cap ? prev.slice(prev.length - cap + 1) : prev.slice();
2898
- next.push(event);
2899
- return next;
2900
- });
2901
- }, filter);
2902
- }, [filter?.agentId, filter?.screenId, filter?.kind, cap]);
2903
- return { events, latest: events.length > 0 ? events[events.length - 1] : null };
2904
- }
2905
- function useAgentActivityForScreen(screenId, options = {}) {
2906
- const { events, latest } = useAgentActivity({ screenId }, options);
2907
- const fadeAfter = latest?.ttlMs ?? 1500;
2908
- const [isAgentActive, setActive] = react.useState(false);
2909
- react.useEffect(() => {
2910
- if (!latest) {
2911
- setActive(false);
2912
- return;
2913
- }
2914
- setActive(true);
2915
- const timer = setTimeout(() => setActive(false), fadeAfter);
2916
- return () => clearTimeout(timer);
2917
- }, [latest, fadeAfter]);
2918
- return { events, latest, isAgentActive };
2919
- }
2920
- function useUndoStack(agentId, intervalMs = 500) {
2921
- const [history2, setHistory] = react.useState(() => readHistory(agentId));
2922
- react.useEffect(() => {
2923
- let cancelled = false;
2924
- const tick = () => {
2925
- if (cancelled) return;
2926
- setHistory(readHistory(agentId));
2927
- };
2928
- const id = setInterval(tick, intervalMs);
2929
- tick();
2930
- return () => {
2931
- cancelled = true;
2932
- clearInterval(id);
2933
- };
2934
- }, [agentId, intervalMs]);
2935
- const refresh = react.useCallback(() => setHistory(readHistory(agentId)), [agentId]);
2936
- return { history: history2, refresh };
2937
- }
2938
2184
 
2939
2185
  exports.AgentActivityHighlight = AgentActivityHighlight;
2940
2186
  exports.AgentCursor = AgentCursor;
@@ -2946,8 +2192,8 @@ exports.MicroMcpServer = MicroMcpServer;
2946
2192
  exports.RelayTransport = RelayTransport;
2947
2193
  exports.ScreensActivityBridge = ScreensActivityBridge;
2948
2194
  exports.ShareControls = ShareControls;
2949
- exports.SharedWhiteboard = SharedWhiteboard;
2950
2195
  exports.SseRelayTransport = SseRelayTransport;
2196
+ exports.ToolRegistry = ToolRegistry;
2951
2197
  exports.attachInProcess = attachInProcess;
2952
2198
  exports.attachRelay = attachRelay;
2953
2199
  exports.attachSseRelay = attachSseRelay;
@@ -2967,12 +2213,11 @@ exports.readUndoHistory = readHistory;
2967
2213
  exports.redoOne = redoOne;
2968
2214
  exports.registerChartsBridge = registerChartsBridge;
2969
2215
  exports.registerCodeBridge = registerCodeBridge;
2970
- exports.registerFlowBridge = registerFlowBridge;
2971
2216
  exports.registerFormBridge = registerFormBridge;
2972
2217
  exports.registerSceneBridge = registerSceneBridge;
2218
+ exports.registerScreensBridge = registerScreensBridge;
2973
2219
  exports.registerSheetsBridge = registerSheetsBridge;
2974
2220
  exports.registerUndoTools = registerUndoTools;
2975
- exports.registerWhiteboardBridge = registerWhiteboardBridge;
2976
2221
  exports.resetActivityRegistry = resetActivityRegistry;
2977
2222
  exports.resetAllUndoStacks = resetAllUndoStacks;
2978
2223
  exports.rpcError = rpcError;
@@ -2980,6 +2225,8 @@ exports.textResult = textResult;
2980
2225
  exports.undoOne = undoOne;
2981
2226
  exports.useAgentActivity = useAgentActivity;
2982
2227
  exports.useAgentActivityForScreen = useAgentActivityForScreen;
2228
+ exports.useSheetsActivityHighlights = useSheetsActivityHighlights;
2229
+ exports.useSheetsAdapter = useSheetsAdapter;
2983
2230
  exports.useUndoStack = useUndoStack;
2984
2231
  exports.wrapToolWithActivity = wrapToolWithActivity;
2985
2232
  //# sourceMappingURL=index.cjs.map