@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 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;