// 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();
