import { fetchOk, normalizeCdpHttpBaseForJsonEndpoints } from "./cdp.helpers.js"; import { appendCdpPath } from "./cdp.js"; import { closeChromeMcpTab, focusChromeMcpTab } from "./chrome-mcp.js"; import type { ResolvedBrowserProfile } from "./config.js"; import { BrowserTabNotFoundError, BrowserTargetAmbiguousError } from "./errors.js"; import { getBrowserProfileCapabilities } from "./profile-capabilities.js"; import type { PwAiModule } from "./pw-ai-module.js"; import { getPwAiModule } from "./pw-ai-module.js"; import type { BrowserTab, ProfileRuntimeState } from "./server-context.types.js"; import { resolveTargetIdFromTabs } from "./target-id.js"; type SelectionDeps = { profile: ResolvedBrowserProfile; getProfileState: () => ProfileRuntimeState; ensureBrowserAvailable: () => Promise; listTabs: () => Promise; openTab: (url: string) => Promise; }; type SelectionOps = { ensureTabAvailable: (targetId?: string) => Promise; focusTab: (targetId: string) => Promise; closeTab: (targetId: string) => Promise; }; export function createProfileSelectionOps({ profile, getProfileState, ensureBrowserAvailable, listTabs, openTab, }: SelectionDeps): SelectionOps { const cdpHttpBase = normalizeCdpHttpBaseForJsonEndpoints(profile.cdpUrl); const capabilities = getBrowserProfileCapabilities(profile); const ensureTabAvailable = async (targetId?: string): Promise => { await ensureBrowserAvailable(); const profileState = getProfileState(); let tabs1 = await listTabs(); if (tabs1.length === 0) { if (capabilities.requiresAttachedTab) { // Chrome extension relay can briefly drop its WebSocket connection (MV3 service worker // lifecycle, relay restart). If we previously had a target selected, wait briefly for // the extension to reconnect and re-announce its attached tabs before failing. if (profileState.lastTargetId?.trim()) { const deadlineAt = Date.now() + 3_000; while (tabs1.length === 0 && Date.now() < deadlineAt) { await new Promise((resolve) => setTimeout(resolve, 200)); tabs1 = await listTabs(); } } if (tabs1.length === 0) { throw new BrowserTabNotFoundError( `tab not found (no attached Chrome tabs for profile "${profile.name}"). ` + "Click the OpenClaw Browser Relay toolbar icon on the tab you want to control (badge ON).", ); } } else { await openTab("about:blank"); } } const tabs = await listTabs(); const candidates = capabilities.supportsPerTabWs ? tabs.filter((t) => Boolean(t.wsUrl)) : tabs; const resolveById = (raw: string) => { const resolved = resolveTargetIdFromTabs(raw, candidates); if (!resolved.ok) { if (resolved.reason === "ambiguous") { return "AMBIGUOUS" as const; } return null; } return candidates.find((t) => t.targetId === resolved.targetId) ?? null; }; const pickDefault = () => { const last = profileState.lastTargetId?.trim() || ""; const lastResolved = last ? resolveById(last) : null; if (lastResolved && lastResolved !== "AMBIGUOUS") { return lastResolved; } // Prefer a real page tab first (avoid service workers/background targets). const page = candidates.find((t) => (t.type ?? "page") === "page"); return page ?? candidates.at(0) ?? null; }; const chosen = targetId ? resolveById(targetId) : pickDefault(); if (chosen === "AMBIGUOUS") { throw new BrowserTargetAmbiguousError(); } if (!chosen) { throw new BrowserTabNotFoundError(); } profileState.lastTargetId = chosen.targetId; return chosen; }; const resolveTargetIdOrThrow = async (targetId: string): Promise => { const tabs = await listTabs(); const resolved = resolveTargetIdFromTabs(targetId, tabs); if (!resolved.ok) { if (resolved.reason === "ambiguous") { throw new BrowserTargetAmbiguousError(); } throw new BrowserTabNotFoundError(); } return resolved.targetId; }; const focusTab = async (targetId: string): Promise => { const resolvedTargetId = await resolveTargetIdOrThrow(targetId); if (capabilities.usesChromeMcp) { await focusChromeMcpTab(profile.name, resolvedTargetId); const profileState = getProfileState(); profileState.lastTargetId = resolvedTargetId; return; } if (capabilities.usesPersistentPlaywright) { const mod = await getPwAiModule({ mode: "strict" }); const focusPageByTargetIdViaPlaywright = (mod as Partial | null) ?.focusPageByTargetIdViaPlaywright; if (typeof focusPageByTargetIdViaPlaywright === "function") { await focusPageByTargetIdViaPlaywright({ cdpUrl: profile.cdpUrl, targetId: resolvedTargetId, }); const profileState = getProfileState(); profileState.lastTargetId = resolvedTargetId; return; } } await fetchOk(appendCdpPath(cdpHttpBase, `/json/activate/${resolvedTargetId}`)); const profileState = getProfileState(); profileState.lastTargetId = resolvedTargetId; }; const closeTab = async (targetId: string): Promise => { const resolvedTargetId = await resolveTargetIdOrThrow(targetId); if (capabilities.usesChromeMcp) { await closeChromeMcpTab(profile.name, resolvedTargetId); return; } // For remote profiles, use Playwright's persistent connection to close tabs if (capabilities.usesPersistentPlaywright) { const mod = await getPwAiModule({ mode: "strict" }); const closePageByTargetIdViaPlaywright = (mod as Partial | null) ?.closePageByTargetIdViaPlaywright; if (typeof closePageByTargetIdViaPlaywright === "function") { await closePageByTargetIdViaPlaywright({ cdpUrl: profile.cdpUrl, targetId: resolvedTargetId, }); return; } } await fetchOk(appendCdpPath(cdpHttpBase, `/json/close/${resolvedTargetId}`)); }; return { ensureTabAvailable, focusTab, closeTab, }; }