@getpaseo/server 0.1.104-beta.2 → 0.1.104-beta.3

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.
Files changed (49) hide show
  1. package/dist/server/server/agent/create-agent/create.d.ts +1 -1
  2. package/dist/server/server/agent/create-agent/create.js +4 -4
  3. package/dist/server/server/agent/providers/claude/agent.js +3 -2
  4. package/dist/server/server/agent/providers/claude/feature-definitions.js +2 -14
  5. package/dist/server/server/agent/providers/claude/model-manifest.d.ts +90 -0
  6. package/dist/server/server/agent/providers/claude/model-manifest.js +205 -0
  7. package/dist/server/server/agent/providers/claude/models.js +4 -141
  8. package/dist/server/server/agent/tools/paseo-tools.d.ts +4 -1
  9. package/dist/server/server/agent/tools/paseo-tools.js +73 -4
  10. package/dist/server/server/bootstrap.js +37 -3
  11. package/dist/server/server/browser-tools/broker.d.ts +16 -5
  12. package/dist/server/server/browser-tools/broker.js +220 -22
  13. package/dist/server/server/browser-tools/tools.js +195 -36
  14. package/dist/server/server/session.d.ts +3 -10
  15. package/dist/server/server/session.js +10 -121
  16. package/dist/server/server/websocket-server.d.ts +3 -1
  17. package/dist/server/server/websocket-server.js +18 -5
  18. package/dist/server/server/workspace-auto-name.d.ts +55 -0
  19. package/dist/server/server/workspace-auto-name.js +128 -0
  20. package/dist/server/server/worktree-session.d.ts +1 -1
  21. package/dist/server/server/worktree-session.js +1 -1
  22. package/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-bridge-d9e471546cbc685a5f1a44f54114fc48.js → desktop-attachment-bridge-95a8163efd836849dd3f83730f1e84f4.js} +1 -1
  23. package/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-bridge-d9e471546cbc685a5f1a44f54114fc48.js.br → desktop-attachment-bridge-95a8163efd836849dd3f83730f1e84f4.js.br} +0 -0
  24. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-95a8163efd836849dd3f83730f1e84f4.js.gz +0 -0
  25. package/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-store-30526c06e214c5937e98bf1e5244cc22.js → desktop-attachment-store-9b3937a7b33a31908fa313b18c15daf6.js} +1 -1
  26. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-9b3937a7b33a31908fa313b18c15daf6.js.br +0 -0
  27. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-9b3937a7b33a31908fa313b18c15daf6.js.gz +0 -0
  28. package/dist/server/web-ui/_expo/static/js/web/{index-9f718c794c8b2995afa4a8f14218e814.js → index-690d9fe101525c0a632093e00322c944.js} +1056 -1055
  29. package/dist/server/web-ui/_expo/static/js/web/index-690d9fe101525c0a632093e00322c944.js.br +0 -0
  30. package/dist/server/web-ui/_expo/static/js/web/index-690d9fe101525c0a632093e00322c944.js.gz +0 -0
  31. package/dist/server/web-ui/_expo/static/js/web/{indexeddb-attachment-store-aa960ad0ab4cef263d0d7664e12f5b65.js → indexeddb-attachment-store-ee4a35a8c1da1c737256427f2fe19c35.js} +1 -1
  32. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-ee4a35a8c1da1c737256427f2fe19c35.js.br +0 -0
  33. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-ee4a35a8c1da1c737256427f2fe19c35.js.gz +0 -0
  34. package/dist/server/web-ui/_expo/static/js/web/{native-file-attachment-store-7837a7115a8a584b7d7231cfa2eae2cf.js → native-file-attachment-store-c4aebe23af349f0f64f418e755c84c00.js} +3 -3
  35. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-c4aebe23af349f0f64f418e755c84c00.js.br +0 -0
  36. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-c4aebe23af349f0f64f418e755c84c00.js.gz +0 -0
  37. package/dist/server/web-ui/index.html +1 -1
  38. package/dist/server/web-ui/index.html.br +0 -0
  39. package/dist/server/web-ui/index.html.gz +0 -0
  40. package/package.json +5 -5
  41. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-d9e471546cbc685a5f1a44f54114fc48.js.gz +0 -0
  42. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-30526c06e214c5937e98bf1e5244cc22.js.br +0 -0
  43. package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-30526c06e214c5937e98bf1e5244cc22.js.gz +0 -0
  44. package/dist/server/web-ui/_expo/static/js/web/index-9f718c794c8b2995afa4a8f14218e814.js.br +0 -0
  45. package/dist/server/web-ui/_expo/static/js/web/index-9f718c794c8b2995afa4a8f14218e814.js.gz +0 -0
  46. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-aa960ad0ab4cef263d0d7664e12f5b65.js.br +0 -0
  47. package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-aa960ad0ab4cef263d0d7664e12f5b65.js.gz +0 -0
  48. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-7837a7115a8a584b7d7231cfa2eae2cf.js.br +0 -0
  49. package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-7837a7115a8a584b7d7231cfa2eae2cf.js.gz +0 -0
@@ -331,6 +331,20 @@ export function createPaseoToolCatalog(options) {
331
331
  }
332
332
  return options.ensureWorkspaceForCreate(resolvedCwd);
333
333
  }
334
+ function resolveWorkspaceIdForRename(requestedWorkspaceId) {
335
+ const explicitWorkspaceId = requestedWorkspaceId?.trim();
336
+ if (explicitWorkspaceId) {
337
+ return explicitWorkspaceId;
338
+ }
339
+ if (callerAgentId) {
340
+ const callerAgent = resolveCallerAgent();
341
+ if (!callerAgent?.workspaceId) {
342
+ throw new Error(`Caller agent ${callerAgentId} has no current workspace`);
343
+ }
344
+ return callerAgent.workspaceId;
345
+ }
346
+ throw new Error("workspaceId is required outside an agent-scoped session");
347
+ }
334
348
  const buildCallerAgentScheduleConfigExtras = (callerAgent) => {
335
349
  return {
336
350
  ...(callerAgent.config.thinkingOptionId
@@ -856,7 +870,9 @@ export function createPaseoToolCatalog(options) {
856
870
  async function resolveCreateAgentToolArgs(args) {
857
871
  if (callerAgentId) {
858
872
  const parsed = agentToAgentCreateAgentArgsSchema.parse(args);
859
- const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace(parsed.workspace);
873
+ const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace(parsed.workspace, {
874
+ prompt: parsed.initialPrompt,
875
+ });
860
876
  return {
861
877
  kind: "agent-scoped",
862
878
  parsedArgs: parsed,
@@ -870,7 +886,9 @@ export function createPaseoToolCatalog(options) {
870
886
  if (parsedArgs.relationship.kind === "subagent") {
871
887
  throw new Error("relationship subagent requires an agent-scoped tool session");
872
888
  }
873
- const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace(parsedArgs.workspace);
889
+ const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace(parsedArgs.workspace, {
890
+ prompt: parsedArgs.initialPrompt,
891
+ });
874
892
  return {
875
893
  kind: "top-level",
876
894
  parsedArgs,
@@ -952,7 +970,7 @@ export function createPaseoToolCatalog(options) {
952
970
  }
953
971
  return null;
954
972
  }
955
- async function resolveCreateAgentWorkspace(workspace) {
973
+ async function resolveCreateAgentWorkspace(workspace, firstAgentContext) {
956
974
  if (workspace.kind === "current") {
957
975
  if (!callerAgentId) {
958
976
  throw new Error("workspace current requires an agent-scoped tool session");
@@ -995,7 +1013,7 @@ export function createPaseoToolCatalog(options) {
995
1013
  }
996
1014
  return {
997
1015
  cwd,
998
- workspaceId: await options.ensureWorkspaceForCreate(cwd),
1016
+ workspaceId: await options.ensureWorkspaceForCreate(cwd, firstAgentContext),
999
1017
  worktree: undefined,
1000
1018
  };
1001
1019
  }
@@ -1339,6 +1357,57 @@ export function createPaseoToolCatalog(options) {
1339
1357
  structuredContent: ensureValidJson({ success: true }),
1340
1358
  };
1341
1359
  });
1360
+ registerTool("rename_workspace", {
1361
+ title: "Rename workspace",
1362
+ description: "Rename a workspace by setting its user-visible title. Omit workspaceId to rename your current workspace.",
1363
+ inputSchema: {
1364
+ workspaceId: z
1365
+ .string()
1366
+ .trim()
1367
+ .min(1)
1368
+ .optional()
1369
+ .describe("Workspace id to rename. Omit to rename your current workspace."),
1370
+ title: z
1371
+ .string()
1372
+ .trim()
1373
+ .min(1, "title is required")
1374
+ .describe("New user-visible workspace title."),
1375
+ },
1376
+ outputSchema: {
1377
+ success: z.boolean(),
1378
+ workspaceId: z.string(),
1379
+ title: z.string(),
1380
+ },
1381
+ }, async ({ workspaceId: requestedWorkspaceId, title }) => {
1382
+ if (!options.workspaceRegistry) {
1383
+ throw new Error("Workspace registry is required to rename workspaces");
1384
+ }
1385
+ if (!options.emitWorkspaceUpdatesForWorkspaceIds) {
1386
+ throw new Error("Workspace update emitter is required to rename workspaces");
1387
+ }
1388
+ const workspaceId = resolveWorkspaceIdForRename(requestedWorkspaceId);
1389
+ const existing = await options.workspaceRegistry.get(workspaceId);
1390
+ if (!existing) {
1391
+ throw new Error(`Workspace ${workspaceId} not found`);
1392
+ }
1393
+ if (existing.archivedAt) {
1394
+ throw new Error(`Workspace ${workspaceId} is archived`);
1395
+ }
1396
+ await options.workspaceRegistry.upsert({
1397
+ ...existing,
1398
+ title,
1399
+ updatedAt: new Date().toISOString(),
1400
+ });
1401
+ await options.emitWorkspaceUpdatesForWorkspaceIds([workspaceId]);
1402
+ return {
1403
+ content: [],
1404
+ structuredContent: ensureValidJson({
1405
+ success: true,
1406
+ workspaceId,
1407
+ title,
1408
+ }),
1409
+ };
1410
+ });
1342
1411
  registerTool("list_terminals", {
1343
1412
  title: "List terminals",
1344
1413
  description: "List terminals for a working directory or across all working directories.",
@@ -116,6 +116,10 @@ import { terminateWithTreeKill } from "../utils/tree-kill.js";
116
116
  import { isHostnameAllowed } from "./hostnames.js";
117
117
  import { createRequireBearerMiddleware, isAgentMcpRequestAuthorized, } from "./auth.js";
118
118
  import { createWebUiMiddleware } from "./web-ui.js";
119
+ import { WorkspaceAutoName } from "./workspace-auto-name.js";
120
+ import { createGitMutationService } from "./session/git-mutation/git-mutation-service.js";
121
+ import { workspaceIdsOnCheckout } from "./workspace-directory.js";
122
+ import { resolveFirstAgentPromptTitle } from "./agent/create-agent-title.js";
119
123
  const MAX_MCP_DEBUG_BATCH_ITEMS = 10;
120
124
  const REDACTED_LOG_VALUE = "[redacted]";
121
125
  const DOWNLOAD_OPEN_FLAGS = process.platform === "win32" ? constants.O_RDONLY : constants.O_RDONLY | constants.O_NOFOLLOW;
@@ -596,8 +600,15 @@ export async function createPaseoDaemon(config, rootLogger) {
596
600
  const findWorkspaceIdForCwdExternal = async (cwd) => {
597
601
  return resolveWorkspaceIdForPath(cwd, await workspaceRegistry.list());
598
602
  };
599
- const ensureWorkspaceForCreateExternal = async (cwd) => {
600
- const workspace = await createLocalCheckoutWorkspace({ cwd }, { projectRegistry, workspaceRegistry, workspaceGitService });
603
+ const ensureWorkspaceForCreateExternal = async (cwd, firstAgentContext) => {
604
+ const workspace = await createLocalCheckoutWorkspace({ cwd, title: resolveFirstAgentPromptTitle(firstAgentContext) }, { projectRegistry, workspaceRegistry, workspaceGitService });
605
+ if (firstAgentContext) {
606
+ workspaceAutoName.scheduleForDirectory({
607
+ workspaceId: workspace.workspaceId,
608
+ cwd: workspace.cwd,
609
+ firstAgentContext,
610
+ });
611
+ }
601
612
  return workspace.workspaceId;
602
613
  };
603
614
  const listActiveWorkspacesExternal = async () => {
@@ -626,9 +637,30 @@ export async function createPaseoDaemon(config, rootLogger) {
626
637
  const workspaceIdList = Array.from(workspaceIds);
627
638
  await Promise.all((wsServer?.listActiveSessions() ?? []).map((session) => session.emitWorkspaceUpdatesForExternalWorkspaceIds(workspaceIdList)));
628
639
  };
640
+ const emitWorkspaceUpdateForCwdExternal = async (cwd) => {
641
+ const workspaceIds = workspaceIdsOnCheckout(await workspaceRegistry.list(), cwd);
642
+ await emitWorkspaceUpdatesExternal(workspaceIds);
643
+ };
629
644
  const emitExternalSessionMessage = (message) => {
630
645
  wsServer?.broadcast(wrapSessionMessage(message));
631
646
  };
647
+ const workspaceAutoName = new WorkspaceAutoName({
648
+ agentManager,
649
+ workspaceRegistry,
650
+ workspaceGitService,
651
+ providerSnapshotManager,
652
+ readDaemonConfig: () => ({ metadataGeneration: daemonConfigStore.get().metadataGeneration }),
653
+ gitMutation: createGitMutationService({
654
+ workspaceGitService,
655
+ github,
656
+ logger,
657
+ }),
658
+ emitWorkspaceUpdateForCwd: emitWorkspaceUpdateForCwdExternal,
659
+ emitWorkspaceUpdateForWorkspaceId: async (workspaceId) => {
660
+ await emitWorkspaceUpdatesExternal([workspaceId]);
661
+ },
662
+ logger,
663
+ });
632
664
  setupAutoArchiveOnMerge({
633
665
  paseoHome: config.paseoHome,
634
666
  paseoWorktreesBaseRoot: config.worktreesRoot,
@@ -668,6 +700,7 @@ export async function createPaseoDaemon(config, rootLogger) {
668
700
  ?.listActiveSessions()
669
701
  .map((session) => session.warmWorkspaceGitDataForWorkspace(workspace)) ?? []);
670
702
  },
703
+ autoNameWorkspaceBranchForFirstAgent: (autoNameInput) => workspaceAutoName.scheduleForWorktree(autoNameInput),
671
704
  emitWorkspaceUpdateForWorkspaceId: async (workspaceId) => {
672
705
  await emitWorkspaceUpdatesExternal([workspaceId]);
673
706
  },
@@ -697,6 +730,7 @@ export async function createPaseoDaemon(config, rootLogger) {
697
730
  listActiveWorkspaces: listActiveWorkspacesExternal,
698
731
  archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
699
732
  emitWorkspaceUpdatesForWorkspaceIds: emitWorkspaceUpdatesExternal,
733
+ workspaceRegistry,
700
734
  markWorkspaceArchiving: markWorkspaceArchivingExternal,
701
735
  clearWorkspaceArchiving: clearWorkspaceArchivingExternal,
702
736
  ensureWorkspaceForCreate: ensureWorkspaceForCreateExternal,
@@ -883,7 +917,7 @@ export async function createPaseoDaemon(config, rootLogger) {
883
917
  if (config.auth?.password) {
884
918
  logger.info("Daemon password authentication enabled");
885
919
  }
886
- wsServer = new VoiceAssistantWebSocketServer(httpServer, logger, serverId, agentManager, agentStorage, downloadTokenStore, config.paseoHome, daemonConfigStore, mcpBaseUrl, { allowedOrigins, hostnames: configuredHostnames }, config.auth, speechService, terminalManager, {
920
+ wsServer = new VoiceAssistantWebSocketServer(httpServer, logger, serverId, agentManager, agentStorage, downloadTokenStore, config.paseoHome, daemonConfigStore, mcpBaseUrl, { allowedOrigins, hostnames: configuredHostnames }, workspaceAutoName, config.auth, speechService, terminalManager, {
887
921
  finalTimeoutMs: config.dictationFinalTimeoutMs,
888
922
  }, daemonVersion, (intent) => {
889
923
  try {
@@ -1,8 +1,10 @@
1
- import { type BrowserAutomationCommand, type BrowserAutomationExecuteRequest, type BrowserAutomationExecuteResponse } from "@getpaseo/protocol/browser-automation/rpc-schemas";
1
+ import { type BrowserAutomationCommand, type BrowserAutomationCommandName, type BrowserAutomationExecuteRequest, type BrowserAutomationExecuteResponse } from "@getpaseo/protocol/browser-automation/rpc-schemas";
2
2
  import { type BrowserToolsResponsePayload } from "./errors.js";
3
3
  import type { BrowserToolsPolicy } from "./policy.js";
4
- export interface BrowserToolsDesktopClient {
4
+ export interface BrowserHostClient {
5
5
  id: string;
6
+ hostKind: string;
7
+ supportedCommands: readonly BrowserAutomationCommandName[];
6
8
  sendBrowserAutomationRequest(request: BrowserAutomationExecuteRequest): void | Promise<void>;
7
9
  }
8
10
  export interface BrowserToolsExecuteInput {
@@ -24,14 +26,23 @@ export declare class BrowserToolsBroker {
24
26
  private readonly createRequestId;
25
27
  private readonly clients;
26
28
  private readonly pending;
29
+ private readonly browserHostByBrowserId;
30
+ private readonly strandedBrowserHostByBrowserId;
31
+ private registrationSequence;
27
32
  constructor(options: BrowserToolsBrokerOptions);
28
- registerClient(client: BrowserToolsDesktopClient): () => void;
29
- unregisterClient(clientId: string): void;
33
+ registerClient(client: BrowserHostClient): () => void;
34
+ unregisterClient(clientId: string, registeredAt?: number): void;
30
35
  getPendingRequestCount(): number;
31
36
  getRegisteredClientCount(): number;
32
37
  execute(input: BrowserToolsExecuteInput): Promise<BrowserToolsResponsePayload>;
33
38
  receiveResponse(response: BrowserAutomationExecuteResponse): boolean;
34
- private selectClient;
39
+ private executeListTabs;
40
+ private selectHostForCommand;
41
+ private selectMostRecentlyRegisteredHost;
42
+ private unsupportedCommandFailure;
43
+ private noBrowserHostFailure;
44
+ private strandedBrowserTabFailure;
45
+ private rememberBrowserHostForPayload;
35
46
  private sendRequest;
36
47
  }
37
48
  //# sourceMappingURL=broker.d.ts.map
@@ -6,19 +6,36 @@ export class BrowserToolsBroker {
6
6
  constructor(options) {
7
7
  this.clients = new Map();
8
8
  this.pending = new Map();
9
+ this.browserHostByBrowserId = new Map();
10
+ this.strandedBrowserHostByBrowserId = new Map();
11
+ this.registrationSequence = 0;
9
12
  this.policy = options.policy;
10
13
  this.defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_BROWSER_TOOLS_TIMEOUT_MS;
11
14
  this.createRequestId = options.createRequestId ?? (() => `browser_${randomUUID()}`);
12
15
  }
13
16
  registerClient(client) {
14
- this.clients.set(client.id, client);
15
- return () => this.unregisterClient(client.id);
17
+ this.unregisterClient(client.id);
18
+ const registeredAt = ++this.registrationSequence;
19
+ this.clients.set(client.id, {
20
+ client,
21
+ registeredAt,
22
+ supportedCommands: new Set(client.supportedCommands),
23
+ });
24
+ return () => this.unregisterClient(client.id, registeredAt);
16
25
  }
17
- unregisterClient(clientId) {
18
- const deleted = this.clients.delete(clientId);
19
- if (!deleted) {
26
+ unregisterClient(clientId, registeredAt) {
27
+ const current = this.clients.get(clientId);
28
+ if (!current || (registeredAt !== undefined && current.registeredAt !== registeredAt)) {
20
29
  return;
21
30
  }
31
+ this.clients.delete(clientId);
32
+ for (const [browserId, ownerClientId] of this.browserHostByBrowserId) {
33
+ if (ownerClientId !== clientId) {
34
+ continue;
35
+ }
36
+ this.browserHostByBrowserId.delete(browserId);
37
+ this.strandedBrowserHostByBrowserId.set(browserId, clientId);
38
+ }
22
39
  for (const [requestId, pending] of this.pending) {
23
40
  if (pending.clientId !== clientId) {
24
41
  continue;
@@ -27,8 +44,8 @@ export class BrowserToolsBroker {
27
44
  clearTimeout(pending.timeout);
28
45
  pending.resolve(browserToolsFailure({
29
46
  requestId,
30
- code: "browser_no_desktop",
31
- message: "The desktop browser automation client disconnected before responding.",
47
+ code: "browser_no_host",
48
+ message: "The browser automation host disconnected before responding.",
32
49
  retryable: true,
33
50
  }));
34
51
  }
@@ -48,15 +65,6 @@ export class BrowserToolsBroker {
48
65
  message: "Browser tools are disabled. Enable daemon.browserTools.enabled to use them.",
49
66
  });
50
67
  }
51
- const client = this.selectClient();
52
- if (!client) {
53
- return browserToolsFailure({
54
- requestId,
55
- code: "browser_no_desktop",
56
- message: "No desktop browser automation client is connected.",
57
- retryable: true,
58
- });
59
- }
60
68
  const request = BrowserAutomationExecuteRequestSchema.safeParse({
61
69
  type: "browser.automation.execute.request",
62
70
  requestId,
@@ -72,8 +80,26 @@ export class BrowserToolsBroker {
72
80
  message: formatBrowserAutomationValidationError(request.error.issues[0]?.message),
73
81
  });
74
82
  }
83
+ if (request.data.command.command === "list_tabs") {
84
+ return this.executeListTabs({
85
+ request: request.data,
86
+ timeoutMs: input.timeoutMs ?? this.defaultTimeoutMs,
87
+ });
88
+ }
89
+ const host = this.selectHostForCommand(request.data.command, requestId);
90
+ if (!host.ok) {
91
+ return host.payload;
92
+ }
93
+ const unsupported = this.unsupportedCommandFailure({
94
+ host: host.value,
95
+ commandName: request.data.command.command,
96
+ requestId,
97
+ });
98
+ if (unsupported) {
99
+ return unsupported;
100
+ }
75
101
  return this.sendRequest({
76
- client,
102
+ host: host.value,
77
103
  request: request.data,
78
104
  timeoutMs: input.timeoutMs ?? this.defaultTimeoutMs,
79
105
  });
@@ -104,17 +130,175 @@ export class BrowserToolsBroker {
104
130
  }
105
131
  this.pending.delete(parsed.data.payload.requestId);
106
132
  clearTimeout(pending.timeout);
133
+ if (pending.rememberAffinity) {
134
+ this.rememberBrowserHostForPayload(pending.clientId, parsed.data.payload);
135
+ }
107
136
  pending.resolve(parsed.data.payload);
108
137
  return true;
109
138
  }
110
- selectClient() {
111
- for (const client of this.clients.values()) {
112
- return client;
139
+ async executeListTabs(params) {
140
+ const hosts = Array.from(this.clients.values());
141
+ if (hosts.length === 0) {
142
+ return this.noBrowserHostFailure(params.request.requestId);
143
+ }
144
+ for (const host of hosts) {
145
+ const unsupported = this.unsupportedCommandFailure({
146
+ host,
147
+ commandName: "list_tabs",
148
+ requestId: params.request.requestId,
149
+ });
150
+ if (unsupported) {
151
+ return unsupported;
152
+ }
153
+ }
154
+ if (hosts.length === 1) {
155
+ return this.sendRequest({
156
+ host: hosts[0],
157
+ request: params.request,
158
+ timeoutMs: params.timeoutMs,
159
+ });
160
+ }
161
+ const hostResponses = await Promise.all(hosts.map(async (host) => ({
162
+ host,
163
+ payload: await this.sendRequest({
164
+ host,
165
+ request: {
166
+ ...params.request,
167
+ requestId: `${params.request.requestId}:${host.client.id}`,
168
+ },
169
+ rememberAffinity: false,
170
+ timeoutMs: params.timeoutMs,
171
+ }),
172
+ })));
173
+ const failed = hostResponses.find(({ payload }) => !payload.ok);
174
+ if (failed) {
175
+ return withBrowserToolsRequestId(failed.payload, params.request.requestId);
176
+ }
177
+ for (const { host, payload } of hostResponses) {
178
+ this.rememberBrowserHostForPayload(host.client.id, payload);
179
+ }
180
+ return {
181
+ requestId: params.request.requestId,
182
+ ok: true,
183
+ result: {
184
+ command: "list_tabs",
185
+ tabs: hostResponses.flatMap(({ payload }) => payload.ok && payload.result.command === "list_tabs" ? payload.result.tabs : []),
186
+ },
187
+ };
188
+ }
189
+ selectHostForCommand(command, requestId) {
190
+ if (command.command === "new_tab") {
191
+ const host = this.selectMostRecentlyRegisteredHost();
192
+ return host
193
+ ? { ok: true, value: host }
194
+ : { ok: false, payload: this.noBrowserHostFailure(requestId) };
195
+ }
196
+ const browserId = getBrowserIdForCommand(command);
197
+ if (!browserId) {
198
+ const host = this.selectMostRecentlyRegisteredHost();
199
+ return host
200
+ ? { ok: true, value: host }
201
+ : { ok: false, payload: this.noBrowserHostFailure(requestId) };
202
+ }
203
+ const ownerClientId = this.browserHostByBrowserId.get(browserId);
204
+ if (ownerClientId) {
205
+ const host = this.clients.get(ownerClientId);
206
+ if (host) {
207
+ return { ok: true, value: host };
208
+ }
209
+ return {
210
+ ok: false,
211
+ payload: this.strandedBrowserTabFailure({ requestId, browserId }),
212
+ };
213
+ }
214
+ const strandedOwnerClientId = this.strandedBrowserHostByBrowserId.get(browserId);
215
+ if (strandedOwnerClientId) {
216
+ const reconnectedHost = this.clients.get(strandedOwnerClientId);
217
+ if (reconnectedHost) {
218
+ this.strandedBrowserHostByBrowserId.delete(browserId);
219
+ this.browserHostByBrowserId.set(browserId, strandedOwnerClientId);
220
+ return { ok: true, value: reconnectedHost };
221
+ }
222
+ return {
223
+ ok: false,
224
+ payload: this.strandedBrowserTabFailure({ requestId, browserId }),
225
+ };
226
+ }
227
+ if (this.clients.size === 1) {
228
+ const host = this.selectMostRecentlyRegisteredHost();
229
+ if (host) {
230
+ return { ok: true, value: host };
231
+ }
232
+ }
233
+ if (this.clients.size === 0) {
234
+ return { ok: false, payload: this.noBrowserHostFailure(requestId) };
235
+ }
236
+ return {
237
+ ok: false,
238
+ payload: browserToolsFailure({
239
+ requestId,
240
+ code: "browser_tab_not_found",
241
+ message: `Browser tab ${browserId} is not associated with a connected browser automation host. Call browser_list_tabs and use one of the returned browserId values.`,
242
+ }),
243
+ };
244
+ }
245
+ selectMostRecentlyRegisteredHost() {
246
+ let selected = null;
247
+ for (const host of this.clients.values()) {
248
+ selected = host;
249
+ }
250
+ return selected;
251
+ }
252
+ unsupportedCommandFailure(params) {
253
+ if (params.host.supportedCommands.has(params.commandName)) {
254
+ return null;
255
+ }
256
+ return browserToolsFailure({
257
+ requestId: params.requestId,
258
+ code: "browser_unsupported",
259
+ message: `Browser automation command "${params.commandName}" is not supported by the ${describeBrowserHost(params.host)}.`,
260
+ });
261
+ }
262
+ noBrowserHostFailure(requestId) {
263
+ return browserToolsFailure({
264
+ requestId,
265
+ code: "browser_no_host",
266
+ message: "No browser automation host is connected.",
267
+ retryable: true,
268
+ });
269
+ }
270
+ strandedBrowserTabFailure(params) {
271
+ return browserToolsFailure({
272
+ requestId: params.requestId,
273
+ code: "browser_no_host",
274
+ message: `The app hosting browser tab ${params.browserId} disconnected.`,
275
+ retryable: true,
276
+ });
277
+ }
278
+ rememberBrowserHostForPayload(clientId, payload) {
279
+ if (!payload.ok) {
280
+ return;
281
+ }
282
+ if (payload.result.command === "list_tabs") {
283
+ for (const tab of payload.result.tabs) {
284
+ this.browserHostByBrowserId.set(tab.browserId, clientId);
285
+ this.strandedBrowserHostByBrowserId.delete(tab.browserId);
286
+ }
287
+ return;
288
+ }
289
+ if (payload.result.command === "close_tab") {
290
+ this.browserHostByBrowserId.delete(payload.result.browserId);
291
+ this.strandedBrowserHostByBrowserId.delete(payload.result.browserId);
292
+ return;
293
+ }
294
+ if ("browserId" in payload.result) {
295
+ this.browserHostByBrowserId.set(payload.result.browserId, clientId);
296
+ this.strandedBrowserHostByBrowserId.delete(payload.result.browserId);
113
297
  }
114
- return null;
115
298
  }
116
299
  sendRequest(params) {
117
- const { client, request, timeoutMs } = params;
300
+ const { host, request, timeoutMs } = params;
301
+ const client = host.client;
118
302
  return new Promise((resolve) => {
119
303
  const timeout = setTimeout(() => {
120
304
  if (!this.pending.delete(request.requestId)) {
@@ -129,6 +313,7 @@ export class BrowserToolsBroker {
129
313
  }, timeoutMs);
130
314
  this.pending.set(request.requestId, {
131
315
  clientId: client.id,
316
+ rememberAffinity: params.rememberAffinity ?? true,
132
317
  timeout,
133
318
  resolve,
134
319
  });
@@ -155,6 +340,19 @@ export class BrowserToolsBroker {
155
340
  });
156
341
  }
157
342
  }
343
+ function getBrowserIdForCommand(command) {
344
+ if (command.command === "list_tabs" || command.command === "new_tab") {
345
+ return null;
346
+ }
347
+ return command.args.browserId;
348
+ }
349
+ function describeBrowserHost(host) {
350
+ const hostKind = host.client.hostKind.trim();
351
+ return hostKind || "browser host";
352
+ }
353
+ function withBrowserToolsRequestId(payload, requestId) {
354
+ return { ...payload, requestId };
355
+ }
158
356
  function resolveSendFailure(params) {
159
357
  if (!params.pending.delete(params.requestId)) {
160
358
  return;