@particle-academy/agent-integrations 0.27.0 → 0.28.1

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-HKQBG2HZ.js';
2
- import { createSessionDescriptor, attachSseRelay } from './chunk-FUI7KXE7.js';
1
+ import { ShareControls, AgentCursor, AgentActivityHighlight, AgentPanel } from './chunk-5KLUYF4D.js';
2
+ import { createSessionDescriptor, attachSseRelay } from './chunk-KMSRG5KH.js';
3
3
  import { attachInProcess } from './chunk-AFUULW5E.js';
4
4
  import { registerWhiteboardBridge } from './chunk-AAMQOXZC.js';
5
5
  import './chunk-ULJL53DL.js';
package/dist/index.cjs CHANGED
@@ -2772,6 +2772,17 @@ function registerNavigationBridge(host, options) {
2772
2772
  const agent = { ...DEFAULT_AGENT11, ...options.agent ?? {} };
2773
2773
  const pendingMode = options.pendingMode ?? true;
2774
2774
  const disposers = [];
2775
+ let groundedRevision = null;
2776
+ const staleIntent = () => {
2777
+ const current = adapter.getRevision?.();
2778
+ if (current === void 0) return null;
2779
+ if (groundedRevision === null || groundedRevision !== current) {
2780
+ return errorResult(
2781
+ `Human took control at page revision ${current}. This agent action was cancelled; call page_describe before acting again.`
2782
+ );
2783
+ }
2784
+ return null;
2785
+ };
2775
2786
  ensureUndoToolsRegistered(host, { defaultAgentId: agent.id });
2776
2787
  const target = (label, elementId) => ({
2777
2788
  kind: "navigation",
@@ -2812,6 +2823,11 @@ function registerNavigationBridge(host, options) {
2812
2823
  [],
2813
2824
  () => {
2814
2825
  const snap = adapter.describe();
2826
+ const revision = adapter.getRevision?.();
2827
+ if (revision !== void 0) {
2828
+ snap.revision = revision;
2829
+ groundedRevision = revision;
2830
+ }
2815
2831
  const text = [
2816
2832
  `URL: ${snap.url}`,
2817
2833
  `Title: ${snap.title}`,
@@ -2820,7 +2836,7 @@ function registerNavigationBridge(host, options) {
2820
2836
  ].join("\n");
2821
2837
  return textResult(text, snap);
2822
2838
  },
2823
- false
2839
+ () => target("Inspect page")
2824
2840
  );
2825
2841
  reg(
2826
2842
  "page_read",
@@ -2828,7 +2844,25 @@ function registerNavigationBridge(host, options) {
2828
2844
  {},
2829
2845
  [],
2830
2846
  () => textResult(adapter.read ? adapter.read() : "(host did not provide page text)"),
2831
- false
2847
+ () => target("Read page")
2848
+ );
2849
+ reg(
2850
+ "page_focus",
2851
+ "Move the agent's visible focus to an interactive element by stable handle without activating it.",
2852
+ { handle: { type: "string" } },
2853
+ ["handle"],
2854
+ (args) => {
2855
+ const stale = staleIntent();
2856
+ if (stale) return stale;
2857
+ const handle = String(args.handle ?? "");
2858
+ const action = adapter.describe().actions.find((candidate) => candidate.handle === handle);
2859
+ if (!action) return errorResult(`No element for handle "${handle}"`);
2860
+ return textResult(`Focused ${handle}`, {
2861
+ handle,
2862
+ rect: adapter.rectFor?.(handle) ?? void 0
2863
+ });
2864
+ },
2865
+ (args) => target(`Focus ${String(args.handle ?? "")}`, String(args.handle ?? ""))
2832
2866
  );
2833
2867
  reg(
2834
2868
  "nav_visit",
@@ -2836,6 +2870,8 @@ function registerNavigationBridge(host, options) {
2836
2870
  { url: { type: "string", description: "Path like /packages or an absolute URL." } },
2837
2871
  ["url"],
2838
2872
  async (args) => {
2873
+ const stale = staleIntent();
2874
+ if (stale) return stale;
2839
2875
  const url = String(args.url ?? "");
2840
2876
  if (!url) return errorResult("url is required");
2841
2877
  const from = adapter.getLocation().url;
@@ -2862,6 +2898,8 @@ function registerNavigationBridge(host, options) {
2862
2898
  {},
2863
2899
  [],
2864
2900
  async () => {
2901
+ const stale = staleIntent();
2902
+ if (stale) return stale;
2865
2903
  if (!adapter.back) return errorResult("Host did not provide back navigation.");
2866
2904
  await adapter.back();
2867
2905
  return textResult("Went back");
@@ -2874,6 +2912,8 @@ function registerNavigationBridge(host, options) {
2874
2912
  {},
2875
2913
  [],
2876
2914
  async () => {
2915
+ const stale = staleIntent();
2916
+ if (stale) return stale;
2877
2917
  if (!adapter.forward) return errorResult("Host did not provide forward navigation.");
2878
2918
  await adapter.forward();
2879
2919
  return textResult("Went forward");
@@ -2890,6 +2930,8 @@ function registerNavigationBridge(host, options) {
2890
2930
  },
2891
2931
  [],
2892
2932
  (args) => {
2933
+ const stale = staleIntent();
2934
+ if (stale) return stale;
2893
2935
  const handle = typeof args.handle === "string" ? args.handle : void 0;
2894
2936
  adapter.scrollTo({
2895
2937
  handle,
@@ -2906,6 +2948,8 @@ function registerNavigationBridge(host, options) {
2906
2948
  { dy: { type: "number" } },
2907
2949
  ["dy"],
2908
2950
  (args) => {
2951
+ const stale = staleIntent();
2952
+ if (stale) return stale;
2909
2953
  adapter.scrollBy(Number(args.dy ?? 0));
2910
2954
  return textResult(`Scrolled by ${Number(args.dy ?? 0)}px`);
2911
2955
  },
@@ -2920,6 +2964,8 @@ function registerNavigationBridge(host, options) {
2920
2964
  },
2921
2965
  ["handle", "value"],
2922
2966
  (args) => {
2967
+ const stale = staleIntent();
2968
+ if (stale) return stale;
2923
2969
  const handle = String(args.handle ?? "");
2924
2970
  const res = adapter.setField(handle, args.value);
2925
2971
  if (!res.ok) return errorResult(res.error ?? `Could not set ${handle}`);
@@ -2933,6 +2979,8 @@ function registerNavigationBridge(host, options) {
2933
2979
  { handle: { type: "string" } },
2934
2980
  ["handle"],
2935
2981
  async (args) => {
2982
+ const stale = staleIntent();
2983
+ if (stale) return stale;
2936
2984
  const handle = String(args.handle ?? "");
2937
2985
  const action = adapter.describe().actions.find((a) => a.handle === handle);
2938
2986
  const rect = adapter.rectFor?.(handle) ?? void 0;
@@ -2952,6 +3000,8 @@ function registerNavigationBridge(host, options) {
2952
3000
  { handle: { type: "string" } },
2953
3001
  ["handle"],
2954
3002
  async (args) => {
3003
+ const stale = staleIntent();
3004
+ if (stale) return stale;
2955
3005
  const handle = String(args.handle ?? "");
2956
3006
  const rect = adapter.rectFor?.(handle) ?? void 0;
2957
3007
  if (pendingMode && adapter.confirm) {
@@ -3853,6 +3903,7 @@ function constantTimeEqual(a, b) {
3853
3903
  }
3854
3904
 
3855
3905
  // src/sharing/sse-relay.ts
3906
+ init_registry();
3856
3907
  var SseRelayTransport = class {
3857
3908
  constructor(options) {
3858
3909
  this.disposed = false;
@@ -3860,6 +3911,7 @@ var SseRelayTransport = class {
3860
3911
  this.connected = false;
3861
3912
  this.listeners = /* @__PURE__ */ new Set();
3862
3913
  this.state = "idle";
3914
+ this.peers = /* @__PURE__ */ new Set();
3863
3915
  this.opts = options;
3864
3916
  this.expectedToken = options.token;
3865
3917
  }
@@ -3958,6 +4010,36 @@ var SseRelayTransport = class {
3958
4010
  } catch {
3959
4011
  return;
3960
4012
  }
4013
+ if ("method" in message && message.method === "notifications/peer_joined") {
4014
+ const subscriberId = String(message.params?.subscriberId ?? "peer");
4015
+ this.peers.add(subscriberId);
4016
+ fancyAutoCommon.emitActivity({
4017
+ agentId: "agent",
4018
+ agentName: "Agent",
4019
+ agentColor: "#a855f7",
4020
+ action: "agent_connected",
4021
+ timestamp: Date.now(),
4022
+ target: { kind: "navigation", label: "Agent connected" },
4023
+ meta: { subscriberId }
4024
+ });
4025
+ return;
4026
+ }
4027
+ if ("method" in message && message.method === "notifications/peer_left") {
4028
+ const subscriberId = String(message.params?.subscriberId ?? "peer");
4029
+ this.peers.delete(subscriberId);
4030
+ if (this.peers.size === 0) {
4031
+ fancyAutoCommon.emitActivity({
4032
+ agentId: "agent",
4033
+ agentName: "Agent",
4034
+ agentColor: "#a855f7",
4035
+ action: "agent_disconnected",
4036
+ timestamp: Date.now(),
4037
+ target: { kind: "navigation", label: "Agent disconnected" },
4038
+ meta: { subscriberId }
4039
+ });
4040
+ }
4041
+ return;
4042
+ }
3961
4043
  await this.server.receive(this, message);
3962
4044
  }
3963
4045
  setState(state) {
@@ -4026,11 +4108,14 @@ function useCoBrowseSession(options) {
4026
4108
  if (!server || relayRef.current) return;
4027
4109
  const descriptor = createSessionDescriptor();
4028
4110
  const csrf = options.csrfToken?.() ?? "";
4029
- await fetch(`${relayBaseUrl}/register`, {
4111
+ const response = await fetch(`${relayBaseUrl}/register`, {
4030
4112
  method: "POST",
4031
4113
  headers: { "content-type": "application/json", "x-csrf-token": csrf },
4032
4114
  body: JSON.stringify({ session: descriptor.id, token: descriptor.token })
4033
4115
  });
4116
+ if (!response.ok) {
4117
+ throw new Error(`Relay registration failed (${response.status})`);
4118
+ }
4034
4119
  const relay = attachSseRelay(server, { baseUrl: relayBaseUrl, sessionId: descriptor.id, token: descriptor.token });
4035
4120
  relay.onStateChange(setRelayState);
4036
4121
  relayRef.current = relay;
@@ -4044,16 +4129,15 @@ function useCoBrowseSession(options) {
4044
4129
  setSession(null);
4045
4130
  if (current) {
4046
4131
  const csrf = options.csrfToken?.() ?? "";
4047
- void fetch(`${relayBaseUrl}/${current.id}/unregister`, {
4132
+ void fetch(`${relayBaseUrl}/${current.id}/unregister?token=${encodeURIComponent(current.token)}`, {
4048
4133
  method: "POST",
4049
- headers: { "content-type": "application/json", "x-csrf-token": csrf },
4050
- body: JSON.stringify({ token: current.token })
4134
+ headers: { "content-type": "application/json", "x-csrf-token": csrf }
4051
4135
  }).catch(() => {
4052
4136
  });
4053
4137
  }
4054
4138
  }, [relayBaseUrl, options, session]);
4055
4139
  const observeUser = react.useCallback((event) => {
4056
- const label = event.kind === "navigation" ? `You navigated to ${event.url}` : event.kind === "scroll" ? "You scrolled" : `You edited ${event.handle}${event.masked ? " (hidden)" : ""}`;
4140
+ const label = event.kind === "navigation" ? `You navigated to ${event.url}` : event.kind === "scroll" ? "You scrolled" : event.kind === "click" ? `You clicked ${event.label ?? event.handle}` : `You edited ${event.handle}${event.masked ? " (hidden)" : ""}`;
4057
4141
  fancyAutoCommon.emitActivity({
4058
4142
  agentId: USER.id,
4059
4143
  agentName: USER.name,
@@ -4829,6 +4913,11 @@ function CoBrowseCursorLayer({ active = true, zIndex = 2147483e3 }) {
4829
4913
  const [pulse, setPulse] = react.useState(null);
4830
4914
  react.useEffect(() => {
4831
4915
  if (!latest || (latest.source ?? "agent") === "user") return;
4916
+ if (latest.action === "agent_disconnected") {
4917
+ setCursor(null);
4918
+ setPulse(null);
4919
+ return;
4920
+ }
4832
4921
  const color = latest.agentColor ?? "#a855f7";
4833
4922
  const name = latest.agentName ?? "Agent";
4834
4923
  const status = latest.target?.label ?? latest.action;