@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.
- package/README.md +65 -0
- package/dist/bridges/charts.d.cts +4 -4
- package/dist/bridges/charts.d.ts +4 -4
- package/dist/bridges/code.d.cts +4 -4
- package/dist/bridges/code.d.ts +4 -4
- package/dist/bridges/flow.d.cts +4 -4
- package/dist/bridges/flow.d.ts +4 -4
- package/dist/bridges/forms.d.cts +4 -4
- package/dist/bridges/forms.d.ts +4 -4
- package/dist/bridges/scene.d.cts +4 -4
- package/dist/bridges/scene.d.ts +4 -4
- package/dist/bridges/screens.d.cts +78 -0
- package/dist/bridges/screens.d.ts +78 -0
- package/dist/bridges/sheets.d.cts +4 -4
- package/dist/bridges/sheets.d.ts +4 -4
- package/dist/bridges/whiteboard.d.cts +4 -4
- package/dist/bridges/whiteboard.d.ts +4 -4
- package/dist/bridges-charts.cjs +2 -2
- package/dist/bridges-charts.cjs.map +1 -1
- package/dist/bridges-charts.js +2 -2
- package/dist/bridges-code.cjs +2 -2
- package/dist/bridges-code.cjs.map +1 -1
- package/dist/bridges-code.js +2 -2
- package/dist/bridges-flow.cjs +2 -2
- package/dist/bridges-flow.cjs.map +1 -1
- package/dist/bridges-flow.js +340 -3
- package/dist/bridges-flow.js.map +1 -1
- package/dist/bridges-forms.cjs +2 -2
- package/dist/bridges-forms.cjs.map +1 -1
- package/dist/bridges-forms.js +2 -2
- package/dist/bridges-scene.cjs +2 -2
- package/dist/bridges-scene.cjs.map +1 -1
- package/dist/bridges-scene.js +2 -2
- package/dist/bridges-screens.cjs +227 -0
- package/dist/bridges-screens.cjs.map +1 -0
- package/dist/bridges-screens.js +6 -0
- package/dist/bridges-screens.js.map +1 -0
- package/dist/bridges-sheets.cjs +2 -2
- package/dist/bridges-sheets.cjs.map +1 -1
- package/dist/bridges-sheets.js +2 -2
- package/dist/bridges-whiteboard.cjs +12 -12
- package/dist/bridges-whiteboard.cjs.map +1 -1
- package/dist/bridges-whiteboard.js +3 -3
- package/dist/{chunk-TBEITXF4.js → chunk-3KSZNGNW.js} +7 -7
- package/dist/chunk-3KSZNGNW.js.map +1 -0
- package/dist/{chunk-OEIULP2L.js → chunk-4BL5M3U3.js} +5 -5
- package/dist/chunk-4BL5M3U3.js.map +1 -0
- package/dist/{chunk-QGCF7YKW.js → chunk-4KAIV6OD.js} +40 -12
- package/dist/chunk-4KAIV6OD.js.map +1 -0
- package/dist/chunk-57ZDHD53.js +180 -0
- package/dist/chunk-57ZDHD53.js.map +1 -0
- package/dist/chunk-5XELJIJR.js +83 -0
- package/dist/chunk-5XELJIJR.js.map +1 -0
- package/dist/{chunk-6LTKCNLF.js → chunk-AFUULW5E.js} +3 -34
- package/dist/chunk-AFUULW5E.js.map +1 -0
- package/dist/chunk-G6N2TQVO.js +34 -0
- package/dist/chunk-G6N2TQVO.js.map +1 -0
- package/dist/{chunk-ACBENYYO.js → chunk-GQ7XXK7G.js} +12 -12
- package/dist/chunk-GQ7XXK7G.js.map +1 -0
- package/dist/{chunk-XYYSTJHW.js → chunk-HSTW7ZNO.js} +5 -5
- package/dist/chunk-HSTW7ZNO.js.map +1 -0
- package/dist/{chunk-PDBF4W7E.js → chunk-IANI25IT.js} +5 -5
- package/dist/chunk-IANI25IT.js.map +1 -0
- package/dist/chunk-IJ6JX5VC.js +3 -0
- package/dist/chunk-IJ6JX5VC.js.map +1 -0
- package/dist/{chunk-JMYPUAFH.js → chunk-LVQXIUJH.js} +2 -2
- package/dist/{chunk-JMYPUAFH.js.map → chunk-LVQXIUJH.js.map} +1 -1
- package/dist/{chunk-PHPXKSWI.js → chunk-NTDZWGYB.js} +5 -5
- package/dist/chunk-NTDZWGYB.js.map +1 -0
- package/dist/{chunk-DJOWMF6Q.js → chunk-RGO42EQ6.js} +3 -3
- package/dist/{chunk-DJOWMF6Q.js.map → chunk-RGO42EQ6.js.map} +1 -1
- package/dist/{chunk-QJUTISFC.js → chunk-XRAJSOPS.js} +5 -5
- package/dist/chunk-XRAJSOPS.js.map +1 -0
- package/dist/chunk-ZHAK2DQR.js +289 -0
- package/dist/chunk-ZHAK2DQR.js.map +1 -0
- package/dist/components/SharedWhiteboard/index.d.cts +55 -0
- package/dist/components/SharedWhiteboard/index.d.ts +55 -0
- package/dist/components-shared-whiteboard.cjs +1533 -0
- package/dist/components-shared-whiteboard.cjs.map +1 -0
- package/dist/components-shared-whiteboard.js +285 -0
- package/dist/components-shared-whiteboard.js.map +1 -0
- package/dist/index.cjs +618 -1371
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -59
- package/dist/index.d.ts +11 -59
- package/dist/index.js +19 -571
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.cts +5 -4
- package/dist/mcp/index.d.ts +5 -4
- package/dist/mcp.cjs +37 -9
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.js +3 -2
- package/dist/presence/index.d.cts +1 -1
- package/dist/presence/index.d.ts +1 -1
- package/dist/{server-BJu_AMH3.d.ts → server-BsSwfemr.d.cts} +4 -5
- package/dist/{server-si-VvFxI.d.cts → server-Du3-IGqM.d.ts} +4 -5
- package/dist/sharing/index.d.cts +5 -36
- package/dist/sharing/index.d.ts +5 -36
- package/dist/sharing.js +2 -1
- package/dist/sheets-adapter.cjs +96 -0
- package/dist/sheets-adapter.cjs.map +1 -0
- package/dist/sheets-adapter.d.cts +119 -0
- package/dist/sheets-adapter.d.ts +119 -0
- package/dist/sheets-adapter.js +4 -0
- package/dist/sheets-adapter.js.map +1 -0
- package/dist/token-CrJF76oH.d.cts +34 -0
- package/dist/token-CrJF76oH.d.ts +34 -0
- package/dist/tool-host-BQuUygLF.d.cts +60 -0
- package/dist/tool-host-C8JMMGYq.d.ts +60 -0
- package/dist/{types-DXKpLuia.d.ts → types-CCSBGW9T.d.cts} +2 -2
- package/dist/{types-Bf1ZoGmI.d.cts → types-DIVNcIQO.d.ts} +2 -2
- package/dist/{types-DksGd5Y7.d.cts → types-aOQLTW0E.d.cts} +1 -1
- package/dist/{types-DksGd5Y7.d.ts → types-aOQLTW0E.d.ts} +1 -1
- package/dist/undo/index.d.cts +4 -4
- package/dist/undo/index.d.ts +4 -4
- package/dist/undo.cjs +9 -9
- package/dist/undo.cjs.map +1 -1
- package/dist/undo.js +3 -3
- package/package.json +57 -7
- package/dist/chunk-4IAVAFUV.js +0 -342
- package/dist/chunk-4IAVAFUV.js.map +0 -1
- package/dist/chunk-6LTKCNLF.js.map +0 -1
- package/dist/chunk-ACBENYYO.js.map +0 -1
- package/dist/chunk-OEIULP2L.js.map +0 -1
- package/dist/chunk-PDBF4W7E.js.map +0 -1
- package/dist/chunk-PHPXKSWI.js.map +0 -1
- package/dist/chunk-QGCF7YKW.js.map +0 -1
- package/dist/chunk-QJUTISFC.js.map +0 -1
- package/dist/chunk-TBEITXF4.js.map +0 -1
- 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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
415
|
-
const
|
|
416
|
-
kind: "
|
|
417
|
-
|
|
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,
|
|
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 =
|
|
332
|
+
const final = isMutation ? wrapToolWithActivity(wrapped, {
|
|
428
333
|
toolName: name,
|
|
429
334
|
agent: { id: agent.id, name: agent.name, color: agent.color },
|
|
430
|
-
kind: "
|
|
431
|
-
|
|
335
|
+
kind: "form",
|
|
336
|
+
screenId: adapter.screenId,
|
|
337
|
+
resolveTarget: ({ args }) => target(args)
|
|
432
338
|
}) : wrapped;
|
|
433
339
|
disposers.push(
|
|
434
|
-
|
|
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
|
-
"
|
|
495
|
-
"
|
|
496
|
-
{
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
360
|
+
false
|
|
533
361
|
);
|
|
534
362
|
reg(
|
|
535
|
-
"
|
|
536
|
-
"
|
|
537
|
-
{
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
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
|
-
|
|
375
|
+
false
|
|
601
376
|
);
|
|
602
377
|
reg(
|
|
603
|
-
"
|
|
604
|
-
|
|
605
|
-
{
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
386
|
+
false
|
|
643
387
|
);
|
|
644
388
|
reg(
|
|
645
|
-
"
|
|
646
|
-
"
|
|
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
|
-
|
|
649
|
-
|
|
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
|
-
["
|
|
658
|
-
|
|
659
|
-
const
|
|
660
|
-
const
|
|
661
|
-
if (!
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
403
|
+
true
|
|
686
404
|
);
|
|
687
405
|
reg(
|
|
688
|
-
"
|
|
689
|
-
"
|
|
690
|
-
{
|
|
691
|
-
|
|
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
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
|
|
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
|
|
1322
|
-
function registerSheetsBridge(
|
|
464
|
+
var DEFAULT_AGENT2 = { id: "agent", name: "Agent", color: "#a855f7" };
|
|
465
|
+
function registerSheetsBridge(host, options) {
|
|
1323
466
|
const { adapter } = options;
|
|
1324
|
-
const 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
|
-
|
|
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
|
|
1596
|
-
function registerCodeBridge(
|
|
817
|
+
var DEFAULT_AGENT3 = { id: "agent", name: "Agent", color: "#a855f7" };
|
|
818
|
+
function registerCodeBridge(host, options) {
|
|
1597
819
|
const { adapter } = options;
|
|
1598
|
-
const 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
|
-
|
|
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
|
|
1762
|
-
function registerChartsBridge(
|
|
983
|
+
var DEFAULT_AGENT4 = { id: "agent", name: "Agent", color: "#a855f7" };
|
|
984
|
+
function registerChartsBridge(host, options) {
|
|
1763
985
|
const { adapter } = options;
|
|
1764
|
-
const 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
|
-
|
|
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
|
|
1876
|
-
function registerSceneBridge(
|
|
1097
|
+
var DEFAULT_AGENT5 = { id: "agent", name: "Agent", color: "#a855f7" };
|
|
1098
|
+
function registerSceneBridge(host, options) {
|
|
1877
1099
|
const { adapter } = options;
|
|
1878
|
-
const 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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
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
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
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
|
|
2479
|
-
const
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
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
|