{"version":3,"file":"bridge-mode/page-browser-side.mjs","sources":["../../../src/bridge-mode/page-browser-side.ts"],"sourcesContent":["import { assert } from '@midscene/shared/utils';\nimport ChromeExtensionProxyPage from '../chrome-extension/page';\nimport type {\n  ChromePageDestroyOptions,\n  KeyboardAction,\n  MouseAction,\n} from '../web-page';\nimport {\n  type BridgeConnectTabOptions,\n  BridgeEvent,\n  DefaultBridgeServerPort,\n  KeyboardEvent,\n  MouseEvent,\n} from './common';\nimport { BridgeClient } from './io-client';\n\ndeclare const __VERSION__: string;\n\nconst NEW_TAB_LOAD_TIMEOUT_MS = 30_000;\n\nfunction isBlankUrl(url: string | undefined): boolean {\n  if (!url) return true;\n  return url === 'about:blank' || url.startsWith('chrome://newtab');\n}\n\n// Wait until the freshly created tab has navigated away from about:blank\n// and reached `status === 'complete'`. Resolves on timeout instead of\n// throwing so callers degrade to the existing lazy-attach behavior.\nfunction waitForTabNavigationComplete(\n  tabId: number,\n  targetUrl: string,\n  timeoutMs = NEW_TAB_LOAD_TIMEOUT_MS,\n): Promise<void> {\n  return new Promise((resolve) => {\n    let settled = false;\n    const finish = () => {\n      if (settled) return;\n      settled = true;\n      try {\n        chrome.tabs.onUpdated.removeListener(onUpdated);\n      } catch {}\n      clearTimeout(timer);\n      resolve();\n    };\n\n    const isReady = (tab: chrome.tabs.Tab | undefined): boolean => {\n      if (!tab) return false;\n      if (tab.status !== 'complete') return false;\n      const currentUrl = tab.url || tab.pendingUrl || '';\n      // Skip the initial about:blank \"complete\" that fires before\n      // the target URL navigation kicks in.\n      if (isBlankUrl(currentUrl) && !isBlankUrl(targetUrl)) return false;\n      return true;\n    };\n\n    const onUpdated = (\n      id: number,\n      _info: chrome.tabs.TabChangeInfo,\n      tab: chrome.tabs.Tab,\n    ) => {\n      if (id !== tabId) return;\n      if (isReady(tab)) finish();\n    };\n\n    chrome.tabs.onUpdated.addListener(onUpdated);\n    const timer = setTimeout(finish, timeoutMs);\n\n    // Handle the race where the tab already finished loading before\n    // we registered the listener.\n    chrome.tabs\n      .get(tabId)\n      .then((tab) => {\n        if (isReady(tab)) finish();\n      })\n      .catch(() => {});\n  });\n}\n\nexport class ExtensionBridgePageBrowserSide extends ChromeExtensionProxyPage {\n  public bridgeClient: BridgeClient | null = null;\n\n  private destroyOptions?: ChromePageDestroyOptions;\n\n  private newlyCreatedTabIds: number[] = [];\n\n  // Connection confirmation state\n  private confirmationPromise: Promise<boolean> | null = null;\n\n  constructor(\n    public serverEndpoint?: string,\n    public onDisconnect: () => void = () => {},\n    public onLogMessage: (\n      message: string,\n      type: 'log' | 'status',\n    ) => void = () => {},\n    forceSameTabNavigation = true,\n    public onConnectionRequest?: () => Promise<boolean>,\n  ) {\n    super(forceSameTabNavigation);\n  }\n\n  private async setupBridgeClient() {\n    const endpoint =\n      this.serverEndpoint || `ws://localhost:${DefaultBridgeServerPort}`;\n\n    // Create confirmation gate BEFORE establishing connection,\n    // so that any calls received immediately after connection are blocked\n    // until user confirms. This prevents a race condition where server-side\n    // queued calls bypass the confirmation dialog.\n    let resolveConfirmationGate: (allowed: boolean) => void = () => {};\n    if (this.onConnectionRequest) {\n      this.confirmationPromise = new Promise<boolean>((resolve) => {\n        resolveConfirmationGate = resolve;\n      });\n    }\n\n    this.bridgeClient = new BridgeClient(\n      endpoint,\n      async (method, args: any[]) => {\n        // Wait for user confirmation before processing any commands\n        if (this.confirmationPromise) {\n          const allowed = await this.confirmationPromise;\n          if (!allowed) {\n            throw new Error('Connection denied by user');\n          }\n        }\n\n        this.onLogMessage(`bridge call from cli side: ${method}`, 'log');\n        if (method === BridgeEvent.ConnectNewTabWithUrl) {\n          return this.connectNewTabWithUrl.apply(\n            this,\n            args as unknown as [string],\n          );\n        }\n\n        if (method === BridgeEvent.GetBrowserTabList) {\n          return this.getBrowserTabList.apply(this, args as any);\n        }\n\n        if (method === BridgeEvent.SetActiveTabId) {\n          return this.setActiveTabId.apply(this, args as any);\n        }\n\n        if (method === BridgeEvent.ConnectCurrentTab) {\n          return this.connectCurrentTab.apply(this, args as any);\n        }\n\n        if (method === BridgeEvent.UpdateAgentStatus) {\n          return this.onLogMessage(args[0] as string, 'status');\n        }\n\n        const tabId = await this.getActiveTabId();\n        if (!tabId || tabId === 0) {\n          throw new Error('no tab is connected');\n        }\n\n        // this.onLogMessage(`calling method: ${method}`);\n\n        if (method.startsWith(MouseEvent.PREFIX)) {\n          const actionName = method.split('.')[1] as keyof MouseAction;\n          if (actionName === 'drag') {\n            return this.mouse[actionName].apply(this.mouse, args as any);\n          }\n          return this.mouse[actionName].apply(this.mouse, args as any);\n        }\n\n        if (method.startsWith(KeyboardEvent.PREFIX)) {\n          const actionName = method.split('.')[1] as keyof KeyboardAction;\n          if (actionName === 'press') {\n            return this.keyboard[actionName].apply(this.keyboard, args as any);\n          }\n          return this.keyboard[actionName].apply(this.keyboard, args as any);\n        }\n\n        // Generic method lookup. Methods like `setWaterFlowAnimationEnabled`\n        // and `getWaterFlowAnimationEnabled` are inherited from\n        // ChromeExtensionProxyPage and found via `this[method]`.\n        if (!this[method as keyof ChromeExtensionProxyPage]) {\n          this.onLogMessage(`method not found: ${method}`, 'log');\n          return undefined;\n        }\n\n        try {\n          // @ts-expect-error\n          const result = await this[method as keyof ChromeExtensionProxyPage](\n            ...args,\n          );\n          return result;\n        } catch (e) {\n          const errorMessage = e instanceof Error ? e.message : 'Unknown error';\n          this.onLogMessage(\n            `Error calling method: ${method}, ${errorMessage}`,\n            'log',\n          );\n          throw new Error(errorMessage, { cause: e });\n        }\n      },\n      // on disconnect\n      () => {\n        return this.destroy();\n      },\n    );\n    await this.bridgeClient.connect();\n\n    // Show confirmation dialog after connection is established\n    if (this.onConnectionRequest) {\n      this.onLogMessage('Waiting for user confirmation...', 'log');\n      const allowed = await this.onConnectionRequest();\n      resolveConfirmationGate(allowed);\n      this.confirmationPromise = null;\n\n      if (!allowed) {\n        this.onLogMessage('Connection denied by user', 'log');\n        this.bridgeClient.disconnect();\n        this.bridgeClient = null;\n        throw new Error('Connection denied by user');\n      }\n    }\n\n    this.onLogMessage(\n      `Bridge connected, cli-side version v${this.bridgeClient.serverVersion}, browser-side version v${__VERSION__}`,\n      'log',\n    );\n  }\n\n  public async connect() {\n    return await this.setupBridgeClient();\n  }\n\n  public async connectNewTabWithUrl(\n    url: string,\n    options: BridgeConnectTabOptions = {\n      forceSameTabNavigation: true,\n    },\n  ) {\n    // Set the water-flow animation flag BEFORE any debugger commands are\n    // sent (which trigger enableWaterFlowAnimation via sendCommandToDebugger).\n    // Otherwise the animation gets injected during the connection flow\n    // before the CLI side's setWaterFlowAnimationEnabled(false) arrives.\n    if (options.enableWaterFlowAnimation !== undefined) {\n      this.waterFlowAnimationEnabled = options.enableWaterFlowAnimation;\n    }\n\n    // Preserve the historical default: activate the new tab unless the caller opts into background mode.\n    const activate = options.activateTab ?? true;\n    const tab = await chrome.tabs.create({ url, active: activate });\n    const tabId = tab.id;\n    assert(tabId, 'failed to get tabId after creating a new tab');\n\n    // new tab\n    this.onLogMessage(`Creating new tab: ${url}`, 'log');\n    this.newlyCreatedTabIds.push(tabId);\n\n    if (options?.forceSameTabNavigation) {\n      this.forceSameTabNavigation = true;\n    }\n\n    // chrome.tabs.create returns immediately with an about:blank target,\n    // then navigates to `url`. If we attach the debugger during that\n    // cross-origin transition Site Isolation will detach it again, leaving\n    // the first CDP command to fail with \"Debugger is not attached to the\n    // tab\". Wait for navigation to settle so the lazy attach lands on a\n    // stable target.\n    await waitForTabNavigationComplete(tabId, url);\n\n    await this.setActiveTabId(tabId, { activate });\n  }\n\n  public async connectCurrentTab(\n    options: BridgeConnectTabOptions = {\n      forceSameTabNavigation: true,\n    },\n  ) {\n    // Set the water-flow animation flag BEFORE any debugger commands are\n    // sent (which trigger enableWaterFlowAnimation via sendCommandToDebugger).\n    if (options.enableWaterFlowAnimation !== undefined) {\n      this.waterFlowAnimationEnabled = options.enableWaterFlowAnimation;\n    }\n\n    const tabs = await chrome.tabs.query({ active: true, currentWindow: true });\n    const tabId = tabs[0]?.id;\n    assert(tabId, 'failed to get tabId');\n\n    this.onLogMessage(`Connected to current tab: ${tabs[0]?.url}`, 'log');\n\n    if (options?.forceSameTabNavigation) {\n      this.forceSameTabNavigation = true;\n    }\n\n    await this.setActiveTabId(tabId);\n  }\n\n  public async setDestroyOptions(options: ChromePageDestroyOptions) {\n    this.destroyOptions = options;\n  }\n\n  async destroy() {\n    if (this.destroyOptions?.closeTab && this.newlyCreatedTabIds.length > 0) {\n      this.onLogMessage('Closing all newly created tabs by bridge...', 'log');\n      for (const tabId of this.newlyCreatedTabIds) {\n        await chrome.tabs.remove(tabId);\n      }\n      this.newlyCreatedTabIds = [];\n    }\n\n    await super.destroy();\n\n    if (this.bridgeClient) {\n      this.bridgeClient.disconnect();\n      this.bridgeClient = null;\n      this.onDisconnect();\n    }\n  }\n}\n"],"names":["NEW_TAB_LOAD_TIMEOUT_MS","isBlankUrl","url","waitForTabNavigationComplete","tabId","targetUrl","timeoutMs","Promise","resolve","settled","finish","chrome","onUpdated","clearTimeout","timer","isReady","tab","currentUrl","id","_info","setTimeout","ExtensionBridgePageBrowserSide","ChromeExtensionProxyPage","endpoint","DefaultBridgeServerPort","resolveConfirmationGate","BridgeClient","method","args","allowed","Error","BridgeEvent","MouseEvent","actionName","KeyboardEvent","result","e","errorMessage","options","undefined","activate","assert","tabs","serverEndpoint","onDisconnect","onLogMessage","forceSameTabNavigation","onConnectionRequest"],"mappings":";;;;;;;;;;;;;;AAkBA,MAAMA,0BAA0B;AAEhC,SAASC,WAAWC,GAAuB;IACzC,IAAI,CAACA,KAAK,OAAO;IACjB,OAAOA,AAAQ,kBAARA,OAAyBA,IAAI,UAAU,CAAC;AACjD;AAKA,SAASC,6BACPC,KAAa,EACbC,SAAiB,EACjBC,YAAYN,uBAAuB;IAEnC,OAAO,IAAIO,QAAQ,CAACC;QAClB,IAAIC,UAAU;QACd,MAAMC,SAAS;YACb,IAAID,SAAS;YACbA,UAAU;YACV,IAAI;gBACFE,OAAO,IAAI,CAAC,SAAS,CAAC,cAAc,CAACC;YACvC,EAAE,OAAM,CAAC;YACTC,aAAaC;YACbN;QACF;QAEA,MAAMO,UAAU,CAACC;YACf,IAAI,CAACA,KAAK,OAAO;YACjB,IAAIA,AAAe,eAAfA,IAAI,MAAM,EAAiB,OAAO;YACtC,MAAMC,aAAaD,IAAI,GAAG,IAAIA,IAAI,UAAU,IAAI;YAGhD,IAAIf,WAAWgB,eAAe,CAAChB,WAAWI,YAAY,OAAO;YAC7D,OAAO;QACT;QAEA,MAAMO,YAAY,CAChBM,IACAC,OACAH;YAEA,IAAIE,OAAOd,OAAO;YAClB,IAAIW,QAAQC,MAAMN;QACpB;QAEAC,OAAO,IAAI,CAAC,SAAS,CAAC,WAAW,CAACC;QAClC,MAAME,QAAQM,WAAWV,QAAQJ;QAIjCK,OAAO,IAAI,CACR,GAAG,CAACP,OACJ,IAAI,CAAC,CAACY;YACL,IAAID,QAAQC,MAAMN;QACpB,GACC,KAAK,CAAC,KAAO;IAClB;AACF;AAEO,MAAMW,uCAAuCC;IAuBlD,MAAc,oBAAoB;QAChC,MAAMC,WACJ,IAAI,CAAC,cAAc,IAAI,CAAC,eAAe,EAAEC,yBAAyB;QAMpE,IAAIC,0BAAsD,KAAO;QACjE,IAAI,IAAI,CAAC,mBAAmB,EAC1B,IAAI,CAAC,mBAAmB,GAAG,IAAIlB,QAAiB,CAACC;YAC/CiB,0BAA0BjB;QAC5B;QAGF,IAAI,CAAC,YAAY,GAAG,IAAIkB,aACtBH,UACA,OAAOI,QAAQC;YAEb,IAAI,IAAI,CAAC,mBAAmB,EAAE;gBAC5B,MAAMC,UAAU,MAAM,IAAI,CAAC,mBAAmB;gBAC9C,IAAI,CAACA,SACH,MAAM,IAAIC,MAAM;YAEpB;YAEA,IAAI,CAAC,YAAY,CAAC,CAAC,2BAA2B,EAAEH,QAAQ,EAAE;YAC1D,IAAIA,WAAWI,YAAY,oBAAoB,EAC7C,OAAO,IAAI,CAAC,oBAAoB,CAAC,KAAK,CACpC,IAAI,EACJH;YAIJ,IAAID,WAAWI,YAAY,iBAAiB,EAC1C,OAAO,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,IAAI,EAAEH;YAG5C,IAAID,WAAWI,YAAY,cAAc,EACvC,OAAO,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,EAAEH;YAGzC,IAAID,WAAWI,YAAY,iBAAiB,EAC1C,OAAO,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,IAAI,EAAEH;YAG5C,IAAID,WAAWI,YAAY,iBAAiB,EAC1C,OAAO,IAAI,CAAC,YAAY,CAACH,IAAI,CAAC,EAAE,EAAY;YAG9C,MAAMxB,QAAQ,MAAM,IAAI,CAAC,cAAc;YACvC,IAAI,CAACA,SAASA,AAAU,MAAVA,OACZ,MAAM,IAAI0B,MAAM;YAKlB,IAAIH,OAAO,UAAU,CAACK,WAAW,MAAM,GAAG;gBACxC,MAAMC,aAAaN,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE;gBAIvC,OAAO,IAAI,CAAC,KAAK,CAACM,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAEL;YAClD;YAEA,IAAID,OAAO,UAAU,CAACO,cAAc,MAAM,GAAG;gBAC3C,MAAMD,aAAaN,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE;gBAIvC,OAAO,IAAI,CAAC,QAAQ,CAACM,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAEL;YACxD;YAKA,IAAI,CAAC,IAAI,CAACD,OAAyC,EAAE,YACnD,IAAI,CAAC,YAAY,CAAC,CAAC,kBAAkB,EAAEA,QAAQ,EAAE;YAInD,IAAI;gBAEF,MAAMQ,SAAS,MAAM,IAAI,CAACR,OAAyC,IAC9DC;gBAEL,OAAOO;YACT,EAAE,OAAOC,GAAG;gBACV,MAAMC,eAAeD,aAAaN,QAAQM,EAAE,OAAO,GAAG;gBACtD,IAAI,CAAC,YAAY,CACf,CAAC,sBAAsB,EAAET,OAAO,EAAE,EAAEU,cAAc,EAClD;gBAEF,MAAM,IAAIP,MAAMO,cAAc;oBAAE,OAAOD;gBAAE;YAC3C;QACF,GAEA,IACS,IAAI,CAAC,OAAO;QAGvB,MAAM,IAAI,CAAC,YAAY,CAAC,OAAO;QAG/B,IAAI,IAAI,CAAC,mBAAmB,EAAE;YAC5B,IAAI,CAAC,YAAY,CAAC,oCAAoC;YACtD,MAAMP,UAAU,MAAM,IAAI,CAAC,mBAAmB;YAC9CJ,wBAAwBI;YACxB,IAAI,CAAC,mBAAmB,GAAG;YAE3B,IAAI,CAACA,SAAS;gBACZ,IAAI,CAAC,YAAY,CAAC,6BAA6B;gBAC/C,IAAI,CAAC,YAAY,CAAC,UAAU;gBAC5B,IAAI,CAAC,YAAY,GAAG;gBACpB,MAAM,IAAIC,MAAM;YAClB;QACF;QAEA,IAAI,CAAC,YAAY,CACf,uCAAuC,IAAI,CAAC,YAAY,CAAC,aAAa,gCAAwC,EAC9G;IAEJ;IAEA,MAAa,UAAU;QACrB,OAAO,MAAM,IAAI,CAAC,iBAAiB;IACrC;IAEA,MAAa,qBACX5B,GAAW,EACXoC,UAAmC;QACjC,wBAAwB;IAC1B,CAAC,EACD;QAKA,IAAIA,AAAqCC,WAArCD,QAAQ,wBAAwB,EAClC,IAAI,CAAC,yBAAyB,GAAGA,QAAQ,wBAAwB;QAInE,MAAME,WAAWF,QAAQ,WAAW,IAAI;QACxC,MAAMtB,MAAM,MAAML,OAAO,IAAI,CAAC,MAAM,CAAC;YAAET;YAAK,QAAQsC;QAAS;QAC7D,MAAMpC,QAAQY,IAAI,EAAE;QACpByB,OAAOrC,OAAO;QAGd,IAAI,CAAC,YAAY,CAAC,CAAC,kBAAkB,EAAEF,KAAK,EAAE;QAC9C,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAACE;QAE7B,IAAIkC,SAAS,wBACX,IAAI,CAAC,sBAAsB,GAAG;QAShC,MAAMnC,6BAA6BC,OAAOF;QAE1C,MAAM,IAAI,CAAC,cAAc,CAACE,OAAO;YAAEoC;QAAS;IAC9C;IAEA,MAAa,kBACXF,UAAmC;QACjC,wBAAwB;IAC1B,CAAC,EACD;QAGA,IAAIA,AAAqCC,WAArCD,QAAQ,wBAAwB,EAClC,IAAI,CAAC,yBAAyB,GAAGA,QAAQ,wBAAwB;QAGnE,MAAMI,OAAO,MAAM/B,OAAO,IAAI,CAAC,KAAK,CAAC;YAAE,QAAQ;YAAM,eAAe;QAAK;QACzE,MAAMP,QAAQsC,IAAI,CAAC,EAAE,EAAE;QACvBD,OAAOrC,OAAO;QAEd,IAAI,CAAC,YAAY,CAAC,CAAC,0BAA0B,EAAEsC,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE;QAE/D,IAAIJ,SAAS,wBACX,IAAI,CAAC,sBAAsB,GAAG;QAGhC,MAAM,IAAI,CAAC,cAAc,CAAClC;IAC5B;IAEA,MAAa,kBAAkBkC,OAAiC,EAAE;QAChE,IAAI,CAAC,cAAc,GAAGA;IACxB;IAEA,MAAM,UAAU;QACd,IAAI,IAAI,CAAC,cAAc,EAAE,YAAY,IAAI,CAAC,kBAAkB,CAAC,MAAM,GAAG,GAAG;YACvE,IAAI,CAAC,YAAY,CAAC,+CAA+C;YACjE,KAAK,MAAMlC,SAAS,IAAI,CAAC,kBAAkB,CACzC,MAAMO,OAAO,IAAI,CAAC,MAAM,CAACP;YAE3B,IAAI,CAAC,kBAAkB,GAAG,EAAE;QAC9B;QAEA,MAAM,KAAK,CAAC;QAEZ,IAAI,IAAI,CAAC,YAAY,EAAE;YACrB,IAAI,CAAC,YAAY,CAAC,UAAU;YAC5B,IAAI,CAAC,YAAY,GAAG;YACpB,IAAI,CAAC,YAAY;QACnB;IACF;IAhOA,YACSuC,cAAuB,EACvBC,eAA2B,KAAO,CAAC,EACnCC,eAGK,KAAO,CAAC,EACpBC,yBAAyB,IAAI,EACtBC,mBAA4C,CACnD;QACA,KAAK,CAACD,yBAAAA,iBAAAA,IAAAA,EAAAA,kBAAAA,KAAAA,IAAAA,iBAAAA,IAAAA,EAAAA,gBAAAA,KAAAA,IAAAA,iBAAAA,IAAAA,EAAAA,gBAAAA,KAAAA,IAAAA,iBAAAA,IAAAA,EAAAA,uBAAAA,KAAAA,IAnBR,uBAAO,gBAAP,SAEA,uBAAQ,kBAAR,SAEA,uBAAQ,sBAAR,SAGA,uBAAQ,uBAAR,cAGSH,cAAc,GAAdA,gBAAAA,IAAAA,CACAC,YAAY,GAAZA,cAAAA,IAAAA,CACAC,YAAY,GAAZA,cAAAA,IAAAA,CAKAE,mBAAmB,GAAnBA,qBAAAA,IAAAA,CAjBF,YAAY,GAAwB,WAInC,kBAAkB,GAAa,EAAE,OAGjC,mBAAmB,GAA4B;IAavD;AAsNF"}