@midscene/web 1.13.3 → 1.13.4-beta-20260924033639.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/es/bridge-mode/io-client.mjs +1 -1
  2. package/dist/es/bridge-mode/io-server.mjs +2 -2
  3. package/dist/es/bridge-mode/io-server.mjs.map +1 -1
  4. package/dist/es/bridge-mode/page-browser-side.mjs +1 -1
  5. package/dist/es/bridge-mode/page-browser-side.mjs.map +1 -1
  6. package/dist/es/cli.mjs +1 -1
  7. package/dist/es/common/browser-agent.mjs +90 -0
  8. package/dist/es/common/browser-agent.mjs.map +1 -1
  9. package/dist/es/common/browser-page-actions.mjs +89 -0
  10. package/dist/es/common/browser-page-actions.mjs.map +1 -0
  11. package/dist/es/playwright/browser-agent.mjs +8 -1
  12. package/dist/es/playwright/browser-agent.mjs.map +1 -1
  13. package/dist/es/puppeteer/browser-agent.mjs +9 -3
  14. package/dist/es/puppeteer/browser-agent.mjs.map +1 -1
  15. package/dist/es/puppeteer/browser-page-manager.mjs +2 -0
  16. package/dist/es/puppeteer/browser-page-manager.mjs.map +1 -1
  17. package/dist/es/puppeteer/scoped-browser-agent.mjs +1 -28
  18. package/dist/es/puppeteer/scoped-browser-agent.mjs.map +1 -1
  19. package/dist/lib/bridge-mode/io-client.js +1 -1
  20. package/dist/lib/bridge-mode/io-server.js +2 -2
  21. package/dist/lib/bridge-mode/io-server.js.map +1 -1
  22. package/dist/lib/bridge-mode/page-browser-side.js +1 -1
  23. package/dist/lib/bridge-mode/page-browser-side.js.map +1 -1
  24. package/dist/lib/cli.js +1 -1
  25. package/dist/lib/common/browser-agent.js +90 -0
  26. package/dist/lib/common/browser-agent.js.map +1 -1
  27. package/dist/lib/common/browser-page-actions.js +126 -0
  28. package/dist/lib/common/browser-page-actions.js.map +1 -0
  29. package/dist/lib/playwright/browser-agent.js +8 -1
  30. package/dist/lib/playwright/browser-agent.js.map +1 -1
  31. package/dist/lib/puppeteer/browser-agent.js +9 -3
  32. package/dist/lib/puppeteer/browser-agent.js.map +1 -1
  33. package/dist/lib/puppeteer/browser-page-manager.js +2 -0
  34. package/dist/lib/puppeteer/browser-page-manager.js.map +1 -1
  35. package/dist/lib/puppeteer/scoped-browser-agent.js +1 -28
  36. package/dist/lib/puppeteer/scoped-browser-agent.js.map +1 -1
  37. package/dist/types/common/browser-agent.d.ts +19 -0
  38. package/dist/types/common/browser-agent.d.ts.map +1 -1
  39. package/dist/types/common/browser-page-actions.d.ts +7 -0
  40. package/dist/types/common/browser-page-actions.d.ts.map +1 -0
  41. package/dist/types/playwright/browser-agent.d.ts.map +1 -1
  42. package/dist/types/puppeteer/browser-agent.d.ts +3 -2
  43. package/dist/types/puppeteer/browser-agent.d.ts.map +1 -1
  44. package/dist/types/puppeteer/browser-page-manager.d.ts.map +1 -1
  45. package/dist/types/puppeteer/scoped-browser-agent.d.ts.map +1 -1
  46. package/package.json +4 -4
@@ -23,7 +23,7 @@ class BridgeClient {
23
23
  ]
24
24
  } : {},
25
25
  query: {
26
- version: "1.13.3"
26
+ version: "1.13.4-beta-20260924033639.0"
27
27
  }
28
28
  });
29
29
  const timeout = setTimeout(()=>{
@@ -101,7 +101,7 @@ class BridgeServer {
101
101
  logMsg('one client connected');
102
102
  this.socket = socket;
103
103
  const clientVersion = socket.handshake.query.version;
104
- logMsg(`Bridge connected, cli-side version v1.13.3, browser-side version v${clientVersion}`);
104
+ logMsg(`Bridge connected, cli-side version v1.13.4-beta-20260924033639.0, browser-side version v${clientVersion}`);
105
105
  socket.on(BridgeEvent.CallResponse, (params)=>{
106
106
  const id = params.id;
107
107
  const response = params.response;
@@ -125,7 +125,7 @@ class BridgeServer {
125
125
  setTimeout(()=>{
126
126
  this.onConnect?.();
127
127
  const payload = {
128
- version: "1.13.3"
128
+ version: "1.13.4-beta-20260924033639.0"
129
129
  };
130
130
  socket.emit(BridgeEvent.Connected, payload);
131
131
  Promise.resolve().then(()=>{
@@ -1 +1 @@
1
- {"version":3,"file":"bridge-mode/io-server.mjs","sources":["../../../src/bridge-mode/io-server.ts"],"sourcesContent":["import {\n type IncomingMessage,\n type ServerResponse,\n createServer,\n} from 'node:http';\nimport { sleep } from '@midscene/core/utils';\nimport { getDebug } from '@midscene/shared/logger';\nimport { logMsg } from '@midscene/shared/utils';\nimport { Server, type Socket as ServerSocket } from 'socket.io';\nimport { io as ClientIO } from 'socket.io-client';\n\nimport {\n type BridgeCall,\n type BridgeCallResponse,\n BridgeCallTimeout,\n type BridgeConnectedEventPayload,\n BridgeErrorCodeNoClientConnected,\n BridgeEvent,\n BridgeSignalKill,\n DefaultBridgeServerHost,\n DefaultBridgeServerPort,\n} from './common';\n\nconst debug = getDebug('web:bridge:io-server');\n\ndeclare const __VERSION__: string;\n\n/**\n * Returns true if the given Origin header value is allowed to connect\n * to the bridge server. Trusted origins are:\n * - No Origin header (local Node.js processes like killRunningServer)\n * - chrome-extension:// origins (the Midscene Chrome extension)\n *\n * Browser pages (https://evil.com, etc.) are rejected to prevent\n * Cross-Site WebSocket Hijacking (CSWSH) attacks.\n */\nfunction isTrustedOrigin(origin: string | undefined): boolean {\n if (!origin) return true;\n if (origin.startsWith('chrome-extension://')) return true;\n return false;\n}\n\nexport const killRunningServer = async (\n port?: number,\n host = DefaultBridgeServerHost,\n) => {\n try {\n const client = ClientIO(`ws://${host}:${port || DefaultBridgeServerPort}`, {\n query: {\n [BridgeSignalKill]: 1,\n },\n });\n await sleep(300);\n await client.close();\n } catch (e) {\n debug('failed to kill port: %O', e);\n }\n};\n\n// ws server, this is where the request is sent\nexport class BridgeServer {\n private callId = 0;\n private io: Server | null = null;\n private socket: ServerSocket | null = null;\n private listeningTimeoutId: NodeJS.Timeout | null = null;\n private listeningTimerFlag = false;\n private connectionTipTimer: NodeJS.Timeout | null = null;\n public calls: Record<string, BridgeCall> = {};\n\n private connectionLost = false;\n private connectionLostReason = '';\n\n constructor(\n public host: string,\n public port: number,\n public onConnect?: () => void,\n public onDisconnect?: (reason: string) => void,\n public closeConflictServer?: boolean,\n ) {}\n\n async listen(\n opts: {\n timeout?: number | false;\n } = {},\n ): Promise<void> {\n const { timeout = 30000 } = opts;\n\n if (this.closeConflictServer) {\n await killRunningServer(this.port, this.host);\n }\n\n return new Promise((resolve, reject) => {\n if (this.listeningTimerFlag) {\n return reject(new Error('already listening'));\n }\n this.listeningTimerFlag = true;\n\n this.listeningTimeoutId = timeout\n ? setTimeout(() => {\n reject(\n new Error(\n `no extension connected after ${timeout}ms (${BridgeErrorCodeNoClientConnected})`,\n ),\n );\n }, timeout)\n : null;\n\n this.connectionTipTimer =\n !timeout || timeout > 3000\n ? setTimeout(() => {\n logMsg('waiting for bridge to connect...');\n }, 2000)\n : null;\n\n // Create HTTP server — no /bridge-auth endpoint needed since we use\n // pure Origin checking in the Socket.IO middleware below.\n const httpServer = createServer(\n (_req: IncomingMessage, res: ServerResponse) => {\n res.writeHead(404);\n res.end();\n },\n );\n\n // Set up HTTP server event listeners FIRST\n httpServer.once('listening', () => {\n resolve();\n });\n\n httpServer.once('error', (err: Error) => {\n reject(new Error(`Bridge Listening Error: ${err.message}`));\n });\n\n // Always honor the configured interface, including the loopback default.\n httpServer.listen(this.port, this.host);\n\n // Now create Socket.IO Server attached to the already-listening HTTP server\n this.io = new Server(httpServer, {\n maxHttpBufferSize: 100 * 1024 * 1024, // 100MB\n // Increase pingTimeout to tolerate Chrome MV3 Service Worker suspension.\n // The SW keepalive alarm fires every ~24s; default pingTimeout (20s) may\n // be too short if the SW is suspended between alarm pings.\n pingTimeout: 60000,\n });\n\n this.io.use((socket, next) => {\n // Reject connections from untrusted browser origins to prevent\n // Cross-Site WebSocket Hijacking (CSWSH) and unauthorized kill signals.\n // The browser automatically sets the Origin header on WebSocket\n // handshakes, and JavaScript cannot override it.\n const origin = socket.handshake.headers.origin;\n if (!isTrustedOrigin(origin)) {\n debug('connection rejected: untrusted origin=%s', origin);\n return next(new Error('origin not allowed'));\n }\n\n // Allow new connections to replace old ones (reconnection after\n // extension Stop→Start). If the old socket is already disconnected\n // or unresponsive, accept the new connection immediately.\n if (\n socket.handshake.url.includes(BridgeSignalKill) === false &&\n this.socket?.connected\n ) {\n return next(new Error('server already connected by another client'));\n }\n next();\n });\n\n this.io.on('connection', (socket) => {\n // check the connection url\n const url = socket.handshake.url;\n if (url.includes(BridgeSignalKill)) {\n console.warn('kill signal received, closing bridge server');\n return this.close();\n }\n\n this.connectionLost = false;\n this.connectionLostReason = '';\n this.listeningTimeoutId && clearTimeout(this.listeningTimeoutId);\n this.listeningTimeoutId = null;\n this.connectionTipTimer && clearTimeout(this.connectionTipTimer);\n this.connectionTipTimer = null;\n if (this.socket?.connected) {\n socket.emit(BridgeEvent.Refused);\n socket.disconnect();\n logMsg(\n 'refused new connection: server already connected by another client',\n );\n return;\n }\n\n // Clean up stale old socket if it exists but is no longer connected\n if (this.socket) {\n try {\n this.socket.disconnect();\n } catch (e) {\n logMsg(`failed to disconnect stale socket: ${e}`);\n }\n this.socket = null;\n }\n\n try {\n logMsg('one client connected');\n this.socket = socket;\n\n const clientVersion = socket.handshake.query.version;\n logMsg(\n `Bridge connected, cli-side version v${__VERSION__}, browser-side version v${clientVersion}`,\n );\n\n socket.on(BridgeEvent.CallResponse, (params: BridgeCallResponse) => {\n const id = params.id;\n const response = params.response;\n const error = params.error;\n\n this.triggerCallResponseCallback(id, error, response);\n });\n\n socket.on('disconnect', (reason: string) => {\n this.connectionLost = true;\n this.connectionLostReason = reason;\n this.socket = null;\n\n // flush all pending calls as error and clean up completed calls\n for (const id in this.calls) {\n const call = this.calls[id];\n\n if (!call.responseTime) {\n const errorMessage = this.connectionLostErrorMsg();\n this.triggerCallResponseCallback(\n id,\n new Error(errorMessage),\n null,\n );\n }\n }\n\n // Clean up completed calls to prevent memory leaks in long-running sessions\n for (const id in this.calls) {\n if (this.calls[id].responseTime) {\n delete this.calls[id];\n }\n }\n\n this.onDisconnect?.(reason);\n });\n\n setTimeout(() => {\n this.onConnect?.();\n\n const payload = {\n version: __VERSION__,\n } as BridgeConnectedEventPayload;\n socket.emit(BridgeEvent.Connected, payload);\n Promise.resolve().then(() => {\n for (const id in this.calls) {\n if (this.calls[id].callTime === 0) {\n this.emitCall(id);\n }\n }\n });\n }, 0);\n } catch (e) {\n logMsg(`failed to handle connection event: ${e}`);\n }\n });\n\n this.io.on('close', () => {\n this.close();\n });\n });\n }\n\n private connectionLostErrorMsg = () => {\n return `Connection lost, reason: ${this.connectionLostReason}`;\n };\n\n private async triggerCallResponseCallback(\n id: string | number,\n error: Error | string | null,\n response: any,\n ) {\n const call = this.calls[id];\n if (!call) {\n throw new Error(`call ${id} not found`);\n }\n // Ensure error is always an Error object (bridge client may send strings)\n if (error) {\n call.error =\n error instanceof Error\n ? error\n : new Error(typeof error === 'string' ? error : String(error));\n } else {\n call.error = undefined;\n }\n call.response = response;\n call.responseTime = Date.now();\n\n call.callback(call.error, response);\n }\n\n private async emitCall(id: string) {\n const call = this.calls[id];\n if (!call) {\n throw new Error(`call ${id} not found`);\n }\n\n if (this.connectionLost) {\n const message = `Connection lost, reason: ${this.connectionLostReason}`;\n call.callback(new Error(message), null);\n return;\n }\n\n if (this.socket) {\n this.socket.emit(BridgeEvent.Call, {\n id,\n method: call.method,\n args: call.args,\n });\n call.callTime = Date.now();\n }\n }\n\n async call<T = any>(\n method: string,\n args: any[],\n timeout = BridgeCallTimeout,\n ): Promise<T> {\n const id = `${this.callId++}`;\n\n return new Promise((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n logMsg(`bridge call timeout, id=${id}, method=${method}, args=`, args);\n this.calls[id].error = new Error(\n `Bridge call timeout after ${timeout}ms: ${method}`,\n );\n reject(this.calls[id].error);\n }, timeout);\n\n this.calls[id] = {\n method,\n args,\n response: null,\n callTime: 0,\n responseTime: 0,\n callback: (error: Error | undefined, response: any) => {\n clearTimeout(timeoutId);\n if (error) {\n reject(error);\n } else {\n resolve(response);\n }\n },\n };\n\n this.emitCall(id);\n });\n }\n\n // do NOT restart after close\n async close() {\n this.listeningTimeoutId && clearTimeout(this.listeningTimeoutId);\n this.connectionTipTimer && clearTimeout(this.connectionTipTimer);\n const closeProcess = this.io?.close();\n this.io = null;\n\n return closeProcess;\n }\n}\n"],"names":["debug","getDebug","isTrustedOrigin","origin","killRunningServer","port","host","DefaultBridgeServerHost","client","ClientIO","DefaultBridgeServerPort","BridgeSignalKill","sleep","e","BridgeServer","opts","timeout","Promise","resolve","reject","Error","setTimeout","BridgeErrorCodeNoClientConnected","logMsg","httpServer","createServer","_req","res","err","Server","socket","next","url","console","clearTimeout","BridgeEvent","clientVersion","params","id","response","error","reason","call","errorMessage","payload","__VERSION__","String","undefined","Date","message","method","args","BridgeCallTimeout","timeoutId","closeProcess","onConnect","onDisconnect","closeConflictServer"],"mappings":"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"}
1
+ {"version":3,"file":"bridge-mode/io-server.mjs","sources":["../../../src/bridge-mode/io-server.ts"],"sourcesContent":["import {\n type IncomingMessage,\n type ServerResponse,\n createServer,\n} from 'node:http';\nimport { sleep } from '@midscene/core/utils';\nimport { getDebug } from '@midscene/shared/logger';\nimport { logMsg } from '@midscene/shared/utils';\nimport { Server, type Socket as ServerSocket } from 'socket.io';\nimport { io as ClientIO } from 'socket.io-client';\n\nimport {\n type BridgeCall,\n type BridgeCallResponse,\n BridgeCallTimeout,\n type BridgeConnectedEventPayload,\n BridgeErrorCodeNoClientConnected,\n BridgeEvent,\n BridgeSignalKill,\n DefaultBridgeServerHost,\n DefaultBridgeServerPort,\n} from './common';\n\nconst debug = getDebug('web:bridge:io-server');\n\ndeclare const __VERSION__: string;\n\n/**\n * Returns true if the given Origin header value is allowed to connect\n * to the bridge server. Trusted origins are:\n * - No Origin header (local Node.js processes like killRunningServer)\n * - chrome-extension:// origins (the Midscene Chrome extension)\n *\n * Browser pages (https://evil.com, etc.) are rejected to prevent\n * Cross-Site WebSocket Hijacking (CSWSH) attacks.\n */\nfunction isTrustedOrigin(origin: string | undefined): boolean {\n if (!origin) return true;\n if (origin.startsWith('chrome-extension://')) return true;\n return false;\n}\n\nexport const killRunningServer = async (\n port?: number,\n host = DefaultBridgeServerHost,\n) => {\n try {\n const client = ClientIO(`ws://${host}:${port || DefaultBridgeServerPort}`, {\n query: {\n [BridgeSignalKill]: 1,\n },\n });\n await sleep(300);\n await client.close();\n } catch (e) {\n debug('failed to kill port: %O', e);\n }\n};\n\n// ws server, this is where the request is sent\nexport class BridgeServer {\n private callId = 0;\n private io: Server | null = null;\n private socket: ServerSocket | null = null;\n private listeningTimeoutId: NodeJS.Timeout | null = null;\n private listeningTimerFlag = false;\n private connectionTipTimer: NodeJS.Timeout | null = null;\n public calls: Record<string, BridgeCall> = {};\n\n private connectionLost = false;\n private connectionLostReason = '';\n\n constructor(\n public host: string,\n public port: number,\n public onConnect?: () => void,\n public onDisconnect?: (reason: string) => void,\n public closeConflictServer?: boolean,\n ) {}\n\n async listen(\n opts: {\n timeout?: number | false;\n } = {},\n ): Promise<void> {\n const { timeout = 30000 } = opts;\n\n if (this.closeConflictServer) {\n await killRunningServer(this.port, this.host);\n }\n\n return new Promise((resolve, reject) => {\n if (this.listeningTimerFlag) {\n return reject(new Error('already listening'));\n }\n this.listeningTimerFlag = true;\n\n this.listeningTimeoutId = timeout\n ? setTimeout(() => {\n reject(\n new Error(\n `no extension connected after ${timeout}ms (${BridgeErrorCodeNoClientConnected})`,\n ),\n );\n }, timeout)\n : null;\n\n this.connectionTipTimer =\n !timeout || timeout > 3000\n ? setTimeout(() => {\n logMsg('waiting for bridge to connect...');\n }, 2000)\n : null;\n\n // Create HTTP server — no /bridge-auth endpoint needed since we use\n // pure Origin checking in the Socket.IO middleware below.\n const httpServer = createServer(\n (_req: IncomingMessage, res: ServerResponse) => {\n res.writeHead(404);\n res.end();\n },\n );\n\n // Set up HTTP server event listeners FIRST\n httpServer.once('listening', () => {\n resolve();\n });\n\n httpServer.once('error', (err: Error) => {\n reject(new Error(`Bridge Listening Error: ${err.message}`));\n });\n\n // Always honor the configured interface, including the loopback default.\n httpServer.listen(this.port, this.host);\n\n // Now create Socket.IO Server attached to the already-listening HTTP server\n this.io = new Server(httpServer, {\n maxHttpBufferSize: 100 * 1024 * 1024, // 100MB\n // Increase pingTimeout to tolerate Chrome MV3 Service Worker suspension.\n // The SW keepalive alarm fires every ~24s; default pingTimeout (20s) may\n // be too short if the SW is suspended between alarm pings.\n pingTimeout: 60000,\n });\n\n this.io.use((socket, next) => {\n // Reject connections from untrusted browser origins to prevent\n // Cross-Site WebSocket Hijacking (CSWSH) and unauthorized kill signals.\n // The browser automatically sets the Origin header on WebSocket\n // handshakes, and JavaScript cannot override it.\n const origin = socket.handshake.headers.origin;\n if (!isTrustedOrigin(origin)) {\n debug('connection rejected: untrusted origin=%s', origin);\n return next(new Error('origin not allowed'));\n }\n\n // Allow new connections to replace old ones (reconnection after\n // extension Stop→Start). If the old socket is already disconnected\n // or unresponsive, accept the new connection immediately.\n if (\n socket.handshake.url.includes(BridgeSignalKill) === false &&\n this.socket?.connected\n ) {\n return next(new Error('server already connected by another client'));\n }\n next();\n });\n\n this.io.on('connection', (socket) => {\n // check the connection url\n const url = socket.handshake.url;\n if (url.includes(BridgeSignalKill)) {\n console.warn('kill signal received, closing bridge server');\n return this.close();\n }\n\n this.connectionLost = false;\n this.connectionLostReason = '';\n this.listeningTimeoutId && clearTimeout(this.listeningTimeoutId);\n this.listeningTimeoutId = null;\n this.connectionTipTimer && clearTimeout(this.connectionTipTimer);\n this.connectionTipTimer = null;\n if (this.socket?.connected) {\n socket.emit(BridgeEvent.Refused);\n socket.disconnect();\n logMsg(\n 'refused new connection: server already connected by another client',\n );\n return;\n }\n\n // Clean up stale old socket if it exists but is no longer connected\n if (this.socket) {\n try {\n this.socket.disconnect();\n } catch (e) {\n logMsg(`failed to disconnect stale socket: ${e}`);\n }\n this.socket = null;\n }\n\n try {\n logMsg('one client connected');\n this.socket = socket;\n\n const clientVersion = socket.handshake.query.version;\n logMsg(\n `Bridge connected, cli-side version v${__VERSION__}, browser-side version v${clientVersion}`,\n );\n\n socket.on(BridgeEvent.CallResponse, (params: BridgeCallResponse) => {\n const id = params.id;\n const response = params.response;\n const error = params.error;\n\n this.triggerCallResponseCallback(id, error, response);\n });\n\n socket.on('disconnect', (reason: string) => {\n this.connectionLost = true;\n this.connectionLostReason = reason;\n this.socket = null;\n\n // flush all pending calls as error and clean up completed calls\n for (const id in this.calls) {\n const call = this.calls[id];\n\n if (!call.responseTime) {\n const errorMessage = this.connectionLostErrorMsg();\n this.triggerCallResponseCallback(\n id,\n new Error(errorMessage),\n null,\n );\n }\n }\n\n // Clean up completed calls to prevent memory leaks in long-running sessions\n for (const id in this.calls) {\n if (this.calls[id].responseTime) {\n delete this.calls[id];\n }\n }\n\n this.onDisconnect?.(reason);\n });\n\n setTimeout(() => {\n this.onConnect?.();\n\n const payload = {\n version: __VERSION__,\n } as BridgeConnectedEventPayload;\n socket.emit(BridgeEvent.Connected, payload);\n Promise.resolve().then(() => {\n for (const id in this.calls) {\n if (this.calls[id].callTime === 0) {\n this.emitCall(id);\n }\n }\n });\n }, 0);\n } catch (e) {\n logMsg(`failed to handle connection event: ${e}`);\n }\n });\n\n this.io.on('close', () => {\n this.close();\n });\n });\n }\n\n private connectionLostErrorMsg = () => {\n return `Connection lost, reason: ${this.connectionLostReason}`;\n };\n\n private async triggerCallResponseCallback(\n id: string | number,\n error: Error | string | null,\n response: any,\n ) {\n const call = this.calls[id];\n if (!call) {\n throw new Error(`call ${id} not found`);\n }\n // Ensure error is always an Error object (bridge client may send strings)\n if (error) {\n call.error =\n error instanceof Error\n ? error\n : new Error(typeof error === 'string' ? error : String(error));\n } else {\n call.error = undefined;\n }\n call.response = response;\n call.responseTime = Date.now();\n\n call.callback(call.error, response);\n }\n\n private async emitCall(id: string) {\n const call = this.calls[id];\n if (!call) {\n throw new Error(`call ${id} not found`);\n }\n\n if (this.connectionLost) {\n const message = `Connection lost, reason: ${this.connectionLostReason}`;\n call.callback(new Error(message), null);\n return;\n }\n\n if (this.socket) {\n this.socket.emit(BridgeEvent.Call, {\n id,\n method: call.method,\n args: call.args,\n });\n call.callTime = Date.now();\n }\n }\n\n async call<T = any>(\n method: string,\n args: any[],\n timeout = BridgeCallTimeout,\n ): Promise<T> {\n const id = `${this.callId++}`;\n\n return new Promise((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n logMsg(`bridge call timeout, id=${id}, method=${method}, args=`, args);\n this.calls[id].error = new Error(\n `Bridge call timeout after ${timeout}ms: ${method}`,\n );\n reject(this.calls[id].error);\n }, timeout);\n\n this.calls[id] = {\n method,\n args,\n response: null,\n callTime: 0,\n responseTime: 0,\n callback: (error: Error | undefined, response: any) => {\n clearTimeout(timeoutId);\n if (error) {\n reject(error);\n } else {\n resolve(response);\n }\n },\n };\n\n this.emitCall(id);\n });\n }\n\n // do NOT restart after close\n async close() {\n this.listeningTimeoutId && clearTimeout(this.listeningTimeoutId);\n this.connectionTipTimer && clearTimeout(this.connectionTipTimer);\n const closeProcess = this.io?.close();\n this.io = null;\n\n return closeProcess;\n }\n}\n"],"names":["debug","getDebug","isTrustedOrigin","origin","killRunningServer","port","host","DefaultBridgeServerHost","client","ClientIO","DefaultBridgeServerPort","BridgeSignalKill","sleep","e","BridgeServer","opts","timeout","Promise","resolve","reject","Error","setTimeout","BridgeErrorCodeNoClientConnected","logMsg","httpServer","createServer","_req","res","err","Server","socket","next","url","console","clearTimeout","BridgeEvent","clientVersion","params","id","response","error","reason","call","errorMessage","payload","__VERSION__","String","undefined","Date","message","method","args","BridgeCallTimeout","timeoutId","closeProcess","onConnect","onDisconnect","closeConflictServer"],"mappings":";;;;;;;;;;;;;;;;;AAuBA,MAAMA,QAAQC,SAAS;AAavB,SAASC,gBAAgBC,MAA0B;IACjD,IAAI,CAACA,QAAQ,OAAO;IACpB,IAAIA,OAAO,UAAU,CAAC,wBAAwB,OAAO;IACrD,OAAO;AACT;AAEO,MAAMC,oBAAoB,OAC/BC,MACAC,OAAOC,uBAAuB;IAE9B,IAAI;QACF,MAAMC,SAASC,GAAS,CAAC,KAAK,EAAEH,KAAK,CAAC,EAAED,QAAQK,yBAAyB,EAAE;YACzE,OAAO;gBACL,CAACC,iBAAiB,EAAE;YACtB;QACF;QACA,MAAMC,MAAM;QACZ,MAAMJ,OAAO,KAAK;IACpB,EAAE,OAAOK,GAAG;QACVb,MAAM,2BAA2Ba;IACnC;AACF;AAGO,MAAMC;IAoBX,MAAM,OACJC,OAEI,CAAC,CAAC,EACS;QACf,MAAM,EAAEC,UAAU,KAAK,EAAE,GAAGD;QAE5B,IAAI,IAAI,CAAC,mBAAmB,EAC1B,MAAMX,kBAAkB,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI;QAG9C,OAAO,IAAIa,QAAQ,CAACC,SAASC;YAC3B,IAAI,IAAI,CAAC,kBAAkB,EACzB,OAAOA,OAAO,IAAIC,MAAM;YAE1B,IAAI,CAAC,kBAAkB,GAAG;YAE1B,IAAI,CAAC,kBAAkB,GAAGJ,UACtBK,WAAW;gBACTF,OACE,IAAIC,MACF,CAAC,6BAA6B,EAAEJ,QAAQ,IAAI,EAAEM,iCAAiC,CAAC,CAAC;YAGvF,GAAGN,WACH;YAEJ,IAAI,CAAC,kBAAkB,GACrB,CAACA,WAAWA,UAAU,OAClBK,WAAW;gBACTE,OAAO;YACT,GAAG,QACH;YAIN,MAAMC,aAAaC,aACjB,CAACC,MAAuBC;gBACtBA,IAAI,SAAS,CAAC;gBACdA,IAAI,GAAG;YACT;YAIFH,WAAW,IAAI,CAAC,aAAa;gBAC3BN;YACF;YAEAM,WAAW,IAAI,CAAC,SAAS,CAACI;gBACxBT,OAAO,IAAIC,MAAM,CAAC,wBAAwB,EAAEQ,IAAI,OAAO,EAAE;YAC3D;YAGAJ,WAAW,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI;YAGtC,IAAI,CAAC,EAAE,GAAG,IAAIK,OAAOL,YAAY;gBAC/B,mBAAmB;gBAInB,aAAa;YACf;YAEA,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,CAACM,QAAQC;gBAKnB,MAAM5B,SAAS2B,OAAO,SAAS,CAAC,OAAO,CAAC,MAAM;gBAC9C,IAAI,CAAC5B,gBAAgBC,SAAS;oBAC5BH,MAAM,4CAA4CG;oBAClD,OAAO4B,KAAK,IAAIX,MAAM;gBACxB;gBAKA,IACEU,AAAoD,UAApDA,OAAO,SAAS,CAAC,GAAG,CAAC,QAAQ,CAACnB,qBAC9B,IAAI,CAAC,MAAM,EAAE,WAEb,OAAOoB,KAAK,IAAIX,MAAM;gBAExBW;YACF;YAEA,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,cAAc,CAACD;gBAExB,MAAME,MAAMF,OAAO,SAAS,CAAC,GAAG;gBAChC,IAAIE,IAAI,QAAQ,CAACrB,mBAAmB;oBAClCsB,QAAQ,IAAI,CAAC;oBACb,OAAO,IAAI,CAAC,KAAK;gBACnB;gBAEA,IAAI,CAAC,cAAc,GAAG;gBACtB,IAAI,CAAC,oBAAoB,GAAG;gBAC5B,IAAI,CAAC,kBAAkB,IAAIC,aAAa,IAAI,CAAC,kBAAkB;gBAC/D,IAAI,CAAC,kBAAkB,GAAG;gBAC1B,IAAI,CAAC,kBAAkB,IAAIA,aAAa,IAAI,CAAC,kBAAkB;gBAC/D,IAAI,CAAC,kBAAkB,GAAG;gBAC1B,IAAI,IAAI,CAAC,MAAM,EAAE,WAAW;oBAC1BJ,OAAO,IAAI,CAACK,YAAY,OAAO;oBAC/BL,OAAO,UAAU;oBACjBP,OACE;oBAEF;gBACF;gBAGA,IAAI,IAAI,CAAC,MAAM,EAAE;oBACf,IAAI;wBACF,IAAI,CAAC,MAAM,CAAC,UAAU;oBACxB,EAAE,OAAOV,GAAG;wBACVU,OAAO,CAAC,mCAAmC,EAAEV,GAAG;oBAClD;oBACA,IAAI,CAAC,MAAM,GAAG;gBAChB;gBAEA,IAAI;oBACFU,OAAO;oBACP,IAAI,CAAC,MAAM,GAAGO;oBAEd,MAAMM,gBAAgBN,OAAO,SAAS,CAAC,KAAK,CAAC,OAAO;oBACpDP,OACE,2FAA6Ea,eAAe;oBAG9FN,OAAO,EAAE,CAACK,YAAY,YAAY,EAAE,CAACE;wBACnC,MAAMC,KAAKD,OAAO,EAAE;wBACpB,MAAME,WAAWF,OAAO,QAAQ;wBAChC,MAAMG,QAAQH,OAAO,KAAK;wBAE1B,IAAI,CAAC,2BAA2B,CAACC,IAAIE,OAAOD;oBAC9C;oBAEAT,OAAO,EAAE,CAAC,cAAc,CAACW;wBACvB,IAAI,CAAC,cAAc,GAAG;wBACtB,IAAI,CAAC,oBAAoB,GAAGA;wBAC5B,IAAI,CAAC,MAAM,GAAG;wBAGd,IAAK,MAAMH,MAAM,IAAI,CAAC,KAAK,CAAE;4BAC3B,MAAMI,OAAO,IAAI,CAAC,KAAK,CAACJ,GAAG;4BAE3B,IAAI,CAACI,KAAK,YAAY,EAAE;gCACtB,MAAMC,eAAe,IAAI,CAAC,sBAAsB;gCAChD,IAAI,CAAC,2BAA2B,CAC9BL,IACA,IAAIlB,MAAMuB,eACV;4BAEJ;wBACF;wBAGA,IAAK,MAAML,MAAM,IAAI,CAAC,KAAK,CACzB,IAAI,IAAI,CAAC,KAAK,CAACA,GAAG,CAAC,YAAY,EAC7B,OAAO,IAAI,CAAC,KAAK,CAACA,GAAG;wBAIzB,IAAI,CAAC,YAAY,GAAGG;oBACtB;oBAEApB,WAAW;wBACT,IAAI,CAAC,SAAS;wBAEd,MAAMuB,UAAU;4BACd,SAASC;wBACX;wBACAf,OAAO,IAAI,CAACK,YAAY,SAAS,EAAES;wBACnC3B,QAAQ,OAAO,GAAG,IAAI,CAAC;4BACrB,IAAK,MAAMqB,MAAM,IAAI,CAAC,KAAK,CACzB,IAAI,AAA4B,MAA5B,IAAI,CAAC,KAAK,CAACA,GAAG,CAAC,QAAQ,EACzB,IAAI,CAAC,QAAQ,CAACA;wBAGpB;oBACF,GAAG;gBACL,EAAE,OAAOzB,GAAG;oBACVU,OAAO,CAAC,mCAAmC,EAAEV,GAAG;gBAClD;YACF;YAEA,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS;gBAClB,IAAI,CAAC,KAAK;YACZ;QACF;IACF;IAMA,MAAc,4BACZyB,EAAmB,EACnBE,KAA4B,EAC5BD,QAAa,EACb;QACA,MAAMG,OAAO,IAAI,CAAC,KAAK,CAACJ,GAAG;QAC3B,IAAI,CAACI,MACH,MAAM,IAAItB,MAAM,CAAC,KAAK,EAAEkB,GAAG,UAAU,CAAC;QAGxC,IAAIE,OACFE,KAAK,KAAK,GACRF,iBAAiBpB,QACboB,QACA,IAAIpB,MAAM,AAAiB,YAAjB,OAAOoB,QAAqBA,QAAQM,OAAON;aAE3DE,KAAK,KAAK,GAAGK;QAEfL,KAAK,QAAQ,GAAGH;QAChBG,KAAK,YAAY,GAAGM,KAAK,GAAG;QAE5BN,KAAK,QAAQ,CAACA,KAAK,KAAK,EAAEH;IAC5B;IAEA,MAAc,SAASD,EAAU,EAAE;QACjC,MAAMI,OAAO,IAAI,CAAC,KAAK,CAACJ,GAAG;QAC3B,IAAI,CAACI,MACH,MAAM,IAAItB,MAAM,CAAC,KAAK,EAAEkB,GAAG,UAAU,CAAC;QAGxC,IAAI,IAAI,CAAC,cAAc,EAAE;YACvB,MAAMW,UAAU,CAAC,yBAAyB,EAAE,IAAI,CAAC,oBAAoB,EAAE;YACvEP,KAAK,QAAQ,CAAC,IAAItB,MAAM6B,UAAU;YAClC;QACF;QAEA,IAAI,IAAI,CAAC,MAAM,EAAE;YACf,IAAI,CAAC,MAAM,CAAC,IAAI,CAACd,YAAY,IAAI,EAAE;gBACjCG;gBACA,QAAQI,KAAK,MAAM;gBACnB,MAAMA,KAAK,IAAI;YACjB;YACAA,KAAK,QAAQ,GAAGM,KAAK,GAAG;QAC1B;IACF;IAEA,MAAM,KACJE,MAAc,EACdC,IAAW,EACXnC,UAAUoC,iBAAiB,EACf;QACZ,MAAMd,KAAK,GAAG,IAAI,CAAC,MAAM,IAAI;QAE7B,OAAO,IAAIrB,QAAQ,CAACC,SAASC;YAC3B,MAAMkC,YAAYhC,WAAW;gBAC3BE,OAAO,CAAC,wBAAwB,EAAEe,GAAG,SAAS,EAAEY,OAAO,OAAO,CAAC,EAAEC;gBACjE,IAAI,CAAC,KAAK,CAACb,GAAG,CAAC,KAAK,GAAG,IAAIlB,MACzB,CAAC,0BAA0B,EAAEJ,QAAQ,IAAI,EAAEkC,QAAQ;gBAErD/B,OAAO,IAAI,CAAC,KAAK,CAACmB,GAAG,CAAC,KAAK;YAC7B,GAAGtB;YAEH,IAAI,CAAC,KAAK,CAACsB,GAAG,GAAG;gBACfY;gBACAC;gBACA,UAAU;gBACV,UAAU;gBACV,cAAc;gBACd,UAAU,CAACX,OAA0BD;oBACnCL,aAAamB;oBACb,IAAIb,OACFrB,OAAOqB;yBAEPtB,QAAQqB;gBAEZ;YACF;YAEA,IAAI,CAAC,QAAQ,CAACD;QAChB;IACF;IAGA,MAAM,QAAQ;QACZ,IAAI,CAAC,kBAAkB,IAAIJ,aAAa,IAAI,CAAC,kBAAkB;QAC/D,IAAI,CAAC,kBAAkB,IAAIA,aAAa,IAAI,CAAC,kBAAkB;QAC/D,MAAMoB,eAAe,IAAI,CAAC,EAAE,EAAE;QAC9B,IAAI,CAAC,EAAE,GAAG;QAEV,OAAOA;IACT;IAtSA,YACShD,IAAY,EACZD,IAAY,EACZkD,SAAsB,EACtBC,YAAuC,EACvCC,mBAA6B,CACpC;;;;;;QAjBF,uBAAQ,UAAR;QACA,uBAAQ,MAAR;QACA,uBAAQ,UAAR;QACA,uBAAQ,sBAAR;QACA,uBAAQ,sBAAR;QACA,uBAAQ,sBAAR;QACA,uBAAO,SAAP;QAEA,uBAAQ,kBAAR;QACA,uBAAQ,wBAAR;QA0MA,uBAAQ,0BAAR;aAvMSnD,IAAI,GAAJA;aACAD,IAAI,GAAJA;aACAkD,SAAS,GAATA;aACAC,YAAY,GAAZA;aACAC,mBAAmB,GAAnBA;aAhBD,MAAM,GAAG;aACT,EAAE,GAAkB;aACpB,MAAM,GAAwB;aAC9B,kBAAkB,GAA0B;aAC5C,kBAAkB,GAAG;aACrB,kBAAkB,GAA0B;aAC7C,KAAK,GAA+B,CAAC;aAEpC,cAAc,GAAG;aACjB,oBAAoB,GAAG;aA0MvB,sBAAsB,GAAG,IACxB,CAAC,yBAAyB,EAAE,IAAI,CAAC,oBAAoB,EAAE;IAnM7D;AAiSL"}
@@ -100,7 +100,7 @@ class ExtensionBridgePageBrowserSide extends page {
100
100
  throw new Error('Connection denied by user');
101
101
  }
102
102
  }
103
- this.onLogMessage(`Bridge connected, cli-side version v${this.bridgeClient.serverVersion}, browser-side version v1.13.3`, 'log');
103
+ this.onLogMessage(`Bridge connected, cli-side version v${this.bridgeClient.serverVersion}, browser-side version v1.13.4-beta-20260924033639.0`, 'log');
104
104
  }
105
105
  async connect() {
106
106
  return await this.setupBridgeClient();
@@ -1 +1 @@
1
- {"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 DefaultBridgeServerHost,\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 ||\n `ws://${DefaultBridgeServerHost}:${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","DefaultBridgeServerHost","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":"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"}
1
+ {"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 DefaultBridgeServerHost,\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 ||\n `ws://${DefaultBridgeServerHost}:${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","DefaultBridgeServerHost","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":";;;;;;;;;;;;;;AAmBA,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,IACnB,CAAC,KAAK,EAAEC,wBAAwB,CAAC,EAAEC,yBAAyB;QAM9D,IAAIC,0BAAsD,KAAO;QACjE,IAAI,IAAI,CAAC,mBAAmB,EAC1B,IAAI,CAAC,mBAAmB,GAAG,IAAInB,QAAiB,CAACC;YAC/CkB,0BAA0BlB;QAC5B;QAGF,IAAI,CAAC,YAAY,GAAG,IAAImB,aACtBJ,UACA,OAAOK,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,MAAMzB,QAAQ,MAAM,IAAI,CAAC,cAAc;YACvC,IAAI,CAACA,SAASA,AAAU,MAAVA,OACZ,MAAM,IAAI2B,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,sDAAwC,EAC9G;IAEJ;IAEA,MAAa,UAAU;QACrB,OAAO,MAAM,IAAI,CAAC,iBAAiB;IACrC;IAEA,MAAa,qBACX7B,GAAW,EACXqC,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,MAAMvB,MAAM,MAAML,OAAO,IAAI,CAAC,MAAM,CAAC;YAAET;YAAK,QAAQuC;QAAS;QAC7D,MAAMrC,QAAQY,IAAI,EAAE;QACpB0B,OAAOtC,OAAO;QAGd,IAAI,CAAC,YAAY,CAAC,CAAC,kBAAkB,EAAEF,KAAK,EAAE;QAC9C,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAACE;QAE7B,IAAImC,SAAS,wBACX,IAAI,CAAC,sBAAsB,GAAG;QAShC,MAAMpC,6BAA6BC,OAAOF;QAE1C,MAAM,IAAI,CAAC,cAAc,CAACE,OAAO;YAAEqC;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,MAAMhC,OAAO,IAAI,CAAC,KAAK,CAAC;YAAE,QAAQ;YAAM,eAAe;QAAK;QACzE,MAAMP,QAAQuC,IAAI,CAAC,EAAE,EAAE;QACvBD,OAAOtC,OAAO;QAEd,IAAI,CAAC,YAAY,CAAC,CAAC,0BAA0B,EAAEuC,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE;QAE/D,IAAIJ,SAAS,wBACX,IAAI,CAAC,sBAAsB,GAAG;QAGhC,MAAM,IAAI,CAAC,cAAc,CAACnC;IAC5B;IAEA,MAAa,kBAAkBmC,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,MAAMnC,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;IAjOA,YACSwC,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;AAuNF"}
package/dist/es/cli.mjs CHANGED
@@ -18,7 +18,7 @@ Promise.resolve().then(()=>{
18
18
  return runToolsCLI(tools, 'midscene-web', {
19
19
  stripPrefix: 'web_',
20
20
  argv: parsedOptions.argv,
21
- version: "1.13.3",
21
+ version: "1.13.4-beta-20260924033639.0",
22
22
  extraCommands: createReportCliCommands()
23
23
  });
24
24
  }).catch((e)=>{
@@ -34,6 +34,18 @@ function resolveBrowserAgentRuntimeOptions({ agentName, pageScope, forceSameTabN
34
34
  newPageTimeout
35
35
  };
36
36
  }
37
+ const normalizeOptionalText = (value)=>{
38
+ const trimmed = value?.trim();
39
+ return trimmed ? trimmed.toLowerCase() : void 0;
40
+ };
41
+ const pageSummaryMatches = (summary, title, url)=>(!title || summary.title.toLowerCase().includes(title)) && (!url || summary.url.toLowerCase().includes(url));
42
+ const describeSelector = (selector)=>{
43
+ const parts = [];
44
+ if (void 0 !== selector.index) parts.push(`index ${selector.index}`);
45
+ if (selector.title?.trim()) parts.push(`title "${selector.title.trim()}"`);
46
+ if (selector.url?.trim()) parts.push(`url "${selector.url.trim()}"`);
47
+ return parts.join(', ');
48
+ };
37
49
  class BrowserPageManager {
38
50
  get activePage() {
39
51
  return this.getActivePageValue();
@@ -41,6 +53,23 @@ class BrowserPageManager {
41
53
  pages() {
42
54
  return this.adapter.pages();
43
55
  }
56
+ async pageSummaries() {
57
+ const pages = await this.openPages();
58
+ const activePage = this.activePage;
59
+ const summaries = await Promise.all(pages.map((page, index)=>this.tryPageSummary(page, index, page === activePage)));
60
+ return summaries.filter((summary)=>void 0 !== summary).map((summary, index)=>({
61
+ ...summary,
62
+ index
63
+ }));
64
+ }
65
+ async pageSummaryByIndex(index) {
66
+ const pages = await this.openPages();
67
+ const page = pages[index];
68
+ if (!page || this.adapter.isPageClosed(page)) throw new Error(`[midscene] Cannot find ${this.agentName} page with index ${index}. Available page indexes: ${pages.map((_, pageIndex)=>pageIndex).join(', ')}`);
69
+ const summary = await this.tryPageSummary(page, index, page === this.activePage);
70
+ if (!summary) throw new Error(`[midscene] ${this.agentName} page at index ${index} closed while reading its metadata. Run ListBrowserPages again.`);
71
+ return summary;
72
+ }
44
73
  async newPage() {
45
74
  const page = await this.adapter.newPage();
46
75
  await this.setActivePage(page);
@@ -56,6 +85,46 @@ class BrowserPageManager {
56
85
  this.debug(`failed to bring page to front: ${error}`);
57
86
  }
58
87
  }
88
+ async setActivePageBySelector(selector) {
89
+ const selectorIndex = selector.index;
90
+ const title = normalizeOptionalText(selector.title);
91
+ const url = normalizeOptionalText(selector.url);
92
+ if (void 0 === selectorIndex && !title && !url) throw new Error(`[midscene] SetActivePage requires index, title, or url for ${this.agentName}.`);
93
+ const pages = await this.openPages();
94
+ if (void 0 !== selectorIndex) {
95
+ const page = pages[selectorIndex];
96
+ if (!page || this.adapter.isPageClosed(page)) throw new Error(`[midscene] Cannot find ${this.agentName} page with index ${selectorIndex}. Available page indexes: ${pages.map((_, index)=>index).join(', ')}`);
97
+ const summary = await this.tryPageSummary(page, selectorIndex, true);
98
+ if (!summary) throw new Error(`[midscene] ${this.agentName} page at index ${selectorIndex} closed while reading its metadata. Run ListBrowserPages again.`);
99
+ if (!pageSummaryMatches(summary, title, url)) {
100
+ const textSelector = describeSelector({
101
+ title: selector.title,
102
+ url: selector.url
103
+ });
104
+ throw new Error(`[midscene] ${this.agentName} page at index ${selectorIndex} does not match ${textSelector}. Run ListBrowserPages again before selecting a page.`);
105
+ }
106
+ await this.setActivePage(page);
107
+ return summary;
108
+ }
109
+ const matchedPages = [];
110
+ for(let index = 0; index < pages.length; index++){
111
+ const page = pages[index];
112
+ if (this.adapter.isPageClosed(page)) continue;
113
+ const summary = await this.tryPageSummary(page, index, false);
114
+ if (summary && pageSummaryMatches(summary, title, url)) matchedPages.push({
115
+ page,
116
+ summary
117
+ });
118
+ }
119
+ if (0 === matchedPages.length) throw new Error(`[midscene] Cannot find ${this.agentName} page matching ${describeSelector(selector)}.`);
120
+ if (matchedPages.length > 1) throw new Error(`[midscene] Multiple ${this.agentName} pages matched ${describeSelector(selector)}. Use ListBrowserPages and pass an index to SetActivePage.`);
121
+ const { page, summary } = matchedPages[0];
122
+ await this.setActivePage(page);
123
+ return {
124
+ ...summary,
125
+ active: true
126
+ };
127
+ }
59
128
  async waitForNewPage(action, opts) {
60
129
  const waiter = this.createNewPageWaiter(opts?.timeout);
61
130
  try {
@@ -69,6 +138,27 @@ class BrowserPageManager {
69
138
  destroy() {
70
139
  this.adapter.offNewPage(this.newPageHandler);
71
140
  }
141
+ async openPages() {
142
+ const pages = await this.adapter.pages();
143
+ return pages.filter((page)=>!this.adapter.isPageClosed(page));
144
+ }
145
+ async tryPageSummary(page, index, active) {
146
+ try {
147
+ const summary = await this.pageSummary(page, index, active);
148
+ return this.adapter.isPageClosed(page) ? void 0 : summary;
149
+ } catch (error) {
150
+ if (this.adapter.isPageClosed(page)) return;
151
+ throw error;
152
+ }
153
+ }
154
+ async pageSummary(page, index, active) {
155
+ return {
156
+ index,
157
+ active,
158
+ title: await this.adapter.pageTitle(page),
159
+ url: this.adapter.pageUrl(page)
160
+ };
161
+ }
72
162
  async followNewPage(event) {
73
163
  if (!this.isNewPageEvent(event)) return;
74
164
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"common/browser-agent.mjs","sources":["../../../src/common/browser-agent.ts"],"sourcesContent":["import { Agent as CoreAgent } from '@midscene/core/agent';\nimport type { AbstractInterface } from '@midscene/core/device';\nimport type { DebugFunction } from '@midscene/shared/logger';\nimport { isRetryableBrowserNavigationError } from './browser-agent-utils';\n\nexport type BrowserAgentPageScope = 'page' | 'browser';\n\nexport type BrowserAgentAdapter<Page, NewPageEvent> = {\n pages(): Page[] | Promise<Page[]>;\n newPage(): Promise<Page>;\n isPageClosed(page: Page): boolean;\n isPageAllowed?(page: Page): boolean;\n bringToFront(page: Page): Promise<void> | void;\n onNewPage(handler: (event: NewPageEvent) => void): void;\n offNewPage(handler: (event: NewPageEvent) => void): void;\n resolveNewPage(event: NewPageEvent): Page | Promise<Page | null> | null;\n isNewPageEvent?: (event: NewPageEvent) => boolean;\n};\n\nexport type BrowserPageManagerOptions<Page, NewPageEvent> = {\n agentName: string;\n adapter: BrowserAgentAdapter<Page, NewPageEvent>;\n getActivePage(): Page;\n setActivePageValue(page: Page): void;\n autoFollowNewPage: boolean;\n newPageTimeout: number;\n debug: DebugFunction;\n};\n\nexport type BrowserAgentRuntimeOptions = {\n agentName: string;\n pageScope: BrowserAgentPageScope;\n forceSameTabNavigation?: boolean;\n autoFollowNewPage?: boolean;\n newPageTimeout?: number;\n};\n\nexport type ResolvedBrowserAgentRuntimeOptions = {\n pageScope: BrowserAgentPageScope;\n forceSameTabNavigation: boolean;\n autoFollowNewPage: boolean;\n newPageTimeout: number;\n};\n\nexport abstract class WebAgentCore<\n InterfaceType extends AbstractInterface,\n> extends CoreAgent<InterfaceType> {\n protected isRetryableContextError(error: unknown): boolean {\n return isRetryableBrowserNavigationError(error);\n }\n}\n\nconst DEFAULT_NEW_PAGE_TIMEOUT = 5000;\n\nexport function resolveBrowserAgentRuntimeOptions({\n agentName,\n pageScope,\n forceSameTabNavigation,\n autoFollowNewPage,\n newPageTimeout = DEFAULT_NEW_PAGE_TIMEOUT,\n}: BrowserAgentRuntimeOptions): ResolvedBrowserAgentRuntimeOptions {\n if (pageScope === 'page') {\n if (autoFollowNewPage) {\n throw new Error(\n `[midscene] autoFollowNewPage requires browser mode for ${agentName}. Use BrowserAgent when one agent should follow newly opened pages.`,\n );\n }\n\n return {\n pageScope,\n forceSameTabNavigation: forceSameTabNavigation ?? true,\n autoFollowNewPage: false,\n newPageTimeout,\n };\n }\n\n if (typeof forceSameTabNavigation !== 'undefined') {\n throw new Error(\n `[midscene] forceSameTabNavigation cannot be used in browser mode for ${agentName}. Use PageAgent when same-tab navigation is required.`,\n );\n }\n\n return {\n pageScope,\n forceSameTabNavigation: false,\n autoFollowNewPage: autoFollowNewPage ?? false,\n newPageTimeout,\n };\n}\n\nexport class BrowserPageManager<Page, NewPageEvent> {\n private readonly agentName: string;\n private readonly adapter: BrowserAgentAdapter<Page, NewPageEvent>;\n private readonly getActivePageValue: () => Page;\n private readonly setActivePageValue: (page: Page) => void;\n private readonly newPageTimeout: number;\n private readonly debug: DebugFunction;\n\n private readonly newPageHandler = (event: NewPageEvent) => {\n void this.followNewPage(event);\n };\n\n constructor(options: BrowserPageManagerOptions<Page, NewPageEvent>) {\n this.agentName = options.agentName;\n this.adapter = options.adapter;\n this.getActivePageValue = options.getActivePage;\n this.setActivePageValue = options.setActivePageValue;\n this.newPageTimeout = options.newPageTimeout;\n this.debug = options.debug;\n\n if (options.autoFollowNewPage) {\n this.adapter.onNewPage(this.newPageHandler);\n }\n }\n\n get activePage() {\n return this.getActivePageValue();\n }\n\n pages() {\n return this.adapter.pages();\n }\n\n async newPage() {\n const page = await this.adapter.newPage();\n await this.setActivePage(page);\n return page;\n }\n\n async setActivePage(page: Page) {\n if (!page || this.adapter.isPageClosed(page)) {\n throw new Error(\n `[midscene] Cannot set ${this.agentName} active page to a closed or invalid page.`,\n );\n }\n if (this.adapter.isPageAllowed?.(page) === false) {\n throw new Error(\n `[midscene] Cannot set ${this.agentName} active page to an out-of-scope page.`,\n );\n }\n\n this.setActivePageValue(page);\n try {\n await this.adapter.bringToFront(page);\n } catch (error) {\n this.debug(`failed to bring page to front: ${error}`);\n }\n }\n\n async waitForNewPage(\n action?: () => Promise<unknown> | unknown,\n opts?: { timeout?: number },\n ) {\n const waiter = this.createNewPageWaiter(opts?.timeout);\n\n try {\n await action?.();\n return await waiter.promise;\n } catch (error) {\n waiter.dispose();\n throw error;\n }\n }\n\n destroy() {\n this.adapter.offNewPage(this.newPageHandler);\n }\n\n private async followNewPage(event: NewPageEvent) {\n if (!this.isNewPageEvent(event)) {\n return;\n }\n\n try {\n const page = await this.adapter.resolveNewPage(event);\n if (page) {\n await this.setActivePage(page);\n }\n } catch (error) {\n this.debug(`failed to follow new page: ${error}`);\n }\n }\n\n private isNewPageEvent(event: NewPageEvent) {\n return this.adapter.isNewPageEvent?.(event) ?? true;\n }\n\n private createNewPageWaiter(timeout = this.newPageTimeout) {\n let settled = false;\n\n const dispose = () => {\n this.adapter.offNewPage(handler);\n clearTimeout(timer);\n };\n\n const handler = async (event: NewPageEvent) => {\n if (settled || !this.isNewPageEvent(event)) {\n return;\n }\n\n settled = true;\n dispose();\n\n try {\n const page = await this.adapter.resolveNewPage(event);\n if (!page) {\n throw new Error('new target did not resolve to a page');\n }\n resolvePage(page);\n } catch (error) {\n rejectPage(error);\n }\n };\n\n let resolvePage!: (page: Page) => void;\n let rejectPage!: (error: unknown) => void;\n const promise = new Promise<Page>((resolve, reject) => {\n resolvePage = resolve;\n rejectPage = reject;\n });\n\n const timer = setTimeout(() => {\n if (settled) {\n return;\n }\n settled = true;\n dispose();\n rejectPage(\n new Error(\n `[midscene] Timed out waiting for a new ${this.agentName} page after ${timeout}ms.`,\n ),\n );\n }, timeout);\n\n this.adapter.onNewPage(handler);\n\n return { promise, dispose };\n }\n}\n"],"names":["WebAgentCore","CoreAgent","error","isRetryableBrowserNavigationError","DEFAULT_NEW_PAGE_TIMEOUT","resolveBrowserAgentRuntimeOptions","agentName","pageScope","forceSameTabNavigation","autoFollowNewPage","newPageTimeout","Error","BrowserPageManager","page","action","opts","waiter","event","timeout","settled","dispose","handler","clearTimeout","timer","resolvePage","rejectPage","promise","Promise","resolve","reject","setTimeout","options"],"mappings":"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"}
1
+ {"version":3,"file":"common/browser-agent.mjs","sources":["../../../src/common/browser-agent.ts"],"sourcesContent":["import { Agent as CoreAgent } from '@midscene/core/agent';\nimport type { AbstractInterface } from '@midscene/core/device';\nimport type { DebugFunction } from '@midscene/shared/logger';\nimport { isRetryableBrowserNavigationError } from './browser-agent-utils';\n\nexport type BrowserAgentPageScope = 'page' | 'browser';\n\nexport type BrowserAgentAdapter<Page, NewPageEvent> = {\n pages(): Page[] | Promise<Page[]>;\n newPage(): Promise<Page>;\n isPageClosed(page: Page): boolean;\n isPageAllowed?(page: Page): boolean;\n bringToFront(page: Page): Promise<void> | void;\n pageTitle(page: Page): Promise<string> | string;\n pageUrl(page: Page): string;\n onNewPage(handler: (event: NewPageEvent) => void): void;\n offNewPage(handler: (event: NewPageEvent) => void): void;\n resolveNewPage(event: NewPageEvent): Page | Promise<Page | null> | null;\n isNewPageEvent?: (event: NewPageEvent) => boolean;\n};\n\nexport type BrowserPageManagerOptions<Page, NewPageEvent> = {\n agentName: string;\n adapter: BrowserAgentAdapter<Page, NewPageEvent>;\n getActivePage(): Page;\n setActivePageValue(page: Page): void;\n autoFollowNewPage: boolean;\n newPageTimeout: number;\n debug: DebugFunction;\n};\n\nexport type BrowserAgentRuntimeOptions = {\n agentName: string;\n pageScope: BrowserAgentPageScope;\n forceSameTabNavigation?: boolean;\n autoFollowNewPage?: boolean;\n newPageTimeout?: number;\n};\n\nexport type ResolvedBrowserAgentRuntimeOptions = {\n pageScope: BrowserAgentPageScope;\n forceSameTabNavigation: boolean;\n autoFollowNewPage: boolean;\n newPageTimeout: number;\n};\n\nexport abstract class WebAgentCore<\n InterfaceType extends AbstractInterface,\n> extends CoreAgent<InterfaceType> {\n protected isRetryableContextError(error: unknown): boolean {\n return isRetryableBrowserNavigationError(error);\n }\n}\n\nconst DEFAULT_NEW_PAGE_TIMEOUT = 5000;\n\nexport function resolveBrowserAgentRuntimeOptions({\n agentName,\n pageScope,\n forceSameTabNavigation,\n autoFollowNewPage,\n newPageTimeout = DEFAULT_NEW_PAGE_TIMEOUT,\n}: BrowserAgentRuntimeOptions): ResolvedBrowserAgentRuntimeOptions {\n if (pageScope === 'page') {\n if (autoFollowNewPage) {\n throw new Error(\n `[midscene] autoFollowNewPage requires browser mode for ${agentName}. Use BrowserAgent when one agent should follow newly opened pages.`,\n );\n }\n\n return {\n pageScope,\n forceSameTabNavigation: forceSameTabNavigation ?? true,\n autoFollowNewPage: false,\n newPageTimeout,\n };\n }\n\n if (typeof forceSameTabNavigation !== 'undefined') {\n throw new Error(\n `[midscene] forceSameTabNavigation cannot be used in browser mode for ${agentName}. Use PageAgent when same-tab navigation is required.`,\n );\n }\n\n return {\n pageScope,\n forceSameTabNavigation: false,\n autoFollowNewPage: autoFollowNewPage ?? false,\n newPageTimeout,\n };\n}\n\nexport type BrowserAgentPageSummary = {\n index: number;\n active: boolean;\n title: string;\n url: string;\n};\n\nexport type BrowserAgentPageSelector = {\n index?: number;\n title?: string;\n url?: string;\n};\n\nconst normalizeOptionalText = (value: string | undefined) => {\n const trimmed = value?.trim();\n return trimmed ? trimmed.toLowerCase() : undefined;\n};\n\nconst pageSummaryMatches = (\n summary: BrowserAgentPageSummary,\n title: string | undefined,\n url: string | undefined,\n) =>\n (!title || summary.title.toLowerCase().includes(title)) &&\n (!url || summary.url.toLowerCase().includes(url));\n\nconst describeSelector = (selector: BrowserAgentPageSelector) => {\n const parts: string[] = [];\n if (selector.index !== undefined) {\n parts.push(`index ${selector.index}`);\n }\n if (selector.title?.trim()) {\n parts.push(`title \"${selector.title.trim()}\"`);\n }\n if (selector.url?.trim()) {\n parts.push(`url \"${selector.url.trim()}\"`);\n }\n return parts.join(', ');\n};\n\nexport class BrowserPageManager<Page, NewPageEvent> {\n private readonly agentName: string;\n private readonly adapter: BrowserAgentAdapter<Page, NewPageEvent>;\n private readonly getActivePageValue: () => Page;\n private readonly setActivePageValue: (page: Page) => void;\n private readonly newPageTimeout: number;\n private readonly debug: DebugFunction;\n\n private readonly newPageHandler = (event: NewPageEvent) => {\n void this.followNewPage(event);\n };\n\n constructor(options: BrowserPageManagerOptions<Page, NewPageEvent>) {\n this.agentName = options.agentName;\n this.adapter = options.adapter;\n this.getActivePageValue = options.getActivePage;\n this.setActivePageValue = options.setActivePageValue;\n this.newPageTimeout = options.newPageTimeout;\n this.debug = options.debug;\n\n if (options.autoFollowNewPage) {\n this.adapter.onNewPage(this.newPageHandler);\n }\n }\n\n get activePage() {\n return this.getActivePageValue();\n }\n\n pages() {\n return this.adapter.pages();\n }\n\n async pageSummaries(): Promise<BrowserAgentPageSummary[]> {\n const pages = await this.openPages();\n const activePage = this.activePage;\n\n const summaries = await Promise.all(\n pages.map((page, index) =>\n this.tryPageSummary(page, index, page === activePage),\n ),\n );\n return summaries\n .filter(\n (summary): summary is BrowserAgentPageSummary => summary !== undefined,\n )\n .map((summary, index) => ({ ...summary, index }));\n }\n\n async pageSummaryByIndex(index: number): Promise<BrowserAgentPageSummary> {\n const pages = await this.openPages();\n const page = pages[index];\n if (!page || this.adapter.isPageClosed(page)) {\n throw new Error(\n `[midscene] Cannot find ${this.agentName} page with index ${index}. Available page indexes: ${pages\n .map((_, pageIndex) => pageIndex)\n .join(', ')}`,\n );\n }\n\n const summary = await this.tryPageSummary(\n page,\n index,\n page === this.activePage,\n );\n if (!summary) {\n throw new Error(\n `[midscene] ${this.agentName} page at index ${index} closed while reading its metadata. Run ListBrowserPages again.`,\n );\n }\n return summary;\n }\n\n async newPage() {\n const page = await this.adapter.newPage();\n await this.setActivePage(page);\n return page;\n }\n\n async setActivePage(page: Page) {\n if (!page || this.adapter.isPageClosed(page)) {\n throw new Error(\n `[midscene] Cannot set ${this.agentName} active page to a closed or invalid page.`,\n );\n }\n if (this.adapter.isPageAllowed?.(page) === false) {\n throw new Error(\n `[midscene] Cannot set ${this.agentName} active page to an out-of-scope page.`,\n );\n }\n\n this.setActivePageValue(page);\n try {\n await this.adapter.bringToFront(page);\n } catch (error) {\n this.debug(`failed to bring page to front: ${error}`);\n }\n }\n\n async setActivePageBySelector(\n selector: BrowserAgentPageSelector,\n ): Promise<BrowserAgentPageSummary> {\n const selectorIndex = selector.index;\n const title = normalizeOptionalText(selector.title);\n const url = normalizeOptionalText(selector.url);\n\n if (selectorIndex === undefined && !title && !url) {\n throw new Error(\n `[midscene] SetActivePage requires index, title, or url for ${this.agentName}.`,\n );\n }\n\n const pages = await this.openPages();\n\n if (selectorIndex !== undefined) {\n const page = pages[selectorIndex];\n if (!page || this.adapter.isPageClosed(page)) {\n throw new Error(\n `[midscene] Cannot find ${this.agentName} page with index ${selectorIndex}. Available page indexes: ${pages\n .map((_, index) => index)\n .join(', ')}`,\n );\n }\n\n const summary = await this.tryPageSummary(page, selectorIndex, true);\n if (!summary) {\n throw new Error(\n `[midscene] ${this.agentName} page at index ${selectorIndex} closed while reading its metadata. Run ListBrowserPages again.`,\n );\n }\n if (!pageSummaryMatches(summary, title, url)) {\n const textSelector = describeSelector({\n title: selector.title,\n url: selector.url,\n });\n throw new Error(\n `[midscene] ${this.agentName} page at index ${selectorIndex} does not match ${textSelector}. Run ListBrowserPages again before selecting a page.`,\n );\n }\n\n await this.setActivePage(page);\n return summary;\n }\n\n const matchedPages: Array<{\n page: Page;\n summary: BrowserAgentPageSummary;\n }> = [];\n for (let index = 0; index < pages.length; index++) {\n const page = pages[index];\n if (this.adapter.isPageClosed(page)) {\n continue;\n }\n\n const summary = await this.tryPageSummary(page, index, false);\n if (summary && pageSummaryMatches(summary, title, url)) {\n matchedPages.push({ page, summary });\n }\n }\n\n if (matchedPages.length === 0) {\n throw new Error(\n `[midscene] Cannot find ${this.agentName} page matching ${describeSelector(selector)}.`,\n );\n }\n\n if (matchedPages.length > 1) {\n throw new Error(\n `[midscene] Multiple ${this.agentName} pages matched ${describeSelector(selector)}. Use ListBrowserPages and pass an index to SetActivePage.`,\n );\n }\n\n const { page, summary } = matchedPages[0];\n await this.setActivePage(page);\n return { ...summary, active: true };\n }\n\n async waitForNewPage(\n action?: () => Promise<unknown> | unknown,\n opts?: { timeout?: number },\n ) {\n const waiter = this.createNewPageWaiter(opts?.timeout);\n\n try {\n await action?.();\n return await waiter.promise;\n } catch (error) {\n waiter.dispose();\n throw error;\n }\n }\n\n destroy() {\n this.adapter.offNewPage(this.newPageHandler);\n }\n\n private async openPages(): Promise<Page[]> {\n const pages = await this.adapter.pages();\n return pages.filter((page) => !this.adapter.isPageClosed(page));\n }\n\n private async tryPageSummary(\n page: Page,\n index: number,\n active: boolean,\n ): Promise<BrowserAgentPageSummary | undefined> {\n try {\n const summary = await this.pageSummary(page, index, active);\n return this.adapter.isPageClosed(page) ? undefined : summary;\n } catch (error) {\n if (this.adapter.isPageClosed(page)) {\n return undefined;\n }\n throw error;\n }\n }\n\n private async pageSummary(\n page: Page,\n index: number,\n active: boolean,\n ): Promise<BrowserAgentPageSummary> {\n return {\n index,\n active,\n title: await this.adapter.pageTitle(page),\n url: this.adapter.pageUrl(page),\n };\n }\n\n private async followNewPage(event: NewPageEvent) {\n if (!this.isNewPageEvent(event)) {\n return;\n }\n\n try {\n const page = await this.adapter.resolveNewPage(event);\n if (page) {\n await this.setActivePage(page);\n }\n } catch (error) {\n this.debug(`failed to follow new page: ${error}`);\n }\n }\n\n private isNewPageEvent(event: NewPageEvent) {\n return this.adapter.isNewPageEvent?.(event) ?? true;\n }\n\n private createNewPageWaiter(timeout = this.newPageTimeout) {\n let settled = false;\n\n const dispose = () => {\n this.adapter.offNewPage(handler);\n clearTimeout(timer);\n };\n\n const handler = async (event: NewPageEvent) => {\n if (settled || !this.isNewPageEvent(event)) {\n return;\n }\n\n settled = true;\n dispose();\n\n try {\n const page = await this.adapter.resolveNewPage(event);\n if (!page) {\n throw new Error('new target did not resolve to a page');\n }\n resolvePage(page);\n } catch (error) {\n rejectPage(error);\n }\n };\n\n let resolvePage!: (page: Page) => void;\n let rejectPage!: (error: unknown) => void;\n const promise = new Promise<Page>((resolve, reject) => {\n resolvePage = resolve;\n rejectPage = reject;\n });\n\n const timer = setTimeout(() => {\n if (settled) {\n return;\n }\n settled = true;\n dispose();\n rejectPage(\n new Error(\n `[midscene] Timed out waiting for a new ${this.agentName} page after ${timeout}ms.`,\n ),\n );\n }, timeout);\n\n this.adapter.onNewPage(handler);\n\n return { promise, dispose };\n }\n}\n"],"names":["WebAgentCore","CoreAgent","error","isRetryableBrowserNavigationError","DEFAULT_NEW_PAGE_TIMEOUT","resolveBrowserAgentRuntimeOptions","agentName","pageScope","forceSameTabNavigation","autoFollowNewPage","newPageTimeout","Error","normalizeOptionalText","value","trimmed","undefined","pageSummaryMatches","summary","title","url","describeSelector","selector","parts","BrowserPageManager","pages","activePage","summaries","Promise","page","index","_","pageIndex","selectorIndex","textSelector","matchedPages","action","opts","waiter","active","event","timeout","settled","dispose","handler","clearTimeout","timer","resolvePage","rejectPage","promise","resolve","reject","setTimeout","options"],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ import { z } from "@midscene/core";
2
+ const pageListSize = 8;
3
+ const maxListTitleLength = 120;
4
+ const maxListUrlLength = 512;
5
+ const normalizeFeedbackValue = (value)=>value.replace(/\s+/g, ' ').trim();
6
+ const truncateFeedbackValue = (value, maxLength)=>{
7
+ if (maxLength <= 0) return '';
8
+ if (value.length <= maxLength) return value;
9
+ if (maxLength <= 2) return `${value.slice(0, maxLength - 1)}…`;
10
+ const headLength = Math.ceil((maxLength - 1) * 2 / 3);
11
+ const tailLength = maxLength - headLength - 1;
12
+ return `${value.slice(0, headLength)}…${value.slice(-tailLength)}`;
13
+ };
14
+ const buildPlanningFeedback = (summaries, total, offset, activeIndex)=>{
15
+ if (0 === total) return 'ListBrowserPages: no open pages.';
16
+ const nextOffset = offset + summaries.length;
17
+ const continuation = nextOffset < total ? ` Next: ListBrowserPages({offset:${nextOffset}}).` : '';
18
+ const header = `ListBrowserPages ${offset}-${nextOffset - 1} of ${total}; active ${activeIndex ?? 'none'} (0-based). Use SetActivePage.${continuation}\n`;
19
+ if (0 === summaries.length) return header.trimEnd();
20
+ const lines = summaries.map((summary)=>{
21
+ const prefix = `${summary.active ? '*' : ' '}${summary.index}|`;
22
+ const title = truncateFeedbackValue(normalizeFeedbackValue(summary.title), maxListTitleLength);
23
+ const url = truncateFeedbackValue(normalizeFeedbackValue(summary.url), maxListUrlLength);
24
+ return `${prefix}${title}|${url}`;
25
+ });
26
+ return `${header}${lines.join('\n')}`;
27
+ };
28
+ const buildPageInfoPlanningFeedback = (summary)=>`GetBrowserPageInfo index ${summary.index}; active ${summary.active}.\nTitle: ${summary.title}\nURL: ${summary.url}`;
29
+ const setActivePageParamSchema = z.object({
30
+ index: z.number().int().min(0).optional().describe('0-based page/tab index returned by ListBrowserPages.'),
31
+ title: z.string().optional().describe('Case-insensitive page title substring to match.'),
32
+ url: z.string().optional().describe('Case-insensitive page URL substring to match.')
33
+ });
34
+ const listBrowserPagesParamSchema = z.object({
35
+ offset: z.number().int().min(0).optional().describe('0-based starting position. Omit for the first page.')
36
+ }).optional();
37
+ const getBrowserPageInfoParamSchema = z.object({
38
+ index: z.number().int().min(0).describe('0-based page/tab index returned by ListBrowserPages.')
39
+ });
40
+ const createBrowserAgentPageActions = (options)=>[
41
+ {
42
+ name: 'ListBrowserPages',
43
+ description: 'List open browser pages/tabs in groups of 8 and show which one is active. Use offset to see the next group before switching pages.',
44
+ paramSchema: listBrowserPagesParamSchema,
45
+ planningFeedbackMaxLength: 'unlimited',
46
+ call: async (param, context)=>{
47
+ const summaries = await options.getPageManager().pageSummaries();
48
+ const offset = param?.offset ?? 0;
49
+ if (summaries.length > 0 && offset >= summaries.length) throw new Error(`[midscene] ListBrowserPages offset ${offset} is out of range for ${summaries.length} pages. Start again with offset 0.`);
50
+ const visibleSummaries = summaries.slice(offset, offset + pageListSize);
51
+ if (context?.task) context.task.planningFeedback = buildPlanningFeedback(visibleSummaries, summaries.length, offset, summaries.find(({ active })=>active)?.index);
52
+ return visibleSummaries;
53
+ }
54
+ },
55
+ {
56
+ name: 'GetBrowserPageInfo',
57
+ description: 'Get the complete title and URL for one browser page/tab without switching the active page. Use this when ListBrowserPages truncates details needed to identify a page.',
58
+ paramSchema: getBrowserPageInfoParamSchema,
59
+ planningFeedbackMaxLength: 'unlimited',
60
+ sample: {
61
+ index: 1
62
+ },
63
+ call: async (param, context)=>{
64
+ const summary = await options.getPageManager().pageSummaryByIndex(param.index);
65
+ if (context?.task) context.task.planningFeedback = buildPageInfoPlanningFeedback(summary);
66
+ return summary;
67
+ }
68
+ },
69
+ {
70
+ name: 'SetActivePage',
71
+ description: 'Set the active browser page/tab by 0-based index, title substring, or URL substring. When index is combined with title or URL, all provided values must match the same page. Use index from ListBrowserPages when more than one page could match.',
72
+ paramSchema: setActivePageParamSchema,
73
+ sample: {
74
+ index: 1
75
+ },
76
+ call: async (param)=>options.getPageManager().setActivePageBySelector(param)
77
+ }
78
+ ];
79
+ const appendBrowserAgentPageActions = (customActions, browserActions)=>{
80
+ if (!customActions?.length) return browserActions;
81
+ const customActionNames = new Set(customActions.map((action)=>action.name));
82
+ return [
83
+ ...customActions,
84
+ ...browserActions.filter((action)=>!customActionNames.has(action.name))
85
+ ];
86
+ };
87
+ export { appendBrowserAgentPageActions, createBrowserAgentPageActions };
88
+
89
+ //# sourceMappingURL=browser-page-actions.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common/browser-page-actions.mjs","sources":["../../../src/common/browser-page-actions.ts"],"sourcesContent":["import { type DeviceAction, z } from '@midscene/core';\nimport type {\n BrowserAgentPageSelector,\n BrowserAgentPageSummary,\n BrowserPageManager,\n} from './browser-agent';\n\nconst pageListSize = 8;\nconst maxListTitleLength = 120;\nconst maxListUrlLength = 512;\n\nconst normalizeFeedbackValue = (value: string) =>\n value.replace(/\\s+/g, ' ').trim();\n\nconst truncateFeedbackValue = (value: string, maxLength: number) => {\n if (maxLength <= 0) {\n return '';\n }\n if (value.length <= maxLength) {\n return value;\n }\n if (maxLength <= 2) {\n return `${value.slice(0, maxLength - 1)}…`;\n }\n const headLength = Math.ceil(((maxLength - 1) * 2) / 3);\n const tailLength = maxLength - headLength - 1;\n return `${value.slice(0, headLength)}…${value.slice(-tailLength)}`;\n};\n\nconst buildPlanningFeedback = (\n summaries: BrowserAgentPageSummary[],\n total: number,\n offset: number,\n activeIndex: number | undefined,\n) => {\n if (total === 0) {\n return 'ListBrowserPages: no open pages.';\n }\n\n const nextOffset = offset + summaries.length;\n const continuation =\n nextOffset < total\n ? ` Next: ListBrowserPages({offset:${nextOffset}}).`\n : '';\n const header = `ListBrowserPages ${offset}-${nextOffset - 1} of ${total}; active ${activeIndex ?? 'none'} (0-based). Use SetActivePage.${continuation}\\n`;\n if (summaries.length === 0) {\n return header.trimEnd();\n }\n const lines = summaries.map((summary) => {\n const prefix = `${summary.active ? '*' : ' '}${summary.index}|`;\n const title = truncateFeedbackValue(\n normalizeFeedbackValue(summary.title),\n maxListTitleLength,\n );\n const url = truncateFeedbackValue(\n normalizeFeedbackValue(summary.url),\n maxListUrlLength,\n );\n return `${prefix}${title}|${url}`;\n });\n\n return `${header}${lines.join('\\n')}`;\n};\n\nconst buildPageInfoPlanningFeedback = (summary: BrowserAgentPageSummary) =>\n `GetBrowserPageInfo index ${summary.index}; active ${summary.active}.\\nTitle: ${summary.title}\\nURL: ${summary.url}`;\n\nconst setActivePageParamSchema: z.ZodType<BrowserAgentPageSelector> = z.object({\n index: z\n .number()\n .int()\n .min(0)\n .optional()\n .describe('0-based page/tab index returned by ListBrowserPages.'),\n title: z\n .string()\n .optional()\n .describe('Case-insensitive page title substring to match.'),\n url: z\n .string()\n .optional()\n .describe('Case-insensitive page URL substring to match.'),\n});\n\nconst listBrowserPagesParamSchema = z\n .object({\n offset: z\n .number()\n .int()\n .min(0)\n .optional()\n .describe('0-based starting position. Omit for the first page.'),\n })\n .optional();\n\nconst getBrowserPageInfoParamSchema = z.object({\n index: z\n .number()\n .int()\n .min(0)\n .describe('0-based page/tab index returned by ListBrowserPages.'),\n});\n\nexport const createBrowserAgentPageActions = <Page, NewPageEvent>(options: {\n getPageManager: () => BrowserPageManager<Page, NewPageEvent>;\n}): DeviceAction<any>[] => [\n {\n name: 'ListBrowserPages',\n description:\n 'List open browser pages/tabs in groups of 8 and show which one is active. Use offset to see the next group before switching pages.',\n paramSchema: listBrowserPagesParamSchema,\n planningFeedbackMaxLength: 'unlimited',\n call: async (param, context) => {\n const summaries = await options.getPageManager().pageSummaries();\n const offset = param?.offset ?? 0;\n if (summaries.length > 0 && offset >= summaries.length) {\n throw new Error(\n `[midscene] ListBrowserPages offset ${offset} is out of range for ${summaries.length} pages. Start again with offset 0.`,\n );\n }\n const visibleSummaries = summaries.slice(offset, offset + pageListSize);\n if (context?.task) {\n context.task.planningFeedback = buildPlanningFeedback(\n visibleSummaries,\n summaries.length,\n offset,\n summaries.find(({ active }) => active)?.index,\n );\n }\n return visibleSummaries;\n },\n },\n {\n name: 'GetBrowserPageInfo',\n description:\n 'Get the complete title and URL for one browser page/tab without switching the active page. Use this when ListBrowserPages truncates details needed to identify a page.',\n paramSchema: getBrowserPageInfoParamSchema,\n planningFeedbackMaxLength: 'unlimited',\n sample: {\n index: 1,\n },\n call: async (param, context) => {\n const summary = await options\n .getPageManager()\n .pageSummaryByIndex(param.index);\n if (context?.task) {\n context.task.planningFeedback = buildPageInfoPlanningFeedback(summary);\n }\n return summary;\n },\n },\n {\n name: 'SetActivePage',\n description:\n 'Set the active browser page/tab by 0-based index, title substring, or URL substring. When index is combined with title or URL, all provided values must match the same page. Use index from ListBrowserPages when more than one page could match.',\n paramSchema: setActivePageParamSchema,\n sample: {\n index: 1,\n },\n call: async (param) =>\n options.getPageManager().setActivePageBySelector(param),\n },\n];\n\nexport const appendBrowserAgentPageActions = (\n customActions: DeviceAction<any>[] | undefined,\n browserActions: DeviceAction<any>[],\n) => {\n if (!customActions?.length) {\n return browserActions;\n }\n\n const customActionNames = new Set(customActions.map((action) => action.name));\n return [\n ...customActions,\n ...browserActions.filter((action) => !customActionNames.has(action.name)),\n ];\n};\n"],"names":["pageListSize","maxListTitleLength","maxListUrlLength","normalizeFeedbackValue","value","truncateFeedbackValue","maxLength","headLength","Math","tailLength","buildPlanningFeedback","summaries","total","offset","activeIndex","nextOffset","continuation","header","lines","summary","prefix","title","url","buildPageInfoPlanningFeedback","setActivePageParamSchema","z","listBrowserPagesParamSchema","getBrowserPageInfoParamSchema","createBrowserAgentPageActions","options","param","context","Error","visibleSummaries","active","appendBrowserAgentPageActions","customActions","browserActions","customActionNames","Set","action"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { BrowserPageManager, WebAgentCore, resolveBrowserAgentRuntimeOptions } from "../common/browser-agent.mjs";
2
2
  import { applyForceChromeSelectRendering } from "../common/browser-agent-utils.mjs";
3
+ import { appendBrowserAgentPageActions, createBrowserAgentPageActions } from "../common/browser-page-actions.mjs";
3
4
  import { getDebug } from "@midscene/shared/logger";
4
5
  import { WebPage } from "./page.mjs";
5
6
  import { playwrightAgentTestRunnerNodeDefinitions } from "./test-runner/agent-nodes.mjs";
@@ -19,6 +20,8 @@ const createPlaywrightBrowserAdapter = (context)=>({
19
20
  newPage: ()=>context.newPage(),
20
21
  isPageClosed: (page)=>page.isClosed(),
21
22
  bringToFront: (page)=>page.bringToFront(),
23
+ pageTitle: (page)=>page.title(),
24
+ pageUrl: (page)=>page.url(),
22
25
  onNewPage: (handler)=>context.on('page', handler),
23
26
  offNewPage: (handler)=>context.off('page', handler),
24
27
  resolveNewPage: (page)=>page
@@ -66,9 +69,13 @@ class PlaywrightBrowserAgent extends WebAgentCore {
66
69
  newPageTimeout
67
70
  });
68
71
  const { forceChromeSelectRendering } = agentOpts;
72
+ const browserActions = createBrowserAgentPageActions({
73
+ getPageManager: ()=>pageManager
74
+ });
69
75
  const webPage = new WebPage(initialPage, {
70
76
  ...agentOpts,
71
- forceSameTabNavigation: runtimeOptions.forceSameTabNavigation
77
+ forceSameTabNavigation: runtimeOptions.forceSameTabNavigation,
78
+ customActions: appendBrowserAgentPageActions(agentOpts.customActions, browserActions)
72
79
  });
73
80
  const pageManager = new BrowserPageManager({
74
81
  agentName: 'PlaywrightBrowserAgent',
@@ -1 +1 @@
1
- {"version":3,"file":"playwright/browser-agent.mjs","sources":["../../../src/playwright/browser-agent.ts"],"sourcesContent":["import {\n type BrowserAgentAdapter,\n BrowserPageManager,\n WebAgentCore,\n resolveBrowserAgentRuntimeOptions,\n} from '@/common/browser-agent';\nimport { applyForceChromeSelectRendering } from '@/common/browser-agent-utils';\nimport type { WebPageAgentOpt } from '@/web-element';\nimport type { AgentTestRunnerNodeDefinition } from '@midscene/core/agent';\nimport { getDebug } from '@midscene/shared/logger';\nimport type {\n BrowserContext as PlaywrightBrowserContext,\n Page as PlaywrightPage,\n} from 'playwright';\nimport { WebPage as PlaywrightWebPage } from './page';\nimport { playwrightAgentTestRunnerNodeDefinitions } from './test-runner/agent-nodes';\nimport type { PlaywrightTestRunnerOptions } from './test-runner/types';\n\nconst debug = getDebug('playwright:browser-agent');\n\nconst createPlaywrightBrowserAdapter = (\n context: PlaywrightBrowserContext,\n): BrowserAgentAdapter<PlaywrightPage, PlaywrightPage> => ({\n pages: () => context.pages(),\n newPage: () => context.newPage(),\n isPageClosed: (page) => page.isClosed(),\n bringToFront: (page) => page.bringToFront(),\n onNewPage: (handler) => context.on('page', handler),\n offNewPage: (handler) => context.off('page', handler),\n resolveNewPage: (page) => page,\n});\n\nexport type PlaywrightBrowserAgentOpt = Omit<\n WebPageAgentOpt,\n 'forceSameTabNavigation'\n> & {\n testRunner?: PlaywrightTestRunnerOptions;\n autoFollowNewPage?: boolean;\n newPageTimeout?: number;\n};\n\nexport type PlaywrightBrowserAgentCreateOpt = PlaywrightBrowserAgentOpt & {\n initialPage?: PlaywrightPage;\n};\n\nexport class PlaywrightBrowserAgent extends WebAgentCore<PlaywrightWebPage> {\n static override getTestRunnerNodeDefinitions(): readonly AgentTestRunnerNodeDefinition[] {\n return [\n ...WebAgentCore.getTestRunnerNodeDefinitions(),\n ...playwrightAgentTestRunnerNodeDefinitions,\n ];\n }\n\n readonly testRunner?: PlaywrightTestRunnerOptions;\n\n private readonly pageManager: BrowserPageManager<\n PlaywrightPage,\n PlaywrightPage\n >;\n\n constructor(\n context: PlaywrightBrowserContext,\n initialPage: PlaywrightPage,\n opts?: PlaywrightBrowserAgentOpt,\n ) {\n if (!context) {\n throw new Error(\n '[midscene] PlaywrightBrowserAgent requires a valid Playwright browser context.',\n );\n }\n if (!initialPage) {\n throw new Error(\n '[midscene] PlaywrightBrowserAgent requires a valid initial page instance.',\n );\n }\n\n const { autoFollowNewPage, newPageTimeout, ...agentOpts } = opts ?? {};\n const runtimeOptions = resolveBrowserAgentRuntimeOptions({\n agentName: 'PlaywrightBrowserAgent',\n pageScope: 'browser',\n forceSameTabNavigation: (opts as WebPageAgentOpt | undefined)\n ?.forceSameTabNavigation,\n autoFollowNewPage,\n newPageTimeout,\n });\n const { forceChromeSelectRendering } = agentOpts;\n const webPage = new PlaywrightWebPage(initialPage, {\n ...agentOpts,\n forceSameTabNavigation: runtimeOptions.forceSameTabNavigation,\n });\n const pageManager = new BrowserPageManager({\n agentName: 'PlaywrightBrowserAgent',\n adapter: createPlaywrightBrowserAdapter(context),\n getActivePage: () => webPage.underlyingPage as PlaywrightPage,\n setActivePageValue: (page) => {\n webPage.underlyingPage = page;\n },\n autoFollowNewPage: runtimeOptions.autoFollowNewPage,\n newPageTimeout: runtimeOptions.newPageTimeout,\n debug,\n });\n super(webPage, agentOpts);\n this.pageManager = pageManager;\n this.testRunner = opts?.testRunner;\n\n applyForceChromeSelectRendering(\n initialPage,\n 'playwright',\n forceChromeSelectRendering,\n );\n }\n\n static async create(\n context: PlaywrightBrowserContext,\n opts?: PlaywrightBrowserAgentCreateOpt,\n ) {\n const { initialPage, ...agentOpts } = opts ?? {};\n const page = initialPage ?? context.pages()[0] ?? (await context.newPage());\n\n return new PlaywrightBrowserAgent(context, page, agentOpts);\n }\n\n get activePage() {\n return this.pageManager.activePage;\n }\n\n pages() {\n return this.pageManager.pages();\n }\n\n async newPage() {\n return this.pageManager.newPage();\n }\n\n async setActivePage(page: PlaywrightPage) {\n await this.pageManager.setActivePage(page);\n }\n\n async waitForNewPage(\n action?: () => Promise<unknown> | unknown,\n opts?: { timeout?: number },\n ) {\n return this.pageManager.waitForNewPage(action, opts);\n }\n\n async destroy() {\n this.pageManager.destroy();\n await super.destroy();\n }\n}\n"],"names":["debug","getDebug","createPlaywrightBrowserAdapter","context","page","handler","PlaywrightBrowserAgent","WebAgentCore","playwrightAgentTestRunnerNodeDefinitions","opts","initialPage","agentOpts","action","Error","autoFollowNewPage","newPageTimeout","runtimeOptions","resolveBrowserAgentRuntimeOptions","forceChromeSelectRendering","webPage","PlaywrightWebPage","pageManager","BrowserPageManager","applyForceChromeSelectRendering"],"mappings":";;;;;;;;;;;;;;;AAkBA,MAAMA,QAAQC,SAAS;AAEvB,MAAMC,iCAAiC,CACrCC,UACyD;QACzD,OAAO,IAAMA,QAAQ,KAAK;QAC1B,SAAS,IAAMA,QAAQ,OAAO;QAC9B,cAAc,CAACC,OAASA,KAAK,QAAQ;QACrC,cAAc,CAACA,OAASA,KAAK,YAAY;QACzC,WAAW,CAACC,UAAYF,QAAQ,EAAE,CAAC,QAAQE;QAC3C,YAAY,CAACA,UAAYF,QAAQ,GAAG,CAAC,QAAQE;QAC7C,gBAAgB,CAACD,OAASA;IAC5B;AAeO,MAAME,+BAA+BC;IAC1C,OAAgB,+BAAyE;QACvF,OAAO;eACFA,aAAa,4BAA4B;eACzCC;SACJ;IACH;IA6DA,aAAa,OACXL,OAAiC,EACjCM,IAAsC,EACtC;QACA,MAAM,EAAEC,WAAW,EAAE,GAAGC,WAAW,GAAGF,QAAQ,CAAC;QAC/C,MAAML,OAAOM,eAAeP,QAAQ,KAAK,EAAE,CAAC,EAAE,IAAK,MAAMA,QAAQ,OAAO;QAExE,OAAO,IAAIG,uBAAuBH,SAASC,MAAMO;IACnD;IAEA,IAAI,aAAa;QACf,OAAO,IAAI,CAAC,WAAW,CAAC,UAAU;IACpC;IAEA,QAAQ;QACN,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK;IAC/B;IAEA,MAAM,UAAU;QACd,OAAO,IAAI,CAAC,WAAW,CAAC,OAAO;IACjC;IAEA,MAAM,cAAcP,IAAoB,EAAE;QACxC,MAAM,IAAI,CAAC,WAAW,CAAC,aAAa,CAACA;IACvC;IAEA,MAAM,eACJQ,MAAyC,EACzCH,IAA2B,EAC3B;QACA,OAAO,IAAI,CAAC,WAAW,CAAC,cAAc,CAACG,QAAQH;IACjD;IAEA,MAAM,UAAU;QACd,IAAI,CAAC,WAAW,CAAC,OAAO;QACxB,MAAM,KAAK,CAAC;IACd;IAxFA,YACEN,OAAiC,EACjCO,WAA2B,EAC3BD,IAAgC,CAChC;QACA,IAAI,CAACN,SACH,MAAM,IAAIU,MACR;QAGJ,IAAI,CAACH,aACH,MAAM,IAAIG,MACR;QAIJ,MAAM,EAAEC,iBAAiB,EAAEC,cAAc,EAAE,GAAGJ,WAAW,GAAGF,QAAQ,CAAC;QACrE,MAAMO,iBAAiBC,kCAAkC;YACvD,WAAW;YACX,WAAW;YACX,wBAAyBR,MACrB;YACJK;YACAC;QACF;QACA,MAAM,EAAEG,0BAA0B,EAAE,GAAGP;QACvC,MAAMQ,UAAU,IAAIC,QAAkBV,aAAa;YACjD,GAAGC,SAAS;YACZ,wBAAwBK,eAAe,sBAAsB;QAC/D;QACA,MAAMK,cAAc,IAAIC,mBAAmB;YACzC,WAAW;YACX,SAASpB,+BAA+BC;YACxC,eAAe,IAAMgB,QAAQ,cAAc;YAC3C,oBAAoB,CAACf;gBACnBe,QAAQ,cAAc,GAAGf;YAC3B;YACA,mBAAmBY,eAAe,iBAAiB;YACnD,gBAAgBA,eAAe,cAAc;YAC7ChB;QACF;QACA,KAAK,CAACmB,SAASR,YAhDjB,uBAAS,cAAT,SAEA,uBAAiB,eAAjB;QA+CE,IAAI,CAAC,WAAW,GAAGU;QACnB,IAAI,CAAC,UAAU,GAAGZ,MAAM;QAExBc,gCACEb,aACA,cACAQ;IAEJ;AAuCF"}
1
+ {"version":3,"file":"playwright/browser-agent.mjs","sources":["../../../src/playwright/browser-agent.ts"],"sourcesContent":["import {\n type BrowserAgentAdapter,\n BrowserPageManager,\n WebAgentCore,\n resolveBrowserAgentRuntimeOptions,\n} from '@/common/browser-agent';\nimport { applyForceChromeSelectRendering } from '@/common/browser-agent-utils';\nimport {\n appendBrowserAgentPageActions,\n createBrowserAgentPageActions,\n} from '@/common/browser-page-actions';\nimport type { WebPageAgentOpt } from '@/web-element';\nimport type { AgentTestRunnerNodeDefinition } from '@midscene/core/agent';\nimport { getDebug } from '@midscene/shared/logger';\nimport type {\n BrowserContext as PlaywrightBrowserContext,\n Page as PlaywrightPage,\n} from 'playwright';\nimport { WebPage as PlaywrightWebPage } from './page';\nimport { playwrightAgentTestRunnerNodeDefinitions } from './test-runner/agent-nodes';\nimport type { PlaywrightTestRunnerOptions } from './test-runner/types';\n\nconst debug = getDebug('playwright:browser-agent');\n\nconst createPlaywrightBrowserAdapter = (\n context: PlaywrightBrowserContext,\n): BrowserAgentAdapter<PlaywrightPage, PlaywrightPage> => ({\n pages: () => context.pages(),\n newPage: () => context.newPage(),\n isPageClosed: (page) => page.isClosed(),\n bringToFront: (page) => page.bringToFront(),\n pageTitle: (page) => page.title(),\n pageUrl: (page) => page.url(),\n onNewPage: (handler) => context.on('page', handler),\n offNewPage: (handler) => context.off('page', handler),\n resolveNewPage: (page) => page,\n});\n\nexport type PlaywrightBrowserAgentOpt = Omit<\n WebPageAgentOpt,\n 'forceSameTabNavigation'\n> & {\n testRunner?: PlaywrightTestRunnerOptions;\n autoFollowNewPage?: boolean;\n newPageTimeout?: number;\n};\n\nexport type PlaywrightBrowserAgentCreateOpt = PlaywrightBrowserAgentOpt & {\n initialPage?: PlaywrightPage;\n};\n\nexport class PlaywrightBrowserAgent extends WebAgentCore<PlaywrightWebPage> {\n static override getTestRunnerNodeDefinitions(): readonly AgentTestRunnerNodeDefinition[] {\n return [\n ...WebAgentCore.getTestRunnerNodeDefinitions(),\n ...playwrightAgentTestRunnerNodeDefinitions,\n ];\n }\n\n readonly testRunner?: PlaywrightTestRunnerOptions;\n\n private readonly pageManager: BrowserPageManager<\n PlaywrightPage,\n PlaywrightPage\n >;\n\n constructor(\n context: PlaywrightBrowserContext,\n initialPage: PlaywrightPage,\n opts?: PlaywrightBrowserAgentOpt,\n ) {\n if (!context) {\n throw new Error(\n '[midscene] PlaywrightBrowserAgent requires a valid Playwright browser context.',\n );\n }\n if (!initialPage) {\n throw new Error(\n '[midscene] PlaywrightBrowserAgent requires a valid initial page instance.',\n );\n }\n\n const { autoFollowNewPage, newPageTimeout, ...agentOpts } = opts ?? {};\n const runtimeOptions = resolveBrowserAgentRuntimeOptions({\n agentName: 'PlaywrightBrowserAgent',\n pageScope: 'browser',\n forceSameTabNavigation: (opts as WebPageAgentOpt | undefined)\n ?.forceSameTabNavigation,\n autoFollowNewPage,\n newPageTimeout,\n });\n const { forceChromeSelectRendering } = agentOpts;\n const browserActions = createBrowserAgentPageActions({\n getPageManager: () => pageManager,\n });\n const webPage: PlaywrightWebPage = new PlaywrightWebPage(initialPage, {\n ...agentOpts,\n forceSameTabNavigation: runtimeOptions.forceSameTabNavigation,\n customActions: appendBrowserAgentPageActions(\n agentOpts.customActions,\n browserActions,\n ),\n });\n const pageManager: BrowserPageManager<PlaywrightPage, PlaywrightPage> =\n new BrowserPageManager({\n agentName: 'PlaywrightBrowserAgent',\n adapter: createPlaywrightBrowserAdapter(context),\n getActivePage: () => webPage.underlyingPage as PlaywrightPage,\n setActivePageValue: (page) => {\n webPage.underlyingPage = page;\n },\n autoFollowNewPage: runtimeOptions.autoFollowNewPage,\n newPageTimeout: runtimeOptions.newPageTimeout,\n debug,\n });\n super(webPage, agentOpts);\n this.pageManager = pageManager;\n this.testRunner = opts?.testRunner;\n\n applyForceChromeSelectRendering(\n initialPage,\n 'playwright',\n forceChromeSelectRendering,\n );\n }\n\n static async create(\n context: PlaywrightBrowserContext,\n opts?: PlaywrightBrowserAgentCreateOpt,\n ) {\n const { initialPage, ...agentOpts } = opts ?? {};\n const page = initialPage ?? context.pages()[0] ?? (await context.newPage());\n\n return new PlaywrightBrowserAgent(context, page, agentOpts);\n }\n\n get activePage() {\n return this.pageManager.activePage;\n }\n\n pages() {\n return this.pageManager.pages();\n }\n\n async newPage() {\n return this.pageManager.newPage();\n }\n\n async setActivePage(page: PlaywrightPage) {\n await this.pageManager.setActivePage(page);\n }\n\n async waitForNewPage(\n action?: () => Promise<unknown> | unknown,\n opts?: { timeout?: number },\n ) {\n return this.pageManager.waitForNewPage(action, opts);\n }\n\n async destroy() {\n this.pageManager.destroy();\n await super.destroy();\n }\n}\n"],"names":["debug","getDebug","createPlaywrightBrowserAdapter","context","page","handler","PlaywrightBrowserAgent","WebAgentCore","playwrightAgentTestRunnerNodeDefinitions","opts","initialPage","agentOpts","action","Error","autoFollowNewPage","newPageTimeout","runtimeOptions","resolveBrowserAgentRuntimeOptions","forceChromeSelectRendering","browserActions","createBrowserAgentPageActions","pageManager","webPage","PlaywrightWebPage","appendBrowserAgentPageActions","BrowserPageManager","applyForceChromeSelectRendering"],"mappings":";;;;;;;;;;;;;;;;AAsBA,MAAMA,QAAQC,SAAS;AAEvB,MAAMC,iCAAiC,CACrCC,UACyD;QACzD,OAAO,IAAMA,QAAQ,KAAK;QAC1B,SAAS,IAAMA,QAAQ,OAAO;QAC9B,cAAc,CAACC,OAASA,KAAK,QAAQ;QACrC,cAAc,CAACA,OAASA,KAAK,YAAY;QACzC,WAAW,CAACA,OAASA,KAAK,KAAK;QAC/B,SAAS,CAACA,OAASA,KAAK,GAAG;QAC3B,WAAW,CAACC,UAAYF,QAAQ,EAAE,CAAC,QAAQE;QAC3C,YAAY,CAACA,UAAYF,QAAQ,GAAG,CAAC,QAAQE;QAC7C,gBAAgB,CAACD,OAASA;IAC5B;AAeO,MAAME,+BAA+BC;IAC1C,OAAgB,+BAAyE;QACvF,OAAO;eACFA,aAAa,4BAA4B;eACzCC;SACJ;IACH;IAqEA,aAAa,OACXL,OAAiC,EACjCM,IAAsC,EACtC;QACA,MAAM,EAAEC,WAAW,EAAE,GAAGC,WAAW,GAAGF,QAAQ,CAAC;QAC/C,MAAML,OAAOM,eAAeP,QAAQ,KAAK,EAAE,CAAC,EAAE,IAAK,MAAMA,QAAQ,OAAO;QAExE,OAAO,IAAIG,uBAAuBH,SAASC,MAAMO;IACnD;IAEA,IAAI,aAAa;QACf,OAAO,IAAI,CAAC,WAAW,CAAC,UAAU;IACpC;IAEA,QAAQ;QACN,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK;IAC/B;IAEA,MAAM,UAAU;QACd,OAAO,IAAI,CAAC,WAAW,CAAC,OAAO;IACjC;IAEA,MAAM,cAAcP,IAAoB,EAAE;QACxC,MAAM,IAAI,CAAC,WAAW,CAAC,aAAa,CAACA;IACvC;IAEA,MAAM,eACJQ,MAAyC,EACzCH,IAA2B,EAC3B;QACA,OAAO,IAAI,CAAC,WAAW,CAAC,cAAc,CAACG,QAAQH;IACjD;IAEA,MAAM,UAAU;QACd,IAAI,CAAC,WAAW,CAAC,OAAO;QACxB,MAAM,KAAK,CAAC;IACd;IAhGA,YACEN,OAAiC,EACjCO,WAA2B,EAC3BD,IAAgC,CAChC;QACA,IAAI,CAACN,SACH,MAAM,IAAIU,MACR;QAGJ,IAAI,CAACH,aACH,MAAM,IAAIG,MACR;QAIJ,MAAM,EAAEC,iBAAiB,EAAEC,cAAc,EAAE,GAAGJ,WAAW,GAAGF,QAAQ,CAAC;QACrE,MAAMO,iBAAiBC,kCAAkC;YACvD,WAAW;YACX,WAAW;YACX,wBAAyBR,MACrB;YACJK;YACAC;QACF;QACA,MAAM,EAAEG,0BAA0B,EAAE,GAAGP;QACvC,MAAMQ,iBAAiBC,8BAA8B;YACnD,gBAAgB,IAAMC;QACxB;QACA,MAAMC,UAA6B,IAAIC,QAAkBb,aAAa;YACpE,GAAGC,SAAS;YACZ,wBAAwBK,eAAe,sBAAsB;YAC7D,eAAeQ,8BACbb,UAAU,aAAa,EACvBQ;QAEJ;QACA,MAAME,cACJ,IAAII,mBAAmB;YACrB,WAAW;YACX,SAASvB,+BAA+BC;YACxC,eAAe,IAAMmB,QAAQ,cAAc;YAC3C,oBAAoB,CAAClB;gBACnBkB,QAAQ,cAAc,GAAGlB;YAC3B;YACA,mBAAmBY,eAAe,iBAAiB;YACnD,gBAAgBA,eAAe,cAAc;YAC7ChB;QACF;QACF,KAAK,CAACsB,SAASX,YAxDjB,uBAAS,cAAT,SAEA,uBAAiB,eAAjB;QAuDE,IAAI,CAAC,WAAW,GAAGU;QACnB,IAAI,CAAC,UAAU,GAAGZ,MAAM;QAExBiB,gCACEhB,aACA,cACAQ;IAEJ;AAuCF"}