@particle-academy/agent-integrations 0.13.0 → 0.14.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/dist/index.cjs +505 -190
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +197 -6
- package/dist/index.d.ts +197 -6
- package/dist/index.js +320 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -2216,6 +2216,459 @@ function registerTerminalBridge(host, options) {
|
|
|
2216
2216
|
pending: () => [...staged.values()]
|
|
2217
2217
|
};
|
|
2218
2218
|
}
|
|
2219
|
+
|
|
2220
|
+
// src/bridges/navigation.ts
|
|
2221
|
+
var DEFAULT_AGENT9 = { id: "agent", name: "Agent", color: "#a855f7" };
|
|
2222
|
+
function registerNavigationBridge(host, options) {
|
|
2223
|
+
const { adapter } = options;
|
|
2224
|
+
const agent = { ...DEFAULT_AGENT9, ...options.agent ?? {} };
|
|
2225
|
+
const pendingMode = options.pendingMode ?? true;
|
|
2226
|
+
const disposers = [];
|
|
2227
|
+
ensureUndoToolsRegistered(host, { defaultAgentId: agent.id });
|
|
2228
|
+
const target = (label, elementId) => ({
|
|
2229
|
+
kind: "navigation",
|
|
2230
|
+
screenId: adapter.screenId,
|
|
2231
|
+
elementId,
|
|
2232
|
+
label
|
|
2233
|
+
});
|
|
2234
|
+
const reg = (name, description, properties, required, handler, activity) => {
|
|
2235
|
+
const wrapped = async (args) => {
|
|
2236
|
+
try {
|
|
2237
|
+
return await handler(args);
|
|
2238
|
+
} catch (e) {
|
|
2239
|
+
return errorResult(e instanceof Error ? e.message : String(e));
|
|
2240
|
+
}
|
|
2241
|
+
};
|
|
2242
|
+
const final = activity ? wrapToolWithActivity(wrapped, {
|
|
2243
|
+
toolName: name,
|
|
2244
|
+
agent: { id: agent.id, name: agent.name, color: agent.color },
|
|
2245
|
+
kind: "navigation",
|
|
2246
|
+
screenId: adapter.screenId,
|
|
2247
|
+
resolveTarget: ({ args }) => activity(args)
|
|
2248
|
+
}) : wrapped;
|
|
2249
|
+
disposers.push(
|
|
2250
|
+
host.registerTool(
|
|
2251
|
+
{
|
|
2252
|
+
name,
|
|
2253
|
+
description,
|
|
2254
|
+
inputSchema: { type: "object", properties, required, additionalProperties: false }
|
|
2255
|
+
},
|
|
2256
|
+
final
|
|
2257
|
+
)
|
|
2258
|
+
);
|
|
2259
|
+
};
|
|
2260
|
+
reg(
|
|
2261
|
+
"page_describe",
|
|
2262
|
+
"Describe the current page: its URL, title, and the interactive elements you can act on (each with a stable `handle`, role, and label). Call this first, and again after navigating.",
|
|
2263
|
+
{},
|
|
2264
|
+
[],
|
|
2265
|
+
() => {
|
|
2266
|
+
const snap = adapter.describe();
|
|
2267
|
+
const text = [
|
|
2268
|
+
`URL: ${snap.url}`,
|
|
2269
|
+
`Title: ${snap.title}`,
|
|
2270
|
+
"",
|
|
2271
|
+
...snap.actions.map((a) => `[${a.handle}] ${a.role}: ${a.label}${a.destructive ? " (destructive)" : ""}`)
|
|
2272
|
+
].join("\n");
|
|
2273
|
+
return textResult(text, snap);
|
|
2274
|
+
},
|
|
2275
|
+
false
|
|
2276
|
+
);
|
|
2277
|
+
reg(
|
|
2278
|
+
"page_read",
|
|
2279
|
+
"Read the page's visible text / heading outline for grounding.",
|
|
2280
|
+
{},
|
|
2281
|
+
[],
|
|
2282
|
+
() => textResult(adapter.read ? adapter.read() : "(host did not provide page text)"),
|
|
2283
|
+
false
|
|
2284
|
+
);
|
|
2285
|
+
reg(
|
|
2286
|
+
"nav_visit",
|
|
2287
|
+
"Navigate to a URL (same-site path or absolute). The human watches the page change.",
|
|
2288
|
+
{ url: { type: "string", description: "Path like /packages or an absolute URL." } },
|
|
2289
|
+
["url"],
|
|
2290
|
+
async (args) => {
|
|
2291
|
+
const url = String(args.url ?? "");
|
|
2292
|
+
if (!url) return errorResult("url is required");
|
|
2293
|
+
const from = adapter.getLocation().url;
|
|
2294
|
+
await adapter.visit(url);
|
|
2295
|
+
fancyAutoCommon.pushUndoEntry(agent.id, {
|
|
2296
|
+
timestamp: Date.now(),
|
|
2297
|
+
bridgeId: "navigation",
|
|
2298
|
+
action: "nav_visit",
|
|
2299
|
+
label: `Navigate to ${url}`,
|
|
2300
|
+
undo: () => {
|
|
2301
|
+
adapter.visit(from);
|
|
2302
|
+
},
|
|
2303
|
+
redo: () => {
|
|
2304
|
+
adapter.visit(url);
|
|
2305
|
+
}
|
|
2306
|
+
});
|
|
2307
|
+
return textResult(`Navigated to ${url}`, { url });
|
|
2308
|
+
},
|
|
2309
|
+
(args) => target(`Navigate \u2192 ${String(args.url ?? "")}`)
|
|
2310
|
+
);
|
|
2311
|
+
reg(
|
|
2312
|
+
"nav_back",
|
|
2313
|
+
"Go back to the previous page.",
|
|
2314
|
+
{},
|
|
2315
|
+
[],
|
|
2316
|
+
async () => {
|
|
2317
|
+
if (!adapter.back) return errorResult("Host did not provide back navigation.");
|
|
2318
|
+
await adapter.back();
|
|
2319
|
+
return textResult("Went back");
|
|
2320
|
+
},
|
|
2321
|
+
() => target("Back")
|
|
2322
|
+
);
|
|
2323
|
+
reg(
|
|
2324
|
+
"nav_forward",
|
|
2325
|
+
"Go forward to the next page.",
|
|
2326
|
+
{},
|
|
2327
|
+
[],
|
|
2328
|
+
async () => {
|
|
2329
|
+
if (!adapter.forward) return errorResult("Host did not provide forward navigation.");
|
|
2330
|
+
await adapter.forward();
|
|
2331
|
+
return textResult("Went forward");
|
|
2332
|
+
},
|
|
2333
|
+
() => target("Forward")
|
|
2334
|
+
);
|
|
2335
|
+
reg(
|
|
2336
|
+
"nav_scroll_to",
|
|
2337
|
+
"Scroll the page to absolute coordinates, or to a specific element by its handle.",
|
|
2338
|
+
{
|
|
2339
|
+
handle: { type: "string", description: "Scroll this element into view." },
|
|
2340
|
+
x: { type: "number" },
|
|
2341
|
+
y: { type: "number" }
|
|
2342
|
+
},
|
|
2343
|
+
[],
|
|
2344
|
+
(args) => {
|
|
2345
|
+
adapter.scrollTo({
|
|
2346
|
+
handle: typeof args.handle === "string" ? args.handle : void 0,
|
|
2347
|
+
x: typeof args.x === "number" ? args.x : void 0,
|
|
2348
|
+
y: typeof args.y === "number" ? args.y : void 0
|
|
2349
|
+
});
|
|
2350
|
+
return textResult("Scrolled");
|
|
2351
|
+
},
|
|
2352
|
+
() => target("Scroll")
|
|
2353
|
+
);
|
|
2354
|
+
reg(
|
|
2355
|
+
"nav_scroll_by",
|
|
2356
|
+
"Scroll the page by a vertical delta in pixels (negative scrolls up).",
|
|
2357
|
+
{ dy: { type: "number" } },
|
|
2358
|
+
["dy"],
|
|
2359
|
+
(args) => {
|
|
2360
|
+
adapter.scrollBy(Number(args.dy ?? 0));
|
|
2361
|
+
return textResult(`Scrolled by ${Number(args.dy ?? 0)}px`);
|
|
2362
|
+
},
|
|
2363
|
+
() => target("Scroll")
|
|
2364
|
+
);
|
|
2365
|
+
reg(
|
|
2366
|
+
"page_set_field",
|
|
2367
|
+
"Set a form field's value by handle. The host updates the controlled input and the human sees it change.",
|
|
2368
|
+
{
|
|
2369
|
+
handle: { type: "string" },
|
|
2370
|
+
value: { description: "Value to set; type matches the field." }
|
|
2371
|
+
},
|
|
2372
|
+
["handle", "value"],
|
|
2373
|
+
(args) => {
|
|
2374
|
+
const handle = String(args.handle ?? "");
|
|
2375
|
+
const res = adapter.setField(handle, args.value);
|
|
2376
|
+
if (!res.ok) return errorResult(res.error ?? `Could not set ${handle}`);
|
|
2377
|
+
return textResult(`${handle} \u2190 ${JSON.stringify(args.value)}`, { handle, value: args.value });
|
|
2378
|
+
},
|
|
2379
|
+
(args) => target(`Set ${String(args.handle ?? "")}`, String(args.handle ?? ""))
|
|
2380
|
+
);
|
|
2381
|
+
reg(
|
|
2382
|
+
"page_click",
|
|
2383
|
+
"Activate an element by handle (link, button, checkbox\u2026). Destructive elements are staged for the human to confirm.",
|
|
2384
|
+
{ handle: { type: "string" } },
|
|
2385
|
+
["handle"],
|
|
2386
|
+
async (args) => {
|
|
2387
|
+
const handle = String(args.handle ?? "");
|
|
2388
|
+
const action = adapter.describe().actions.find((a) => a.handle === handle);
|
|
2389
|
+
if (pendingMode && action?.destructive && adapter.confirm) {
|
|
2390
|
+
const ok = await adapter.confirm({ action: "click", handle, label: action.label });
|
|
2391
|
+
if (!ok) return errorResult("Declined by user");
|
|
2392
|
+
}
|
|
2393
|
+
const res = adapter.click(handle);
|
|
2394
|
+
if (!res.ok) return errorResult(res.error ?? `Could not click ${handle}`);
|
|
2395
|
+
return textResult(`Clicked ${handle}`, { handle });
|
|
2396
|
+
},
|
|
2397
|
+
(args) => target(`Click ${String(args.handle ?? "")}`, String(args.handle ?? ""))
|
|
2398
|
+
);
|
|
2399
|
+
reg(
|
|
2400
|
+
"page_submit",
|
|
2401
|
+
"Submit a form by handle. Always staged for the human to confirm when pendingMode is on.",
|
|
2402
|
+
{ handle: { type: "string" } },
|
|
2403
|
+
["handle"],
|
|
2404
|
+
async (args) => {
|
|
2405
|
+
const handle = String(args.handle ?? "");
|
|
2406
|
+
if (pendingMode && adapter.confirm) {
|
|
2407
|
+
const ok = await adapter.confirm({ action: "submit", handle, label: handle });
|
|
2408
|
+
if (!ok) return errorResult("Declined by user");
|
|
2409
|
+
}
|
|
2410
|
+
const res = await adapter.submit(handle);
|
|
2411
|
+
if (!res.ok) return errorResult(res.error ?? "Submit failed");
|
|
2412
|
+
return textResult(`Submitted ${handle}`, { handle });
|
|
2413
|
+
},
|
|
2414
|
+
(args) => target(`Submit ${String(args.handle ?? "")}`, String(args.handle ?? ""))
|
|
2415
|
+
);
|
|
2416
|
+
return {
|
|
2417
|
+
id: "navigation",
|
|
2418
|
+
title: "Co-browsing",
|
|
2419
|
+
dispose: () => {
|
|
2420
|
+
for (const d of disposers) d();
|
|
2421
|
+
}
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
// src/sharing/token.ts
|
|
2426
|
+
var TOKEN_BYTES = 24;
|
|
2427
|
+
function createSessionDescriptor() {
|
|
2428
|
+
const id = randomId(8);
|
|
2429
|
+
const token = randomToken();
|
|
2430
|
+
return { id, token, display: token.slice(0, 8) };
|
|
2431
|
+
}
|
|
2432
|
+
function describeSession(id, token) {
|
|
2433
|
+
return { id, token, display: token.slice(0, 8) };
|
|
2434
|
+
}
|
|
2435
|
+
function buildShareUrl(descriptor, baseUrl = typeof window !== "undefined" ? window.location.href.split("?")[0] : "") {
|
|
2436
|
+
const u = new URL(baseUrl);
|
|
2437
|
+
u.searchParams.set("session", descriptor.id);
|
|
2438
|
+
u.searchParams.set("token", descriptor.token);
|
|
2439
|
+
return u.toString();
|
|
2440
|
+
}
|
|
2441
|
+
function buildShareConfig(descriptor, transport = "broadcast-channel") {
|
|
2442
|
+
return {
|
|
2443
|
+
name: `whiteboard-${descriptor.id}`,
|
|
2444
|
+
transport,
|
|
2445
|
+
session: descriptor.id,
|
|
2446
|
+
token: descriptor.token,
|
|
2447
|
+
channel: `fai:share:${descriptor.id}`,
|
|
2448
|
+
protocol_version: "2025-06-18"
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
function readSessionFromUrl() {
|
|
2452
|
+
if (typeof window === "undefined") return null;
|
|
2453
|
+
const params = new URL(window.location.href).searchParams;
|
|
2454
|
+
const id = params.get("session");
|
|
2455
|
+
const token = params.get("token");
|
|
2456
|
+
if (!id || !token) return null;
|
|
2457
|
+
return describeSession(id, token);
|
|
2458
|
+
}
|
|
2459
|
+
function randomToken() {
|
|
2460
|
+
const bytes = new Uint8Array(TOKEN_BYTES);
|
|
2461
|
+
crypto.getRandomValues(bytes);
|
|
2462
|
+
return base64Url(bytes);
|
|
2463
|
+
}
|
|
2464
|
+
function randomId(len) {
|
|
2465
|
+
const bytes = new Uint8Array(Math.ceil(len * 3 / 4));
|
|
2466
|
+
crypto.getRandomValues(bytes);
|
|
2467
|
+
return base64Url(bytes).slice(0, len);
|
|
2468
|
+
}
|
|
2469
|
+
function base64Url(bytes) {
|
|
2470
|
+
let s = "";
|
|
2471
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
2472
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2473
|
+
}
|
|
2474
|
+
function constantTimeEqual(a, b) {
|
|
2475
|
+
if (a.length !== b.length) return false;
|
|
2476
|
+
let diff = 0;
|
|
2477
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
2478
|
+
return diff === 0;
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
// src/sharing/sse-relay.ts
|
|
2482
|
+
var SseRelayTransport = class {
|
|
2483
|
+
constructor(options) {
|
|
2484
|
+
this.sendQueue = [];
|
|
2485
|
+
this.connected = false;
|
|
2486
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
2487
|
+
this.state = "idle";
|
|
2488
|
+
this.opts = options;
|
|
2489
|
+
this.expectedToken = options.token;
|
|
2490
|
+
}
|
|
2491
|
+
bindServer(server) {
|
|
2492
|
+
this.server = server;
|
|
2493
|
+
}
|
|
2494
|
+
/** Open the SSE channel. Idempotent. */
|
|
2495
|
+
start() {
|
|
2496
|
+
if (this.connected || typeof window === "undefined") return;
|
|
2497
|
+
const url = `${this.opts.baseUrl}/${encodeURIComponent(this.opts.sessionId)}/events?token=${encodeURIComponent(this.opts.token)}`;
|
|
2498
|
+
this.setState("connecting");
|
|
2499
|
+
const es = new EventSource(url, { withCredentials: false });
|
|
2500
|
+
this.es = es;
|
|
2501
|
+
es.addEventListener("open", () => {
|
|
2502
|
+
this.connected = true;
|
|
2503
|
+
this.setState("open");
|
|
2504
|
+
const queued = this.sendQueue.splice(0);
|
|
2505
|
+
for (const msg of queued) this.postOut(msg);
|
|
2506
|
+
});
|
|
2507
|
+
es.addEventListener("mcp", (ev) => {
|
|
2508
|
+
const raw = ev.data;
|
|
2509
|
+
this.handleInbound(raw);
|
|
2510
|
+
});
|
|
2511
|
+
es.addEventListener("error", () => {
|
|
2512
|
+
this.setState("error");
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
send(message) {
|
|
2516
|
+
if (!this.connected) {
|
|
2517
|
+
this.sendQueue.push(message);
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
this.postOut(message);
|
|
2521
|
+
}
|
|
2522
|
+
close() {
|
|
2523
|
+
this.es?.close();
|
|
2524
|
+
this.es = void 0;
|
|
2525
|
+
this.connected = false;
|
|
2526
|
+
this.setState("closed");
|
|
2527
|
+
}
|
|
2528
|
+
onStateChange(listener) {
|
|
2529
|
+
this.listeners.add(listener);
|
|
2530
|
+
listener(this.state);
|
|
2531
|
+
return () => this.listeners.delete(listener);
|
|
2532
|
+
}
|
|
2533
|
+
/**
|
|
2534
|
+
* For relays that wrap each frame with auth metadata: hosts can call this
|
|
2535
|
+
* directly when a frame arrives via a non-SSE path. The transport will
|
|
2536
|
+
* dispatch it to the bound server.
|
|
2537
|
+
*/
|
|
2538
|
+
async deliverFromRemote(payload, token) {
|
|
2539
|
+
if (token !== void 0 && !constantTimeEqual(token, this.expectedToken)) return;
|
|
2540
|
+
if (!this.server) throw new Error("SseRelayTransport has no bound server");
|
|
2541
|
+
const message = typeof payload === "string" ? JSON.parse(payload) : payload;
|
|
2542
|
+
await this.server.receive(this, message);
|
|
2543
|
+
}
|
|
2544
|
+
async postOut(message) {
|
|
2545
|
+
const url = `${this.opts.baseUrl}/${encodeURIComponent(this.opts.sessionId)}/outbox?token=${encodeURIComponent(this.opts.token)}`;
|
|
2546
|
+
const f = this.opts.fetch ?? fetch;
|
|
2547
|
+
try {
|
|
2548
|
+
await f(url, {
|
|
2549
|
+
method: "POST",
|
|
2550
|
+
headers: { "content-type": "application/json", "accept": "application/json" },
|
|
2551
|
+
body: JSON.stringify(message)
|
|
2552
|
+
});
|
|
2553
|
+
} catch {
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
async handleInbound(raw) {
|
|
2557
|
+
if (!this.server) return;
|
|
2558
|
+
let message;
|
|
2559
|
+
try {
|
|
2560
|
+
message = JSON.parse(raw);
|
|
2561
|
+
} catch {
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
await this.server.receive(this, message);
|
|
2565
|
+
}
|
|
2566
|
+
setState(state) {
|
|
2567
|
+
this.state = state;
|
|
2568
|
+
for (const l of this.listeners) l(state);
|
|
2569
|
+
}
|
|
2570
|
+
};
|
|
2571
|
+
function attachSseRelay(server, options) {
|
|
2572
|
+
const transport = new SseRelayTransport(options);
|
|
2573
|
+
transport.bindServer(server);
|
|
2574
|
+
server.attach(transport);
|
|
2575
|
+
transport.start();
|
|
2576
|
+
Promise.resolve().then(() => (init_registry(), registry_exports)).then(({ onActivity: onActivity2 }) => {
|
|
2577
|
+
const off = onActivity2((event) => {
|
|
2578
|
+
transport.send({
|
|
2579
|
+
jsonrpc: "2.0",
|
|
2580
|
+
method: "notifications/agent_activity",
|
|
2581
|
+
params: event
|
|
2582
|
+
});
|
|
2583
|
+
});
|
|
2584
|
+
const origClose = transport.close.bind(transport);
|
|
2585
|
+
transport.close = () => {
|
|
2586
|
+
off();
|
|
2587
|
+
origClose();
|
|
2588
|
+
};
|
|
2589
|
+
}).catch(() => {
|
|
2590
|
+
});
|
|
2591
|
+
return transport;
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
// src/sharing/use-co-browse-session.ts
|
|
2595
|
+
init_registry();
|
|
2596
|
+
var DEFAULT_AGENT10 = { id: "agent", name: "Agent", color: "#a855f7" };
|
|
2597
|
+
var USER = { id: "human", name: "You" };
|
|
2598
|
+
function useCoBrowseSession(options) {
|
|
2599
|
+
const { adapter, extraBridges } = options;
|
|
2600
|
+
const agent = { ...DEFAULT_AGENT10, ...options.agent ?? {} };
|
|
2601
|
+
const relayBaseUrl = options.relayBaseUrl ?? "/whiteboard-share";
|
|
2602
|
+
const serverRef = react.useRef(null);
|
|
2603
|
+
const relayRef = react.useRef(null);
|
|
2604
|
+
const detachInProc = react.useRef(null);
|
|
2605
|
+
const disposeBridge = react.useRef(null);
|
|
2606
|
+
const [session, setSession] = react.useState(null);
|
|
2607
|
+
const [relayState, setRelayState] = react.useState("idle");
|
|
2608
|
+
react.useEffect(() => {
|
|
2609
|
+
const server = new MicroMcpServer({
|
|
2610
|
+
info: options.info ?? { name: "fancy-co-browse", version: "0.1.0" },
|
|
2611
|
+
instructions: options.info?.instructions ?? `Co-browse with a watching human. Call page_describe first; navigate, scroll, and (with confirm) fill/click via stable handles. You receive notifications/agent_activity for the human's actions (source:"user").`
|
|
2612
|
+
});
|
|
2613
|
+
const bridge = registerNavigationBridge(server, { adapter, agent, pendingMode: options.pendingMode });
|
|
2614
|
+
extraBridges?.(server);
|
|
2615
|
+
const inProc = attachInProcess(server);
|
|
2616
|
+
detachInProc.current = () => inProc.close();
|
|
2617
|
+
disposeBridge.current = bridge.dispose;
|
|
2618
|
+
serverRef.current = server;
|
|
2619
|
+
return () => {
|
|
2620
|
+
relayRef.current?.close();
|
|
2621
|
+
relayRef.current = null;
|
|
2622
|
+
disposeBridge.current?.();
|
|
2623
|
+
detachInProc.current?.();
|
|
2624
|
+
serverRef.current = null;
|
|
2625
|
+
};
|
|
2626
|
+
}, []);
|
|
2627
|
+
const startShare = react.useCallback(async () => {
|
|
2628
|
+
const server = serverRef.current;
|
|
2629
|
+
if (!server || relayRef.current) return;
|
|
2630
|
+
const descriptor = createSessionDescriptor();
|
|
2631
|
+
const csrf = options.csrfToken?.() ?? "";
|
|
2632
|
+
await fetch(`${relayBaseUrl}/register`, {
|
|
2633
|
+
method: "POST",
|
|
2634
|
+
headers: { "content-type": "application/json", "x-csrf-token": csrf },
|
|
2635
|
+
body: JSON.stringify({ session: descriptor.id, token: descriptor.token })
|
|
2636
|
+
});
|
|
2637
|
+
const relay = attachSseRelay(server, { baseUrl: relayBaseUrl, sessionId: descriptor.id, token: descriptor.token });
|
|
2638
|
+
relay.onStateChange(setRelayState);
|
|
2639
|
+
relayRef.current = relay;
|
|
2640
|
+
setSession(descriptor);
|
|
2641
|
+
}, [relayBaseUrl, options]);
|
|
2642
|
+
const stopShare = react.useCallback(() => {
|
|
2643
|
+
const current = session;
|
|
2644
|
+
relayRef.current?.close();
|
|
2645
|
+
relayRef.current = null;
|
|
2646
|
+
setRelayState("idle");
|
|
2647
|
+
setSession(null);
|
|
2648
|
+
if (current) {
|
|
2649
|
+
const csrf = options.csrfToken?.() ?? "";
|
|
2650
|
+
void fetch(`${relayBaseUrl}/${current.id}/unregister`, {
|
|
2651
|
+
method: "POST",
|
|
2652
|
+
headers: { "content-type": "application/json", "x-csrf-token": csrf },
|
|
2653
|
+
body: JSON.stringify({ token: current.token })
|
|
2654
|
+
}).catch(() => {
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
}, [relayBaseUrl, options, session]);
|
|
2658
|
+
const observeUser = react.useCallback((event) => {
|
|
2659
|
+
const label = event.kind === "navigation" ? `You navigated to ${event.url}` : event.kind === "scroll" ? "You scrolled" : `You edited ${event.handle}${event.masked ? " (hidden)" : ""}`;
|
|
2660
|
+
fancyAutoCommon.emitActivity({
|
|
2661
|
+
agentId: USER.id,
|
|
2662
|
+
agentName: USER.name,
|
|
2663
|
+
source: "user",
|
|
2664
|
+
target: { kind: "navigation", label },
|
|
2665
|
+
action: `user_${event.kind}`,
|
|
2666
|
+
timestamp: Date.now(),
|
|
2667
|
+
meta: event
|
|
2668
|
+
});
|
|
2669
|
+
}, []);
|
|
2670
|
+
return { server: serverRef.current, session, relayState, startShare, stopShare, observeUser };
|
|
2671
|
+
}
|
|
2219
2672
|
function AgentPanel({ agent, activity, onSubmit, busy, actions, className, style }) {
|
|
2220
2673
|
const scrollRef = react.useRef(null);
|
|
2221
2674
|
const inputRef = react.useRef(null);
|
|
@@ -2474,62 +2927,6 @@ function ScreensActivityBridge({ system, fadeMs = 1500 }) {
|
|
|
2474
2927
|
}, [system, fadeMs]);
|
|
2475
2928
|
return null;
|
|
2476
2929
|
}
|
|
2477
|
-
|
|
2478
|
-
// src/sharing/token.ts
|
|
2479
|
-
var TOKEN_BYTES = 24;
|
|
2480
|
-
function createSessionDescriptor() {
|
|
2481
|
-
const id = randomId(8);
|
|
2482
|
-
const token = randomToken();
|
|
2483
|
-
return { id, token, display: token.slice(0, 8) };
|
|
2484
|
-
}
|
|
2485
|
-
function describeSession(id, token) {
|
|
2486
|
-
return { id, token, display: token.slice(0, 8) };
|
|
2487
|
-
}
|
|
2488
|
-
function buildShareUrl(descriptor, baseUrl = typeof window !== "undefined" ? window.location.href.split("?")[0] : "") {
|
|
2489
|
-
const u = new URL(baseUrl);
|
|
2490
|
-
u.searchParams.set("session", descriptor.id);
|
|
2491
|
-
u.searchParams.set("token", descriptor.token);
|
|
2492
|
-
return u.toString();
|
|
2493
|
-
}
|
|
2494
|
-
function buildShareConfig(descriptor, transport = "broadcast-channel") {
|
|
2495
|
-
return {
|
|
2496
|
-
name: `whiteboard-${descriptor.id}`,
|
|
2497
|
-
transport,
|
|
2498
|
-
session: descriptor.id,
|
|
2499
|
-
token: descriptor.token,
|
|
2500
|
-
channel: `fai:share:${descriptor.id}`,
|
|
2501
|
-
protocol_version: "2025-06-18"
|
|
2502
|
-
};
|
|
2503
|
-
}
|
|
2504
|
-
function readSessionFromUrl() {
|
|
2505
|
-
if (typeof window === "undefined") return null;
|
|
2506
|
-
const params = new URL(window.location.href).searchParams;
|
|
2507
|
-
const id = params.get("session");
|
|
2508
|
-
const token = params.get("token");
|
|
2509
|
-
if (!id || !token) return null;
|
|
2510
|
-
return describeSession(id, token);
|
|
2511
|
-
}
|
|
2512
|
-
function randomToken() {
|
|
2513
|
-
const bytes = new Uint8Array(TOKEN_BYTES);
|
|
2514
|
-
crypto.getRandomValues(bytes);
|
|
2515
|
-
return base64Url(bytes);
|
|
2516
|
-
}
|
|
2517
|
-
function randomId(len) {
|
|
2518
|
-
const bytes = new Uint8Array(Math.ceil(len * 3 / 4));
|
|
2519
|
-
crypto.getRandomValues(bytes);
|
|
2520
|
-
return base64Url(bytes).slice(0, len);
|
|
2521
|
-
}
|
|
2522
|
-
function base64Url(bytes) {
|
|
2523
|
-
let s = "";
|
|
2524
|
-
for (const b of bytes) s += String.fromCharCode(b);
|
|
2525
|
-
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2526
|
-
}
|
|
2527
|
-
function constantTimeEqual(a, b) {
|
|
2528
|
-
if (a.length !== b.length) return false;
|
|
2529
|
-
let diff = 0;
|
|
2530
|
-
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
2531
|
-
return diff === 0;
|
|
2532
|
-
}
|
|
2533
2930
|
function ShareControls({
|
|
2534
2931
|
session,
|
|
2535
2932
|
onStart,
|
|
@@ -2649,9 +3046,6 @@ function buildCurlRecipe(session) {
|
|
|
2649
3046
|
].join("\n");
|
|
2650
3047
|
}
|
|
2651
3048
|
|
|
2652
|
-
// src/presence/index.ts
|
|
2653
|
-
init_registry();
|
|
2654
|
-
|
|
2655
3049
|
// src/presence/use-agent-activity.ts
|
|
2656
3050
|
init_registry();
|
|
2657
3051
|
function useAgentActivity(filter, options = {}) {
|
|
@@ -2684,137 +3078,6 @@ function useAgentActivityForScreen(screenId, options = {}) {
|
|
|
2684
3078
|
}, [latest, fadeAfter]);
|
|
2685
3079
|
return { events, latest, isAgentActive };
|
|
2686
3080
|
}
|
|
2687
|
-
function useUndoStack(agentId, intervalMs = 500) {
|
|
2688
|
-
const [history, setHistory] = react.useState(() => fancyAutoCommon.readHistory(agentId));
|
|
2689
|
-
react.useEffect(() => {
|
|
2690
|
-
let cancelled = false;
|
|
2691
|
-
const tick = () => {
|
|
2692
|
-
if (cancelled) return;
|
|
2693
|
-
setHistory(fancyAutoCommon.readHistory(agentId));
|
|
2694
|
-
};
|
|
2695
|
-
const id = setInterval(tick, intervalMs);
|
|
2696
|
-
tick();
|
|
2697
|
-
return () => {
|
|
2698
|
-
cancelled = true;
|
|
2699
|
-
clearInterval(id);
|
|
2700
|
-
};
|
|
2701
|
-
}, [agentId, intervalMs]);
|
|
2702
|
-
const refresh = react.useCallback(() => setHistory(fancyAutoCommon.readHistory(agentId)), [agentId]);
|
|
2703
|
-
return { history, refresh };
|
|
2704
|
-
}
|
|
2705
|
-
|
|
2706
|
-
// src/sharing/sse-relay.ts
|
|
2707
|
-
var SseRelayTransport = class {
|
|
2708
|
-
constructor(options) {
|
|
2709
|
-
this.sendQueue = [];
|
|
2710
|
-
this.connected = false;
|
|
2711
|
-
this.listeners = /* @__PURE__ */ new Set();
|
|
2712
|
-
this.state = "idle";
|
|
2713
|
-
this.opts = options;
|
|
2714
|
-
this.expectedToken = options.token;
|
|
2715
|
-
}
|
|
2716
|
-
bindServer(server) {
|
|
2717
|
-
this.server = server;
|
|
2718
|
-
}
|
|
2719
|
-
/** Open the SSE channel. Idempotent. */
|
|
2720
|
-
start() {
|
|
2721
|
-
if (this.connected || typeof window === "undefined") return;
|
|
2722
|
-
const url = `${this.opts.baseUrl}/${encodeURIComponent(this.opts.sessionId)}/events?token=${encodeURIComponent(this.opts.token)}`;
|
|
2723
|
-
this.setState("connecting");
|
|
2724
|
-
const es = new EventSource(url, { withCredentials: false });
|
|
2725
|
-
this.es = es;
|
|
2726
|
-
es.addEventListener("open", () => {
|
|
2727
|
-
this.connected = true;
|
|
2728
|
-
this.setState("open");
|
|
2729
|
-
const queued = this.sendQueue.splice(0);
|
|
2730
|
-
for (const msg of queued) this.postOut(msg);
|
|
2731
|
-
});
|
|
2732
|
-
es.addEventListener("mcp", (ev) => {
|
|
2733
|
-
const raw = ev.data;
|
|
2734
|
-
this.handleInbound(raw);
|
|
2735
|
-
});
|
|
2736
|
-
es.addEventListener("error", () => {
|
|
2737
|
-
this.setState("error");
|
|
2738
|
-
});
|
|
2739
|
-
}
|
|
2740
|
-
send(message) {
|
|
2741
|
-
if (!this.connected) {
|
|
2742
|
-
this.sendQueue.push(message);
|
|
2743
|
-
return;
|
|
2744
|
-
}
|
|
2745
|
-
this.postOut(message);
|
|
2746
|
-
}
|
|
2747
|
-
close() {
|
|
2748
|
-
this.es?.close();
|
|
2749
|
-
this.es = void 0;
|
|
2750
|
-
this.connected = false;
|
|
2751
|
-
this.setState("closed");
|
|
2752
|
-
}
|
|
2753
|
-
onStateChange(listener) {
|
|
2754
|
-
this.listeners.add(listener);
|
|
2755
|
-
listener(this.state);
|
|
2756
|
-
return () => this.listeners.delete(listener);
|
|
2757
|
-
}
|
|
2758
|
-
/**
|
|
2759
|
-
* For relays that wrap each frame with auth metadata: hosts can call this
|
|
2760
|
-
* directly when a frame arrives via a non-SSE path. The transport will
|
|
2761
|
-
* dispatch it to the bound server.
|
|
2762
|
-
*/
|
|
2763
|
-
async deliverFromRemote(payload, token) {
|
|
2764
|
-
if (token !== void 0 && !constantTimeEqual(token, this.expectedToken)) return;
|
|
2765
|
-
if (!this.server) throw new Error("SseRelayTransport has no bound server");
|
|
2766
|
-
const message = typeof payload === "string" ? JSON.parse(payload) : payload;
|
|
2767
|
-
await this.server.receive(this, message);
|
|
2768
|
-
}
|
|
2769
|
-
async postOut(message) {
|
|
2770
|
-
const url = `${this.opts.baseUrl}/${encodeURIComponent(this.opts.sessionId)}/outbox?token=${encodeURIComponent(this.opts.token)}`;
|
|
2771
|
-
const f = this.opts.fetch ?? fetch;
|
|
2772
|
-
try {
|
|
2773
|
-
await f(url, {
|
|
2774
|
-
method: "POST",
|
|
2775
|
-
headers: { "content-type": "application/json", "accept": "application/json" },
|
|
2776
|
-
body: JSON.stringify(message)
|
|
2777
|
-
});
|
|
2778
|
-
} catch {
|
|
2779
|
-
}
|
|
2780
|
-
}
|
|
2781
|
-
async handleInbound(raw) {
|
|
2782
|
-
if (!this.server) return;
|
|
2783
|
-
let message;
|
|
2784
|
-
try {
|
|
2785
|
-
message = JSON.parse(raw);
|
|
2786
|
-
} catch {
|
|
2787
|
-
return;
|
|
2788
|
-
}
|
|
2789
|
-
await this.server.receive(this, message);
|
|
2790
|
-
}
|
|
2791
|
-
setState(state) {
|
|
2792
|
-
this.state = state;
|
|
2793
|
-
for (const l of this.listeners) l(state);
|
|
2794
|
-
}
|
|
2795
|
-
};
|
|
2796
|
-
function attachSseRelay(server, options) {
|
|
2797
|
-
const transport = new SseRelayTransport(options);
|
|
2798
|
-
transport.bindServer(server);
|
|
2799
|
-
server.attach(transport);
|
|
2800
|
-
transport.start();
|
|
2801
|
-
Promise.resolve().then(() => (init_registry(), registry_exports)).then(({ onActivity: onActivity2 }) => {
|
|
2802
|
-
const off = onActivity2((event) => {
|
|
2803
|
-
transport.send({
|
|
2804
|
-
jsonrpc: "2.0",
|
|
2805
|
-
method: "notifications/agent_activity",
|
|
2806
|
-
params: event
|
|
2807
|
-
});
|
|
2808
|
-
});
|
|
2809
|
-
const origClose = transport.close.bind(transport);
|
|
2810
|
-
transport.close = () => {
|
|
2811
|
-
off();
|
|
2812
|
-
origClose();
|
|
2813
|
-
};
|
|
2814
|
-
}).catch(() => {
|
|
2815
|
-
});
|
|
2816
|
-
return transport;
|
|
2817
|
-
}
|
|
2818
3081
|
|
|
2819
3082
|
// src/connectors/targets.ts
|
|
2820
3083
|
var CLAUDE_CONNECTORS_URL = "https://claude.ai/settings/connectors";
|
|
@@ -3092,6 +3355,55 @@ function ManualPopover({
|
|
|
3092
3355
|
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "fai-connect__copy-btn", onClick: onCopy, children: copied ? "Copied" : "Copy snippet" })
|
|
3093
3356
|
] });
|
|
3094
3357
|
}
|
|
3358
|
+
function CoBrowsePresence({ session, connectUrl, shareBaseUrl, className }) {
|
|
3359
|
+
const { events } = useAgentActivity(void 0, { capacity: 40 });
|
|
3360
|
+
const lastAgentAction = [...events].reverse().find((e) => (e.source ?? "agent") !== "user");
|
|
3361
|
+
const connected = session.relayState === "open";
|
|
3362
|
+
if (!session.session) {
|
|
3363
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className, "data-co-browse-presence": "idle", children: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => void session.startShare(), "data-co-browse-start": true, children: "Let an agent drive" }) });
|
|
3364
|
+
}
|
|
3365
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, "data-co-browse-presence": connected ? "connected" : "waiting", children: [
|
|
3366
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { "data-co-browse-bar": true, children: [
|
|
3367
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "data-co-browse-dot": true, "data-state": session.relayState }),
|
|
3368
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "data-co-browse-status": true, children: connected ? "Agent is driving" : `Waiting for an agent\u2026 (${session.relayState})` }),
|
|
3369
|
+
lastAgentAction && /* @__PURE__ */ jsxRuntime.jsx("span", { "data-co-browse-last": true, children: lastAgentAction.target?.label ?? lastAgentAction.action }),
|
|
3370
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: session.stopShare, "data-co-browse-stop": true, children: "Stop" })
|
|
3371
|
+
] }),
|
|
3372
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3373
|
+
ShareControls,
|
|
3374
|
+
{
|
|
3375
|
+
session: session.session,
|
|
3376
|
+
onStart: () => void session.startShare(),
|
|
3377
|
+
onStop: session.stopShare,
|
|
3378
|
+
status: session.relayState,
|
|
3379
|
+
shareBaseUrl
|
|
3380
|
+
}
|
|
3381
|
+
),
|
|
3382
|
+
connectUrl && /* @__PURE__ */ jsxRuntime.jsx(ConnectorButtons, { serverName: "Fancy UI co-browse", mcpUrl: connectUrl })
|
|
3383
|
+
] });
|
|
3384
|
+
}
|
|
3385
|
+
CoBrowsePresence.displayName = "CoBrowsePresence";
|
|
3386
|
+
|
|
3387
|
+
// src/presence/index.ts
|
|
3388
|
+
init_registry();
|
|
3389
|
+
function useUndoStack(agentId, intervalMs = 500) {
|
|
3390
|
+
const [history, setHistory] = react.useState(() => fancyAutoCommon.readHistory(agentId));
|
|
3391
|
+
react.useEffect(() => {
|
|
3392
|
+
let cancelled = false;
|
|
3393
|
+
const tick = () => {
|
|
3394
|
+
if (cancelled) return;
|
|
3395
|
+
setHistory(fancyAutoCommon.readHistory(agentId));
|
|
3396
|
+
};
|
|
3397
|
+
const id = setInterval(tick, intervalMs);
|
|
3398
|
+
tick();
|
|
3399
|
+
return () => {
|
|
3400
|
+
cancelled = true;
|
|
3401
|
+
clearInterval(id);
|
|
3402
|
+
};
|
|
3403
|
+
}, [agentId, intervalMs]);
|
|
3404
|
+
const refresh = react.useCallback(() => setHistory(fancyAutoCommon.readHistory(agentId)), [agentId]);
|
|
3405
|
+
return { history, refresh };
|
|
3406
|
+
}
|
|
3095
3407
|
|
|
3096
3408
|
// src/connectors/mcpb.ts
|
|
3097
3409
|
var MCPB_MANIFEST_VERSION = "0.2";
|
|
@@ -3200,6 +3512,7 @@ exports.CLAUDE_CONNECTORS_URL = CLAUDE_CONNECTORS_URL;
|
|
|
3200
3512
|
exports.CONNECTOR_GLYPHS = CONNECTOR_GLYPHS;
|
|
3201
3513
|
exports.CONNECTOR_TARGETS = CONNECTOR_TARGETS;
|
|
3202
3514
|
exports.ClaudeMark = ClaudeMark;
|
|
3515
|
+
exports.CoBrowsePresence = CoBrowsePresence;
|
|
3203
3516
|
exports.ConnectorButtons = ConnectorButtons;
|
|
3204
3517
|
exports.CursorMark = CursorMark;
|
|
3205
3518
|
exports.DEFAULT_MCPB_ENTRY_POINT = DEFAULT_MCPB_ENTRY_POINT;
|
|
@@ -3237,6 +3550,7 @@ exports.readSessionFromUrl = readSessionFromUrl;
|
|
|
3237
3550
|
exports.registerChartsBridge = registerChartsBridge;
|
|
3238
3551
|
exports.registerCodeBridge = registerCodeBridge;
|
|
3239
3552
|
exports.registerFormBridge = registerFormBridge;
|
|
3553
|
+
exports.registerNavigationBridge = registerNavigationBridge;
|
|
3240
3554
|
exports.registerSceneBridge = registerSceneBridge;
|
|
3241
3555
|
exports.registerScreensBridge = registerScreensBridge;
|
|
3242
3556
|
exports.registerSheetsBridge = registerSheetsBridge;
|
|
@@ -3248,6 +3562,7 @@ exports.slugifyServerName = slugifyServerName;
|
|
|
3248
3562
|
exports.textResult = textResult;
|
|
3249
3563
|
exports.useAgentActivity = useAgentActivity;
|
|
3250
3564
|
exports.useAgentActivityForScreen = useAgentActivityForScreen;
|
|
3565
|
+
exports.useCoBrowseSession = useCoBrowseSession;
|
|
3251
3566
|
exports.useSheetsActivityHighlights = useSheetsActivityHighlights;
|
|
3252
3567
|
exports.useSheetsAdapter = useSheetsAdapter;
|
|
3253
3568
|
exports.useUndoStack = useUndoStack;
|