{"version":3,"file":"agent-tools-cdp.mjs","sources":["../../src/agent-tools-cdp.ts"],"sourcesContent":["import { ScreenshotItem } from '@midscene/core';\nimport {\n  extractAgentBehaviorInitArgs,\n  getAgentInitArgsSignature,\n  shouldRebuildAgentForInitArgs,\n} from '@midscene/shared/agent-tools/agent-behavior-init-args';\nimport {\n  BaseMidsceneTools,\n  type InitArgSpec,\n} from '@midscene/shared/agent-tools/base-tools';\nimport type { ToolDefinition } from '@midscene/shared/agent-tools/types';\nimport { getDebug } from '@midscene/shared/logger';\nimport type { Page as PuppeteerPage } from 'puppeteer';\nimport puppeteer from 'puppeteer-core';\nimport type { Browser, Page } from 'puppeteer-core';\nimport {\n  type WebCdpAgentInitArgs,\n  adaptWebCdpAgentInitArgs,\n  webCdpAgentInitArgShape,\n} from './agent-init-args';\nimport { getProxyEndpoint } from './cdp-proxy-manager';\nimport {\n  cleanupTargetIdFile,\n  readSavedTargetId,\n  saveTargetId,\n} from './cdp-target-store';\nimport { defaultStaticPageViewportSize } from './common/viewport';\nimport { PuppeteerAgent } from './puppeteer';\nimport { StaticPage } from './static';\n\nconst debug = getDebug('agent-tools:cdp');\n\n/** CDP target discovery may need a brief moment after WebSocket open. */\nconst CDP_TARGET_DISCOVERY_DELAY_MS = 500;\n\n/**\n * puppeteer-core does not expose a public method for the underlying CDP\n * target id, so we reach into `_targetId`. Centralised here so a future\n * puppeteer release exposing this properly only requires one change.\n * Callers must treat the result as optional.\n */\nfunction getTargetId(page: Page): string | undefined {\n  return (page.target() as unknown as { _targetId?: string })._targetId;\n}\n\n/**\n * Tools manager for Web CDP mode.\n * Connects to an existing Chrome browser via CDP (Chrome DevTools Protocol) endpoint.\n * Unlike WebPuppeteerMidsceneTools which launches its own Chrome, this connects\n * to a browser that is already running with remote debugging enabled.\n *\n * Uses a persistent WebSocket proxy to avoid repeated Chrome permission popups\n * when Chrome's settings-based remote debugging is used.\n */\nexport class WebCdpMidsceneTools extends BaseMidsceneTools<\n  PuppeteerAgent,\n  WebCdpAgentInitArgs\n> {\n  protected getCliReportSessionName() {\n    return 'midscene-web';\n  }\n  private cdpEndpoint: string;\n  private activeBrowser: Browser | null = null;\n  private lastInitArgsSignature?: string;\n\n  constructor(cdpEndpoint: string) {\n    super();\n    this.cdpEndpoint = cdpEndpoint;\n  }\n\n  protected readonly initArgSpec: InitArgSpec<WebCdpAgentInitArgs> = {\n    namespace: 'web',\n    shape: webCdpAgentInitArgShape,\n    cli: {\n      preferBareKeys: true,\n    },\n    adapt: adaptWebCdpAgentInitArgs,\n  };\n\n  protected createTemporaryDevice() {\n    return new StaticPage({\n      screenshot: ScreenshotItem.create('', Date.now()),\n      shotSize: defaultStaticPageViewportSize,\n      shrunkShotToLogicalRatio: 1,\n    });\n  }\n\n  protected async ensureAgent(\n    initArgs?: WebCdpAgentInitArgs,\n  ): Promise<PuppeteerAgent> {\n    const navigateToUrl = initArgs?.url;\n    const nextSignature = getAgentInitArgsSignature(initArgs);\n    const shouldNavigateToUrl = typeof navigateToUrl === 'string';\n\n    if (\n      this.agent &&\n      (shouldNavigateToUrl ||\n        shouldRebuildAgentForInitArgs(\n          this.lastInitArgsSignature,\n          nextSignature,\n        ))\n    ) {\n      try {\n        await this.agent?.destroy?.();\n      } catch (error) {\n        console.debug('Failed to destroy agent during re-init:', error);\n      }\n      this.agent = undefined;\n    }\n\n    if (this.agent) return this.agent;\n\n    // Connect via proxy to avoid repeated Chrome permission popups\n    if (!this.activeBrowser) {\n      const endpoint = await getProxyEndpoint(this.cdpEndpoint);\n      this.activeBrowser = await puppeteer.connect({\n        browserWSEndpoint: endpoint,\n        defaultViewport: null,\n      });\n    }\n\n    const browser = this.activeBrowser;\n    let pages = await browser.pages();\n\n    // If no pages discovered, wait briefly and retry — some CDP targets\n    // need a moment to appear after the WebSocket connection is established.\n    if (pages.length === 0) {\n      await new Promise((r) => setTimeout(r, CDP_TARGET_DISCOVERY_DELAY_MS));\n      pages = await browser.pages();\n    }\n\n    const webPages = pages.filter((p) => /^https?:\\/\\//.test(p.url()));\n    debug(\n      'Found %d page(s), %d web page(s): %o',\n      pages.length,\n      webPages.length,\n      pages.map((p) => p.url()),\n    );\n    let page: Page;\n\n    if (navigateToUrl) {\n      if (webPages.length > 0) {\n        // Reuse an existing page and navigate it — avoids creating invisible\n        // tabs when Chrome uses settings-based remote debugging (no HTTP\n        // discovery endpoints, /devtools/page/* returns 403).\n        page = webPages[webPages.length - 1];\n        await page.bringToFront();\n      } else {\n        // No existing web pages — fall back to creating a new tab\n        page = await browser.newPage();\n      }\n    } else {\n      // Try to find the exact tab from a previous `connect` command via saved targetId.\n      const savedTargetId = readSavedTargetId();\n      let matchedPage: Page | undefined;\n\n      if (savedTargetId && pages.length > 0) {\n        matchedPage = pages.find((p) => getTargetId(p) === savedTargetId);\n        if (matchedPage) {\n          debug('Matched saved targetId %s', savedTargetId);\n        } else {\n          debug(\n            'Saved targetId %s not found among %d pages, falling back',\n            savedTargetId,\n            pages.length,\n          );\n        }\n      }\n\n      if (matchedPage) {\n        page = matchedPage;\n      } else if (webPages.length > 0) {\n        page = webPages[webPages.length - 1];\n      } else if (pages.length > 0) {\n        page = pages[pages.length - 1];\n      } else {\n        page = await browser.newPage();\n      }\n\n      await page.bringToFront();\n    }\n\n    if (initArgs?.extraHTTPHeaders) {\n      await page.setExtraHTTPHeaders(initArgs.extraHTTPHeaders);\n    }\n\n    if (navigateToUrl) {\n      await page.goto(navigateToUrl, {\n        timeout: 30000,\n        waitUntil: 'domcontentloaded',\n      });\n    }\n\n    // Persist the targetId so subsequent CLI commands can find this exact tab\n    const targetId = getTargetId(page);\n    if (targetId) {\n      saveTargetId(targetId);\n    } else {\n      // If puppeteer ever drops the private _targetId field, this branch\n      // makes the regression visible instead of silently disabling the\n      // cross-command tab reuse path.\n      debug(\n        'No targetId on page.target(); cross-command tab reuse disabled until puppeteer integration is updated.',\n      );\n    }\n\n    const reportOptions = this.readCliReportAgentOptions();\n    this.agent = new PuppeteerAgent(page as unknown as PuppeteerPage, {\n      ...(extractAgentBehaviorInitArgs(initArgs) ?? {}),\n      ...(reportOptions ?? {}),\n    });\n    this.lastInitArgsSignature = nextSignature;\n    return this.agent;\n  }\n\n  public async destroy(): Promise<void> {\n    await super.destroy();\n    if (this.activeBrowser) {\n      this.activeBrowser.disconnect();\n      this.activeBrowser = null;\n    }\n  }\n\n  protected preparePlatformTools(): ToolDefinition[] {\n    return [\n      {\n        name: 'web_connect',\n        description:\n          'Connect to a web page via CDP. Opens a new tab with the given URL, or reuses the current page.',\n        schema: this.getAgentInitArgSchema(),\n        cli: this.getAgentInitArgCliMetadata(),\n        handler: async (args) => {\n          const initArgs = this.extractAgentInitParam(args);\n          const url = initArgs?.url;\n\n          // Explicit connect always starts a fresh page session.\n          if (this.agent) {\n            try {\n              await this.agent.destroy?.();\n            } catch (e) {\n              console.debug('Failed to destroy agent during connect:', e);\n            }\n            this.agent = undefined;\n            this.lastInitArgsSignature = undefined;\n          }\n\n          const reportSession = this.createNewCliReportSession(\n            url ?? 'current-page',\n          );\n          this.commitCliReportSession(reportSession);\n          this.agent = await this.ensureAgent(initArgs);\n\n          const screenshot = await this.agent.page?.screenshotBase64();\n          const label = url ?? 'current page';\n\n          return {\n            content: [\n              { type: 'text', text: `Connected via CDP to: ${label}` },\n              ...(screenshot ? this.buildScreenshotContent(screenshot) : []),\n            ],\n          };\n        },\n      },\n      {\n        name: 'web_disconnect',\n        description:\n          'Disconnect from current web page. The browser stays running (managed externally).',\n        schema: {},\n        handler: async () => {\n          if (this.agent) {\n            try {\n              await this.agent.destroy?.();\n            } catch (e) {\n              console.debug('Failed to destroy agent during disconnect:', e);\n            }\n            this.agent = undefined;\n            this.lastInitArgsSignature = undefined;\n          }\n          if (this.activeBrowser) {\n            this.activeBrowser.disconnect();\n            this.activeBrowser = null;\n          }\n          cleanupTargetIdFile();\n          return this.buildTextResult(\n            'Disconnected from web page (browser still running externally)',\n          );\n        },\n      },\n    ];\n  }\n}\n"],"names":["debug","getDebug","CDP_TARGET_DISCOVERY_DELAY_MS","getTargetId","page","WebCdpMidsceneTools","BaseMidsceneTools","StaticPage","ScreenshotItem","Date","defaultStaticPageViewportSize","initArgs","navigateToUrl","nextSignature","getAgentInitArgsSignature","shouldNavigateToUrl","shouldRebuildAgentForInitArgs","error","console","undefined","endpoint","getProxyEndpoint","puppeteer","browser","pages","Promise","r","setTimeout","webPages","p","savedTargetId","readSavedTargetId","matchedPage","targetId","saveTargetId","reportOptions","PuppeteerAgent","extractAgentBehaviorInitArgs","args","url","e","reportSession","screenshot","label","cleanupTargetIdFile","cdpEndpoint","webCdpAgentInitArgShape","adaptWebCdpAgentInitArgs"],"mappings":"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"}