@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.
- package/dist/server/server/agent/create-agent/create.d.ts +1 -1
- package/dist/server/server/agent/create-agent/create.js +4 -4
- package/dist/server/server/agent/providers/claude/agent.js +3 -2
- package/dist/server/server/agent/providers/claude/feature-definitions.js +2 -14
- package/dist/server/server/agent/providers/claude/model-manifest.d.ts +90 -0
- package/dist/server/server/agent/providers/claude/model-manifest.js +205 -0
- package/dist/server/server/agent/providers/claude/models.js +4 -141
- package/dist/server/server/agent/tools/paseo-tools.d.ts +4 -1
- package/dist/server/server/agent/tools/paseo-tools.js +73 -4
- package/dist/server/server/bootstrap.js +37 -3
- package/dist/server/server/browser-tools/broker.d.ts +16 -5
- package/dist/server/server/browser-tools/broker.js +220 -22
- package/dist/server/server/browser-tools/tools.js +195 -36
- package/dist/server/server/session.d.ts +3 -10
- package/dist/server/server/session.js +10 -121
- package/dist/server/server/websocket-server.d.ts +3 -1
- package/dist/server/server/websocket-server.js +18 -5
- package/dist/server/server/workspace-auto-name.d.ts +55 -0
- package/dist/server/server/workspace-auto-name.js +128 -0
- package/dist/server/server/worktree-session.d.ts +1 -1
- package/dist/server/server/worktree-session.js +1 -1
- package/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-bridge-d9e471546cbc685a5f1a44f54114fc48.js → desktop-attachment-bridge-95a8163efd836849dd3f83730f1e84f4.js} +1 -1
- package/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-bridge-d9e471546cbc685a5f1a44f54114fc48.js.br → desktop-attachment-bridge-95a8163efd836849dd3f83730f1e84f4.js.br} +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-95a8163efd836849dd3f83730f1e84f4.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-store-30526c06e214c5937e98bf1e5244cc22.js → desktop-attachment-store-9b3937a7b33a31908fa313b18c15daf6.js} +1 -1
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-9b3937a7b33a31908fa313b18c15daf6.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-9b3937a7b33a31908fa313b18c15daf6.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-9f718c794c8b2995afa4a8f14218e814.js → index-690d9fe101525c0a632093e00322c944.js} +1056 -1055
- package/dist/server/web-ui/_expo/static/js/web/index-690d9fe101525c0a632093e00322c944.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-690d9fe101525c0a632093e00322c944.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{indexeddb-attachment-store-aa960ad0ab4cef263d0d7664e12f5b65.js → indexeddb-attachment-store-ee4a35a8c1da1c737256427f2fe19c35.js} +1 -1
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-ee4a35a8c1da1c737256427f2fe19c35.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-ee4a35a8c1da1c737256427f2fe19c35.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{native-file-attachment-store-7837a7115a8a584b7d7231cfa2eae2cf.js → native-file-attachment-store-c4aebe23af349f0f64f418e755c84c00.js} +3 -3
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-c4aebe23af349f0f64f418e755c84c00.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-c4aebe23af349f0f64f418e755c84c00.js.gz +0 -0
- package/dist/server/web-ui/index.html +1 -1
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/package.json +5 -5
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-d9e471546cbc685a5f1a44f54114fc48.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-30526c06e214c5937e98bf1e5244cc22.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-30526c06e214c5937e98bf1e5244cc22.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-9f718c794c8b2995afa4a8f14218e814.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-9f718c794c8b2995afa4a8f14218e814.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-aa960ad0ab4cef263d0d7664e12f5b65.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-aa960ad0ab4cef263d0d7664e12f5b65.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-7837a7115a8a584b7d7231cfa2eae2cf.js.br +0 -0
- 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
|
|
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:
|
|
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
|
|
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.
|
|
15
|
-
|
|
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
|
|
19
|
-
if (!
|
|
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: "
|
|
31
|
-
message: "The
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 {
|
|
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;
|