@omercnet/paseo-shared-browser 0.4.2-next.133.1 → 0.4.2-next.146.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.
package/README.md CHANGED
@@ -148,9 +148,9 @@ User-supplied `AGENT_BROWSER_*` variables are deliberately ignored.
148
148
  isolation boundary between local OS users.
149
149
  - `agent-browser` IPC metadata and workspace profile directories are owner-only on POSIX systems.
150
150
  The plugin rejects a non-loopback CDP endpoint.
151
- - Viewer and control tokens coordinate clients already paired to the same Paseo daemon. Paseo v0.8
152
- plugin RPC callbacks expose no authenticated caller identity, so these human-viewer tokens are a
153
- workflow safeguard, not an authorization boundary. The stdio MCP adapter separately uses an
151
+ - Viewer and control tokens coordinate clients already paired to the same Paseo daemon. Plugin RPC
152
+ callbacks expose no authenticated caller identity, so these human-viewer tokens are a workflow
153
+ safeguard, not an authorization boundary. The stdio MCP adapter separately uses an
154
154
  opaque, workspace-bound credential.
155
155
  - Downloads, uploads, clipboard synchronization, media permissions, extensions, native passkeys,
156
156
  and platform authenticators are not exposed by this plugin.
@@ -176,6 +176,5 @@ persistence, and archive teardown.
176
176
  Release Please maintains the version, changelog, component tag, and GitHub release from
177
177
  Conventional Commits in the monorepo.
178
178
 
179
- Both the Paseo daemon and app must satisfy `>=0.8.0 <0.10.0`, which supports Paseo 0.8
180
- and Paseo 0.9. The client surface uses React Native primitives and works in desktop, web, iOS,
181
- and Android Paseo clients.
179
+ Both the Paseo daemon and app must satisfy `^0.9.0`. The client surface uses React Native primitives
180
+ and works in desktop, web, iOS, and Android Paseo clients.
@@ -72,6 +72,7 @@ const CAPTURE_INTERVAL_READY = 250;
72
72
  const CAPTURE_INTERVAL_WAITING = 1_500;
73
73
  const MAX_VIEWER_LABEL_LENGTH = 64;
74
74
  const PILL_PRESENCE_POLL_MS = 2_000;
75
+ const AGENT_DIRECTORY_PAGE_LIMIT = 200;
75
76
  const MAX_URL_LENGTH = 8_192;
76
77
  const MAX_TEXT_LENGTH = 4_000;
77
78
  const BYTES_PER_KIBIBYTE = 1_024;
@@ -803,18 +804,33 @@ function CanvasPlaceholder({
803
804
  );
804
805
  }
805
806
 
807
+ interface AgentDirectoryPage {
808
+ entries: Array<{ agent: { id: string; workspaceId?: string | undefined } }>;
809
+ pageInfo: { hasMore: boolean; nextCursor: string | null };
810
+ }
811
+
812
+ type AgentDirectoryUpdate =
813
+ | { kind: "remove"; agentId: string }
814
+ | { kind: "upsert"; agent: { id: string; workspaceId?: string | undefined } };
815
+ type AgentPlacement = { id: string; workspaceId: string };
816
+
806
817
  export function contributeSharedBrowserClient(client: PluginClientContext) {
807
- const agents = new Map<string, { id: string; workspaceId: string }>();
818
+ const agents = new Map<string, AgentPlacement>();
808
819
  const pills = new Map<string, { workspaceId: string; remove: () => void }>();
820
+ const lifetime = new AbortController();
809
821
  let openWorkspaceIds = new Set<string>();
810
822
  let refreshing = false;
811
823
  let stopped = false;
824
+ let directoryGeneration = 0;
825
+ let pendingDirectory: { generation: number; updates: AgentDirectoryUpdate[] } | null = null;
826
+ let unsubscribeDirectory: (() => void) | null = null;
827
+ let releaseDirectory: (() => Promise<void>) | null = null;
812
828
 
813
829
  const removePill = (agentId: string) => {
814
830
  pills.get(agentId)?.remove();
815
831
  pills.delete(agentId);
816
832
  };
817
- const syncPill = (agent: { id: string; workspaceId: string }) => {
833
+ const syncPill = (agent: AgentPlacement) => {
818
834
  const current = pills.get(agent.id);
819
835
  if (!openWorkspaceIds.has(agent.workspaceId)) {
820
836
  removePill(agent.id);
@@ -858,32 +874,94 @@ export function contributeSharedBrowserClient(client: PluginClientContext) {
858
874
  refreshing = false;
859
875
  }
860
876
  };
861
-
862
- const unsubscribe = client.paseo.agents.subscribe((update) => {
877
+ const applyUpdate = (target: Map<string, AgentPlacement>, update: AgentDirectoryUpdate) => {
878
+ if (update.kind === "remove") {
879
+ target.delete(update.agentId);
880
+ return;
881
+ }
882
+ const { id, workspaceId } = update.agent;
883
+ if (workspaceId) target.set(id, { id, workspaceId });
884
+ else target.delete(id);
885
+ };
886
+ const applyLiveUpdate = (update: AgentDirectoryUpdate) => {
887
+ if (stopped) return;
888
+ pendingDirectory?.updates.push(update);
889
+ applyUpdate(agents, update);
863
890
  if (update.kind === "remove") {
864
- agents.delete(update.agentId);
865
891
  removePill(update.agentId);
866
892
  return;
867
893
  }
868
894
  const { id, workspaceId } = update.agent;
869
895
  if (!workspaceId) {
870
- agents.delete(id);
871
896
  removePill(id);
872
897
  return;
873
898
  }
874
- const agent = { id, workspaceId };
875
- agents.set(id, agent);
876
- syncPill(agent);
899
+ syncPill({ id, workspaceId });
877
900
  void refreshPresence();
878
- });
879
- void client.paseo.agents
880
- .list()
881
- .then(async ({ entries }) => {
882
- for (const { agent } of entries) {
883
- if (agent.workspaceId)
884
- agents.set(agent.id, { id: agent.id, workspaceId: agent.workspaceId });
901
+ };
902
+ const replaceAgents = (next: Map<string, AgentPlacement>) => {
903
+ for (const agentId of agents.keys()) {
904
+ if (!next.has(agentId)) removePill(agentId);
905
+ }
906
+ agents.clear();
907
+ for (const [agentId, agent] of next) agents.set(agentId, agent);
908
+ syncAllPills();
909
+ };
910
+ const followSnapshot = async (snapshot: AgentDirectoryPage) => {
911
+ if (stopped) return;
912
+ const generation = ++directoryGeneration;
913
+ const transaction = { generation, updates: [] as AgentDirectoryUpdate[] };
914
+ pendingDirectory = transaction;
915
+ const next = new Map<string, AgentPlacement>();
916
+ for (const { agent } of snapshot.entries) {
917
+ if (agent.workspaceId) next.set(agent.id, { id: agent.id, workspaceId: agent.workspaceId });
918
+ }
919
+
920
+ try {
921
+ let cursor = snapshot.pageInfo.hasMore ? snapshot.pageInfo.nextCursor : null;
922
+ while (cursor) {
923
+ const page = await client.paseo.agents.list({
924
+ scope: "active",
925
+ page: { limit: AGENT_DIRECTORY_PAGE_LIMIT, cursor },
926
+ signal: lifetime.signal,
927
+ });
928
+ if (stopped || pendingDirectory?.generation !== generation) return;
929
+ for (const { agent } of page.entries) {
930
+ if (agent.workspaceId)
931
+ next.set(agent.id, { id: agent.id, workspaceId: agent.workspaceId });
932
+ }
933
+ cursor = page.pageInfo.hasMore ? page.pageInfo.nextCursor : null;
885
934
  }
935
+ if (stopped || pendingDirectory?.generation !== generation) return;
936
+ for (const update of transaction.updates) applyUpdate(next, update);
937
+ pendingDirectory = null;
938
+ replaceAgents(next);
886
939
  await refreshPresence();
940
+ } catch {
941
+ if (!stopped && pendingDirectory?.generation === generation) pendingDirectory = null;
942
+ }
943
+ };
944
+
945
+ void client.paseo.agents
946
+ .list({
947
+ scope: "active",
948
+ page: { limit: AGENT_DIRECTORY_PAGE_LIMIT },
949
+ subscribe: {},
950
+ signal: lifetime.signal,
951
+ })
952
+ .then(({ subscription }) => {
953
+ if (stopped) {
954
+ void subscription.release().catch(() => undefined);
955
+ return undefined;
956
+ }
957
+ releaseDirectory = subscription.release;
958
+ unsubscribeDirectory = subscription.subscribe({
959
+ snapshot: (snapshot) => void followSnapshot(snapshot),
960
+ update: (message) => {
961
+ if (message.type === "agent_update") applyLiveUpdate(message.payload);
962
+ },
963
+ });
964
+ return undefined;
887
965
  })
888
966
  .catch(() => undefined);
889
967
  const presenceTimer = setInterval(() => void refreshPresence(), PILL_PRESENCE_POLL_MS);
@@ -891,8 +969,12 @@ export function contributeSharedBrowserClient(client: PluginClientContext) {
891
969
  return () => {
892
970
  if (stopped) return;
893
971
  stopped = true;
972
+ directoryGeneration += 1;
973
+ pendingDirectory = null;
894
974
  clearInterval(presenceTimer);
895
- unsubscribe();
975
+ unsubscribeDirectory?.();
976
+ void releaseDirectory?.().catch(() => undefined);
977
+ lifetime.abort();
896
978
  for (const { remove } of pills.values()) remove();
897
979
  pills.clear();
898
980
  agents.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-shared-browser",
3
- "version": "0.4.2-next.133.1",
3
+ "version": "0.4.2-next.146.1",
4
4
  "description": "One real Chromium session per Paseo workspace, shared live across every connected client.",
5
5
  "keywords": [
6
6
  "browser",
@@ -42,8 +42,8 @@
42
42
  "check:write": "biome check --write .",
43
43
  "typecheck": "tsc --noEmit",
44
44
  "prepare:runtime": "node scripts/prepare-runtime.mjs",
45
- "test": "vitest run tests/contracts.test.ts tests/lease.test.ts tests/runtime-lifecycle.test.ts tests/agent-access.test.ts",
46
- "test:unit": "vitest run tests/contracts.test.ts tests/lease.test.ts tests/runtime-lifecycle.test.ts tests/agent-access.test.ts",
45
+ "test": "vitest run tests/contracts.test.ts tests/lease.test.ts tests/runtime-lifecycle.test.ts tests/agent-access.test.ts tests/composer-pills.test.ts",
46
+ "test:unit": "vitest run tests/contracts.test.ts tests/lease.test.ts tests/runtime-lifecycle.test.ts tests/agent-access.test.ts tests/composer-pills.test.ts",
47
47
  "test:smoke": "vitest run tests/browser.smoke.test.ts --testTimeout=30000"
48
48
  },
49
49
  "dependencies": {
package/paseo-plugin.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "id": "shared-browser",
3
- "requirements": {
4
- "paseo": ">=0.8.0 <0.10.0"
5
- },
3
+ "description": "One real Chromium session per Paseo workspace, shared live across every connected client.",
4
+ "requirements": { "paseo": "^0.9.0" },
6
5
  "build": [["node", "scripts/prepare-dependencies.mjs"], ["npm", "run", "prepare:runtime"]]
7
6
  }
@@ -93,6 +93,7 @@ export interface AgentBrowserRuntimeOptions {
93
93
  ipcDirectory: string;
94
94
  session: string;
95
95
  initialUrl?: string;
96
+ headed?: boolean;
96
97
  timeoutMs?: number;
97
98
  }
98
99
 
@@ -189,6 +190,7 @@ export class AgentBrowserRuntime {
189
190
  readonly ipcDirectory: string;
190
191
  readonly session: string;
191
192
  private readonly initialUrl: string;
193
+ private readonly headed: boolean;
192
194
  private readonly timeoutMs: number;
193
195
  private readonly environment: NodeJS.ProcessEnv;
194
196
  private connection: CdpConnection | null = null;
@@ -212,6 +214,7 @@ export class AgentBrowserRuntime {
212
214
  throw new AgentBrowserIncompatibleError("agent-browser session name is invalid");
213
215
  }
214
216
  this.session = options.session;
217
+ this.headed = options.headed ?? false;
215
218
  this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
216
219
  this.environment = runtimeEnvironment(this.ipcDirectory);
217
220
  this.initialUrl = options.initialUrl ?? "about:blank";
@@ -236,6 +239,7 @@ export class AgentBrowserRuntime {
236
239
  this.profilePath,
237
240
  "--executable-path",
238
241
  this.executablePath,
242
+ ...(this.headed ? ["--headed"] : []),
239
243
  ...(chromiumArguments ? ["--args", chromiumArguments] : []),
240
244
  "--json",
241
245
  "open",
@@ -11,12 +11,18 @@ interface OwnedRuntime {
11
11
  runtimeId: string;
12
12
  runtime: AgentBrowserRuntime;
13
13
  }
14
+ interface RuntimeOwnerOptions {
15
+ initialUrl?: string;
16
+ headed?: boolean;
17
+ }
14
18
 
15
19
  function paseoHome(): string {
16
20
  return process.env.PASEO_HOME ?? join(homedir(), ".paseo");
17
21
  }
18
22
 
19
- export async function createRuntimeOwner(): Promise<RuntimeOwner<OwnedRuntime>> {
23
+ export async function createRuntimeOwner(
24
+ options: RuntimeOwnerOptions = {},
25
+ ): Promise<RuntimeOwner<OwnedRuntime>> {
20
26
  const root = join(paseoHome(), "plugin-data", "shared-browser");
21
27
  const ipcDirectory =
22
28
  process.platform === "win32"
@@ -46,7 +52,8 @@ export async function createRuntimeOwner(): Promise<RuntimeOwner<OwnedRuntime>>
46
52
  profilePath: join(root, "profiles", hash),
47
53
  ipcDirectory,
48
54
  session: `ws-${hash.slice(0, 16)}`,
49
- initialUrl: DEFAULT_BROWSER_URL,
55
+ initialUrl: options.initialUrl ?? DEFAULT_BROWSER_URL,
56
+ headed: options.headed ?? false,
50
57
  });
51
58
  await runtime.launch();
52
59
  return { runtimeId: randomUUID(), runtime };