@particle-academy/agent-integrations 0.34.0 → 0.35.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.
@@ -1,5 +1,5 @@
1
- import { ShareControls, AgentCursor, AgentActivityHighlight, AgentPanel } from './chunk-SJTLTITO.js';
2
- import { createSessionDescriptor, attachSseRelay } from './chunk-N6RLSLOF.js';
1
+ import { ShareControls, AgentCursor, AgentActivityHighlight, AgentPanel } from './chunk-ABP2THQV.js';
2
+ import { createSessionDescriptor, attachSseRelay } from './chunk-JDCB2NMM.js';
3
3
  import { attachInProcess } from './chunk-AFUULW5E.js';
4
4
  import { registerWhiteboardBridge } from './chunk-2U5JB6ZA.js';
5
5
  import './chunk-CT3LRNH7.js';
package/dist/index.cjs CHANGED
@@ -4457,6 +4457,7 @@ var SseRelayTransport = class {
4457
4457
  this.listeners = /* @__PURE__ */ new Set();
4458
4458
  this.state = "idle";
4459
4459
  this.peers = /* @__PURE__ */ new Set();
4460
+ this.peerListeners = /* @__PURE__ */ new Set();
4460
4461
  this.opts = options;
4461
4462
  this.expectedToken = options.token;
4462
4463
  }
@@ -4517,6 +4518,8 @@ var SseRelayTransport = class {
4517
4518
  this.es?.close();
4518
4519
  this.es = void 0;
4519
4520
  this.connected = false;
4521
+ this.peers.clear();
4522
+ this.notifyPeers();
4520
4523
  this.setState("closed");
4521
4524
  }
4522
4525
  onStateChange(listener) {
@@ -4524,6 +4527,28 @@ var SseRelayTransport = class {
4524
4527
  listener(this.state);
4525
4528
  return () => this.listeners.delete(listener);
4526
4529
  }
4530
+ /** How many remote peers (agents) are currently attached to this session. */
4531
+ peerCount() {
4532
+ return this.peers.size;
4533
+ }
4534
+ /**
4535
+ * Subscribe to peer arrivals/departures. Fires immediately with the current
4536
+ * count.
4537
+ *
4538
+ * This is the ONLY honest answer to "is an agent actually here?".
4539
+ * `onStateChange` reports the BROWSER's own channel to the relay, which opens
4540
+ * the instant sharing starts — so a UI keyed on it announces "Agent is
4541
+ * driving" to a human who has not yet handed the link to anybody, and can
4542
+ * never tell them when one really arrives.
4543
+ */
4544
+ onPeersChange(listener) {
4545
+ this.peerListeners.add(listener);
4546
+ listener(this.peers.size);
4547
+ return () => this.peerListeners.delete(listener);
4548
+ }
4549
+ notifyPeers() {
4550
+ for (const l of this.peerListeners) l(this.peers.size);
4551
+ }
4527
4552
  /**
4528
4553
  * For relays that wrap each frame with auth metadata: hosts can call this
4529
4554
  * directly when a frame arrives via a non-SSE path. The transport will
@@ -4558,6 +4583,7 @@ var SseRelayTransport = class {
4558
4583
  if ("method" in message && message.method === "notifications/peer_joined") {
4559
4584
  const subscriberId = String(message.params?.subscriberId ?? "peer");
4560
4585
  this.peers.add(subscriberId);
4586
+ this.notifyPeers();
4561
4587
  fancyAutoCommon.emitActivity({
4562
4588
  agentId: "agent",
4563
4589
  agentName: "Agent",
@@ -4572,6 +4598,7 @@ var SseRelayTransport = class {
4572
4598
  if ("method" in message && message.method === "notifications/peer_left") {
4573
4599
  const subscriberId = String(message.params?.subscriberId ?? "peer");
4574
4600
  this.peers.delete(subscriberId);
4601
+ this.notifyPeers();
4575
4602
  if (this.peers.size === 0) {
4576
4603
  fancyAutoCommon.emitActivity({
4577
4604
  agentId: "agent",
@@ -4629,6 +4656,7 @@ function useCoBrowseSession(options) {
4629
4656
  const disposeBridge = react.useRef(null);
4630
4657
  const [session, setSession] = react.useState(null);
4631
4658
  const [relayState, setRelayState] = react.useState("idle");
4659
+ const [agentCount, setAgentCount] = react.useState(0);
4632
4660
  react.useEffect(() => {
4633
4661
  const server = new MicroMcpServer({
4634
4662
  info: options.info ?? { name: "fancy-co-browse", version: "0.1.0" },
@@ -4663,6 +4691,7 @@ function useCoBrowseSession(options) {
4663
4691
  }
4664
4692
  const relay = attachSseRelay(server, { baseUrl: relayBaseUrl, sessionId: descriptor.id, token: descriptor.token });
4665
4693
  relay.onStateChange(setRelayState);
4694
+ relay.onPeersChange(setAgentCount);
4666
4695
  relayRef.current = relay;
4667
4696
  setSession(descriptor);
4668
4697
  }, [relayBaseUrl, options]);
@@ -4671,6 +4700,7 @@ function useCoBrowseSession(options) {
4671
4700
  relayRef.current?.close();
4672
4701
  relayRef.current = null;
4673
4702
  setRelayState("idle");
4703
+ setAgentCount(0);
4674
4704
  setSession(null);
4675
4705
  if (current) {
4676
4706
  const csrf = options.csrfToken?.() ?? "";
@@ -4693,7 +4723,16 @@ function useCoBrowseSession(options) {
4693
4723
  meta: event
4694
4724
  });
4695
4725
  }, []);
4696
- return { server: serverRef.current, session, relayState, startShare, stopShare, observeUser };
4726
+ return {
4727
+ server: serverRef.current,
4728
+ session,
4729
+ relayState,
4730
+ agentConnected: agentCount > 0,
4731
+ agentCount,
4732
+ startShare,
4733
+ stopShare,
4734
+ observeUser
4735
+ };
4697
4736
  }
4698
4737
  function AgentPanel({ agent, activity, onSubmit, busy, actions, className, style }) {
4699
4738
  const scrollRef = react.useRef(null);
@@ -5416,10 +5455,12 @@ function ManualPopover({
5416
5455
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "fai-connect__copy-btn", onClick: onCopy, children: copied ? "Copied" : "Copy snippet" })
5417
5456
  ] });
5418
5457
  }
5458
+ var LIFECYCLE_ACTIONS = /* @__PURE__ */ new Set(["agent_connected", "agent_disconnected"]);
5419
5459
  function CoBrowsePresence({ session, connectUrl, shareBaseUrl, className }) {
5420
5460
  const { events } = useAgentActivity(void 0, { capacity: 40 });
5421
- const lastAgentAction = [...events].reverse().find((e) => (e.source ?? "agent") !== "user");
5422
- const connected = session.relayState === "open";
5461
+ const lastAgentAction = [...events].reverse().find((e) => (e.source ?? "agent") !== "user" && !LIFECYCLE_ACTIONS.has(e.action));
5462
+ const connected = session.agentConnected;
5463
+ const activity = react.useMemo(() => events.map(toPanelActivity), [events]);
5423
5464
  if (!session.session) {
5424
5465
  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" }) });
5425
5466
  }
@@ -5437,12 +5478,25 @@ function CoBrowsePresence({ session, connectUrl, shareBaseUrl, className }) {
5437
5478
  onStart: () => void session.startShare(),
5438
5479
  onStop: session.stopShare,
5439
5480
  status: session.relayState,
5481
+ agentConnected: connected,
5482
+ activity,
5440
5483
  shareBaseUrl
5441
5484
  }
5442
5485
  ),
5443
5486
  connectUrl && /* @__PURE__ */ jsxRuntime.jsx(ConnectorButtons, { serverName: "Fancy UI co-browse", mcpUrl: connectUrl })
5444
5487
  ] });
5445
5488
  }
5489
+ function toPanelActivity(event) {
5490
+ const who = (event.source ?? "agent") === "user" ? "You" : event.agentName ?? "Agent";
5491
+ return {
5492
+ id: `${event.timestamp}-${event.action}-${event.target?.elementId ?? event.target?.label ?? ""}`,
5493
+ at: event.timestamp,
5494
+ kind: LIFECYCLE_ACTIONS.has(event.action) ? "info" : "tool",
5495
+ source: who,
5496
+ text: event.target?.label ?? event.action,
5497
+ detail: event.meta
5498
+ };
5499
+ }
5446
5500
  CoBrowsePresence.displayName = "CoBrowsePresence";
5447
5501
  var CURSOR_EDGE_INSET = 24;
5448
5502
  var FLUID_CURSOR_SPEED_PX_PER_SECOND = 1050;
@@ -5461,19 +5515,26 @@ function fluidGlideDuration(from, to) {
5461
5515
  const distance = Math.hypot(to.x - from.x, to.y - from.y);
5462
5516
  return Math.round(Math.min(MAX_GLIDE_MS, Math.max(MIN_GLIDE_MS, distance / FLUID_CURSOR_SPEED_PX_PER_SECOND * 1e3)));
5463
5517
  }
5464
- function CoBrowseCursorLayer({ active = true, zIndex = 2147483e3 }) {
5518
+ var LIFECYCLE_ACTIONS2 = /* @__PURE__ */ new Set(["agent_connected", "agent_disconnected"]);
5519
+ function CoBrowseCursorLayer({
5520
+ active = true,
5521
+ zIndex = 2147483e3,
5522
+ idleAfterMs = 15e3
5523
+ }) {
5465
5524
  const { latest } = useAgentActivity(void 0, { capacity: 12 });
5466
5525
  const [cursor, setCursor] = react.useState(
5467
5526
  null
5468
5527
  );
5469
5528
  const [pulse, setPulse] = react.useState(null);
5529
+ const [lastActionAt, setLastActionAt] = react.useState(0);
5470
5530
  react.useEffect(() => {
5471
5531
  if (!latest || (latest.source ?? "agent") === "user") return;
5472
- if (latest.action === "agent_disconnected") {
5473
- setCursor((prev) => prev ? { ...prev, status: "Standing by", glideMs: 0 } : prev);
5532
+ if (LIFECYCLE_ACTIONS2.has(latest.action)) {
5533
+ setCursor((prev) => prev ? { ...prev, status: "Standing by", glideMs: 0 } : null);
5474
5534
  setPulse(null);
5475
5535
  return;
5476
5536
  }
5537
+ setLastActionAt(latest.timestamp);
5477
5538
  const color = latest.agentColor ?? "#a855f7";
5478
5539
  const name = latest.agentName ?? "Agent";
5479
5540
  const status = latest.target?.label ?? latest.action;
@@ -5495,6 +5556,14 @@ function CoBrowseCursorLayer({ active = true, zIndex = 2147483e3 }) {
5495
5556
  );
5496
5557
  }
5497
5558
  }, [latest?.timestamp]);
5559
+ react.useEffect(() => {
5560
+ if (!idleAfterMs || !lastActionAt) return;
5561
+ const timer = setTimeout(() => {
5562
+ setCursor(null);
5563
+ setPulse(null);
5564
+ }, idleAfterMs);
5565
+ return () => clearTimeout(timer);
5566
+ }, [lastActionAt, idleAfterMs]);
5498
5567
  if (!active || !cursor || typeof document === "undefined") return null;
5499
5568
  return reactDom.createPortal(
5500
5569
  /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-co-browse-cursor-layer": "", style: { position: "fixed", inset: 0, pointerEvents: "none", zIndex }, children: [