// packages/coding-agent/src/tools/browser/relay/protocol.ts var EXTENSION_RELOAD_PROTOCOL_VERSION = 1; var FOREIGN_FRAME_REFUSAL = /chrome-extension:\/\/ URL of different extension/i; var DOWNLOADS_PROTOCOL_VERSION = 1; // packages/browser-relay/extension/debugger-ownership.ts async function ownedDebuggerTabs(targets, probe) { const owned = await Promise.all(targets.map(async (target) => { if (!target.attached || target.tabId === undefined) return; try { await probe(target.tabId); return target.tabId; } catch { return; } })); return owned.filter((tabId) => tabId !== undefined); } // packages/browser-relay/extension/download-pairing.ts var DOWNLOAD_PAIRING_MS = 5000; var DOWNLOAD_LOOKUP_MS = 60000; class DownloadPairing { #now; #pairs = new Map; #reports = []; constructor(now = Date.now) { this.#now = now; } report(guid, url) { if (this.#pairs.has(guid) || this.#reports.some((report) => report.guid === guid)) return; this.#dropAgedReports(); this.#reports.push({ guid, url, at: this.#now() }); } created(id, url) { this.#dropAgedReports(); const index = this.#reports.findIndex((report) => report.url === url); if (index < 0) return; this.#pairs.set(this.#reports[index].guid, { id }); this.#reports.splice(index, 1); } #dropAgedReports() { const oldest = this.#now() - DOWNLOAD_PAIRING_MS; while (this.#reports.length > 0 && this.#reports[0].at < oldest) this.#reports.shift(); } finished(id, state) { const now = this.#now(); for (const [guid, pair] of this.#pairs) { if (pair.id === id) { if (state === "interrupted") this.#pairs.delete(guid); else pair.completedAt = now; } else if (pair.completedAt !== undefined && pair.completedAt < now - DOWNLOAD_LOOKUP_MS) { this.#pairs.delete(guid); } } } idFor(guid) { return this.#pairs.get(guid)?.id; } forget(guid) { this.#pairs.delete(guid); } } // packages/browser-relay/extension/foreign-frames.ts var RETRY_DELAYS_MS = [50, 100, 200, 400]; var sweeps = new Map; async function pastForeignFrames(tabId, request) { for (let attempt = 0;; attempt++) { try { return await request(); } catch (error) { if (attempt === RETRY_DELAYS_MS.length || !FOREIGN_FRAME_REFUSAL.test(String(error)) || await sweep(tabId) === 0) throw error; } const { promise, resolve } = Promise.withResolvers(); setTimeout(resolve, RETRY_DELAYS_MS[attempt]); await promise; } } function sweep(tabId) { let pending = sweeps.get(tabId); if (!pending) { pending = neutralizeForeignFrames(tabId).finally(() => sweeps.delete(tabId)); sweeps.set(tabId, pending); } return pending; } async function neutralizeForeignFrames(tabId) { try { const results = await chrome.scripting.executeScript({ target: { tabId, allFrames: true }, injectImmediately: true, args: [chrome.runtime.id], func: (self) => { let found = 0; const visit = (root) => { const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); for (let element = walker.nextNode();element; element = walker.nextNode()) { const tag = element.tagName.toUpperCase(); if (tag === "IFRAME" || tag === "FRAME") { const src = element.getAttribute("src") ?? ""; if (src.startsWith("chrome-extension://") && !src.startsWith(`chrome-extension://${self}/`)) { if (tag === "FRAME") element.setAttribute("src", "about:blank"); else if (element.getAttribute("srcdoc") === null) element.setAttribute("srcdoc", ""); element.style.setProperty("display", "none", "important"); found++; } } const shadow = chrome.dom.openOrClosedShadowRoot(element); if (shadow) visit(shadow); } }; visit(document); return found; } }); return results.reduce((total, frame) => total + (frame.result ?? 0), 0); } catch { return 0; } } // packages/browser-relay/extension/background.ts var DEFAULT_PORT = 9224; var PING_INTERVAL_MS = 20000; var RECONNECT_MIN_MS = 1000; var RECONNECT_MAX_MS = 1e4; var REMOVE_TAB_ATTEMPTS = 3; var REMOVE_TAB_RETRY_MS = 500; var ws = null; var reconnectDelay = RECONNECT_MIN_MS; var pingTimer = null; var relayInitiatedDetachTabs = new Set; var instanceId = null; async function ensureInstanceId() { if (instanceId) return instanceId; const key = "relayInstanceId"; const stored = await chrome.storage.local.get({ [key]: "" }); const existing = stored[key]; instanceId = typeof existing === "string" && existing.length > 0 ? existing : crypto.randomUUID(); await chrome.storage.local.set({ [key]: instanceId }); return instanceId; } var browserSession = null; function ensureBrowserSession() { browserSession ??= (async () => { const key = "relayBrowserSession"; const stored = await chrome.storage.session.get({ [key]: "" }); const existing = stored[key]; if (typeof existing === "string" && existing.length > 0) return existing; const created = crypto.randomUUID(); await chrome.storage.session.set({ [key]: created }); return created; })(); return browserSession; } async function loadSettings() { const stored = await chrome.storage.local.get({ port: DEFAULT_PORT, token: "" }); const port = Number(stored.port); return { port: Number.isInteger(port) && port > 0 && port <= 65535 ? port : DEFAULT_PORT, token: typeof stored.token === "string" ? stored.token : "" }; } function snapshot(tab) { if (tab.id === undefined) return null; return { tabId: tab.id, url: tab.url ?? tab.pendingUrl ?? "", title: tab.title ?? "", active: tab.active, discarded: tab.discarded === true, windowId: tab.windowId, pinned: tab.pinned, groupId: tab.groupId }; } var ompGroupTitle = null; var groupOps = Promise.resolve(); function enqueueGroupOp(fn) { const result = groupOps.then(fn, fn); groupOps = result.catch(() => {}); return result; } async function groupTabs(tabIds, title, color) { ompGroupTitle = title; chrome.storage.session.set({ ompGroupTitle: title }); const byWindow = new Map; for (const tabId of tabIds) { try { const tab = await chrome.tabs.get(tabId); if (tab.pinned || tab.id === undefined) continue; const bucket = byWindow.get(tab.windowId) ?? []; bucket.push(tab.id); byWindow.set(tab.windowId, bucket); } catch {} } const grouped = {}; for (const [windowId, ids] of byWindow) { const existing = await chrome.tabGroups.query({ title, windowId }); let groupId; if (existing[0]) { groupId = existing[0].id; for (const dupe of existing.slice(1)) { const dupeTabs = await chrome.tabs.query({ groupId: dupe.id }); const dupeIds = dupeTabs.map((tab) => tab.id).filter((id) => id !== undefined); if (dupeIds.length > 0) await chrome.tabs.group({ tabIds: dupeIds, groupId }); } await chrome.tabs.group({ tabIds: ids, groupId }); } else { groupId = await chrome.tabs.group({ tabIds: ids }); } await chrome.tabGroups.update(groupId, { title, color }); for (const id of ids) grouped[String(id)] = groupId; } return { grouped }; } async function restoreGroups() { if (!ompGroupTitle) { const stored = await chrome.storage.session.get({ ompGroupTitle: "" }).catch(() => ({ ompGroupTitle: "" })); ompGroupTitle = typeof stored.ompGroupTitle === "string" && stored.ompGroupTitle ? stored.ompGroupTitle : null; } if (!ompGroupTitle) return; const groups = await chrome.tabGroups.query({ title: ompGroupTitle }).catch(() => []); for (const group of groups) { const tabs = await chrome.tabs.query({ groupId: group.id }).catch(() => []); const ids = tabs.map((tab) => tab.id).filter((id) => id !== undefined); if (ids.length > 0) await chrome.tabs.ungroup(ids).catch(() => {}); } } function post(msg) { if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg)); } async function removeTab(tabId) { for (let attempt = 1;; attempt++) { try { await chrome.tabs.remove(tabId); return; } catch (error) { if (/No tab with id/.test(String(error))) return; if (attempt >= REMOVE_TAB_ATTEMPTS) throw error; const pause = Promise.withResolvers(); setTimeout(pause.resolve, REMOVE_TAB_RETRY_MS); await pause.promise; } } } async function setBadge(connected) { try { await chrome.action.setBadgeText({ text: connected ? "on" : "off" }); await chrome.action.setBadgeBackgroundColor({ color: connected ? "#1a7f37" : "#8b8b8b" }); } catch {} } async function buildHello() { const [tabs, targets] = await Promise.all([chrome.tabs.query({}), chrome.debugger.getTargets()]); const snapshots = []; for (const tab of tabs) { const snap = snapshot(tab); if (snap) snapshots.push(snap); } const attachedTabIds = await ownedDebuggerTabs(targets, (tabId) => chrome.debugger.sendCommand({ tabId }, "Target.getTargetInfo")); const versionMatch = /Chrome\/[\d.]+/.exec(navigator.userAgent); return { t: "hello", instanceId: await ensureInstanceId(), browserSession: await ensureBrowserSession(), userAgent: navigator.userAgent, browserVersion: versionMatch?.[0] ?? "Chrome/unknown", discardedTabsProtocol: 1, ompVersion: typeof OMP_INSTALL_VERSION === "string" ? OMP_INSTALL_VERSION : undefined, reloadProtocol: EXTENSION_RELOAD_PROTOCOL_VERSION, downloadsProtocol: DOWNLOADS_PROTOCOL_VERSION, tabs: snapshots, attachedTabIds }; } async function runRpc(msg, origin) { switch (msg.op) { case "attach": { const attach = () => chrome.debugger.attach({ tabId: msg.tabId }, "1.3"); await (msg.clearForeignFrames ? pastForeignFrames(msg.tabId, attach) : attach()); return {}; } case "detach": relayInitiatedDetachTabs.add(msg.tabId); try { await chrome.debugger.detach({ tabId: msg.tabId }); return {}; } catch (error) { relayInitiatedDetachTabs.delete(msg.tabId); throw error; } case "send": { const send = () => chrome.debugger.sendCommand(msg.sessionId ? { tabId: msg.tabId, sessionId: msg.sessionId } : { tabId: msg.tabId }, msg.method, msg.params); return await (msg.clearForeignFrames ? pastForeignFrames(msg.tabId, send) : send()); } case "createTab": { const tab = await chrome.tabs.create({ url: msg.url, active: msg.active ?? true }); const snap = snapshot(tab); if (!snap) throw new Error("created tab has no id"); if (origin.readyState !== WebSocket.OPEN) removeTab(snap.tabId).catch(() => {}); return { tab: snap }; } case "removeTab": await removeTab(msg.tabId); return {}; case "activateTab": { const tab = await chrome.tabs.get(msg.tabId); await chrome.windows.update(tab.windowId, { focused: true }); await chrome.tabs.update(msg.tabId, { active: true }); return {}; } case "group": return await enqueueGroupOp(() => groupTabs(msg.tabIds, msg.title, msg.color)); case "ungroup": await enqueueGroupOp(() => chrome.tabs.ungroup(msg.tabIds).catch(() => {})); return {}; case "findDownload": { const id = downloads.idFor(msg.guid); const [item] = id === undefined ? [] : await chrome.downloads.search({ id }); if (item && item.state !== "in_progress") downloads.forget(msg.guid); const found = item ? { filename: item.filename, state: item.state } : null; return found; } } } function handleRelayMessage(socket, raw) { let msg; try { msg = JSON.parse(raw); } catch { return; } if (msg.t === "pong") return; if (msg.t === "reload") { chrome.runtime.reload(); return; } const answer = (reply) => { if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(reply)); }; runRpc(msg, socket).then((result) => answer({ t: "rpcResult", id: msg.id, ok: true, result })).catch((err) => { answer({ t: "rpcResult", id: msg.id, ok: false, error: err instanceof Error ? err.message : String(err) }); }); } function scheduleReconnect() { const delay = reconnectDelay; reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS); setTimeout(() => void connect(), delay); } async function connect() { if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return; const settings = await loadSettings(); const url = `ws://127.0.0.1:${settings.port}/ext${settings.token ? `?token=${encodeURIComponent(settings.token)}` : ""}`; const socket = new WebSocket(url); ws = socket; socket.onopen = () => { reconnectDelay = RECONNECT_MIN_MS; setBadge(true); buildHello().then((hello) => post(hello)); clearInterval(pingTimer ?? undefined); pingTimer = setInterval(() => post({ t: "ping" }), PING_INTERVAL_MS); }; socket.onmessage = (event) => { if (typeof event.data === "string") handleRelayMessage(socket, event.data); }; socket.onclose = () => { if (ws !== socket) return; ws = null; if (pingTimer !== null) { clearInterval(pingTimer); pingTimer = null; } setBadge(false); restoreGroups(); scheduleReconnect(); }; socket.onerror = () => { socket.close(); }; } var downloads = new DownloadPairing; chrome.downloads.onCreated.addListener((item) => downloads.created(item.id, item.finalUrl)); chrome.downloads.onChanged.addListener((delta) => { const state = delta.state?.current; if (state === "complete" || state === "interrupted") downloads.finished(delta.id, state); }); chrome.debugger.onEvent.addListener((source, method, params) => { if (source.tabId === undefined) return; if (method === "Page.downloadWillBegin" && typeof params?.guid === "string" && typeof params.url === "string") { downloads.report(params.guid, params.url); } post({ t: "cdpEvent", tabId: source.tabId, sessionId: source.sessionId, method, params }); }); chrome.debugger.onDetach.addListener((source, reason) => { if (source.tabId === undefined) return; const relayInitiated = relayInitiatedDetachTabs.delete(source.tabId); post({ t: "detached", tabId: source.tabId, reason, relayInitiated }); }); chrome.tabs.onCreated.addListener((tab) => { const snap = snapshot(tab); if (snap) post({ t: "tabCreated", tab: snap }); }); chrome.tabs.onUpdated.addListener((_tabId, _changeInfo, tab) => { const snap = snapshot(tab); if (snap) post({ t: "tabUpdated", tab: snap }); }); chrome.tabs.onActivated.addListener(({ tabId }) => { chrome.tabs.get(tabId).then((tab) => { const snap = snapshot(tab); if (snap) post({ t: "tabUpdated", tab: snap }); }).catch(() => {}); }); chrome.tabs.onRemoved.addListener((tabId) => { post({ t: "tabRemoved", tabId }); }); chrome.alarms.create("omp-relay-keepalive", { periodInMinutes: 0.5 }); chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === "omp-relay-keepalive") connect(); }); chrome.storage.onChanged.addListener((_changes, areaName) => { if (areaName !== "local") return; ws?.close(); connect(); }); chrome.action.onClicked.addListener(() => void chrome.runtime.openOptionsPage()); chrome.runtime.onInstalled.addListener(() => void connect()); chrome.runtime.onStartup.addListener(() => void connect()); connect();