🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@mastra/browser-viewer

Package Overview
Dependencies
Maintainers
9
Versions
91
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mastra/browser-viewer - npm Package Compare versions

Comparing version
0.1.1
to
0.1.2-alpha.0
+12
-0
CHANGELOG.md
# @mastra/browser-viewer
## 0.1.2-alpha.0
### Patch Changes
- dependencies updates: ([#17851](https://github.com/mastra-ai/mastra/pull/17851))
- Updated dependency [`playwright-core@^1.60.0` ↗︎](https://www.npmjs.com/package/playwright-core/v/1.60.0) (from `^1.52.0`, in `dependencies`)
- Added support for the current Browserbase browse CLI config while keeping browse-cli configs working. ([#17780](https://github.com/mastra-ai/mastra/pull/17780))
- Updated dependencies [[`575f815`](https://github.com/mastra-ai/mastra/commit/575f815c5c3567b71c0b83cbb7fa98c8253a9d9c), [`306909a`](https://github.com/mastra-ai/mastra/commit/306909a693de77d709b38706e2673c9547d24a28), [`5191af8`](https://github.com/mastra-ai/mastra/commit/5191af80c799eea25357c545fc05d91b3883531d), [`43bd3d4`](https://github.com/mastra-ai/mastra/commit/43bd3d421987463fdf35386a45199c49499ed069), [`e6fa79e`](https://github.com/mastra-ai/mastra/commit/e6fa79ec72a2ddffdd25e85270398951e9d552a4), [`904bcdf`](https://github.com/mastra-ai/mastra/commit/904bcdf7b8004aa7be823f9f70ca63580e47e470), [`7f5ee1d`](https://github.com/mastra-ai/mastra/commit/7f5ee1dca46daee8d2817f2ebe49e6335da81956), [`1e9aab5`](https://github.com/mastra-ai/mastra/commit/1e9aab50ff11e6e88fde4d7cbf512c44a9fe8d61), [`bf8eb6d`](https://github.com/mastra-ai/mastra/commit/bf8eb6d0ec213a403eb9265a594ad283c44ab3dc), [`493a328`](https://github.com/mastra-ai/mastra/commit/493a328f4346a1deeb9f1e2e44c8f2a3a4d7591b), [`029a414`](https://github.com/mastra-ai/mastra/commit/029a4141719793bd3e898a39eb5a0466a55f5f3a), [`b147b29`](https://github.com/mastra-ai/mastra/commit/b147b2907f0cd1aa812efe6d6e3f58d22e66fc88), [`d371ac1`](https://github.com/mastra-ai/mastra/commit/d371ac1d9820afaaf7cfdbc380a475946a994d8f), [`cf182b7`](https://github.com/mastra-ai/mastra/commit/cf182b7fb495767946d9840ef29f19cfa906f31f), [`a049c2a`](https://github.com/mastra-ai/mastra/commit/a049c2a9dfb41d0ee2e7a28874a88cd64fd5669f), [`b147b29`](https://github.com/mastra-ai/mastra/commit/b147b2907f0cd1aa812efe6d6e3f58d22e66fc88), [`2a96528`](https://github.com/mastra-ai/mastra/commit/2a9652848dfa3c5a2426f952e9d93554c26fd90f), [`2656d9c`](https://github.com/mastra-ai/mastra/commit/2656d9c2976d4f3354253bfbbbf9b88a1b2bbf34), [`63e3fe1`](https://github.com/mastra-ai/mastra/commit/63e3fe13cc1ea96f91d7c68aea92f400faf9e4da), [`1d4ce8d`](https://github.com/mastra-ai/mastra/commit/1d4ce8daaa54511f325c1b609d31b8e54009d677), [`8c68372`](https://github.com/mastra-ai/mastra/commit/8c68372e85fe0b066ec12c58bd29ffb93e54c552)]:
- @mastra/core@1.42.0-alpha.4
## 0.1.1

@@ -4,0 +16,0 @@

+1
-1

@@ -1,1 +0,1 @@

{"version":3,"sources":["../src/index.ts","../src/browser-viewer.ts","../src/thread-manager.ts"],"sourcesContent":["/**\n * @mastra/browser-viewer\n *\n * Browser viewer for Mastra workspaces with CLI provider support.\n * Launches Chrome via Playwright and exposes CDP URL for CLI tools.\n */\n\nexport { BrowserViewer } from './browser-viewer';\nexport { BrowserViewerThreadManager } from './thread-manager';\nexport type { BrowserViewerConfig, CLIProvider } from './types';\nexport type { BrowserViewerThreadManagerConfig } from './thread-manager';\n","/**\n * BrowserViewer - Playwright-managed Chrome for CLI providers\n *\n * Launches Chrome via Playwright and exposes the CDP URL for CLI tools\n * (agent-browser, browser-use, browse-cli) to connect as secondary clients.\n *\n * This gives us:\n * - Direct page-level CDP sessions (fixes screencast sessionId issues)\n * - Full browser lifecycle control\n * - Predictable CDP URL for CLI injection\n * - Thread-scoped browser isolation\n */\n\nimport { MastraBrowser, ScreencastStreamImpl } from '@mastra/core/browser';\nimport type {\n BrowserState,\n BrowserTabState,\n ScreencastOptions,\n ScreencastStream,\n CdpSessionProvider,\n MouseEventParams,\n KeyboardEventParams,\n} from '@mastra/core/browser';\nimport type { Tool } from '@mastra/core/tools';\nimport type { Page } from 'playwright-core';\nimport { BrowserViewerThreadManager } from './thread-manager';\nimport type { BrowserViewerConfig, CLIProvider } from './types';\n\n/**\n * BrowserViewer - CLI provider with Playwright-managed Chrome\n *\n * Use this with Workspace to enable browser automation via CLI tools.\n * The agent uses skills + workspace_execute_command to drive the CLI,\n * while Mastra handles screencast, input injection, and lifecycle.\n *\n * @example\n * ```ts\n * import { Workspace } from '@mastra/core';\n * import { BrowserViewer } from '@mastra/browser-viewer';\n *\n * const workspace = new Workspace({\n * browser: new BrowserViewer({\n * cli: 'agent-browser',\n * headless: false,\n * }),\n * });\n * ```\n */\nexport class BrowserViewer extends MastraBrowser {\n override readonly id: string;\n override readonly name = 'BrowserViewer';\n override readonly provider = 'browser-viewer';\n readonly providerType = 'cli' as const;\n\n /** Which CLI the agent uses */\n readonly cli: CLIProvider;\n\n /** Viewer-specific config (stored for reference) */\n readonly viewerConfig: BrowserViewerConfig;\n\n /** Thread manager for browser sessions */\n declare protected threadManager: BrowserViewerThreadManager;\n\n constructor(config: BrowserViewerConfig) {\n // Default to 'thread' scope (each thread gets its own Chrome)\n // Use 'shared' if connecting to an existing browser\n const effectiveScope = config.cdpUrl ? (config.scope ?? 'shared') : (config.scope ?? 'thread');\n\n // Build base config (exclude CLI-specific options)\n // Use type assertion because BrowserConfig is a discriminated union\n const { cli: _cli, cdpPort: _cdpPort, ...baseConfig } = config;\n\n super({\n ...baseConfig,\n scope: effectiveScope,\n } as any);\n\n this.id = `browser-viewer-${Date.now()}`;\n this.cli = config.cli;\n this.viewerConfig = config;\n\n // Initialize thread manager\n this.threadManager = new BrowserViewerThreadManager({\n scope: effectiveScope,\n browserConfig: { ...config, headless: this.headless },\n logger: this.logger,\n onSessionCreated: session => {\n // Notify listeners so screencast can start for this thread\n this.notifyBrowserReady(session.threadId);\n },\n onBrowserCreated: (_browser, threadId, _cdpUrl) => {\n this.logger?.debug?.(`Browser created for thread ${threadId}`);\n },\n onBrowserClosed: threadId => {\n this.logger?.debug?.(`Browser closed for thread ${threadId}`);\n // Notify base class callbacks so ViewerRegistry gets notified\n this.notifyBrowserClosed(threadId);\n },\n });\n }\n\n // ---------------------------------------------------------------------------\n // CDP URL Access\n // ---------------------------------------------------------------------------\n\n /**\n * Get the CDP WebSocket URL for CLI tools to connect.\n * For thread scope, returns the CDP URL for the specified thread.\n * For shared scope, returns the single shared CDP URL.\n *\n * @param threadId - Thread identifier (optional, uses current thread if not specified)\n * @returns CDP URL or null if browser not running for that thread\n */\n override getCdpUrl(threadId?: string): string | null {\n return this.threadManager.getCdpUrlForThread(threadId ?? this.getCurrentThread());\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle (implements MastraBrowser abstract methods)\n // ---------------------------------------------------------------------------\n\n protected override async doLaunch(): Promise<void> {\n const scope = this.threadManager.getScope();\n const cdpUrl = this.config.cdpUrl;\n\n if (cdpUrl) {\n // Connect mode: connect to existing browser (always shared)\n const url = typeof cdpUrl === 'function' ? await cdpUrl() : cdpUrl;\n await this.connectToExisting(url);\n } else if (scope === 'shared') {\n // Shared mode: launch single browser\n await this.threadManager.createSharedSession();\n }\n // For thread scope, browsers are launched lazily per thread via ensureReady()\n }\n\n protected override async doClose(): Promise<void> {\n await this.threadManager.closeAll();\n }\n\n /**\n * Connect to an existing browser via CDP URL.\n */\n private async connectToExisting(cdpUrl: string): Promise<void> {\n this.logger?.debug?.(`Connecting to existing browser at ${cdpUrl}`);\n\n // Create a shared session from the external CDP connection\n await this.threadManager.createSharedSessionFromCdp(cdpUrl);\n\n this.logger?.debug?.('Connected to existing browser');\n }\n\n /**\n * Ensure browser is ready for the current thread.\n * For thread scope, creates a new browser if needed.\n */\n override async ensureReady(): Promise<void> {\n const scope = this.threadManager.getScope();\n const threadId = this.getCurrentThread();\n\n // For thread scope, create browser for this thread if needed\n if (scope === 'thread' && !this.threadManager.isBrowserRunning(threadId)) {\n await this.threadManager.getManagerForThread(threadId);\n }\n\n await super.ensureReady();\n }\n\n /**\n * Check if browser is running (for current thread in thread scope).\n */\n override isBrowserRunning(threadId?: string): boolean {\n return this.threadManager.isBrowserRunning(threadId ?? this.getCurrentThread());\n }\n\n /**\n * Launch browser, optionally for a specific thread.\n * For thread scope, creates a browser for that thread.\n * For shared scope, launches the single shared browser.\n */\n override async launch(threadId?: string): Promise<void> {\n const scope = this.threadManager.getScope();\n const effectiveThreadId = threadId ?? this.getCurrentThread();\n\n if (scope === 'shared') {\n // For shared scope, use base class launch (handles racing, status, etc.)\n if (!this.threadManager.isBrowserRunning()) {\n await super.launch();\n }\n } else {\n // For thread scope, launch for this specific thread\n if (!this.threadManager.isBrowserRunning(effectiveThreadId)) {\n await this.threadManager.getManagerForThread(effectiveThreadId);\n // Set status to ready so isBrowserRunning() returns true\n // (base class launch() does this, but we bypass it for thread scope)\n this.status = 'ready';\n }\n }\n }\n\n /**\n * Handle browser disconnection.\n * Overrides base class method.\n */\n override handleBrowserDisconnected(): void {\n // Call parent to handle status and notifications\n super.handleBrowserDisconnected();\n }\n\n /**\n * Connect to an external browser via CDP URL for screencast.\n *\n * Use this when an agent is using their own external CDP (e.g., browser-use cloud).\n * Connects Playwright to the external browser to enable screencast without launching\n * our own browser.\n *\n * @param cdpUrl - The external CDP WebSocket URL (wss://... or ws://...)\n * @param threadId - Thread ID to associate the session with\n */\n override async connectToExternalCdp(cdpUrl: string, threadId?: string): Promise<void> {\n const effectiveThreadId = threadId ?? this.getCurrentThread();\n await this.threadManager.connectToExternalCdp(cdpUrl, effectiveThreadId);\n // Mark as ready\n this.status = 'ready';\n }\n\n // ---------------------------------------------------------------------------\n // Browser State (implements MastraBrowser abstract methods)\n // ---------------------------------------------------------------------------\n\n protected override async getActivePage(threadId?: string): Promise<Page | null> {\n return this.threadManager.getActivePageForThread(threadId ?? this.getCurrentThread());\n }\n\n protected override getBrowserStateForThread(threadId?: string): BrowserState | null {\n const context = this.threadManager.getContextForThread(threadId ?? this.getCurrentThread());\n if (!context) {\n return null;\n }\n\n const pages = context.pages();\n // Active page is the last one (most recently opened), consistent with resolveActivePage\n const activeIndex = pages.length > 0 ? pages.length - 1 : 0;\n const tabs: BrowserTabState[] = pages.map((page, index) => ({\n url: page.url(),\n title: '', // Would need async call to get title\n isActive: index === activeIndex,\n }));\n\n return {\n tabs,\n activeTabIndex: activeIndex,\n };\n }\n\n // ---------------------------------------------------------------------------\n // Screencast Support\n // ---------------------------------------------------------------------------\n\n override async startScreencast(options?: ScreencastOptions): Promise<ScreencastStream> {\n const threadId = options?.threadId ?? this.getCurrentThread();\n\n // Create CDP session provider that creates FRESH sessions on each call\n // This is critical for tab switching - when reconnecting, we need a CDP session\n // attached to the CURRENT page, not the original page from launch\n const provider: CdpSessionProvider = {\n getCdpSession: async () => {\n const cdpSession = await this.threadManager.createFreshCdpSession(threadId);\n if (!cdpSession) {\n throw new Error('No browser context available for screencast');\n }\n\n // Return wrapper that implements CdpSessionLike\n return {\n send: async (method: string, params?: Record<string, unknown>) => {\n return cdpSession.send(method as any, params);\n },\n on: (event: string, handler: (params: unknown) => void) => {\n cdpSession.on(event as any, handler);\n },\n off: (event: string, handler: (params: unknown) => void) => {\n cdpSession.off(event as any, handler);\n },\n };\n },\n isBrowserRunning: () => this.isBrowserRunning(threadId),\n };\n\n // Create and start screencast stream\n const stream = new ScreencastStreamImpl(provider, {\n format: options?.format ?? 'jpeg',\n quality: options?.quality ?? 80,\n maxWidth: options?.maxWidth ?? 1280,\n maxHeight: options?.maxHeight ?? 720,\n everyNthFrame: options?.everyNthFrame ?? 1,\n });\n\n // Set up tab change detection - reconnect screencast when tabs change\n const context = this.threadManager.getContextForThread(threadId);\n if (context) {\n // Track all listeners for cleanup\n const pageListeners = new Map<Page, (frame: { url: () => string; parentFrame: () => unknown }) => void>();\n\n // New tab opened - reconnect screencast\n const onNewPage = () => {\n setTimeout(() => {\n if (stream.isActive()) {\n stream.reconnect().catch(() => {});\n }\n }, 100);\n };\n\n // Handler for new pages that sets up listeners\n const onPageCreated = (page: Page) => {\n setupPageListeners(page);\n };\n\n // Set up page close listener for each page\n const setupPageListeners = (page: Page) => {\n page.once('close', () => {\n // Clean up this page's listener\n pageListeners.delete(page);\n setTimeout(() => {\n if (stream.isActive() && context.pages().length > 0) {\n stream.reconnect().catch(() => {});\n }\n }, 100);\n });\n\n // Navigation listener for URL updates\n const onFrameNavigated = (frame: { url: () => string; parentFrame: () => unknown }) => {\n if (!frame.parentFrame()) {\n stream.emitUrl(frame.url());\n }\n };\n page.on('framenavigated', onFrameNavigated);\n pageListeners.set(page, onFrameNavigated);\n };\n\n // Set up listeners\n context.on('page', onNewPage);\n context.on('page', onPageCreated);\n\n // Set up for existing pages\n for (const page of context.pages()) {\n setupPageListeners(page);\n }\n\n // Clean up all listeners on stream stop\n stream.once('stop', () => {\n context.off('page', onNewPage);\n context.off('page', onPageCreated);\n // Remove framenavigated listeners from all pages\n for (const [page, listener] of pageListeners) {\n page.off('framenavigated', listener);\n }\n pageListeners.clear();\n });\n }\n\n await stream.start();\n return stream;\n }\n\n // ---------------------------------------------------------------------------\n // Input Injection\n // ---------------------------------------------------------------------------\n\n override async injectMouseEvent(params: MouseEventParams, threadId?: string): Promise<void> {\n const cdpSession = await this.threadManager.getCdpSessionForThread(threadId ?? this.getCurrentThread());\n if (!cdpSession) {\n throw new Error('CDP session not available for mouse injection');\n }\n\n await cdpSession.send('Input.dispatchMouseEvent', params);\n }\n\n override async injectKeyboardEvent(params: KeyboardEventParams, threadId?: string): Promise<void> {\n const cdpSession = await this.threadManager.getCdpSessionForThread(threadId ?? this.getCurrentThread());\n if (!cdpSession) {\n throw new Error('CDP session not available for keyboard injection');\n }\n\n await cdpSession.send('Input.dispatchKeyEvent', params);\n }\n\n // ---------------------------------------------------------------------------\n // Tools (CLI agents don't use SDK tools - they use workspace commands)\n // ---------------------------------------------------------------------------\n\n getTools(): Record<string, Tool> {\n // CLI agents use workspace_execute_command with CLI skills\n // No SDK tools needed\n return {};\n }\n}\n","/**\n * BrowserViewerThreadManager - Thread scope management for BrowserViewer\n *\n * Manages thread-scoped browser sessions using Playwright to launch\n * separate Chrome instances per thread.\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { ThreadManager, DEFAULT_THREAD_ID } from '@mastra/core/browser';\nimport type { ThreadSession, ThreadManagerConfig } from '@mastra/core/browser';\nimport { chromium } from 'playwright-core';\nimport type { Browser, BrowserContext, BrowserServer, CDPSession, Page } from 'playwright-core';\nimport type { BrowserViewerConfig } from './types';\n\n/**\n * Extended session info for BrowserViewer.\n */\ninterface BrowserViewerSession extends ThreadSession {\n /**\n * Playwright browser server (owns the Chrome process).\n * Null for external CDP connections where we don't own the browser process.\n */\n browserServer: BrowserServer | null;\n /** Playwright browser instance (connected to server) */\n browser: Browser;\n /** Browser context */\n context: BrowserContext;\n /** CDP session for the active page */\n cdpSession: CDPSession | null;\n /** CDP WebSocket URL (null if discovery failed) */\n cdpUrl: string | null;\n}\n\n/**\n * Configuration for BrowserViewerThreadManager.\n */\nexport interface BrowserViewerThreadManagerConfig extends ThreadManagerConfig {\n /** Browser configuration */\n browserConfig: BrowserViewerConfig;\n /** Callback when a browser is created for a thread */\n onBrowserCreated?: (browser: Browser, threadId: string, cdpUrl: string | null) => void;\n /** Callback when a browser is closed for a thread */\n onBrowserClosed?: (threadId: string) => void;\n}\n\n/**\n * Thread manager implementation for BrowserViewer.\n *\n * Supports two scope modes:\n * - 'shared': All threads share one Chrome instance\n * - 'thread': Each thread gets a dedicated Chrome instance\n */\nexport class BrowserViewerThreadManager extends ThreadManager<Browser> {\n private readonly browserConfig: BrowserViewerConfig;\n private readonly onBrowserCreated?: (browser: Browser, threadId: string, cdpUrl: string | null) => void;\n private readonly onBrowserClosed?: (threadId: string) => void;\n\n /** Map of thread ID to session info (for 'thread' scope) */\n private readonly threadSessions = new Map<string, BrowserViewerSession>();\n\n /** Shared session info (for 'shared' scope) */\n private sharedSession: BrowserViewerSession | null = null;\n\n /** Cached CDP sessions for input injection, keyed by threadId */\n private inputCdpSessions = new Map<string, { session: CDPSession; pageUrl: string }>();\n\n constructor(config: BrowserViewerThreadManagerConfig) {\n super(config);\n this.browserConfig = config.browserConfig;\n this.onBrowserCreated = config.onBrowserCreated;\n this.onBrowserClosed = config.onBrowserClosed;\n }\n\n /**\n * Check if a thread should use the shared session slot.\n * In shared scope, all threads use the shared session.\n * In thread scope, DEFAULT_THREAD_ID also uses the shared session.\n */\n private usesSharedSlot(threadId: string): boolean {\n return this.scope === 'shared' || threadId === DEFAULT_THREAD_ID;\n }\n\n /**\n * Get the viewer session for a thread, using consistent routing.\n * Handles both shared and thread-scoped sessions.\n */\n private getViewerSession(threadId: string): BrowserViewerSession | null {\n if (this.usesSharedSlot(threadId)) {\n return this.sharedSession;\n }\n return this.threadSessions.get(threadId) ?? null;\n }\n\n // ---------------------------------------------------------------------------\n // Session Storage & Cleanup Helpers\n // ---------------------------------------------------------------------------\n\n /**\n * Store a session in the appropriate slot based on scope.\n * Consolidates session storage logic used by createSession, createSharedSession,\n * createSharedSessionFromCdp, and connectToExternalCdp.\n */\n private storeSession(session: BrowserViewerSession, threadId: string): void {\n if (this.usesSharedSlot(threadId)) {\n this.sharedSession = session;\n this.sessions.set(DEFAULT_THREAD_ID, session);\n this.setSharedManager(session.browser);\n } else {\n this.threadSessions.set(threadId, session);\n this.sessions.set(threadId, session);\n this.threadManagers.set(threadId, session.browser);\n }\n }\n\n /**\n * Clear a session from the appropriate slot based on scope.\n * Must be called BEFORE async cleanup operations to prevent double callbacks\n * from disconnect handlers.\n */\n private clearSessionState(threadId: string): void {\n if (this.usesSharedSlot(threadId)) {\n this.sharedSession = null;\n this.clearSharedManager();\n this.sessions.delete(DEFAULT_THREAD_ID);\n } else {\n this.threadSessions.delete(threadId);\n this.threadManagers.delete(threadId);\n this.sessions.delete(threadId);\n }\n }\n\n /**\n * Clean up a session's resources (CDP session, browser, server).\n * Consolidates cleanup logic used by closeThreadBrowser, closeSharedBrowser,\n * and doDestroySession.\n *\n * @param session - The session to clean up\n * @param threadId - The thread ID (for onBrowserClosed callback)\n */\n private async cleanupSession(session: BrowserViewerSession, threadId: string): Promise<void> {\n // Clear state BEFORE async operations to prevent double callback from disconnect handler\n this.clearSessionState(threadId);\n // Clear cached input CDP session\n this.inputCdpSessions.delete(threadId);\n\n // Detach CDP session\n if (session.cdpSession) {\n try {\n await session.cdpSession.detach();\n } catch {\n // Ignore - session may already be detached\n }\n }\n\n // Close browser connection\n try {\n await session.browser.close();\n } catch {\n // Ignore - browser may already be closed\n }\n\n // Close browser server (kills the Chrome process) - only if we own it\n if (session.browserServer) {\n try {\n await session.browserServer.close();\n } catch {\n // Ignore - server may already be closed\n }\n }\n\n this.onBrowserClosed?.(threadId);\n }\n\n /**\n * Launch a new browser instance and return the components.\n * Consolidates the launch logic shared by createSession and createSharedSession.\n *\n * @param threadId - Thread ID for logging and disconnect handler\n */\n private async launchBrowser(threadId: string): Promise<{\n browserServer: BrowserServer;\n browser: Browser;\n context: BrowserContext;\n cdpSession: CDPSession | null;\n cdpUrl: string | null;\n }> {\n const cdpPort = this.browserConfig.cdpPort ?? 0;\n\n this.logger?.debug?.(`Launching Chrome for thread ${threadId} with remote-debugging-port=${cdpPort}`);\n\n const launchOptions: Parameters<typeof chromium.launchServer>[0] = {\n headless: this.browserConfig.headless,\n args: [`--remote-debugging-port=${cdpPort}`, '--no-first-run', '--no-default-browser-check'],\n };\n\n if (this.browserConfig.executablePath) {\n launchOptions.executablePath = this.browserConfig.executablePath;\n }\n\n // Track partially initialized resources for cleanup on failure\n let browserServer: BrowserServer | null = null;\n let browser: Browser | null = null;\n\n try {\n // Launch server - this starts Chrome\n browserServer = await chromium.launchServer(launchOptions);\n\n // Discover the actual CDP WebSocket URL from Chrome's DevToolsActivePort file\n const cdpUrl = this.discoverCdpUrl(browserServer);\n\n // Connect to the browser via Playwright for screencast/session management\n browser = await chromium.connect(browserServer.wsEndpoint());\n\n // Create context and initial page\n const context = await browser.newContext({\n viewport: this.browserConfig.viewport ?? { width: 1280, height: 720 },\n });\n\n await context.newPage();\n\n // Set up CDP session for active page (used for screencast/input injection)\n const pages = context.pages();\n const cdpSession = pages[0] ? await context.newCDPSession(pages[0]) : null;\n\n // Set up disconnection handlers - multiple events can indicate browser closure:\n // - browserServer.on('close'): fires when Chrome process exits\n // - browser.on('disconnected'): fires when Playwright connection is lost\n // - CDP Target.targetDestroyed: fires when any target (page/context) is destroyed\n let disconnectHandled = false;\n const handleDisconnect = () => {\n if (disconnectHandled) return;\n disconnectHandled = true;\n this.handleBrowserDisconnected(threadId);\n };\n\n // Listen for browser server close (fires when Chrome process exits)\n browserServer.on('close', handleDisconnect);\n // Listen for browser connection lost\n browser.on('disconnected', handleDisconnect);\n\n // Use browser-level CDP session to watch for ALL target destruction\n // Page-level CDP session only sees events for that specific page, but CLI creates its own pages\n // Browser-level session sees all targets across all contexts\n try {\n const browserCdpSession = await browser.newBrowserCDPSession();\n // Enable target discovery to get notified of all targets\n await browserCdpSession.send('Target.setDiscoverTargets', { discover: true });\n\n browserCdpSession.on('Target.targetDestroyed', async () => {\n // When a target is destroyed, check if any page targets remain\n // browser.isConnected() stays true because browserServer keeps Chrome alive,\n // so we need to check for actual page targets instead\n try {\n const { targetInfos } = (await browserCdpSession.send('Target.getTargets')) as {\n targetInfos: Array<{ type: string; url: string }>;\n };\n // Filter to actual page targets (not background pages, service workers, etc.)\n const pageTargets = targetInfos.filter(\n t => t.type === 'page' && !t.url.startsWith('chrome://') && !t.url.startsWith('devtools://'),\n );\n if (pageTargets.length === 0) {\n handleDisconnect();\n }\n } catch {\n // CDP session dead, browser definitely closed\n handleDisconnect();\n }\n });\n\n // Also listen for detached event (fires when CDP connection is lost)\n browserCdpSession.on('Inspector.detached', handleDisconnect);\n } catch {\n // Non-fatal: target watching is a reliability enhancement, not required\n this.logger?.debug?.('Failed to set up browser-level CDP target watching');\n }\n\n return { browserServer, browser, context, cdpSession, cdpUrl };\n } catch (error) {\n // Clean up partially initialized resources\n this.logger?.warn?.(`Failed to launch browser for thread ${threadId}: ${error}`);\n await browser?.close().catch(() => {});\n await browserServer?.close().catch(() => {});\n throw error;\n }\n }\n\n /**\n * Get CDP URL for a specific thread.\n */\n getCdpUrlForThread(threadId?: string): string | null {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n return this.getViewerSession(effectiveThreadId)?.cdpUrl ?? null;\n }\n\n /**\n * Get the active page for a thread.\n */\n async getActivePageForThread(threadId?: string): Promise<Page | null> {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n const session = this.getViewerSession(effectiveThreadId);\n\n if (!session?.context) {\n return null;\n }\n\n return this.resolveActivePage(session.context);\n }\n\n /**\n * Resolve the active page from a browser context.\n * Uses last page (most recently opened) with fallback to first page.\n */\n private resolveActivePage(context: BrowserContext): Page | null {\n const pages = context.pages();\n return pages[pages.length - 1] ?? pages[0] ?? null;\n }\n\n /**\n * Get or create a CDP session for the active page in a thread.\n *\n * CDP sessions are page-scoped, so we create a fresh one for the currently active page\n * rather than caching one that may point to a closed or inactive page.\n */\n async getCdpSessionForThread(threadId?: string): Promise<CDPSession | null> {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n const session = this.getViewerSession(effectiveThreadId);\n\n if (!session?.context) {\n return null;\n }\n\n // Check if browser is still connected - if not, trigger cleanup\n if (session.browser && !session.browser.isConnected()) {\n this.handleBrowserDisconnected(effectiveThreadId);\n return null;\n }\n\n const activePage = this.resolveActivePage(session.context);\n\n if (!activePage) {\n return null;\n }\n\n // Check if we have a cached CDP session for the current active page\n const cached = this.inputCdpSessions.get(effectiveThreadId);\n const currentUrl = activePage.url();\n if (cached && cached.pageUrl === currentUrl) {\n // Reuse cached session if same page\n return cached.session;\n }\n\n // Create a new CDP session for the active page\n try {\n const cdpSession = await session.context.newCDPSession(activePage);\n // Cache it for future input events\n this.inputCdpSessions.set(effectiveThreadId, { session: cdpSession, pageUrl: currentUrl });\n return cdpSession;\n } catch {\n // Page may have been closed between getting pages and creating session\n // This often indicates browser was closed - trigger cleanup\n this.inputCdpSessions.delete(effectiveThreadId);\n this.handleBrowserDisconnected(effectiveThreadId);\n return null;\n }\n }\n\n /**\n * Get the browser context for a thread.\n */\n getContextForThread(threadId?: string): BrowserContext | null {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n return this.getViewerSession(effectiveThreadId)?.context ?? null;\n }\n\n /**\n * Create a fresh CDP session for the active page (not cached).\n * Used by screencast which needs fresh sessions on tab switches.\n */\n async createFreshCdpSession(threadId?: string): Promise<CDPSession | null> {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n const session = this.getViewerSession(effectiveThreadId);\n\n if (!session?.context) {\n return null;\n }\n\n const activePage = this.resolveActivePage(session.context);\n if (!activePage) {\n return null;\n }\n\n try {\n return await session.context.newCDPSession(activePage);\n } catch {\n return null;\n }\n }\n\n /**\n * Create a new session for a thread.\n */\n protected async createSession(threadId: string): Promise<BrowserViewerSession> {\n const savedState = this.getSavedBrowserState(threadId);\n const { browserServer, browser, context, cdpSession, cdpUrl } = await this.launchBrowser(threadId);\n\n const session: BrowserViewerSession = {\n threadId,\n createdAt: Date.now(),\n browserState: savedState,\n browserServer,\n browser,\n context,\n cdpSession,\n cdpUrl,\n };\n\n // Store session using consolidated helper\n this.storeSession(session, threadId);\n\n this.logger?.debug?.(`Chrome launched for thread ${threadId}, CDP URL: ${cdpUrl}`);\n\n // Notify callback\n this.onBrowserCreated?.(browser, threadId, cdpUrl);\n\n return session;\n }\n\n /**\n * Discover the actual CDP WebSocket URL from Chrome's DevToolsActivePort file.\n *\n * Playwright's BrowserServer exposes _userDataDirForTest which points to Chrome's\n * user data directory. Chrome writes a DevToolsActivePort file there containing:\n * Line 1: The debugging port number\n * Line 2: The browser WebSocket path (e.g., /devtools/browser/<guid>)\n *\n * This gives us the real CDP URL that external tools like agent-browser can connect to.\n * Returns null if discovery fails - callers should handle this case.\n */\n private discoverCdpUrl(browserServer: BrowserServer): string | null {\n // Access Playwright's internal user data directory\n const userDataDir = (browserServer as BrowserServer & { _userDataDirForTest?: string })._userDataDirForTest;\n\n if (!userDataDir) {\n this.logger?.warn?.('Could not access browser user data directory');\n return null;\n }\n\n const portFilePath = join(userDataDir, 'DevToolsActivePort');\n\n // Chrome may still be writing the file during startup - retry with a short deadline\n const deadline = Date.now() + 1500;\n while (!existsSync(portFilePath) && Date.now() < deadline) {\n // Use Atomics.wait for a non-blocking ~50ms sleep\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);\n }\n\n if (!existsSync(portFilePath)) {\n this.logger?.warn?.('DevToolsActivePort file not found');\n return null;\n }\n\n try {\n const content = readFileSync(portFilePath, 'utf-8').trim().split('\\n');\n const port = content[0];\n const browserPath = content[1];\n\n if (!port || !browserPath) {\n this.logger?.warn?.('Invalid DevToolsActivePort content');\n return null;\n }\n\n const cdpUrl = `ws://127.0.0.1:${port}${browserPath}`;\n this.logger?.debug?.(`Discovered CDP URL from DevToolsActivePort: ${cdpUrl}`);\n return cdpUrl;\n } catch (error) {\n this.logger?.warn?.('Failed to read DevToolsActivePort file:', error);\n return null;\n }\n }\n\n /**\n * Create a shared session by connecting to an existing browser via CDP URL.\n * Used when BrowserViewer is configured with a cdpUrl to connect to an external browser.\n */\n async createSharedSessionFromCdp(cdpUrl: string): Promise<void> {\n if (this.sharedSession) {\n return; // Already created\n }\n await this.connectToCdp(cdpUrl, DEFAULT_THREAD_ID);\n }\n\n /**\n * Create a shared session (for 'shared' scope).\n */\n async createSharedSession(): Promise<void> {\n if (this.sharedSession) {\n return; // Already created\n }\n\n const { browserServer, browser, context, cdpSession, cdpUrl } = await this.launchBrowser(DEFAULT_THREAD_ID);\n\n const session: BrowserViewerSession = {\n threadId: DEFAULT_THREAD_ID,\n createdAt: Date.now(),\n browserServer,\n browser,\n context,\n cdpSession,\n cdpUrl,\n };\n\n // Store session using consolidated helper\n this.storeSession(session, DEFAULT_THREAD_ID);\n\n this.logger?.debug?.(`Shared Chrome launched, CDP URL: ${cdpUrl}`);\n\n // Notify callbacks\n this.onBrowserCreated?.(browser, DEFAULT_THREAD_ID, cdpUrl);\n this.onSessionCreated?.(session);\n }\n\n /**\n * Handle browser disconnection for a thread.\n */\n private handleBrowserDisconnected(threadId: string): void {\n this.logger?.debug?.(`Browser disconnected for thread ${threadId}`);\n\n // Guard against already-closed session (browser.close() triggers 'disconnected')\n if (!this.getViewerSession(threadId)) return;\n\n // Use consolidated helper for state cleanup\n this.clearSessionState(threadId);\n this.onBrowserClosed?.(threadId);\n }\n\n /**\n * Connect to an external browser via CDP URL for screencast.\n *\n * This is used when an agent is using their own external CDP (e.g., browser-use cloud).\n * We connect Playwright to the external browser to enable screencast without launching\n * our own browser.\n *\n * @param cdpUrl - The external CDP WebSocket URL (wss://... or ws://...)\n * @param threadId - Thread ID to associate the session with\n */\n async connectToExternalCdp(cdpUrl: string, threadId: string): Promise<BrowserViewerSession> {\n // Close any existing session for this thread to avoid leaking browser processes\n if (this.getViewerSession(threadId)) {\n if (this.usesSharedSlot(threadId)) {\n await this.closeSharedBrowser();\n } else {\n await this.closeThreadBrowser(threadId);\n }\n }\n\n return this.connectToCdp(cdpUrl, threadId);\n }\n\n /**\n * Connect to a browser via CDP URL and create a session.\n * Shared implementation for createSharedSessionFromCdp and connectToExternalCdp.\n */\n private async connectToCdp(cdpUrl: string, threadId: string): Promise<BrowserViewerSession> {\n const effectiveThreadId = this.usesSharedSlot(threadId) ? DEFAULT_THREAD_ID : threadId;\n this.logger?.debug?.(`Connecting to CDP for thread ${effectiveThreadId}: ${cdpUrl}`);\n\n let browser: Browser | null = null;\n\n try {\n browser = await chromium.connectOverCDP(cdpUrl);\n\n // Get or create context\n const contexts = browser.contexts();\n const context = contexts[0] ?? (await browser.newContext());\n\n // Get or create page\n let pages = context.pages();\n if (pages.length === 0) {\n // Wait briefly for external browser to create a page, or create one\n await new Promise(resolve => setTimeout(resolve, 500));\n pages = context.pages();\n if (pages.length === 0) {\n await context.newPage();\n pages = context.pages();\n }\n }\n\n // Set up CDP session for active page\n const cdpSession = pages[0] ? await context.newCDPSession(pages[0]) : null;\n\n // Set up disconnection handlers - use effectiveThreadId for consistent lifecycle callbacks\n let disconnectHandled = false;\n const handleDisconnect = () => {\n if (disconnectHandled) return;\n disconnectHandled = true;\n this.handleBrowserDisconnected(effectiveThreadId);\n };\n\n // Listen for context close (fires when browser window is closed manually)\n context.on('close', handleDisconnect);\n // Listen for browser connection lost\n browser.on('disconnected', handleDisconnect);\n\n const session: BrowserViewerSession = {\n threadId: effectiveThreadId,\n createdAt: Date.now(),\n browserServer: null, // We don't own the server for external CDP connections\n browser,\n context,\n cdpSession,\n cdpUrl,\n };\n\n this.storeSession(session, threadId);\n this.logger?.debug?.(`Connected to CDP for thread ${effectiveThreadId}`);\n\n // Notify callbacks\n this.onBrowserCreated?.(browser, effectiveThreadId, cdpUrl);\n this.onSessionCreated?.(session);\n\n return session;\n } catch (error) {\n this.logger?.warn?.(`Failed to connect to CDP: ${error}`);\n await browser?.close().catch(() => {});\n throw error;\n }\n }\n\n /**\n * Close a specific thread's browser.\n */\n async closeThreadBrowser(threadId: string): Promise<void> {\n const session = this.threadSessions.get(threadId);\n if (!session) {\n return;\n }\n await this.cleanupSession(session, threadId);\n }\n\n /**\n * Close the shared browser.\n */\n async closeSharedBrowser(): Promise<void> {\n if (!this.sharedSession) {\n return;\n }\n await this.cleanupSession(this.sharedSession, DEFAULT_THREAD_ID);\n }\n\n /**\n * Close all browsers.\n */\n async closeAll(): Promise<void> {\n // Close all thread browsers\n const threadIds = Array.from(this.threadSessions.keys());\n await Promise.all(threadIds.map(id => this.closeThreadBrowser(id)));\n\n // Close shared browser\n await this.closeSharedBrowser();\n }\n\n /**\n * Get the manager for a session.\n * Required by base class.\n */\n protected getManagerForSession(session: ThreadSession): Browser {\n const viewerSession = session as BrowserViewerSession;\n return viewerSession.browser;\n }\n\n /**\n * Get the shared manager.\n * Required by base class.\n */\n protected getSharedManager(): Browser {\n if (!this.sharedSession) {\n throw new Error('Shared browser not launched. Call createSharedSession() first.');\n }\n return this.sharedSession.browser;\n }\n\n /**\n * Destroy a session and clean up resources.\n * Required by base class.\n */\n protected async doDestroySession(session: ThreadSession): Promise<void> {\n const viewerSession = this.getViewerSession(session.threadId);\n if (!viewerSession) {\n return;\n }\n await this.cleanupSession(viewerSession, session.threadId);\n }\n\n /**\n * Check if browser is running for a thread.\n */\n isBrowserRunning(threadId?: string): boolean {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n return this.getViewerSession(effectiveThreadId) !== null;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACaA,IAAAA,kBAAoD;;;ACNpD,qBAAyC;AACzC,uBAAqB;AAErB,qBAAiD;AAEjD,6BAAyB;AA0ClB,IAAM,6BAAN,cAAyC,6BAAuB;AAAA,EACpD;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAGA,iBAAiB,oBAAI,IAAkC;AAAA;AAAA,EAGhE,gBAA6C;AAAA;AAAA,EAG7C,mBAAmB,oBAAI,IAAsD;AAAA,EAErF,YAAY,QAA0C;AACpD,UAAM,MAAM;AACZ,SAAK,gBAAgB,OAAO;AAC5B,SAAK,mBAAmB,OAAO;AAC/B,SAAK,kBAAkB,OAAO;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,eAAe,UAA2B;AAChD,WAAO,KAAK,UAAU,YAAY,aAAa;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,iBAAiB,UAA+C;AACtE,QAAI,KAAK,eAAe,QAAQ,GAAG;AACjC,aAAO,KAAK;AAAA,IACd;AACA,WAAO,KAAK,eAAe,IAAI,QAAQ,KAAK;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,aAAa,SAA+B,UAAwB;AAC1E,QAAI,KAAK,eAAe,QAAQ,GAAG;AACjC,WAAK,gBAAgB;AACrB,WAAK,SAAS,IAAI,kCAAmB,OAAO;AAC5C,WAAK,iBAAiB,QAAQ,OAAO;AAAA,IACvC,OAAO;AACL,WAAK,eAAe,IAAI,UAAU,OAAO;AACzC,WAAK,SAAS,IAAI,UAAU,OAAO;AACnC,WAAK,eAAe,IAAI,UAAU,QAAQ,OAAO;AAAA,IACnD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,kBAAkB,UAAwB;AAChD,QAAI,KAAK,eAAe,QAAQ,GAAG;AACjC,WAAK,gBAAgB;AACrB,WAAK,mBAAmB;AACxB,WAAK,SAAS,OAAO,gCAAiB;AAAA,IACxC,OAAO;AACL,WAAK,eAAe,OAAO,QAAQ;AACnC,WAAK,eAAe,OAAO,QAAQ;AACnC,WAAK,SAAS,OAAO,QAAQ;AAAA,IAC/B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAc,eAAe,SAA+B,UAAiC;AAE3F,SAAK,kBAAkB,QAAQ;AAE/B,SAAK,iBAAiB,OAAO,QAAQ;AAGrC,QAAI,QAAQ,YAAY;AACtB,UAAI;AACF,cAAM,QAAQ,WAAW,OAAO;AAAA,MAClC,QAAQ;AAAA,MAER;AAAA,IACF;AAGA,QAAI;AACF,YAAM,QAAQ,QAAQ,MAAM;AAAA,IAC9B,QAAQ;AAAA,IAER;AAGA,QAAI,QAAQ,eAAe;AACzB,UAAI;AACF,cAAM,QAAQ,cAAc,MAAM;AAAA,MACpC,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,SAAK,kBAAkB,QAAQ;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,cAAc,UAMzB;AACD,UAAM,UAAU,KAAK,cAAc,WAAW;AAE9C,SAAK,QAAQ,QAAQ,+BAA+B,QAAQ,+BAA+B,OAAO,EAAE;AAEpG,UAAM,gBAA6D;AAAA,MACjE,UAAU,KAAK,cAAc;AAAA,MAC7B,MAAM,CAAC,2BAA2B,OAAO,IAAI,kBAAkB,4BAA4B;AAAA,IAC7F;AAEA,QAAI,KAAK,cAAc,gBAAgB;AACrC,oBAAc,iBAAiB,KAAK,cAAc;AAAA,IACpD;AAGA,QAAI,gBAAsC;AAC1C,QAAI,UAA0B;AAE9B,QAAI;AAEF,sBAAgB,MAAM,gCAAS,aAAa,aAAa;AAGzD,YAAM,SAAS,KAAK,eAAe,aAAa;AAGhD,gBAAU,MAAM,gCAAS,QAAQ,cAAc,WAAW,CAAC;AAG3D,YAAM,UAAU,MAAM,QAAQ,WAAW;AAAA,QACvC,UAAU,KAAK,cAAc,YAAY,EAAE,OAAO,MAAM,QAAQ,IAAI;AAAA,MACtE,CAAC;AAED,YAAM,QAAQ,QAAQ;AAGtB,YAAM,QAAQ,QAAQ,MAAM;AAC5B,YAAM,aAAa,MAAM,CAAC,IAAI,MAAM,QAAQ,cAAc,MAAM,CAAC,CAAC,IAAI;AAMtE,UAAI,oBAAoB;AACxB,YAAM,mBAAmB,MAAM;AAC7B,YAAI,kBAAmB;AACvB,4BAAoB;AACpB,aAAK,0BAA0B,QAAQ;AAAA,MACzC;AAGA,oBAAc,GAAG,SAAS,gBAAgB;AAE1C,cAAQ,GAAG,gBAAgB,gBAAgB;AAK3C,UAAI;AACF,cAAM,oBAAoB,MAAM,QAAQ,qBAAqB;AAE7D,cAAM,kBAAkB,KAAK,6BAA6B,EAAE,UAAU,KAAK,CAAC;AAE5E,0BAAkB,GAAG,0BAA0B,YAAY;AAIzD,cAAI;AACF,kBAAM,EAAE,YAAY,IAAK,MAAM,kBAAkB,KAAK,mBAAmB;AAIzE,kBAAM,cAAc,YAAY;AAAA,cAC9B,OAAK,EAAE,SAAS,UAAU,CAAC,EAAE,IAAI,WAAW,WAAW,KAAK,CAAC,EAAE,IAAI,WAAW,aAAa;AAAA,YAC7F;AACA,gBAAI,YAAY,WAAW,GAAG;AAC5B,+BAAiB;AAAA,YACnB;AAAA,UACF,QAAQ;AAEN,6BAAiB;AAAA,UACnB;AAAA,QACF,CAAC;AAGD,0BAAkB,GAAG,sBAAsB,gBAAgB;AAAA,MAC7D,QAAQ;AAEN,aAAK,QAAQ,QAAQ,oDAAoD;AAAA,MAC3E;AAEA,aAAO,EAAE,eAAe,SAAS,SAAS,YAAY,OAAO;AAAA,IAC/D,SAAS,OAAO;AAEd,WAAK,QAAQ,OAAO,uCAAuC,QAAQ,KAAK,KAAK,EAAE;AAC/E,YAAM,SAAS,MAAM,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AACrC,YAAM,eAAe,MAAM,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAC3C,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAmB,UAAkC;AACnD,UAAM,oBAAoB,YAAY;AACtC,WAAO,KAAK,iBAAiB,iBAAiB,GAAG,UAAU;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,uBAAuB,UAAyC;AACpE,UAAM,oBAAoB,YAAY;AACtC,UAAM,UAAU,KAAK,iBAAiB,iBAAiB;AAEvD,QAAI,CAAC,SAAS,SAAS;AACrB,aAAO;AAAA,IACT;AAEA,WAAO,KAAK,kBAAkB,QAAQ,OAAO;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,kBAAkB,SAAsC;AAC9D,UAAM,QAAQ,QAAQ,MAAM;AAC5B,WAAO,MAAM,MAAM,SAAS,CAAC,KAAK,MAAM,CAAC,KAAK;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,uBAAuB,UAA+C;AAC1E,UAAM,oBAAoB,YAAY;AACtC,UAAM,UAAU,KAAK,iBAAiB,iBAAiB;AAEvD,QAAI,CAAC,SAAS,SAAS;AACrB,aAAO;AAAA,IACT;AAGA,QAAI,QAAQ,WAAW,CAAC,QAAQ,QAAQ,YAAY,GAAG;AACrD,WAAK,0BAA0B,iBAAiB;AAChD,aAAO;AAAA,IACT;AAEA,UAAM,aAAa,KAAK,kBAAkB,QAAQ,OAAO;AAEzD,QAAI,CAAC,YAAY;AACf,aAAO;AAAA,IACT;AAGA,UAAM,SAAS,KAAK,iBAAiB,IAAI,iBAAiB;AAC1D,UAAM,aAAa,WAAW,IAAI;AAClC,QAAI,UAAU,OAAO,YAAY,YAAY;AAE3C,aAAO,OAAO;AAAA,IAChB;AAGA,QAAI;AACF,YAAM,aAAa,MAAM,QAAQ,QAAQ,cAAc,UAAU;AAEjE,WAAK,iBAAiB,IAAI,mBAAmB,EAAE,SAAS,YAAY,SAAS,WAAW,CAAC;AACzF,aAAO;AAAA,IACT,QAAQ;AAGN,WAAK,iBAAiB,OAAO,iBAAiB;AAC9C,WAAK,0BAA0B,iBAAiB;AAChD,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,oBAAoB,UAA0C;AAC5D,UAAM,oBAAoB,YAAY;AACtC,WAAO,KAAK,iBAAiB,iBAAiB,GAAG,WAAW;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,sBAAsB,UAA+C;AACzE,UAAM,oBAAoB,YAAY;AACtC,UAAM,UAAU,KAAK,iBAAiB,iBAAiB;AAEvD,QAAI,CAAC,SAAS,SAAS;AACrB,aAAO;AAAA,IACT;AAEA,UAAM,aAAa,KAAK,kBAAkB,QAAQ,OAAO;AACzD,QAAI,CAAC,YAAY;AACf,aAAO;AAAA,IACT;AAEA,QAAI;AACF,aAAO,MAAM,QAAQ,QAAQ,cAAc,UAAU;AAAA,IACvD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAgB,cAAc,UAAiD;AAC7E,UAAM,aAAa,KAAK,qBAAqB,QAAQ;AACrD,UAAM,EAAE,eAAe,SAAS,SAAS,YAAY,OAAO,IAAI,MAAM,KAAK,cAAc,QAAQ;AAEjG,UAAM,UAAgC;AAAA,MACpC;AAAA,MACA,WAAW,KAAK,IAAI;AAAA,MACpB,cAAc;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAGA,SAAK,aAAa,SAAS,QAAQ;AAEnC,SAAK,QAAQ,QAAQ,8BAA8B,QAAQ,cAAc,MAAM,EAAE;AAGjF,SAAK,mBAAmB,SAAS,UAAU,MAAM;AAEjD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaQ,eAAe,eAA6C;AAElE,UAAM,cAAe,cAAmE;AAExF,QAAI,CAAC,aAAa;AAChB,WAAK,QAAQ,OAAO,8CAA8C;AAClE,aAAO;AAAA,IACT;AAEA,UAAM,mBAAe,uBAAK,aAAa,oBAAoB;AAG3D,UAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,WAAO,KAAC,2BAAW,YAAY,KAAK,KAAK,IAAI,IAAI,UAAU;AAEzD,cAAQ,KAAK,IAAI,WAAW,IAAI,kBAAkB,CAAC,CAAC,GAAG,GAAG,GAAG,EAAE;AAAA,IACjE;AAEA,QAAI,KAAC,2BAAW,YAAY,GAAG;AAC7B,WAAK,QAAQ,OAAO,mCAAmC;AACvD,aAAO;AAAA,IACT;AAEA,QAAI;AACF,YAAM,cAAU,6BAAa,cAAc,OAAO,EAAE,KAAK,EAAE,MAAM,IAAI;AACrE,YAAM,OAAO,QAAQ,CAAC;AACtB,YAAM,cAAc,QAAQ,CAAC;AAE7B,UAAI,CAAC,QAAQ,CAAC,aAAa;AACzB,aAAK,QAAQ,OAAO,oCAAoC;AACxD,eAAO;AAAA,MACT;AAEA,YAAM,SAAS,kBAAkB,IAAI,GAAG,WAAW;AACnD,WAAK,QAAQ,QAAQ,+CAA+C,MAAM,EAAE;AAC5E,aAAO;AAAA,IACT,SAAS,OAAO;AACd,WAAK,QAAQ,OAAO,2CAA2C,KAAK;AACpE,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,2BAA2B,QAA+B;AAC9D,QAAI,KAAK,eAAe;AACtB;AAAA,IACF;AACA,UAAM,KAAK,aAAa,QAAQ,gCAAiB;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,sBAAqC;AACzC,QAAI,KAAK,eAAe;AACtB;AAAA,IACF;AAEA,UAAM,EAAE,eAAe,SAAS,SAAS,YAAY,OAAO,IAAI,MAAM,KAAK,cAAc,gCAAiB;AAE1G,UAAM,UAAgC;AAAA,MACpC,UAAU;AAAA,MACV,WAAW,KAAK,IAAI;AAAA,MACpB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAGA,SAAK,aAAa,SAAS,gCAAiB;AAE5C,SAAK,QAAQ,QAAQ,oCAAoC,MAAM,EAAE;AAGjE,SAAK,mBAAmB,SAAS,kCAAmB,MAAM;AAC1D,SAAK,mBAAmB,OAAO;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKQ,0BAA0B,UAAwB;AACxD,SAAK,QAAQ,QAAQ,mCAAmC,QAAQ,EAAE;AAGlE,QAAI,CAAC,KAAK,iBAAiB,QAAQ,EAAG;AAGtC,SAAK,kBAAkB,QAAQ;AAC/B,SAAK,kBAAkB,QAAQ;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,qBAAqB,QAAgB,UAAiD;AAE1F,QAAI,KAAK,iBAAiB,QAAQ,GAAG;AACnC,UAAI,KAAK,eAAe,QAAQ,GAAG;AACjC,cAAM,KAAK,mBAAmB;AAAA,MAChC,OAAO;AACL,cAAM,KAAK,mBAAmB,QAAQ;AAAA,MACxC;AAAA,IACF;AAEA,WAAO,KAAK,aAAa,QAAQ,QAAQ;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,aAAa,QAAgB,UAAiD;AAC1F,UAAM,oBAAoB,KAAK,eAAe,QAAQ,IAAI,mCAAoB;AAC9E,SAAK,QAAQ,QAAQ,gCAAgC,iBAAiB,KAAK,MAAM,EAAE;AAEnF,QAAI,UAA0B;AAE9B,QAAI;AACF,gBAAU,MAAM,gCAAS,eAAe,MAAM;AAG9C,YAAM,WAAW,QAAQ,SAAS;AAClC,YAAM,UAAU,SAAS,CAAC,KAAM,MAAM,QAAQ,WAAW;AAGzD,UAAI,QAAQ,QAAQ,MAAM;AAC1B,UAAI,MAAM,WAAW,GAAG;AAEtB,cAAM,IAAI,QAAQ,aAAW,WAAW,SAAS,GAAG,CAAC;AACrD,gBAAQ,QAAQ,MAAM;AACtB,YAAI,MAAM,WAAW,GAAG;AACtB,gBAAM,QAAQ,QAAQ;AACtB,kBAAQ,QAAQ,MAAM;AAAA,QACxB;AAAA,MACF;AAGA,YAAM,aAAa,MAAM,CAAC,IAAI,MAAM,QAAQ,cAAc,MAAM,CAAC,CAAC,IAAI;AAGtE,UAAI,oBAAoB;AACxB,YAAM,mBAAmB,MAAM;AAC7B,YAAI,kBAAmB;AACvB,4BAAoB;AACpB,aAAK,0BAA0B,iBAAiB;AAAA,MAClD;AAGA,cAAQ,GAAG,SAAS,gBAAgB;AAEpC,cAAQ,GAAG,gBAAgB,gBAAgB;AAE3C,YAAM,UAAgC;AAAA,QACpC,UAAU;AAAA,QACV,WAAW,KAAK,IAAI;AAAA,QACpB,eAAe;AAAA;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAEA,WAAK,aAAa,SAAS,QAAQ;AACnC,WAAK,QAAQ,QAAQ,+BAA+B,iBAAiB,EAAE;AAGvE,WAAK,mBAAmB,SAAS,mBAAmB,MAAM;AAC1D,WAAK,mBAAmB,OAAO;AAE/B,aAAO;AAAA,IACT,SAAS,OAAO;AACd,WAAK,QAAQ,OAAO,6BAA6B,KAAK,EAAE;AACxD,YAAM,SAAS,MAAM,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AACrC,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,mBAAmB,UAAiC;AACxD,UAAM,UAAU,KAAK,eAAe,IAAI,QAAQ;AAChD,QAAI,CAAC,SAAS;AACZ;AAAA,IACF;AACA,UAAM,KAAK,eAAe,SAAS,QAAQ;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,qBAAoC;AACxC,QAAI,CAAC,KAAK,eAAe;AACvB;AAAA,IACF;AACA,UAAM,KAAK,eAAe,KAAK,eAAe,gCAAiB;AAAA,EACjE;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAA0B;AAE9B,UAAM,YAAY,MAAM,KAAK,KAAK,eAAe,KAAK,CAAC;AACvD,UAAM,QAAQ,IAAI,UAAU,IAAI,QAAM,KAAK,mBAAmB,EAAE,CAAC,CAAC;AAGlE,UAAM,KAAK,mBAAmB;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMU,qBAAqB,SAAiC;AAC9D,UAAM,gBAAgB;AACtB,WAAO,cAAc;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMU,mBAA4B;AACpC,QAAI,CAAC,KAAK,eAAe;AACvB,YAAM,IAAI,MAAM,gEAAgE;AAAA,IAClF;AACA,WAAO,KAAK,cAAc;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAgB,iBAAiB,SAAuC;AACtE,UAAM,gBAAgB,KAAK,iBAAiB,QAAQ,QAAQ;AAC5D,QAAI,CAAC,eAAe;AAClB;AAAA,IACF;AACA,UAAM,KAAK,eAAe,eAAe,QAAQ,QAAQ;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAiB,UAA4B;AAC3C,UAAM,oBAAoB,YAAY;AACtC,WAAO,KAAK,iBAAiB,iBAAiB,MAAM;AAAA,EACtD;AACF;;;AD9oBO,IAAM,gBAAN,cAA4B,8BAAc;AAAA,EAC7B;AAAA,EACA,OAAO;AAAA,EACP,WAAW;AAAA,EACpB,eAAe;AAAA;AAAA,EAGf;AAAA;AAAA,EAGA;AAAA,EAKT,YAAY,QAA6B;AAGvC,UAAM,iBAAiB,OAAO,SAAU,OAAO,SAAS,WAAa,OAAO,SAAS;AAIrF,UAAM,EAAE,KAAK,MAAM,SAAS,UAAU,GAAG,WAAW,IAAI;AAExD,UAAM;AAAA,MACJ,GAAG;AAAA,MACH,OAAO;AAAA,IACT,CAAQ;AAER,SAAK,KAAK,kBAAkB,KAAK,IAAI,CAAC;AACtC,SAAK,MAAM,OAAO;AAClB,SAAK,eAAe;AAGpB,SAAK,gBAAgB,IAAI,2BAA2B;AAAA,MAClD,OAAO;AAAA,MACP,eAAe,EAAE,GAAG,QAAQ,UAAU,KAAK,SAAS;AAAA,MACpD,QAAQ,KAAK;AAAA,MACb,kBAAkB,aAAW;AAE3B,aAAK,mBAAmB,QAAQ,QAAQ;AAAA,MAC1C;AAAA,MACA,kBAAkB,CAAC,UAAU,UAAU,YAAY;AACjD,aAAK,QAAQ,QAAQ,8BAA8B,QAAQ,EAAE;AAAA,MAC/D;AAAA,MACA,iBAAiB,cAAY;AAC3B,aAAK,QAAQ,QAAQ,6BAA6B,QAAQ,EAAE;AAE5D,aAAK,oBAAoB,QAAQ;AAAA,MACnC;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcS,UAAU,UAAkC;AACnD,WAAO,KAAK,cAAc,mBAAmB,YAAY,KAAK,iBAAiB,CAAC;AAAA,EAClF;AAAA;AAAA;AAAA;AAAA,EAMA,MAAyB,WAA0B;AACjD,UAAM,QAAQ,KAAK,cAAc,SAAS;AAC1C,UAAM,SAAS,KAAK,OAAO;AAE3B,QAAI,QAAQ;AAEV,YAAM,MAAM,OAAO,WAAW,aAAa,MAAM,OAAO,IAAI;AAC5D,YAAM,KAAK,kBAAkB,GAAG;AAAA,IAClC,WAAW,UAAU,UAAU;AAE7B,YAAM,KAAK,cAAc,oBAAoB;AAAA,IAC/C;AAAA,EAEF;AAAA,EAEA,MAAyB,UAAyB;AAChD,UAAM,KAAK,cAAc,SAAS;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,kBAAkB,QAA+B;AAC7D,SAAK,QAAQ,QAAQ,qCAAqC,MAAM,EAAE;AAGlE,UAAM,KAAK,cAAc,2BAA2B,MAAM;AAE1D,SAAK,QAAQ,QAAQ,+BAA+B;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAe,cAA6B;AAC1C,UAAM,QAAQ,KAAK,cAAc,SAAS;AAC1C,UAAM,WAAW,KAAK,iBAAiB;AAGvC,QAAI,UAAU,YAAY,CAAC,KAAK,cAAc,iBAAiB,QAAQ,GAAG;AACxE,YAAM,KAAK,cAAc,oBAAoB,QAAQ;AAAA,IACvD;AAEA,UAAM,MAAM,YAAY;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKS,iBAAiB,UAA4B;AACpD,WAAO,KAAK,cAAc,iBAAiB,YAAY,KAAK,iBAAiB,CAAC;AAAA,EAChF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAe,OAAO,UAAkC;AACtD,UAAM,QAAQ,KAAK,cAAc,SAAS;AAC1C,UAAM,oBAAoB,YAAY,KAAK,iBAAiB;AAE5D,QAAI,UAAU,UAAU;AAEtB,UAAI,CAAC,KAAK,cAAc,iBAAiB,GAAG;AAC1C,cAAM,MAAM,OAAO;AAAA,MACrB;AAAA,IACF,OAAO;AAEL,UAAI,CAAC,KAAK,cAAc,iBAAiB,iBAAiB,GAAG;AAC3D,cAAM,KAAK,cAAc,oBAAoB,iBAAiB;AAG9D,aAAK,SAAS;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMS,4BAAkC;AAEzC,UAAM,0BAA0B;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAe,qBAAqB,QAAgB,UAAkC;AACpF,UAAM,oBAAoB,YAAY,KAAK,iBAAiB;AAC5D,UAAM,KAAK,cAAc,qBAAqB,QAAQ,iBAAiB;AAEvE,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA,EAMA,MAAyB,cAAc,UAAyC;AAC9E,WAAO,KAAK,cAAc,uBAAuB,YAAY,KAAK,iBAAiB,CAAC;AAAA,EACtF;AAAA,EAEmB,yBAAyB,UAAwC;AAClF,UAAM,UAAU,KAAK,cAAc,oBAAoB,YAAY,KAAK,iBAAiB,CAAC;AAC1F,QAAI,CAAC,SAAS;AACZ,aAAO;AAAA,IACT;AAEA,UAAM,QAAQ,QAAQ,MAAM;AAE5B,UAAM,cAAc,MAAM,SAAS,IAAI,MAAM,SAAS,IAAI;AAC1D,UAAM,OAA0B,MAAM,IAAI,CAAC,MAAM,WAAW;AAAA,MAC1D,KAAK,KAAK,IAAI;AAAA,MACd,OAAO;AAAA;AAAA,MACP,UAAU,UAAU;AAAA,IACtB,EAAE;AAEF,WAAO;AAAA,MACL;AAAA,MACA,gBAAgB;AAAA,IAClB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAMA,MAAe,gBAAgB,SAAwD;AACrF,UAAM,WAAW,SAAS,YAAY,KAAK,iBAAiB;AAK5D,UAAM,WAA+B;AAAA,MACnC,eAAe,YAAY;AACzB,cAAM,aAAa,MAAM,KAAK,cAAc,sBAAsB,QAAQ;AAC1E,YAAI,CAAC,YAAY;AACf,gBAAM,IAAI,MAAM,6CAA6C;AAAA,QAC/D;AAGA,eAAO;AAAA,UACL,MAAM,OAAO,QAAgB,WAAqC;AAChE,mBAAO,WAAW,KAAK,QAAe,MAAM;AAAA,UAC9C;AAAA,UACA,IAAI,CAAC,OAAe,YAAuC;AACzD,uBAAW,GAAG,OAAc,OAAO;AAAA,UACrC;AAAA,UACA,KAAK,CAAC,OAAe,YAAuC;AAC1D,uBAAW,IAAI,OAAc,OAAO;AAAA,UACtC;AAAA,QACF;AAAA,MACF;AAAA,MACA,kBAAkB,MAAM,KAAK,iBAAiB,QAAQ;AAAA,IACxD;AAGA,UAAM,SAAS,IAAI,qCAAqB,UAAU;AAAA,MAChD,QAAQ,SAAS,UAAU;AAAA,MAC3B,SAAS,SAAS,WAAW;AAAA,MAC7B,UAAU,SAAS,YAAY;AAAA,MAC/B,WAAW,SAAS,aAAa;AAAA,MACjC,eAAe,SAAS,iBAAiB;AAAA,IAC3C,CAAC;AAGD,UAAM,UAAU,KAAK,cAAc,oBAAoB,QAAQ;AAC/D,QAAI,SAAS;AAEX,YAAM,gBAAgB,oBAAI,IAA8E;AAGxG,YAAM,YAAY,MAAM;AACtB,mBAAW,MAAM;AACf,cAAI,OAAO,SAAS,GAAG;AACrB,mBAAO,UAAU,EAAE,MAAM,MAAM;AAAA,YAAC,CAAC;AAAA,UACnC;AAAA,QACF,GAAG,GAAG;AAAA,MACR;AAGA,YAAM,gBAAgB,CAAC,SAAe;AACpC,2BAAmB,IAAI;AAAA,MACzB;AAGA,YAAM,qBAAqB,CAAC,SAAe;AACzC,aAAK,KAAK,SAAS,MAAM;AAEvB,wBAAc,OAAO,IAAI;AACzB,qBAAW,MAAM;AACf,gBAAI,OAAO,SAAS,KAAK,QAAQ,MAAM,EAAE,SAAS,GAAG;AACnD,qBAAO,UAAU,EAAE,MAAM,MAAM;AAAA,cAAC,CAAC;AAAA,YACnC;AAAA,UACF,GAAG,GAAG;AAAA,QACR,CAAC;AAGD,cAAM,mBAAmB,CAAC,UAA6D;AACrF,cAAI,CAAC,MAAM,YAAY,GAAG;AACxB,mBAAO,QAAQ,MAAM,IAAI,CAAC;AAAA,UAC5B;AAAA,QACF;AACA,aAAK,GAAG,kBAAkB,gBAAgB;AAC1C,sBAAc,IAAI,MAAM,gBAAgB;AAAA,MAC1C;AAGA,cAAQ,GAAG,QAAQ,SAAS;AAC5B,cAAQ,GAAG,QAAQ,aAAa;AAGhC,iBAAW,QAAQ,QAAQ,MAAM,GAAG;AAClC,2BAAmB,IAAI;AAAA,MACzB;AAGA,aAAO,KAAK,QAAQ,MAAM;AACxB,gBAAQ,IAAI,QAAQ,SAAS;AAC7B,gBAAQ,IAAI,QAAQ,aAAa;AAEjC,mBAAW,CAAC,MAAM,QAAQ,KAAK,eAAe;AAC5C,eAAK,IAAI,kBAAkB,QAAQ;AAAA,QACrC;AACA,sBAAc,MAAM;AAAA,MACtB,CAAC;AAAA,IACH;AAEA,UAAM,OAAO,MAAM;AACnB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAMA,MAAe,iBAAiB,QAA0B,UAAkC;AAC1F,UAAM,aAAa,MAAM,KAAK,cAAc,uBAAuB,YAAY,KAAK,iBAAiB,CAAC;AACtG,QAAI,CAAC,YAAY;AACf,YAAM,IAAI,MAAM,+CAA+C;AAAA,IACjE;AAEA,UAAM,WAAW,KAAK,4BAA4B,MAAM;AAAA,EAC1D;AAAA,EAEA,MAAe,oBAAoB,QAA6B,UAAkC;AAChG,UAAM,aAAa,MAAM,KAAK,cAAc,uBAAuB,YAAY,KAAK,iBAAiB,CAAC;AACtG,QAAI,CAAC,YAAY;AACf,YAAM,IAAI,MAAM,kDAAkD;AAAA,IACpE;AAEA,UAAM,WAAW,KAAK,0BAA0B,MAAM;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA,EAMA,WAAiC;AAG/B,WAAO,CAAC;AAAA,EACV;AACF;","names":["import_browser"]}
{"version":3,"sources":["../src/index.ts","../src/browser-viewer.ts","../src/thread-manager.ts"],"sourcesContent":["/**\n * @mastra/browser-viewer\n *\n * Browser viewer for Mastra workspaces with CLI provider support.\n * Launches Chrome via Playwright and exposes CDP URL for CLI tools.\n */\n\nexport { BrowserViewer } from './browser-viewer';\nexport { BrowserViewerThreadManager } from './thread-manager';\nexport type { BrowserViewerConfig, CLIProvider } from './types';\nexport type { BrowserViewerThreadManagerConfig } from './thread-manager';\n","/**\n * BrowserViewer - Playwright-managed Chrome for CLI providers\n *\n * Launches Chrome via Playwright and exposes the CDP URL for CLI tools\n * (agent-browser, browser-use, and browse) to connect as secondary clients.\n *\n * This gives us:\n * - Direct page-level CDP sessions (fixes screencast sessionId issues)\n * - Full browser lifecycle control\n * - Predictable CDP URL for CLI injection\n * - Thread-scoped browser isolation\n */\n\nimport { MastraBrowser, ScreencastStreamImpl } from '@mastra/core/browser';\nimport type {\n BrowserState,\n BrowserTabState,\n ScreencastOptions,\n ScreencastStream,\n CdpSessionProvider,\n MouseEventParams,\n KeyboardEventParams,\n} from '@mastra/core/browser';\nimport type { Tool } from '@mastra/core/tools';\nimport type { Page } from 'playwright-core';\nimport { BrowserViewerThreadManager } from './thread-manager';\nimport type { BrowserViewerConfig, CLIProvider } from './types';\n\n/**\n * BrowserViewer - CLI provider with Playwright-managed Chrome\n *\n * Use this with Workspace to enable browser automation via CLI tools.\n * The agent uses skills + workspace_execute_command to drive the CLI,\n * while Mastra handles screencast, input injection, and lifecycle.\n *\n * @example\n * ```ts\n * import { Workspace } from '@mastra/core';\n * import { BrowserViewer } from '@mastra/browser-viewer';\n *\n * const workspace = new Workspace({\n * browser: new BrowserViewer({\n * cli: 'agent-browser',\n * headless: false,\n * }),\n * });\n * ```\n */\nexport class BrowserViewer extends MastraBrowser {\n override readonly id: string;\n override readonly name = 'BrowserViewer';\n override readonly provider = 'browser-viewer';\n readonly providerType = 'cli' as const;\n\n /** Which CLI the agent uses */\n readonly cli: CLIProvider;\n\n /** Viewer-specific config (stored for reference) */\n readonly viewerConfig: BrowserViewerConfig;\n\n /** Thread manager for browser sessions */\n declare protected threadManager: BrowserViewerThreadManager;\n\n constructor(config: BrowserViewerConfig) {\n // Default to 'thread' scope (each thread gets its own Chrome)\n // Use 'shared' if connecting to an existing browser\n const effectiveScope = config.cdpUrl ? (config.scope ?? 'shared') : (config.scope ?? 'thread');\n\n // Build base config (exclude CLI-specific options)\n // Use type assertion because BrowserConfig is a discriminated union\n const { cli: _cli, cdpPort: _cdpPort, ...baseConfig } = config;\n\n super({\n ...baseConfig,\n scope: effectiveScope,\n } as any);\n\n this.id = `browser-viewer-${Date.now()}`;\n this.cli = config.cli;\n this.viewerConfig = config;\n\n // Initialize thread manager\n this.threadManager = new BrowserViewerThreadManager({\n scope: effectiveScope,\n browserConfig: { ...config, headless: this.headless },\n logger: this.logger,\n onSessionCreated: session => {\n // Notify listeners so screencast can start for this thread\n this.notifyBrowserReady(session.threadId);\n },\n onBrowserCreated: (_browser, threadId, _cdpUrl) => {\n this.logger?.debug?.(`Browser created for thread ${threadId}`);\n },\n onBrowserClosed: threadId => {\n this.logger?.debug?.(`Browser closed for thread ${threadId}`);\n // Notify base class callbacks so ViewerRegistry gets notified\n this.notifyBrowserClosed(threadId);\n },\n });\n }\n\n // ---------------------------------------------------------------------------\n // CDP URL Access\n // ---------------------------------------------------------------------------\n\n /**\n * Get the CDP WebSocket URL for CLI tools to connect.\n * For thread scope, returns the CDP URL for the specified thread.\n * For shared scope, returns the single shared CDP URL.\n *\n * @param threadId - Thread identifier (optional, uses current thread if not specified)\n * @returns CDP URL or null if browser not running for that thread\n */\n override getCdpUrl(threadId?: string): string | null {\n return this.threadManager.getCdpUrlForThread(threadId ?? this.getCurrentThread());\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle (implements MastraBrowser abstract methods)\n // ---------------------------------------------------------------------------\n\n protected override async doLaunch(): Promise<void> {\n const scope = this.threadManager.getScope();\n const cdpUrl = this.config.cdpUrl;\n\n if (cdpUrl) {\n // Connect mode: connect to existing browser (always shared)\n const url = typeof cdpUrl === 'function' ? await cdpUrl() : cdpUrl;\n await this.connectToExisting(url);\n } else if (scope === 'shared') {\n // Shared mode: launch single browser\n await this.threadManager.createSharedSession();\n }\n // For thread scope, browsers are launched lazily per thread via ensureReady()\n }\n\n protected override async doClose(): Promise<void> {\n await this.threadManager.closeAll();\n }\n\n /**\n * Connect to an existing browser via CDP URL.\n */\n private async connectToExisting(cdpUrl: string): Promise<void> {\n this.logger?.debug?.(`Connecting to existing browser at ${cdpUrl}`);\n\n // Create a shared session from the external CDP connection\n await this.threadManager.createSharedSessionFromCdp(cdpUrl);\n\n this.logger?.debug?.('Connected to existing browser');\n }\n\n /**\n * Ensure browser is ready for the current thread.\n * For thread scope, creates a new browser if needed.\n */\n override async ensureReady(): Promise<void> {\n const scope = this.threadManager.getScope();\n const threadId = this.getCurrentThread();\n\n // For thread scope, create browser for this thread if needed\n if (scope === 'thread' && !this.threadManager.isBrowserRunning(threadId)) {\n await this.threadManager.getManagerForThread(threadId);\n }\n\n await super.ensureReady();\n }\n\n /**\n * Check if browser is running (for current thread in thread scope).\n */\n override isBrowserRunning(threadId?: string): boolean {\n return this.threadManager.isBrowserRunning(threadId ?? this.getCurrentThread());\n }\n\n /**\n * Launch browser, optionally for a specific thread.\n * For thread scope, creates a browser for that thread.\n * For shared scope, launches the single shared browser.\n */\n override async launch(threadId?: string): Promise<void> {\n const scope = this.threadManager.getScope();\n const effectiveThreadId = threadId ?? this.getCurrentThread();\n\n if (scope === 'shared') {\n // For shared scope, use base class launch (handles racing, status, etc.)\n if (!this.threadManager.isBrowserRunning()) {\n await super.launch();\n }\n } else {\n // For thread scope, launch for this specific thread\n if (!this.threadManager.isBrowserRunning(effectiveThreadId)) {\n await this.threadManager.getManagerForThread(effectiveThreadId);\n // Set status to ready so isBrowserRunning() returns true\n // (base class launch() does this, but we bypass it for thread scope)\n this.status = 'ready';\n }\n }\n }\n\n /**\n * Handle browser disconnection.\n * Overrides base class method.\n */\n override handleBrowserDisconnected(): void {\n // Call parent to handle status and notifications\n super.handleBrowserDisconnected();\n }\n\n /**\n * Connect to an external browser via CDP URL for screencast.\n *\n * Use this when an agent is using their own external CDP (e.g., browser-use cloud).\n * Connects Playwright to the external browser to enable screencast without launching\n * our own browser.\n *\n * @param cdpUrl - The external CDP WebSocket URL (wss://... or ws://...)\n * @param threadId - Thread ID to associate the session with\n */\n override async connectToExternalCdp(cdpUrl: string, threadId?: string): Promise<void> {\n const effectiveThreadId = threadId ?? this.getCurrentThread();\n await this.threadManager.connectToExternalCdp(cdpUrl, effectiveThreadId);\n // Mark as ready\n this.status = 'ready';\n }\n\n // ---------------------------------------------------------------------------\n // Browser State (implements MastraBrowser abstract methods)\n // ---------------------------------------------------------------------------\n\n protected override async getActivePage(threadId?: string): Promise<Page | null> {\n return this.threadManager.getActivePageForThread(threadId ?? this.getCurrentThread());\n }\n\n protected override getBrowserStateForThread(threadId?: string): BrowserState | null {\n const context = this.threadManager.getContextForThread(threadId ?? this.getCurrentThread());\n if (!context) {\n return null;\n }\n\n const pages = context.pages();\n // Active page is the last one (most recently opened), consistent with resolveActivePage\n const activeIndex = pages.length > 0 ? pages.length - 1 : 0;\n const tabs: BrowserTabState[] = pages.map((page, index) => ({\n url: page.url(),\n title: '', // Would need async call to get title\n isActive: index === activeIndex,\n }));\n\n return {\n tabs,\n activeTabIndex: activeIndex,\n };\n }\n\n // ---------------------------------------------------------------------------\n // Screencast Support\n // ---------------------------------------------------------------------------\n\n override async startScreencast(options?: ScreencastOptions): Promise<ScreencastStream> {\n const threadId = options?.threadId ?? this.getCurrentThread();\n\n // Create CDP session provider that creates FRESH sessions on each call\n // This is critical for tab switching - when reconnecting, we need a CDP session\n // attached to the CURRENT page, not the original page from launch\n const provider: CdpSessionProvider = {\n getCdpSession: async () => {\n const cdpSession = await this.threadManager.createFreshCdpSession(threadId);\n if (!cdpSession) {\n throw new Error('No browser context available for screencast');\n }\n\n // Return wrapper that implements CdpSessionLike\n return {\n send: async (method: string, params?: Record<string, unknown>) => {\n return cdpSession.send(method as any, params);\n },\n on: (event: string, handler: (params: unknown) => void) => {\n cdpSession.on(event as any, handler);\n },\n off: (event: string, handler: (params: unknown) => void) => {\n cdpSession.off(event as any, handler);\n },\n };\n },\n isBrowserRunning: () => this.isBrowserRunning(threadId),\n };\n\n // Create and start screencast stream\n const stream = new ScreencastStreamImpl(provider, {\n format: options?.format ?? 'jpeg',\n quality: options?.quality ?? 80,\n maxWidth: options?.maxWidth ?? 1280,\n maxHeight: options?.maxHeight ?? 720,\n everyNthFrame: options?.everyNthFrame ?? 1,\n });\n\n // Set up tab change detection - reconnect screencast when tabs change\n const context = this.threadManager.getContextForThread(threadId);\n if (context) {\n // Track all listeners for cleanup\n const pageListeners = new Map<Page, (frame: { url: () => string; parentFrame: () => unknown }) => void>();\n\n // New tab opened - reconnect screencast\n const onNewPage = () => {\n setTimeout(() => {\n if (stream.isActive()) {\n stream.reconnect().catch(() => {});\n }\n }, 100);\n };\n\n // Handler for new pages that sets up listeners\n const onPageCreated = (page: Page) => {\n setupPageListeners(page);\n };\n\n // Set up page close listener for each page\n const setupPageListeners = (page: Page) => {\n page.once('close', () => {\n // Clean up this page's listener\n pageListeners.delete(page);\n setTimeout(() => {\n if (stream.isActive() && context.pages().length > 0) {\n stream.reconnect().catch(() => {});\n }\n }, 100);\n });\n\n // Navigation listener for URL updates\n const onFrameNavigated = (frame: { url: () => string; parentFrame: () => unknown }) => {\n if (!frame.parentFrame()) {\n stream.emitUrl(frame.url());\n }\n };\n page.on('framenavigated', onFrameNavigated);\n pageListeners.set(page, onFrameNavigated);\n };\n\n // Set up listeners\n context.on('page', onNewPage);\n context.on('page', onPageCreated);\n\n // Set up for existing pages\n for (const page of context.pages()) {\n setupPageListeners(page);\n }\n\n // Clean up all listeners on stream stop\n stream.once('stop', () => {\n context.off('page', onNewPage);\n context.off('page', onPageCreated);\n // Remove framenavigated listeners from all pages\n for (const [page, listener] of pageListeners) {\n page.off('framenavigated', listener);\n }\n pageListeners.clear();\n });\n }\n\n await stream.start();\n return stream;\n }\n\n // ---------------------------------------------------------------------------\n // Input Injection\n // ---------------------------------------------------------------------------\n\n override async injectMouseEvent(params: MouseEventParams, threadId?: string): Promise<void> {\n const cdpSession = await this.threadManager.getCdpSessionForThread(threadId ?? this.getCurrentThread());\n if (!cdpSession) {\n throw new Error('CDP session not available for mouse injection');\n }\n\n await cdpSession.send('Input.dispatchMouseEvent', params);\n }\n\n override async injectKeyboardEvent(params: KeyboardEventParams, threadId?: string): Promise<void> {\n const cdpSession = await this.threadManager.getCdpSessionForThread(threadId ?? this.getCurrentThread());\n if (!cdpSession) {\n throw new Error('CDP session not available for keyboard injection');\n }\n\n await cdpSession.send('Input.dispatchKeyEvent', params);\n }\n\n // ---------------------------------------------------------------------------\n // Tools (CLI agents don't use SDK tools - they use workspace commands)\n // ---------------------------------------------------------------------------\n\n getTools(): Record<string, Tool> {\n // CLI agents use workspace_execute_command with CLI skills\n // No SDK tools needed\n return {};\n }\n}\n","/**\n * BrowserViewerThreadManager - Thread scope management for BrowserViewer\n *\n * Manages thread-scoped browser sessions using Playwright to launch\n * separate Chrome instances per thread.\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { ThreadManager, DEFAULT_THREAD_ID } from '@mastra/core/browser';\nimport type { ThreadSession, ThreadManagerConfig } from '@mastra/core/browser';\nimport { chromium } from 'playwright-core';\nimport type { Browser, BrowserContext, BrowserServer, CDPSession, Page } from 'playwright-core';\nimport type { BrowserViewerConfig } from './types';\n\n/**\n * Extended session info for BrowserViewer.\n */\ninterface BrowserViewerSession extends ThreadSession {\n /**\n * Playwright browser server (owns the Chrome process).\n * Null for external CDP connections where we don't own the browser process.\n */\n browserServer: BrowserServer | null;\n /** Playwright browser instance (connected to server) */\n browser: Browser;\n /** Browser context */\n context: BrowserContext;\n /** CDP session for the active page */\n cdpSession: CDPSession | null;\n /** CDP WebSocket URL (null if discovery failed) */\n cdpUrl: string | null;\n}\n\n/**\n * Configuration for BrowserViewerThreadManager.\n */\nexport interface BrowserViewerThreadManagerConfig extends ThreadManagerConfig {\n /** Browser configuration */\n browserConfig: BrowserViewerConfig;\n /** Callback when a browser is created for a thread */\n onBrowserCreated?: (browser: Browser, threadId: string, cdpUrl: string | null) => void;\n /** Callback when a browser is closed for a thread */\n onBrowserClosed?: (threadId: string) => void;\n}\n\n/**\n * Thread manager implementation for BrowserViewer.\n *\n * Supports two scope modes:\n * - 'shared': All threads share one Chrome instance\n * - 'thread': Each thread gets a dedicated Chrome instance\n */\nexport class BrowserViewerThreadManager extends ThreadManager<Browser> {\n private readonly browserConfig: BrowserViewerConfig;\n private readonly onBrowserCreated?: (browser: Browser, threadId: string, cdpUrl: string | null) => void;\n private readonly onBrowserClosed?: (threadId: string) => void;\n\n /** Map of thread ID to session info (for 'thread' scope) */\n private readonly threadSessions = new Map<string, BrowserViewerSession>();\n\n /** Shared session info (for 'shared' scope) */\n private sharedSession: BrowserViewerSession | null = null;\n\n /** Cached CDP sessions for input injection, keyed by threadId */\n private inputCdpSessions = new Map<string, { session: CDPSession; pageUrl: string }>();\n\n constructor(config: BrowserViewerThreadManagerConfig) {\n super(config);\n this.browserConfig = config.browserConfig;\n this.onBrowserCreated = config.onBrowserCreated;\n this.onBrowserClosed = config.onBrowserClosed;\n }\n\n /**\n * Check if a thread should use the shared session slot.\n * In shared scope, all threads use the shared session.\n * In thread scope, DEFAULT_THREAD_ID also uses the shared session.\n */\n private usesSharedSlot(threadId: string): boolean {\n return this.scope === 'shared' || threadId === DEFAULT_THREAD_ID;\n }\n\n /**\n * Get the viewer session for a thread, using consistent routing.\n * Handles both shared and thread-scoped sessions.\n */\n private getViewerSession(threadId: string): BrowserViewerSession | null {\n if (this.usesSharedSlot(threadId)) {\n return this.sharedSession;\n }\n return this.threadSessions.get(threadId) ?? null;\n }\n\n // ---------------------------------------------------------------------------\n // Session Storage & Cleanup Helpers\n // ---------------------------------------------------------------------------\n\n /**\n * Store a session in the appropriate slot based on scope.\n * Consolidates session storage logic used by createSession, createSharedSession,\n * createSharedSessionFromCdp, and connectToExternalCdp.\n */\n private storeSession(session: BrowserViewerSession, threadId: string): void {\n if (this.usesSharedSlot(threadId)) {\n this.sharedSession = session;\n this.sessions.set(DEFAULT_THREAD_ID, session);\n this.setSharedManager(session.browser);\n } else {\n this.threadSessions.set(threadId, session);\n this.sessions.set(threadId, session);\n this.threadManagers.set(threadId, session.browser);\n }\n }\n\n /**\n * Clear a session from the appropriate slot based on scope.\n * Must be called BEFORE async cleanup operations to prevent double callbacks\n * from disconnect handlers.\n */\n private clearSessionState(threadId: string): void {\n if (this.usesSharedSlot(threadId)) {\n this.sharedSession = null;\n this.clearSharedManager();\n this.sessions.delete(DEFAULT_THREAD_ID);\n } else {\n this.threadSessions.delete(threadId);\n this.threadManagers.delete(threadId);\n this.sessions.delete(threadId);\n }\n }\n\n /**\n * Clean up a session's resources (CDP session, browser, server).\n * Consolidates cleanup logic used by closeThreadBrowser, closeSharedBrowser,\n * and doDestroySession.\n *\n * @param session - The session to clean up\n * @param threadId - The thread ID (for onBrowserClosed callback)\n */\n private async cleanupSession(session: BrowserViewerSession, threadId: string): Promise<void> {\n // Clear state BEFORE async operations to prevent double callback from disconnect handler\n this.clearSessionState(threadId);\n // Clear cached input CDP session\n this.inputCdpSessions.delete(threadId);\n\n // Detach CDP session\n if (session.cdpSession) {\n try {\n await session.cdpSession.detach();\n } catch {\n // Ignore - session may already be detached\n }\n }\n\n // Close browser connection\n try {\n await session.browser.close();\n } catch {\n // Ignore - browser may already be closed\n }\n\n // Close browser server (kills the Chrome process) - only if we own it\n if (session.browserServer) {\n try {\n await session.browserServer.close();\n } catch {\n // Ignore - server may already be closed\n }\n }\n\n this.onBrowserClosed?.(threadId);\n }\n\n /**\n * Launch a new browser instance and return the components.\n * Consolidates the launch logic shared by createSession and createSharedSession.\n *\n * @param threadId - Thread ID for logging and disconnect handler\n */\n private async launchBrowser(threadId: string): Promise<{\n browserServer: BrowserServer;\n browser: Browser;\n context: BrowserContext;\n cdpSession: CDPSession | null;\n cdpUrl: string | null;\n }> {\n const cdpPort = this.browserConfig.cdpPort ?? 0;\n\n this.logger?.debug?.(`Launching Chrome for thread ${threadId} with remote-debugging-port=${cdpPort}`);\n\n const launchOptions: Parameters<typeof chromium.launchServer>[0] = {\n headless: this.browserConfig.headless,\n args: [`--remote-debugging-port=${cdpPort}`, '--no-first-run', '--no-default-browser-check'],\n };\n\n if (this.browserConfig.executablePath) {\n launchOptions.executablePath = this.browserConfig.executablePath;\n }\n\n // Track partially initialized resources for cleanup on failure\n let browserServer: BrowserServer | null = null;\n let browser: Browser | null = null;\n\n try {\n // Launch server - this starts Chrome\n browserServer = await chromium.launchServer(launchOptions);\n\n // Discover the actual CDP WebSocket URL from Chrome's DevToolsActivePort file\n const cdpUrl = this.discoverCdpUrl(browserServer);\n\n // Connect to the browser via Playwright for screencast/session management\n browser = await chromium.connect(browserServer.wsEndpoint());\n\n // Create context and initial page\n const context = await browser.newContext({\n viewport: this.browserConfig.viewport ?? { width: 1280, height: 720 },\n });\n\n await context.newPage();\n\n // Set up CDP session for active page (used for screencast/input injection)\n const pages = context.pages();\n const cdpSession = pages[0] ? await context.newCDPSession(pages[0]) : null;\n\n // Set up disconnection handlers - multiple events can indicate browser closure:\n // - browserServer.on('close'): fires when Chrome process exits\n // - browser.on('disconnected'): fires when Playwright connection is lost\n // - CDP Target.targetDestroyed: fires when any target (page/context) is destroyed\n let disconnectHandled = false;\n const handleDisconnect = () => {\n if (disconnectHandled) return;\n disconnectHandled = true;\n this.handleBrowserDisconnected(threadId);\n };\n\n // Listen for browser server close (fires when Chrome process exits)\n browserServer.on('close', handleDisconnect);\n // Listen for browser connection lost\n browser.on('disconnected', handleDisconnect);\n\n // Use browser-level CDP session to watch for ALL target destruction\n // Page-level CDP session only sees events for that specific page, but CLI creates its own pages\n // Browser-level session sees all targets across all contexts\n try {\n const browserCdpSession = await browser.newBrowserCDPSession();\n // Enable target discovery to get notified of all targets\n await browserCdpSession.send('Target.setDiscoverTargets', { discover: true });\n\n browserCdpSession.on('Target.targetDestroyed', async () => {\n // When a target is destroyed, check if any page targets remain\n // browser.isConnected() stays true because browserServer keeps Chrome alive,\n // so we need to check for actual page targets instead\n try {\n const { targetInfos } = (await browserCdpSession.send('Target.getTargets')) as {\n targetInfos: Array<{ type: string; url: string }>;\n };\n // Filter to actual page targets (not background pages, service workers, etc.)\n const pageTargets = targetInfos.filter(\n t => t.type === 'page' && !t.url.startsWith('chrome://') && !t.url.startsWith('devtools://'),\n );\n if (pageTargets.length === 0) {\n handleDisconnect();\n }\n } catch {\n // CDP session dead, browser definitely closed\n handleDisconnect();\n }\n });\n\n // Also listen for detached event (fires when CDP connection is lost)\n browserCdpSession.on('Inspector.detached', handleDisconnect);\n } catch {\n // Non-fatal: target watching is a reliability enhancement, not required\n this.logger?.debug?.('Failed to set up browser-level CDP target watching');\n }\n\n return { browserServer, browser, context, cdpSession, cdpUrl };\n } catch (error) {\n // Clean up partially initialized resources\n this.logger?.warn?.(`Failed to launch browser for thread ${threadId}: ${error}`);\n await browser?.close().catch(() => {});\n await browserServer?.close().catch(() => {});\n throw error;\n }\n }\n\n /**\n * Get CDP URL for a specific thread.\n */\n getCdpUrlForThread(threadId?: string): string | null {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n return this.getViewerSession(effectiveThreadId)?.cdpUrl ?? null;\n }\n\n /**\n * Get the active page for a thread.\n */\n async getActivePageForThread(threadId?: string): Promise<Page | null> {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n const session = this.getViewerSession(effectiveThreadId);\n\n if (!session?.context) {\n return null;\n }\n\n return this.resolveActivePage(session.context);\n }\n\n /**\n * Resolve the active page from a browser context.\n * Uses last page (most recently opened) with fallback to first page.\n */\n private resolveActivePage(context: BrowserContext): Page | null {\n const pages = context.pages();\n return pages[pages.length - 1] ?? pages[0] ?? null;\n }\n\n /**\n * Get or create a CDP session for the active page in a thread.\n *\n * CDP sessions are page-scoped, so we create a fresh one for the currently active page\n * rather than caching one that may point to a closed or inactive page.\n */\n async getCdpSessionForThread(threadId?: string): Promise<CDPSession | null> {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n const session = this.getViewerSession(effectiveThreadId);\n\n if (!session?.context) {\n return null;\n }\n\n // Check if browser is still connected - if not, trigger cleanup\n if (session.browser && !session.browser.isConnected()) {\n this.handleBrowserDisconnected(effectiveThreadId);\n return null;\n }\n\n const activePage = this.resolveActivePage(session.context);\n\n if (!activePage) {\n return null;\n }\n\n // Check if we have a cached CDP session for the current active page\n const cached = this.inputCdpSessions.get(effectiveThreadId);\n const currentUrl = activePage.url();\n if (cached && cached.pageUrl === currentUrl) {\n // Reuse cached session if same page\n return cached.session;\n }\n\n // Create a new CDP session for the active page\n try {\n const cdpSession = await session.context.newCDPSession(activePage);\n // Cache it for future input events\n this.inputCdpSessions.set(effectiveThreadId, { session: cdpSession, pageUrl: currentUrl });\n return cdpSession;\n } catch {\n // Page may have been closed between getting pages and creating session\n // This often indicates browser was closed - trigger cleanup\n this.inputCdpSessions.delete(effectiveThreadId);\n this.handleBrowserDisconnected(effectiveThreadId);\n return null;\n }\n }\n\n /**\n * Get the browser context for a thread.\n */\n getContextForThread(threadId?: string): BrowserContext | null {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n return this.getViewerSession(effectiveThreadId)?.context ?? null;\n }\n\n /**\n * Create a fresh CDP session for the active page (not cached).\n * Used by screencast which needs fresh sessions on tab switches.\n */\n async createFreshCdpSession(threadId?: string): Promise<CDPSession | null> {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n const session = this.getViewerSession(effectiveThreadId);\n\n if (!session?.context) {\n return null;\n }\n\n const activePage = this.resolveActivePage(session.context);\n if (!activePage) {\n return null;\n }\n\n try {\n return await session.context.newCDPSession(activePage);\n } catch {\n return null;\n }\n }\n\n /**\n * Create a new session for a thread.\n */\n protected async createSession(threadId: string): Promise<BrowserViewerSession> {\n const savedState = this.getSavedBrowserState(threadId);\n const { browserServer, browser, context, cdpSession, cdpUrl } = await this.launchBrowser(threadId);\n\n const session: BrowserViewerSession = {\n threadId,\n createdAt: Date.now(),\n browserState: savedState,\n browserServer,\n browser,\n context,\n cdpSession,\n cdpUrl,\n };\n\n // Store session using consolidated helper\n this.storeSession(session, threadId);\n\n this.logger?.debug?.(`Chrome launched for thread ${threadId}, CDP URL: ${cdpUrl}`);\n\n // Notify callback\n this.onBrowserCreated?.(browser, threadId, cdpUrl);\n\n return session;\n }\n\n /**\n * Discover the actual CDP WebSocket URL from Chrome's DevToolsActivePort file.\n *\n * Playwright's BrowserServer exposes _userDataDirForTest which points to Chrome's\n * user data directory. Chrome writes a DevToolsActivePort file there containing:\n * Line 1: The debugging port number\n * Line 2: The browser WebSocket path (e.g., /devtools/browser/<guid>)\n *\n * This gives us the real CDP URL that external tools like agent-browser can connect to.\n * Returns null if discovery fails - callers should handle this case.\n */\n private discoverCdpUrl(browserServer: BrowserServer): string | null {\n // Access Playwright's internal user data directory\n const userDataDir = (browserServer as BrowserServer & { _userDataDirForTest?: string })._userDataDirForTest;\n\n if (!userDataDir) {\n this.logger?.warn?.('Could not access browser user data directory');\n return null;\n }\n\n const portFilePath = join(userDataDir, 'DevToolsActivePort');\n\n // Chrome may still be writing the file during startup - retry with a short deadline\n const deadline = Date.now() + 1500;\n while (!existsSync(portFilePath) && Date.now() < deadline) {\n // Use Atomics.wait for a non-blocking ~50ms sleep\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);\n }\n\n if (!existsSync(portFilePath)) {\n this.logger?.warn?.('DevToolsActivePort file not found');\n return null;\n }\n\n try {\n const content = readFileSync(portFilePath, 'utf-8').trim().split('\\n');\n const port = content[0];\n const browserPath = content[1];\n\n if (!port || !browserPath) {\n this.logger?.warn?.('Invalid DevToolsActivePort content');\n return null;\n }\n\n const cdpUrl = `ws://127.0.0.1:${port}${browserPath}`;\n this.logger?.debug?.(`Discovered CDP URL from DevToolsActivePort: ${cdpUrl}`);\n return cdpUrl;\n } catch (error) {\n this.logger?.warn?.('Failed to read DevToolsActivePort file:', error);\n return null;\n }\n }\n\n /**\n * Create a shared session by connecting to an existing browser via CDP URL.\n * Used when BrowserViewer is configured with a cdpUrl to connect to an external browser.\n */\n async createSharedSessionFromCdp(cdpUrl: string): Promise<void> {\n if (this.sharedSession) {\n return; // Already created\n }\n await this.connectToCdp(cdpUrl, DEFAULT_THREAD_ID);\n }\n\n /**\n * Create a shared session (for 'shared' scope).\n */\n async createSharedSession(): Promise<void> {\n if (this.sharedSession) {\n return; // Already created\n }\n\n const { browserServer, browser, context, cdpSession, cdpUrl } = await this.launchBrowser(DEFAULT_THREAD_ID);\n\n const session: BrowserViewerSession = {\n threadId: DEFAULT_THREAD_ID,\n createdAt: Date.now(),\n browserServer,\n browser,\n context,\n cdpSession,\n cdpUrl,\n };\n\n // Store session using consolidated helper\n this.storeSession(session, DEFAULT_THREAD_ID);\n\n this.logger?.debug?.(`Shared Chrome launched, CDP URL: ${cdpUrl}`);\n\n // Notify callbacks\n this.onBrowserCreated?.(browser, DEFAULT_THREAD_ID, cdpUrl);\n this.onSessionCreated?.(session);\n }\n\n /**\n * Handle browser disconnection for a thread.\n */\n private handleBrowserDisconnected(threadId: string): void {\n this.logger?.debug?.(`Browser disconnected for thread ${threadId}`);\n\n // Guard against already-closed session (browser.close() triggers 'disconnected')\n if (!this.getViewerSession(threadId)) return;\n\n // Use consolidated helper for state cleanup\n this.clearSessionState(threadId);\n this.onBrowserClosed?.(threadId);\n }\n\n /**\n * Connect to an external browser via CDP URL for screencast.\n *\n * This is used when an agent is using their own external CDP (e.g., browser-use cloud).\n * We connect Playwright to the external browser to enable screencast without launching\n * our own browser.\n *\n * @param cdpUrl - The external CDP WebSocket URL (wss://... or ws://...)\n * @param threadId - Thread ID to associate the session with\n */\n async connectToExternalCdp(cdpUrl: string, threadId: string): Promise<BrowserViewerSession> {\n // Close any existing session for this thread to avoid leaking browser processes\n if (this.getViewerSession(threadId)) {\n if (this.usesSharedSlot(threadId)) {\n await this.closeSharedBrowser();\n } else {\n await this.closeThreadBrowser(threadId);\n }\n }\n\n return this.connectToCdp(cdpUrl, threadId);\n }\n\n /**\n * Connect to a browser via CDP URL and create a session.\n * Shared implementation for createSharedSessionFromCdp and connectToExternalCdp.\n */\n private async connectToCdp(cdpUrl: string, threadId: string): Promise<BrowserViewerSession> {\n const effectiveThreadId = this.usesSharedSlot(threadId) ? DEFAULT_THREAD_ID : threadId;\n this.logger?.debug?.(`Connecting to CDP for thread ${effectiveThreadId}: ${cdpUrl}`);\n\n let browser: Browser | null = null;\n\n try {\n browser = await chromium.connectOverCDP(cdpUrl);\n\n // Get or create context\n const contexts = browser.contexts();\n const context = contexts[0] ?? (await browser.newContext());\n\n // Get or create page\n let pages = context.pages();\n if (pages.length === 0) {\n // Wait briefly for external browser to create a page, or create one\n await new Promise(resolve => setTimeout(resolve, 500));\n pages = context.pages();\n if (pages.length === 0) {\n await context.newPage();\n pages = context.pages();\n }\n }\n\n // Set up CDP session for active page\n const cdpSession = pages[0] ? await context.newCDPSession(pages[0]) : null;\n\n // Set up disconnection handlers - use effectiveThreadId for consistent lifecycle callbacks\n let disconnectHandled = false;\n const handleDisconnect = () => {\n if (disconnectHandled) return;\n disconnectHandled = true;\n this.handleBrowserDisconnected(effectiveThreadId);\n };\n\n // Listen for context close (fires when browser window is closed manually)\n context.on('close', handleDisconnect);\n // Listen for browser connection lost\n browser.on('disconnected', handleDisconnect);\n\n const session: BrowserViewerSession = {\n threadId: effectiveThreadId,\n createdAt: Date.now(),\n browserServer: null, // We don't own the server for external CDP connections\n browser,\n context,\n cdpSession,\n cdpUrl,\n };\n\n this.storeSession(session, threadId);\n this.logger?.debug?.(`Connected to CDP for thread ${effectiveThreadId}`);\n\n // Notify callbacks\n this.onBrowserCreated?.(browser, effectiveThreadId, cdpUrl);\n this.onSessionCreated?.(session);\n\n return session;\n } catch (error) {\n this.logger?.warn?.(`Failed to connect to CDP: ${error}`);\n await browser?.close().catch(() => {});\n throw error;\n }\n }\n\n /**\n * Close a specific thread's browser.\n */\n async closeThreadBrowser(threadId: string): Promise<void> {\n const session = this.threadSessions.get(threadId);\n if (!session) {\n return;\n }\n await this.cleanupSession(session, threadId);\n }\n\n /**\n * Close the shared browser.\n */\n async closeSharedBrowser(): Promise<void> {\n if (!this.sharedSession) {\n return;\n }\n await this.cleanupSession(this.sharedSession, DEFAULT_THREAD_ID);\n }\n\n /**\n * Close all browsers.\n */\n async closeAll(): Promise<void> {\n // Close all thread browsers\n const threadIds = Array.from(this.threadSessions.keys());\n await Promise.all(threadIds.map(id => this.closeThreadBrowser(id)));\n\n // Close shared browser\n await this.closeSharedBrowser();\n }\n\n /**\n * Get the manager for a session.\n * Required by base class.\n */\n protected getManagerForSession(session: ThreadSession): Browser {\n const viewerSession = session as BrowserViewerSession;\n return viewerSession.browser;\n }\n\n /**\n * Get the shared manager.\n * Required by base class.\n */\n protected getSharedManager(): Browser {\n if (!this.sharedSession) {\n throw new Error('Shared browser not launched. Call createSharedSession() first.');\n }\n return this.sharedSession.browser;\n }\n\n /**\n * Destroy a session and clean up resources.\n * Required by base class.\n */\n protected async doDestroySession(session: ThreadSession): Promise<void> {\n const viewerSession = this.getViewerSession(session.threadId);\n if (!viewerSession) {\n return;\n }\n await this.cleanupSession(viewerSession, session.threadId);\n }\n\n /**\n * Check if browser is running for a thread.\n */\n isBrowserRunning(threadId?: string): boolean {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n return this.getViewerSession(effectiveThreadId) !== null;\n }\n}\n"],"mappings":"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","names":["import_browser"]}

@@ -12,3 +12,3 @@ import { BrowserConfigBase, ThreadManager, ThreadManagerConfig, ThreadSession, MastraBrowser, BrowserState, ScreencastOptions, ScreencastStream, MouseEventParams, KeyboardEventParams } from '@mastra/core/browser';

*/
type CLIProvider = 'agent-browser' | 'browser-use' | 'browse-cli';
type CLIProvider = 'agent-browser' | 'browser-use' | 'browse' | 'browse-cli';
/**

@@ -236,3 +236,3 @@ * Configuration for BrowserViewer.

* Launches Chrome via Playwright and exposes the CDP URL for CLI tools
* (agent-browser, browser-use, browse-cli) to connect as secondary clients.
* (agent-browser, browser-use, and browse) to connect as secondary clients.
*

@@ -239,0 +239,0 @@ * This gives us:

@@ -12,3 +12,3 @@ import { BrowserConfigBase, ThreadManager, ThreadManagerConfig, ThreadSession, MastraBrowser, BrowserState, ScreencastOptions, ScreencastStream, MouseEventParams, KeyboardEventParams } from '@mastra/core/browser';

*/
type CLIProvider = 'agent-browser' | 'browser-use' | 'browse-cli';
type CLIProvider = 'agent-browser' | 'browser-use' | 'browse' | 'browse-cli';
/**

@@ -236,3 +236,3 @@ * Configuration for BrowserViewer.

* Launches Chrome via Playwright and exposes the CDP URL for CLI tools
* (agent-browser, browser-use, browse-cli) to connect as secondary clients.
* (agent-browser, browser-use, and browse) to connect as secondary clients.
*

@@ -239,0 +239,0 @@ * This gives us:

@@ -1,1 +0,1 @@

{"version":3,"sources":["../src/browser-viewer.ts","../src/thread-manager.ts"],"sourcesContent":["/**\n * BrowserViewer - Playwright-managed Chrome for CLI providers\n *\n * Launches Chrome via Playwright and exposes the CDP URL for CLI tools\n * (agent-browser, browser-use, browse-cli) to connect as secondary clients.\n *\n * This gives us:\n * - Direct page-level CDP sessions (fixes screencast sessionId issues)\n * - Full browser lifecycle control\n * - Predictable CDP URL for CLI injection\n * - Thread-scoped browser isolation\n */\n\nimport { MastraBrowser, ScreencastStreamImpl } from '@mastra/core/browser';\nimport type {\n BrowserState,\n BrowserTabState,\n ScreencastOptions,\n ScreencastStream,\n CdpSessionProvider,\n MouseEventParams,\n KeyboardEventParams,\n} from '@mastra/core/browser';\nimport type { Tool } from '@mastra/core/tools';\nimport type { Page } from 'playwright-core';\nimport { BrowserViewerThreadManager } from './thread-manager';\nimport type { BrowserViewerConfig, CLIProvider } from './types';\n\n/**\n * BrowserViewer - CLI provider with Playwright-managed Chrome\n *\n * Use this with Workspace to enable browser automation via CLI tools.\n * The agent uses skills + workspace_execute_command to drive the CLI,\n * while Mastra handles screencast, input injection, and lifecycle.\n *\n * @example\n * ```ts\n * import { Workspace } from '@mastra/core';\n * import { BrowserViewer } from '@mastra/browser-viewer';\n *\n * const workspace = new Workspace({\n * browser: new BrowserViewer({\n * cli: 'agent-browser',\n * headless: false,\n * }),\n * });\n * ```\n */\nexport class BrowserViewer extends MastraBrowser {\n override readonly id: string;\n override readonly name = 'BrowserViewer';\n override readonly provider = 'browser-viewer';\n readonly providerType = 'cli' as const;\n\n /** Which CLI the agent uses */\n readonly cli: CLIProvider;\n\n /** Viewer-specific config (stored for reference) */\n readonly viewerConfig: BrowserViewerConfig;\n\n /** Thread manager for browser sessions */\n declare protected threadManager: BrowserViewerThreadManager;\n\n constructor(config: BrowserViewerConfig) {\n // Default to 'thread' scope (each thread gets its own Chrome)\n // Use 'shared' if connecting to an existing browser\n const effectiveScope = config.cdpUrl ? (config.scope ?? 'shared') : (config.scope ?? 'thread');\n\n // Build base config (exclude CLI-specific options)\n // Use type assertion because BrowserConfig is a discriminated union\n const { cli: _cli, cdpPort: _cdpPort, ...baseConfig } = config;\n\n super({\n ...baseConfig,\n scope: effectiveScope,\n } as any);\n\n this.id = `browser-viewer-${Date.now()}`;\n this.cli = config.cli;\n this.viewerConfig = config;\n\n // Initialize thread manager\n this.threadManager = new BrowserViewerThreadManager({\n scope: effectiveScope,\n browserConfig: { ...config, headless: this.headless },\n logger: this.logger,\n onSessionCreated: session => {\n // Notify listeners so screencast can start for this thread\n this.notifyBrowserReady(session.threadId);\n },\n onBrowserCreated: (_browser, threadId, _cdpUrl) => {\n this.logger?.debug?.(`Browser created for thread ${threadId}`);\n },\n onBrowserClosed: threadId => {\n this.logger?.debug?.(`Browser closed for thread ${threadId}`);\n // Notify base class callbacks so ViewerRegistry gets notified\n this.notifyBrowserClosed(threadId);\n },\n });\n }\n\n // ---------------------------------------------------------------------------\n // CDP URL Access\n // ---------------------------------------------------------------------------\n\n /**\n * Get the CDP WebSocket URL for CLI tools to connect.\n * For thread scope, returns the CDP URL for the specified thread.\n * For shared scope, returns the single shared CDP URL.\n *\n * @param threadId - Thread identifier (optional, uses current thread if not specified)\n * @returns CDP URL or null if browser not running for that thread\n */\n override getCdpUrl(threadId?: string): string | null {\n return this.threadManager.getCdpUrlForThread(threadId ?? this.getCurrentThread());\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle (implements MastraBrowser abstract methods)\n // ---------------------------------------------------------------------------\n\n protected override async doLaunch(): Promise<void> {\n const scope = this.threadManager.getScope();\n const cdpUrl = this.config.cdpUrl;\n\n if (cdpUrl) {\n // Connect mode: connect to existing browser (always shared)\n const url = typeof cdpUrl === 'function' ? await cdpUrl() : cdpUrl;\n await this.connectToExisting(url);\n } else if (scope === 'shared') {\n // Shared mode: launch single browser\n await this.threadManager.createSharedSession();\n }\n // For thread scope, browsers are launched lazily per thread via ensureReady()\n }\n\n protected override async doClose(): Promise<void> {\n await this.threadManager.closeAll();\n }\n\n /**\n * Connect to an existing browser via CDP URL.\n */\n private async connectToExisting(cdpUrl: string): Promise<void> {\n this.logger?.debug?.(`Connecting to existing browser at ${cdpUrl}`);\n\n // Create a shared session from the external CDP connection\n await this.threadManager.createSharedSessionFromCdp(cdpUrl);\n\n this.logger?.debug?.('Connected to existing browser');\n }\n\n /**\n * Ensure browser is ready for the current thread.\n * For thread scope, creates a new browser if needed.\n */\n override async ensureReady(): Promise<void> {\n const scope = this.threadManager.getScope();\n const threadId = this.getCurrentThread();\n\n // For thread scope, create browser for this thread if needed\n if (scope === 'thread' && !this.threadManager.isBrowserRunning(threadId)) {\n await this.threadManager.getManagerForThread(threadId);\n }\n\n await super.ensureReady();\n }\n\n /**\n * Check if browser is running (for current thread in thread scope).\n */\n override isBrowserRunning(threadId?: string): boolean {\n return this.threadManager.isBrowserRunning(threadId ?? this.getCurrentThread());\n }\n\n /**\n * Launch browser, optionally for a specific thread.\n * For thread scope, creates a browser for that thread.\n * For shared scope, launches the single shared browser.\n */\n override async launch(threadId?: string): Promise<void> {\n const scope = this.threadManager.getScope();\n const effectiveThreadId = threadId ?? this.getCurrentThread();\n\n if (scope === 'shared') {\n // For shared scope, use base class launch (handles racing, status, etc.)\n if (!this.threadManager.isBrowserRunning()) {\n await super.launch();\n }\n } else {\n // For thread scope, launch for this specific thread\n if (!this.threadManager.isBrowserRunning(effectiveThreadId)) {\n await this.threadManager.getManagerForThread(effectiveThreadId);\n // Set status to ready so isBrowserRunning() returns true\n // (base class launch() does this, but we bypass it for thread scope)\n this.status = 'ready';\n }\n }\n }\n\n /**\n * Handle browser disconnection.\n * Overrides base class method.\n */\n override handleBrowserDisconnected(): void {\n // Call parent to handle status and notifications\n super.handleBrowserDisconnected();\n }\n\n /**\n * Connect to an external browser via CDP URL for screencast.\n *\n * Use this when an agent is using their own external CDP (e.g., browser-use cloud).\n * Connects Playwright to the external browser to enable screencast without launching\n * our own browser.\n *\n * @param cdpUrl - The external CDP WebSocket URL (wss://... or ws://...)\n * @param threadId - Thread ID to associate the session with\n */\n override async connectToExternalCdp(cdpUrl: string, threadId?: string): Promise<void> {\n const effectiveThreadId = threadId ?? this.getCurrentThread();\n await this.threadManager.connectToExternalCdp(cdpUrl, effectiveThreadId);\n // Mark as ready\n this.status = 'ready';\n }\n\n // ---------------------------------------------------------------------------\n // Browser State (implements MastraBrowser abstract methods)\n // ---------------------------------------------------------------------------\n\n protected override async getActivePage(threadId?: string): Promise<Page | null> {\n return this.threadManager.getActivePageForThread(threadId ?? this.getCurrentThread());\n }\n\n protected override getBrowserStateForThread(threadId?: string): BrowserState | null {\n const context = this.threadManager.getContextForThread(threadId ?? this.getCurrentThread());\n if (!context) {\n return null;\n }\n\n const pages = context.pages();\n // Active page is the last one (most recently opened), consistent with resolveActivePage\n const activeIndex = pages.length > 0 ? pages.length - 1 : 0;\n const tabs: BrowserTabState[] = pages.map((page, index) => ({\n url: page.url(),\n title: '', // Would need async call to get title\n isActive: index === activeIndex,\n }));\n\n return {\n tabs,\n activeTabIndex: activeIndex,\n };\n }\n\n // ---------------------------------------------------------------------------\n // Screencast Support\n // ---------------------------------------------------------------------------\n\n override async startScreencast(options?: ScreencastOptions): Promise<ScreencastStream> {\n const threadId = options?.threadId ?? this.getCurrentThread();\n\n // Create CDP session provider that creates FRESH sessions on each call\n // This is critical for tab switching - when reconnecting, we need a CDP session\n // attached to the CURRENT page, not the original page from launch\n const provider: CdpSessionProvider = {\n getCdpSession: async () => {\n const cdpSession = await this.threadManager.createFreshCdpSession(threadId);\n if (!cdpSession) {\n throw new Error('No browser context available for screencast');\n }\n\n // Return wrapper that implements CdpSessionLike\n return {\n send: async (method: string, params?: Record<string, unknown>) => {\n return cdpSession.send(method as any, params);\n },\n on: (event: string, handler: (params: unknown) => void) => {\n cdpSession.on(event as any, handler);\n },\n off: (event: string, handler: (params: unknown) => void) => {\n cdpSession.off(event as any, handler);\n },\n };\n },\n isBrowserRunning: () => this.isBrowserRunning(threadId),\n };\n\n // Create and start screencast stream\n const stream = new ScreencastStreamImpl(provider, {\n format: options?.format ?? 'jpeg',\n quality: options?.quality ?? 80,\n maxWidth: options?.maxWidth ?? 1280,\n maxHeight: options?.maxHeight ?? 720,\n everyNthFrame: options?.everyNthFrame ?? 1,\n });\n\n // Set up tab change detection - reconnect screencast when tabs change\n const context = this.threadManager.getContextForThread(threadId);\n if (context) {\n // Track all listeners for cleanup\n const pageListeners = new Map<Page, (frame: { url: () => string; parentFrame: () => unknown }) => void>();\n\n // New tab opened - reconnect screencast\n const onNewPage = () => {\n setTimeout(() => {\n if (stream.isActive()) {\n stream.reconnect().catch(() => {});\n }\n }, 100);\n };\n\n // Handler for new pages that sets up listeners\n const onPageCreated = (page: Page) => {\n setupPageListeners(page);\n };\n\n // Set up page close listener for each page\n const setupPageListeners = (page: Page) => {\n page.once('close', () => {\n // Clean up this page's listener\n pageListeners.delete(page);\n setTimeout(() => {\n if (stream.isActive() && context.pages().length > 0) {\n stream.reconnect().catch(() => {});\n }\n }, 100);\n });\n\n // Navigation listener for URL updates\n const onFrameNavigated = (frame: { url: () => string; parentFrame: () => unknown }) => {\n if (!frame.parentFrame()) {\n stream.emitUrl(frame.url());\n }\n };\n page.on('framenavigated', onFrameNavigated);\n pageListeners.set(page, onFrameNavigated);\n };\n\n // Set up listeners\n context.on('page', onNewPage);\n context.on('page', onPageCreated);\n\n // Set up for existing pages\n for (const page of context.pages()) {\n setupPageListeners(page);\n }\n\n // Clean up all listeners on stream stop\n stream.once('stop', () => {\n context.off('page', onNewPage);\n context.off('page', onPageCreated);\n // Remove framenavigated listeners from all pages\n for (const [page, listener] of pageListeners) {\n page.off('framenavigated', listener);\n }\n pageListeners.clear();\n });\n }\n\n await stream.start();\n return stream;\n }\n\n // ---------------------------------------------------------------------------\n // Input Injection\n // ---------------------------------------------------------------------------\n\n override async injectMouseEvent(params: MouseEventParams, threadId?: string): Promise<void> {\n const cdpSession = await this.threadManager.getCdpSessionForThread(threadId ?? this.getCurrentThread());\n if (!cdpSession) {\n throw new Error('CDP session not available for mouse injection');\n }\n\n await cdpSession.send('Input.dispatchMouseEvent', params);\n }\n\n override async injectKeyboardEvent(params: KeyboardEventParams, threadId?: string): Promise<void> {\n const cdpSession = await this.threadManager.getCdpSessionForThread(threadId ?? this.getCurrentThread());\n if (!cdpSession) {\n throw new Error('CDP session not available for keyboard injection');\n }\n\n await cdpSession.send('Input.dispatchKeyEvent', params);\n }\n\n // ---------------------------------------------------------------------------\n // Tools (CLI agents don't use SDK tools - they use workspace commands)\n // ---------------------------------------------------------------------------\n\n getTools(): Record<string, Tool> {\n // CLI agents use workspace_execute_command with CLI skills\n // No SDK tools needed\n return {};\n }\n}\n","/**\n * BrowserViewerThreadManager - Thread scope management for BrowserViewer\n *\n * Manages thread-scoped browser sessions using Playwright to launch\n * separate Chrome instances per thread.\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { ThreadManager, DEFAULT_THREAD_ID } from '@mastra/core/browser';\nimport type { ThreadSession, ThreadManagerConfig } from '@mastra/core/browser';\nimport { chromium } from 'playwright-core';\nimport type { Browser, BrowserContext, BrowserServer, CDPSession, Page } from 'playwright-core';\nimport type { BrowserViewerConfig } from './types';\n\n/**\n * Extended session info for BrowserViewer.\n */\ninterface BrowserViewerSession extends ThreadSession {\n /**\n * Playwright browser server (owns the Chrome process).\n * Null for external CDP connections where we don't own the browser process.\n */\n browserServer: BrowserServer | null;\n /** Playwright browser instance (connected to server) */\n browser: Browser;\n /** Browser context */\n context: BrowserContext;\n /** CDP session for the active page */\n cdpSession: CDPSession | null;\n /** CDP WebSocket URL (null if discovery failed) */\n cdpUrl: string | null;\n}\n\n/**\n * Configuration for BrowserViewerThreadManager.\n */\nexport interface BrowserViewerThreadManagerConfig extends ThreadManagerConfig {\n /** Browser configuration */\n browserConfig: BrowserViewerConfig;\n /** Callback when a browser is created for a thread */\n onBrowserCreated?: (browser: Browser, threadId: string, cdpUrl: string | null) => void;\n /** Callback when a browser is closed for a thread */\n onBrowserClosed?: (threadId: string) => void;\n}\n\n/**\n * Thread manager implementation for BrowserViewer.\n *\n * Supports two scope modes:\n * - 'shared': All threads share one Chrome instance\n * - 'thread': Each thread gets a dedicated Chrome instance\n */\nexport class BrowserViewerThreadManager extends ThreadManager<Browser> {\n private readonly browserConfig: BrowserViewerConfig;\n private readonly onBrowserCreated?: (browser: Browser, threadId: string, cdpUrl: string | null) => void;\n private readonly onBrowserClosed?: (threadId: string) => void;\n\n /** Map of thread ID to session info (for 'thread' scope) */\n private readonly threadSessions = new Map<string, BrowserViewerSession>();\n\n /** Shared session info (for 'shared' scope) */\n private sharedSession: BrowserViewerSession | null = null;\n\n /** Cached CDP sessions for input injection, keyed by threadId */\n private inputCdpSessions = new Map<string, { session: CDPSession; pageUrl: string }>();\n\n constructor(config: BrowserViewerThreadManagerConfig) {\n super(config);\n this.browserConfig = config.browserConfig;\n this.onBrowserCreated = config.onBrowserCreated;\n this.onBrowserClosed = config.onBrowserClosed;\n }\n\n /**\n * Check if a thread should use the shared session slot.\n * In shared scope, all threads use the shared session.\n * In thread scope, DEFAULT_THREAD_ID also uses the shared session.\n */\n private usesSharedSlot(threadId: string): boolean {\n return this.scope === 'shared' || threadId === DEFAULT_THREAD_ID;\n }\n\n /**\n * Get the viewer session for a thread, using consistent routing.\n * Handles both shared and thread-scoped sessions.\n */\n private getViewerSession(threadId: string): BrowserViewerSession | null {\n if (this.usesSharedSlot(threadId)) {\n return this.sharedSession;\n }\n return this.threadSessions.get(threadId) ?? null;\n }\n\n // ---------------------------------------------------------------------------\n // Session Storage & Cleanup Helpers\n // ---------------------------------------------------------------------------\n\n /**\n * Store a session in the appropriate slot based on scope.\n * Consolidates session storage logic used by createSession, createSharedSession,\n * createSharedSessionFromCdp, and connectToExternalCdp.\n */\n private storeSession(session: BrowserViewerSession, threadId: string): void {\n if (this.usesSharedSlot(threadId)) {\n this.sharedSession = session;\n this.sessions.set(DEFAULT_THREAD_ID, session);\n this.setSharedManager(session.browser);\n } else {\n this.threadSessions.set(threadId, session);\n this.sessions.set(threadId, session);\n this.threadManagers.set(threadId, session.browser);\n }\n }\n\n /**\n * Clear a session from the appropriate slot based on scope.\n * Must be called BEFORE async cleanup operations to prevent double callbacks\n * from disconnect handlers.\n */\n private clearSessionState(threadId: string): void {\n if (this.usesSharedSlot(threadId)) {\n this.sharedSession = null;\n this.clearSharedManager();\n this.sessions.delete(DEFAULT_THREAD_ID);\n } else {\n this.threadSessions.delete(threadId);\n this.threadManagers.delete(threadId);\n this.sessions.delete(threadId);\n }\n }\n\n /**\n * Clean up a session's resources (CDP session, browser, server).\n * Consolidates cleanup logic used by closeThreadBrowser, closeSharedBrowser,\n * and doDestroySession.\n *\n * @param session - The session to clean up\n * @param threadId - The thread ID (for onBrowserClosed callback)\n */\n private async cleanupSession(session: BrowserViewerSession, threadId: string): Promise<void> {\n // Clear state BEFORE async operations to prevent double callback from disconnect handler\n this.clearSessionState(threadId);\n // Clear cached input CDP session\n this.inputCdpSessions.delete(threadId);\n\n // Detach CDP session\n if (session.cdpSession) {\n try {\n await session.cdpSession.detach();\n } catch {\n // Ignore - session may already be detached\n }\n }\n\n // Close browser connection\n try {\n await session.browser.close();\n } catch {\n // Ignore - browser may already be closed\n }\n\n // Close browser server (kills the Chrome process) - only if we own it\n if (session.browserServer) {\n try {\n await session.browserServer.close();\n } catch {\n // Ignore - server may already be closed\n }\n }\n\n this.onBrowserClosed?.(threadId);\n }\n\n /**\n * Launch a new browser instance and return the components.\n * Consolidates the launch logic shared by createSession and createSharedSession.\n *\n * @param threadId - Thread ID for logging and disconnect handler\n */\n private async launchBrowser(threadId: string): Promise<{\n browserServer: BrowserServer;\n browser: Browser;\n context: BrowserContext;\n cdpSession: CDPSession | null;\n cdpUrl: string | null;\n }> {\n const cdpPort = this.browserConfig.cdpPort ?? 0;\n\n this.logger?.debug?.(`Launching Chrome for thread ${threadId} with remote-debugging-port=${cdpPort}`);\n\n const launchOptions: Parameters<typeof chromium.launchServer>[0] = {\n headless: this.browserConfig.headless,\n args: [`--remote-debugging-port=${cdpPort}`, '--no-first-run', '--no-default-browser-check'],\n };\n\n if (this.browserConfig.executablePath) {\n launchOptions.executablePath = this.browserConfig.executablePath;\n }\n\n // Track partially initialized resources for cleanup on failure\n let browserServer: BrowserServer | null = null;\n let browser: Browser | null = null;\n\n try {\n // Launch server - this starts Chrome\n browserServer = await chromium.launchServer(launchOptions);\n\n // Discover the actual CDP WebSocket URL from Chrome's DevToolsActivePort file\n const cdpUrl = this.discoverCdpUrl(browserServer);\n\n // Connect to the browser via Playwright for screencast/session management\n browser = await chromium.connect(browserServer.wsEndpoint());\n\n // Create context and initial page\n const context = await browser.newContext({\n viewport: this.browserConfig.viewport ?? { width: 1280, height: 720 },\n });\n\n await context.newPage();\n\n // Set up CDP session for active page (used for screencast/input injection)\n const pages = context.pages();\n const cdpSession = pages[0] ? await context.newCDPSession(pages[0]) : null;\n\n // Set up disconnection handlers - multiple events can indicate browser closure:\n // - browserServer.on('close'): fires when Chrome process exits\n // - browser.on('disconnected'): fires when Playwright connection is lost\n // - CDP Target.targetDestroyed: fires when any target (page/context) is destroyed\n let disconnectHandled = false;\n const handleDisconnect = () => {\n if (disconnectHandled) return;\n disconnectHandled = true;\n this.handleBrowserDisconnected(threadId);\n };\n\n // Listen for browser server close (fires when Chrome process exits)\n browserServer.on('close', handleDisconnect);\n // Listen for browser connection lost\n browser.on('disconnected', handleDisconnect);\n\n // Use browser-level CDP session to watch for ALL target destruction\n // Page-level CDP session only sees events for that specific page, but CLI creates its own pages\n // Browser-level session sees all targets across all contexts\n try {\n const browserCdpSession = await browser.newBrowserCDPSession();\n // Enable target discovery to get notified of all targets\n await browserCdpSession.send('Target.setDiscoverTargets', { discover: true });\n\n browserCdpSession.on('Target.targetDestroyed', async () => {\n // When a target is destroyed, check if any page targets remain\n // browser.isConnected() stays true because browserServer keeps Chrome alive,\n // so we need to check for actual page targets instead\n try {\n const { targetInfos } = (await browserCdpSession.send('Target.getTargets')) as {\n targetInfos: Array<{ type: string; url: string }>;\n };\n // Filter to actual page targets (not background pages, service workers, etc.)\n const pageTargets = targetInfos.filter(\n t => t.type === 'page' && !t.url.startsWith('chrome://') && !t.url.startsWith('devtools://'),\n );\n if (pageTargets.length === 0) {\n handleDisconnect();\n }\n } catch {\n // CDP session dead, browser definitely closed\n handleDisconnect();\n }\n });\n\n // Also listen for detached event (fires when CDP connection is lost)\n browserCdpSession.on('Inspector.detached', handleDisconnect);\n } catch {\n // Non-fatal: target watching is a reliability enhancement, not required\n this.logger?.debug?.('Failed to set up browser-level CDP target watching');\n }\n\n return { browserServer, browser, context, cdpSession, cdpUrl };\n } catch (error) {\n // Clean up partially initialized resources\n this.logger?.warn?.(`Failed to launch browser for thread ${threadId}: ${error}`);\n await browser?.close().catch(() => {});\n await browserServer?.close().catch(() => {});\n throw error;\n }\n }\n\n /**\n * Get CDP URL for a specific thread.\n */\n getCdpUrlForThread(threadId?: string): string | null {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n return this.getViewerSession(effectiveThreadId)?.cdpUrl ?? null;\n }\n\n /**\n * Get the active page for a thread.\n */\n async getActivePageForThread(threadId?: string): Promise<Page | null> {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n const session = this.getViewerSession(effectiveThreadId);\n\n if (!session?.context) {\n return null;\n }\n\n return this.resolveActivePage(session.context);\n }\n\n /**\n * Resolve the active page from a browser context.\n * Uses last page (most recently opened) with fallback to first page.\n */\n private resolveActivePage(context: BrowserContext): Page | null {\n const pages = context.pages();\n return pages[pages.length - 1] ?? pages[0] ?? null;\n }\n\n /**\n * Get or create a CDP session for the active page in a thread.\n *\n * CDP sessions are page-scoped, so we create a fresh one for the currently active page\n * rather than caching one that may point to a closed or inactive page.\n */\n async getCdpSessionForThread(threadId?: string): Promise<CDPSession | null> {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n const session = this.getViewerSession(effectiveThreadId);\n\n if (!session?.context) {\n return null;\n }\n\n // Check if browser is still connected - if not, trigger cleanup\n if (session.browser && !session.browser.isConnected()) {\n this.handleBrowserDisconnected(effectiveThreadId);\n return null;\n }\n\n const activePage = this.resolveActivePage(session.context);\n\n if (!activePage) {\n return null;\n }\n\n // Check if we have a cached CDP session for the current active page\n const cached = this.inputCdpSessions.get(effectiveThreadId);\n const currentUrl = activePage.url();\n if (cached && cached.pageUrl === currentUrl) {\n // Reuse cached session if same page\n return cached.session;\n }\n\n // Create a new CDP session for the active page\n try {\n const cdpSession = await session.context.newCDPSession(activePage);\n // Cache it for future input events\n this.inputCdpSessions.set(effectiveThreadId, { session: cdpSession, pageUrl: currentUrl });\n return cdpSession;\n } catch {\n // Page may have been closed between getting pages and creating session\n // This often indicates browser was closed - trigger cleanup\n this.inputCdpSessions.delete(effectiveThreadId);\n this.handleBrowserDisconnected(effectiveThreadId);\n return null;\n }\n }\n\n /**\n * Get the browser context for a thread.\n */\n getContextForThread(threadId?: string): BrowserContext | null {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n return this.getViewerSession(effectiveThreadId)?.context ?? null;\n }\n\n /**\n * Create a fresh CDP session for the active page (not cached).\n * Used by screencast which needs fresh sessions on tab switches.\n */\n async createFreshCdpSession(threadId?: string): Promise<CDPSession | null> {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n const session = this.getViewerSession(effectiveThreadId);\n\n if (!session?.context) {\n return null;\n }\n\n const activePage = this.resolveActivePage(session.context);\n if (!activePage) {\n return null;\n }\n\n try {\n return await session.context.newCDPSession(activePage);\n } catch {\n return null;\n }\n }\n\n /**\n * Create a new session for a thread.\n */\n protected async createSession(threadId: string): Promise<BrowserViewerSession> {\n const savedState = this.getSavedBrowserState(threadId);\n const { browserServer, browser, context, cdpSession, cdpUrl } = await this.launchBrowser(threadId);\n\n const session: BrowserViewerSession = {\n threadId,\n createdAt: Date.now(),\n browserState: savedState,\n browserServer,\n browser,\n context,\n cdpSession,\n cdpUrl,\n };\n\n // Store session using consolidated helper\n this.storeSession(session, threadId);\n\n this.logger?.debug?.(`Chrome launched for thread ${threadId}, CDP URL: ${cdpUrl}`);\n\n // Notify callback\n this.onBrowserCreated?.(browser, threadId, cdpUrl);\n\n return session;\n }\n\n /**\n * Discover the actual CDP WebSocket URL from Chrome's DevToolsActivePort file.\n *\n * Playwright's BrowserServer exposes _userDataDirForTest which points to Chrome's\n * user data directory. Chrome writes a DevToolsActivePort file there containing:\n * Line 1: The debugging port number\n * Line 2: The browser WebSocket path (e.g., /devtools/browser/<guid>)\n *\n * This gives us the real CDP URL that external tools like agent-browser can connect to.\n * Returns null if discovery fails - callers should handle this case.\n */\n private discoverCdpUrl(browserServer: BrowserServer): string | null {\n // Access Playwright's internal user data directory\n const userDataDir = (browserServer as BrowserServer & { _userDataDirForTest?: string })._userDataDirForTest;\n\n if (!userDataDir) {\n this.logger?.warn?.('Could not access browser user data directory');\n return null;\n }\n\n const portFilePath = join(userDataDir, 'DevToolsActivePort');\n\n // Chrome may still be writing the file during startup - retry with a short deadline\n const deadline = Date.now() + 1500;\n while (!existsSync(portFilePath) && Date.now() < deadline) {\n // Use Atomics.wait for a non-blocking ~50ms sleep\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);\n }\n\n if (!existsSync(portFilePath)) {\n this.logger?.warn?.('DevToolsActivePort file not found');\n return null;\n }\n\n try {\n const content = readFileSync(portFilePath, 'utf-8').trim().split('\\n');\n const port = content[0];\n const browserPath = content[1];\n\n if (!port || !browserPath) {\n this.logger?.warn?.('Invalid DevToolsActivePort content');\n return null;\n }\n\n const cdpUrl = `ws://127.0.0.1:${port}${browserPath}`;\n this.logger?.debug?.(`Discovered CDP URL from DevToolsActivePort: ${cdpUrl}`);\n return cdpUrl;\n } catch (error) {\n this.logger?.warn?.('Failed to read DevToolsActivePort file:', error);\n return null;\n }\n }\n\n /**\n * Create a shared session by connecting to an existing browser via CDP URL.\n * Used when BrowserViewer is configured with a cdpUrl to connect to an external browser.\n */\n async createSharedSessionFromCdp(cdpUrl: string): Promise<void> {\n if (this.sharedSession) {\n return; // Already created\n }\n await this.connectToCdp(cdpUrl, DEFAULT_THREAD_ID);\n }\n\n /**\n * Create a shared session (for 'shared' scope).\n */\n async createSharedSession(): Promise<void> {\n if (this.sharedSession) {\n return; // Already created\n }\n\n const { browserServer, browser, context, cdpSession, cdpUrl } = await this.launchBrowser(DEFAULT_THREAD_ID);\n\n const session: BrowserViewerSession = {\n threadId: DEFAULT_THREAD_ID,\n createdAt: Date.now(),\n browserServer,\n browser,\n context,\n cdpSession,\n cdpUrl,\n };\n\n // Store session using consolidated helper\n this.storeSession(session, DEFAULT_THREAD_ID);\n\n this.logger?.debug?.(`Shared Chrome launched, CDP URL: ${cdpUrl}`);\n\n // Notify callbacks\n this.onBrowserCreated?.(browser, DEFAULT_THREAD_ID, cdpUrl);\n this.onSessionCreated?.(session);\n }\n\n /**\n * Handle browser disconnection for a thread.\n */\n private handleBrowserDisconnected(threadId: string): void {\n this.logger?.debug?.(`Browser disconnected for thread ${threadId}`);\n\n // Guard against already-closed session (browser.close() triggers 'disconnected')\n if (!this.getViewerSession(threadId)) return;\n\n // Use consolidated helper for state cleanup\n this.clearSessionState(threadId);\n this.onBrowserClosed?.(threadId);\n }\n\n /**\n * Connect to an external browser via CDP URL for screencast.\n *\n * This is used when an agent is using their own external CDP (e.g., browser-use cloud).\n * We connect Playwright to the external browser to enable screencast without launching\n * our own browser.\n *\n * @param cdpUrl - The external CDP WebSocket URL (wss://... or ws://...)\n * @param threadId - Thread ID to associate the session with\n */\n async connectToExternalCdp(cdpUrl: string, threadId: string): Promise<BrowserViewerSession> {\n // Close any existing session for this thread to avoid leaking browser processes\n if (this.getViewerSession(threadId)) {\n if (this.usesSharedSlot(threadId)) {\n await this.closeSharedBrowser();\n } else {\n await this.closeThreadBrowser(threadId);\n }\n }\n\n return this.connectToCdp(cdpUrl, threadId);\n }\n\n /**\n * Connect to a browser via CDP URL and create a session.\n * Shared implementation for createSharedSessionFromCdp and connectToExternalCdp.\n */\n private async connectToCdp(cdpUrl: string, threadId: string): Promise<BrowserViewerSession> {\n const effectiveThreadId = this.usesSharedSlot(threadId) ? DEFAULT_THREAD_ID : threadId;\n this.logger?.debug?.(`Connecting to CDP for thread ${effectiveThreadId}: ${cdpUrl}`);\n\n let browser: Browser | null = null;\n\n try {\n browser = await chromium.connectOverCDP(cdpUrl);\n\n // Get or create context\n const contexts = browser.contexts();\n const context = contexts[0] ?? (await browser.newContext());\n\n // Get or create page\n let pages = context.pages();\n if (pages.length === 0) {\n // Wait briefly for external browser to create a page, or create one\n await new Promise(resolve => setTimeout(resolve, 500));\n pages = context.pages();\n if (pages.length === 0) {\n await context.newPage();\n pages = context.pages();\n }\n }\n\n // Set up CDP session for active page\n const cdpSession = pages[0] ? await context.newCDPSession(pages[0]) : null;\n\n // Set up disconnection handlers - use effectiveThreadId for consistent lifecycle callbacks\n let disconnectHandled = false;\n const handleDisconnect = () => {\n if (disconnectHandled) return;\n disconnectHandled = true;\n this.handleBrowserDisconnected(effectiveThreadId);\n };\n\n // Listen for context close (fires when browser window is closed manually)\n context.on('close', handleDisconnect);\n // Listen for browser connection lost\n browser.on('disconnected', handleDisconnect);\n\n const session: BrowserViewerSession = {\n threadId: effectiveThreadId,\n createdAt: Date.now(),\n browserServer: null, // We don't own the server for external CDP connections\n browser,\n context,\n cdpSession,\n cdpUrl,\n };\n\n this.storeSession(session, threadId);\n this.logger?.debug?.(`Connected to CDP for thread ${effectiveThreadId}`);\n\n // Notify callbacks\n this.onBrowserCreated?.(browser, effectiveThreadId, cdpUrl);\n this.onSessionCreated?.(session);\n\n return session;\n } catch (error) {\n this.logger?.warn?.(`Failed to connect to CDP: ${error}`);\n await browser?.close().catch(() => {});\n throw error;\n }\n }\n\n /**\n * Close a specific thread's browser.\n */\n async closeThreadBrowser(threadId: string): Promise<void> {\n const session = this.threadSessions.get(threadId);\n if (!session) {\n return;\n }\n await this.cleanupSession(session, threadId);\n }\n\n /**\n * Close the shared browser.\n */\n async closeSharedBrowser(): Promise<void> {\n if (!this.sharedSession) {\n return;\n }\n await this.cleanupSession(this.sharedSession, DEFAULT_THREAD_ID);\n }\n\n /**\n * Close all browsers.\n */\n async closeAll(): Promise<void> {\n // Close all thread browsers\n const threadIds = Array.from(this.threadSessions.keys());\n await Promise.all(threadIds.map(id => this.closeThreadBrowser(id)));\n\n // Close shared browser\n await this.closeSharedBrowser();\n }\n\n /**\n * Get the manager for a session.\n * Required by base class.\n */\n protected getManagerForSession(session: ThreadSession): Browser {\n const viewerSession = session as BrowserViewerSession;\n return viewerSession.browser;\n }\n\n /**\n * Get the shared manager.\n * Required by base class.\n */\n protected getSharedManager(): Browser {\n if (!this.sharedSession) {\n throw new Error('Shared browser not launched. Call createSharedSession() first.');\n }\n return this.sharedSession.browser;\n }\n\n /**\n * Destroy a session and clean up resources.\n * Required by base class.\n */\n protected async doDestroySession(session: ThreadSession): Promise<void> {\n const viewerSession = this.getViewerSession(session.threadId);\n if (!viewerSession) {\n return;\n }\n await this.cleanupSession(viewerSession, session.threadId);\n }\n\n /**\n * Check if browser is running for a thread.\n */\n isBrowserRunning(threadId?: string): boolean {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n return this.getViewerSession(effectiveThreadId) !== null;\n }\n}\n"],"mappings":"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","names":[]}
{"version":3,"sources":["../src/browser-viewer.ts","../src/thread-manager.ts"],"sourcesContent":["/**\n * BrowserViewer - Playwright-managed Chrome for CLI providers\n *\n * Launches Chrome via Playwright and exposes the CDP URL for CLI tools\n * (agent-browser, browser-use, and browse) to connect as secondary clients.\n *\n * This gives us:\n * - Direct page-level CDP sessions (fixes screencast sessionId issues)\n * - Full browser lifecycle control\n * - Predictable CDP URL for CLI injection\n * - Thread-scoped browser isolation\n */\n\nimport { MastraBrowser, ScreencastStreamImpl } from '@mastra/core/browser';\nimport type {\n BrowserState,\n BrowserTabState,\n ScreencastOptions,\n ScreencastStream,\n CdpSessionProvider,\n MouseEventParams,\n KeyboardEventParams,\n} from '@mastra/core/browser';\nimport type { Tool } from '@mastra/core/tools';\nimport type { Page } from 'playwright-core';\nimport { BrowserViewerThreadManager } from './thread-manager';\nimport type { BrowserViewerConfig, CLIProvider } from './types';\n\n/**\n * BrowserViewer - CLI provider with Playwright-managed Chrome\n *\n * Use this with Workspace to enable browser automation via CLI tools.\n * The agent uses skills + workspace_execute_command to drive the CLI,\n * while Mastra handles screencast, input injection, and lifecycle.\n *\n * @example\n * ```ts\n * import { Workspace } from '@mastra/core';\n * import { BrowserViewer } from '@mastra/browser-viewer';\n *\n * const workspace = new Workspace({\n * browser: new BrowserViewer({\n * cli: 'agent-browser',\n * headless: false,\n * }),\n * });\n * ```\n */\nexport class BrowserViewer extends MastraBrowser {\n override readonly id: string;\n override readonly name = 'BrowserViewer';\n override readonly provider = 'browser-viewer';\n readonly providerType = 'cli' as const;\n\n /** Which CLI the agent uses */\n readonly cli: CLIProvider;\n\n /** Viewer-specific config (stored for reference) */\n readonly viewerConfig: BrowserViewerConfig;\n\n /** Thread manager for browser sessions */\n declare protected threadManager: BrowserViewerThreadManager;\n\n constructor(config: BrowserViewerConfig) {\n // Default to 'thread' scope (each thread gets its own Chrome)\n // Use 'shared' if connecting to an existing browser\n const effectiveScope = config.cdpUrl ? (config.scope ?? 'shared') : (config.scope ?? 'thread');\n\n // Build base config (exclude CLI-specific options)\n // Use type assertion because BrowserConfig is a discriminated union\n const { cli: _cli, cdpPort: _cdpPort, ...baseConfig } = config;\n\n super({\n ...baseConfig,\n scope: effectiveScope,\n } as any);\n\n this.id = `browser-viewer-${Date.now()}`;\n this.cli = config.cli;\n this.viewerConfig = config;\n\n // Initialize thread manager\n this.threadManager = new BrowserViewerThreadManager({\n scope: effectiveScope,\n browserConfig: { ...config, headless: this.headless },\n logger: this.logger,\n onSessionCreated: session => {\n // Notify listeners so screencast can start for this thread\n this.notifyBrowserReady(session.threadId);\n },\n onBrowserCreated: (_browser, threadId, _cdpUrl) => {\n this.logger?.debug?.(`Browser created for thread ${threadId}`);\n },\n onBrowserClosed: threadId => {\n this.logger?.debug?.(`Browser closed for thread ${threadId}`);\n // Notify base class callbacks so ViewerRegistry gets notified\n this.notifyBrowserClosed(threadId);\n },\n });\n }\n\n // ---------------------------------------------------------------------------\n // CDP URL Access\n // ---------------------------------------------------------------------------\n\n /**\n * Get the CDP WebSocket URL for CLI tools to connect.\n * For thread scope, returns the CDP URL for the specified thread.\n * For shared scope, returns the single shared CDP URL.\n *\n * @param threadId - Thread identifier (optional, uses current thread if not specified)\n * @returns CDP URL or null if browser not running for that thread\n */\n override getCdpUrl(threadId?: string): string | null {\n return this.threadManager.getCdpUrlForThread(threadId ?? this.getCurrentThread());\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle (implements MastraBrowser abstract methods)\n // ---------------------------------------------------------------------------\n\n protected override async doLaunch(): Promise<void> {\n const scope = this.threadManager.getScope();\n const cdpUrl = this.config.cdpUrl;\n\n if (cdpUrl) {\n // Connect mode: connect to existing browser (always shared)\n const url = typeof cdpUrl === 'function' ? await cdpUrl() : cdpUrl;\n await this.connectToExisting(url);\n } else if (scope === 'shared') {\n // Shared mode: launch single browser\n await this.threadManager.createSharedSession();\n }\n // For thread scope, browsers are launched lazily per thread via ensureReady()\n }\n\n protected override async doClose(): Promise<void> {\n await this.threadManager.closeAll();\n }\n\n /**\n * Connect to an existing browser via CDP URL.\n */\n private async connectToExisting(cdpUrl: string): Promise<void> {\n this.logger?.debug?.(`Connecting to existing browser at ${cdpUrl}`);\n\n // Create a shared session from the external CDP connection\n await this.threadManager.createSharedSessionFromCdp(cdpUrl);\n\n this.logger?.debug?.('Connected to existing browser');\n }\n\n /**\n * Ensure browser is ready for the current thread.\n * For thread scope, creates a new browser if needed.\n */\n override async ensureReady(): Promise<void> {\n const scope = this.threadManager.getScope();\n const threadId = this.getCurrentThread();\n\n // For thread scope, create browser for this thread if needed\n if (scope === 'thread' && !this.threadManager.isBrowserRunning(threadId)) {\n await this.threadManager.getManagerForThread(threadId);\n }\n\n await super.ensureReady();\n }\n\n /**\n * Check if browser is running (for current thread in thread scope).\n */\n override isBrowserRunning(threadId?: string): boolean {\n return this.threadManager.isBrowserRunning(threadId ?? this.getCurrentThread());\n }\n\n /**\n * Launch browser, optionally for a specific thread.\n * For thread scope, creates a browser for that thread.\n * For shared scope, launches the single shared browser.\n */\n override async launch(threadId?: string): Promise<void> {\n const scope = this.threadManager.getScope();\n const effectiveThreadId = threadId ?? this.getCurrentThread();\n\n if (scope === 'shared') {\n // For shared scope, use base class launch (handles racing, status, etc.)\n if (!this.threadManager.isBrowserRunning()) {\n await super.launch();\n }\n } else {\n // For thread scope, launch for this specific thread\n if (!this.threadManager.isBrowserRunning(effectiveThreadId)) {\n await this.threadManager.getManagerForThread(effectiveThreadId);\n // Set status to ready so isBrowserRunning() returns true\n // (base class launch() does this, but we bypass it for thread scope)\n this.status = 'ready';\n }\n }\n }\n\n /**\n * Handle browser disconnection.\n * Overrides base class method.\n */\n override handleBrowserDisconnected(): void {\n // Call parent to handle status and notifications\n super.handleBrowserDisconnected();\n }\n\n /**\n * Connect to an external browser via CDP URL for screencast.\n *\n * Use this when an agent is using their own external CDP (e.g., browser-use cloud).\n * Connects Playwright to the external browser to enable screencast without launching\n * our own browser.\n *\n * @param cdpUrl - The external CDP WebSocket URL (wss://... or ws://...)\n * @param threadId - Thread ID to associate the session with\n */\n override async connectToExternalCdp(cdpUrl: string, threadId?: string): Promise<void> {\n const effectiveThreadId = threadId ?? this.getCurrentThread();\n await this.threadManager.connectToExternalCdp(cdpUrl, effectiveThreadId);\n // Mark as ready\n this.status = 'ready';\n }\n\n // ---------------------------------------------------------------------------\n // Browser State (implements MastraBrowser abstract methods)\n // ---------------------------------------------------------------------------\n\n protected override async getActivePage(threadId?: string): Promise<Page | null> {\n return this.threadManager.getActivePageForThread(threadId ?? this.getCurrentThread());\n }\n\n protected override getBrowserStateForThread(threadId?: string): BrowserState | null {\n const context = this.threadManager.getContextForThread(threadId ?? this.getCurrentThread());\n if (!context) {\n return null;\n }\n\n const pages = context.pages();\n // Active page is the last one (most recently opened), consistent with resolveActivePage\n const activeIndex = pages.length > 0 ? pages.length - 1 : 0;\n const tabs: BrowserTabState[] = pages.map((page, index) => ({\n url: page.url(),\n title: '', // Would need async call to get title\n isActive: index === activeIndex,\n }));\n\n return {\n tabs,\n activeTabIndex: activeIndex,\n };\n }\n\n // ---------------------------------------------------------------------------\n // Screencast Support\n // ---------------------------------------------------------------------------\n\n override async startScreencast(options?: ScreencastOptions): Promise<ScreencastStream> {\n const threadId = options?.threadId ?? this.getCurrentThread();\n\n // Create CDP session provider that creates FRESH sessions on each call\n // This is critical for tab switching - when reconnecting, we need a CDP session\n // attached to the CURRENT page, not the original page from launch\n const provider: CdpSessionProvider = {\n getCdpSession: async () => {\n const cdpSession = await this.threadManager.createFreshCdpSession(threadId);\n if (!cdpSession) {\n throw new Error('No browser context available for screencast');\n }\n\n // Return wrapper that implements CdpSessionLike\n return {\n send: async (method: string, params?: Record<string, unknown>) => {\n return cdpSession.send(method as any, params);\n },\n on: (event: string, handler: (params: unknown) => void) => {\n cdpSession.on(event as any, handler);\n },\n off: (event: string, handler: (params: unknown) => void) => {\n cdpSession.off(event as any, handler);\n },\n };\n },\n isBrowserRunning: () => this.isBrowserRunning(threadId),\n };\n\n // Create and start screencast stream\n const stream = new ScreencastStreamImpl(provider, {\n format: options?.format ?? 'jpeg',\n quality: options?.quality ?? 80,\n maxWidth: options?.maxWidth ?? 1280,\n maxHeight: options?.maxHeight ?? 720,\n everyNthFrame: options?.everyNthFrame ?? 1,\n });\n\n // Set up tab change detection - reconnect screencast when tabs change\n const context = this.threadManager.getContextForThread(threadId);\n if (context) {\n // Track all listeners for cleanup\n const pageListeners = new Map<Page, (frame: { url: () => string; parentFrame: () => unknown }) => void>();\n\n // New tab opened - reconnect screencast\n const onNewPage = () => {\n setTimeout(() => {\n if (stream.isActive()) {\n stream.reconnect().catch(() => {});\n }\n }, 100);\n };\n\n // Handler for new pages that sets up listeners\n const onPageCreated = (page: Page) => {\n setupPageListeners(page);\n };\n\n // Set up page close listener for each page\n const setupPageListeners = (page: Page) => {\n page.once('close', () => {\n // Clean up this page's listener\n pageListeners.delete(page);\n setTimeout(() => {\n if (stream.isActive() && context.pages().length > 0) {\n stream.reconnect().catch(() => {});\n }\n }, 100);\n });\n\n // Navigation listener for URL updates\n const onFrameNavigated = (frame: { url: () => string; parentFrame: () => unknown }) => {\n if (!frame.parentFrame()) {\n stream.emitUrl(frame.url());\n }\n };\n page.on('framenavigated', onFrameNavigated);\n pageListeners.set(page, onFrameNavigated);\n };\n\n // Set up listeners\n context.on('page', onNewPage);\n context.on('page', onPageCreated);\n\n // Set up for existing pages\n for (const page of context.pages()) {\n setupPageListeners(page);\n }\n\n // Clean up all listeners on stream stop\n stream.once('stop', () => {\n context.off('page', onNewPage);\n context.off('page', onPageCreated);\n // Remove framenavigated listeners from all pages\n for (const [page, listener] of pageListeners) {\n page.off('framenavigated', listener);\n }\n pageListeners.clear();\n });\n }\n\n await stream.start();\n return stream;\n }\n\n // ---------------------------------------------------------------------------\n // Input Injection\n // ---------------------------------------------------------------------------\n\n override async injectMouseEvent(params: MouseEventParams, threadId?: string): Promise<void> {\n const cdpSession = await this.threadManager.getCdpSessionForThread(threadId ?? this.getCurrentThread());\n if (!cdpSession) {\n throw new Error('CDP session not available for mouse injection');\n }\n\n await cdpSession.send('Input.dispatchMouseEvent', params);\n }\n\n override async injectKeyboardEvent(params: KeyboardEventParams, threadId?: string): Promise<void> {\n const cdpSession = await this.threadManager.getCdpSessionForThread(threadId ?? this.getCurrentThread());\n if (!cdpSession) {\n throw new Error('CDP session not available for keyboard injection');\n }\n\n await cdpSession.send('Input.dispatchKeyEvent', params);\n }\n\n // ---------------------------------------------------------------------------\n // Tools (CLI agents don't use SDK tools - they use workspace commands)\n // ---------------------------------------------------------------------------\n\n getTools(): Record<string, Tool> {\n // CLI agents use workspace_execute_command with CLI skills\n // No SDK tools needed\n return {};\n }\n}\n","/**\n * BrowserViewerThreadManager - Thread scope management for BrowserViewer\n *\n * Manages thread-scoped browser sessions using Playwright to launch\n * separate Chrome instances per thread.\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { ThreadManager, DEFAULT_THREAD_ID } from '@mastra/core/browser';\nimport type { ThreadSession, ThreadManagerConfig } from '@mastra/core/browser';\nimport { chromium } from 'playwright-core';\nimport type { Browser, BrowserContext, BrowserServer, CDPSession, Page } from 'playwright-core';\nimport type { BrowserViewerConfig } from './types';\n\n/**\n * Extended session info for BrowserViewer.\n */\ninterface BrowserViewerSession extends ThreadSession {\n /**\n * Playwright browser server (owns the Chrome process).\n * Null for external CDP connections where we don't own the browser process.\n */\n browserServer: BrowserServer | null;\n /** Playwright browser instance (connected to server) */\n browser: Browser;\n /** Browser context */\n context: BrowserContext;\n /** CDP session for the active page */\n cdpSession: CDPSession | null;\n /** CDP WebSocket URL (null if discovery failed) */\n cdpUrl: string | null;\n}\n\n/**\n * Configuration for BrowserViewerThreadManager.\n */\nexport interface BrowserViewerThreadManagerConfig extends ThreadManagerConfig {\n /** Browser configuration */\n browserConfig: BrowserViewerConfig;\n /** Callback when a browser is created for a thread */\n onBrowserCreated?: (browser: Browser, threadId: string, cdpUrl: string | null) => void;\n /** Callback when a browser is closed for a thread */\n onBrowserClosed?: (threadId: string) => void;\n}\n\n/**\n * Thread manager implementation for BrowserViewer.\n *\n * Supports two scope modes:\n * - 'shared': All threads share one Chrome instance\n * - 'thread': Each thread gets a dedicated Chrome instance\n */\nexport class BrowserViewerThreadManager extends ThreadManager<Browser> {\n private readonly browserConfig: BrowserViewerConfig;\n private readonly onBrowserCreated?: (browser: Browser, threadId: string, cdpUrl: string | null) => void;\n private readonly onBrowserClosed?: (threadId: string) => void;\n\n /** Map of thread ID to session info (for 'thread' scope) */\n private readonly threadSessions = new Map<string, BrowserViewerSession>();\n\n /** Shared session info (for 'shared' scope) */\n private sharedSession: BrowserViewerSession | null = null;\n\n /** Cached CDP sessions for input injection, keyed by threadId */\n private inputCdpSessions = new Map<string, { session: CDPSession; pageUrl: string }>();\n\n constructor(config: BrowserViewerThreadManagerConfig) {\n super(config);\n this.browserConfig = config.browserConfig;\n this.onBrowserCreated = config.onBrowserCreated;\n this.onBrowserClosed = config.onBrowserClosed;\n }\n\n /**\n * Check if a thread should use the shared session slot.\n * In shared scope, all threads use the shared session.\n * In thread scope, DEFAULT_THREAD_ID also uses the shared session.\n */\n private usesSharedSlot(threadId: string): boolean {\n return this.scope === 'shared' || threadId === DEFAULT_THREAD_ID;\n }\n\n /**\n * Get the viewer session for a thread, using consistent routing.\n * Handles both shared and thread-scoped sessions.\n */\n private getViewerSession(threadId: string): BrowserViewerSession | null {\n if (this.usesSharedSlot(threadId)) {\n return this.sharedSession;\n }\n return this.threadSessions.get(threadId) ?? null;\n }\n\n // ---------------------------------------------------------------------------\n // Session Storage & Cleanup Helpers\n // ---------------------------------------------------------------------------\n\n /**\n * Store a session in the appropriate slot based on scope.\n * Consolidates session storage logic used by createSession, createSharedSession,\n * createSharedSessionFromCdp, and connectToExternalCdp.\n */\n private storeSession(session: BrowserViewerSession, threadId: string): void {\n if (this.usesSharedSlot(threadId)) {\n this.sharedSession = session;\n this.sessions.set(DEFAULT_THREAD_ID, session);\n this.setSharedManager(session.browser);\n } else {\n this.threadSessions.set(threadId, session);\n this.sessions.set(threadId, session);\n this.threadManagers.set(threadId, session.browser);\n }\n }\n\n /**\n * Clear a session from the appropriate slot based on scope.\n * Must be called BEFORE async cleanup operations to prevent double callbacks\n * from disconnect handlers.\n */\n private clearSessionState(threadId: string): void {\n if (this.usesSharedSlot(threadId)) {\n this.sharedSession = null;\n this.clearSharedManager();\n this.sessions.delete(DEFAULT_THREAD_ID);\n } else {\n this.threadSessions.delete(threadId);\n this.threadManagers.delete(threadId);\n this.sessions.delete(threadId);\n }\n }\n\n /**\n * Clean up a session's resources (CDP session, browser, server).\n * Consolidates cleanup logic used by closeThreadBrowser, closeSharedBrowser,\n * and doDestroySession.\n *\n * @param session - The session to clean up\n * @param threadId - The thread ID (for onBrowserClosed callback)\n */\n private async cleanupSession(session: BrowserViewerSession, threadId: string): Promise<void> {\n // Clear state BEFORE async operations to prevent double callback from disconnect handler\n this.clearSessionState(threadId);\n // Clear cached input CDP session\n this.inputCdpSessions.delete(threadId);\n\n // Detach CDP session\n if (session.cdpSession) {\n try {\n await session.cdpSession.detach();\n } catch {\n // Ignore - session may already be detached\n }\n }\n\n // Close browser connection\n try {\n await session.browser.close();\n } catch {\n // Ignore - browser may already be closed\n }\n\n // Close browser server (kills the Chrome process) - only if we own it\n if (session.browserServer) {\n try {\n await session.browserServer.close();\n } catch {\n // Ignore - server may already be closed\n }\n }\n\n this.onBrowserClosed?.(threadId);\n }\n\n /**\n * Launch a new browser instance and return the components.\n * Consolidates the launch logic shared by createSession and createSharedSession.\n *\n * @param threadId - Thread ID for logging and disconnect handler\n */\n private async launchBrowser(threadId: string): Promise<{\n browserServer: BrowserServer;\n browser: Browser;\n context: BrowserContext;\n cdpSession: CDPSession | null;\n cdpUrl: string | null;\n }> {\n const cdpPort = this.browserConfig.cdpPort ?? 0;\n\n this.logger?.debug?.(`Launching Chrome for thread ${threadId} with remote-debugging-port=${cdpPort}`);\n\n const launchOptions: Parameters<typeof chromium.launchServer>[0] = {\n headless: this.browserConfig.headless,\n args: [`--remote-debugging-port=${cdpPort}`, '--no-first-run', '--no-default-browser-check'],\n };\n\n if (this.browserConfig.executablePath) {\n launchOptions.executablePath = this.browserConfig.executablePath;\n }\n\n // Track partially initialized resources for cleanup on failure\n let browserServer: BrowserServer | null = null;\n let browser: Browser | null = null;\n\n try {\n // Launch server - this starts Chrome\n browserServer = await chromium.launchServer(launchOptions);\n\n // Discover the actual CDP WebSocket URL from Chrome's DevToolsActivePort file\n const cdpUrl = this.discoverCdpUrl(browserServer);\n\n // Connect to the browser via Playwright for screencast/session management\n browser = await chromium.connect(browserServer.wsEndpoint());\n\n // Create context and initial page\n const context = await browser.newContext({\n viewport: this.browserConfig.viewport ?? { width: 1280, height: 720 },\n });\n\n await context.newPage();\n\n // Set up CDP session for active page (used for screencast/input injection)\n const pages = context.pages();\n const cdpSession = pages[0] ? await context.newCDPSession(pages[0]) : null;\n\n // Set up disconnection handlers - multiple events can indicate browser closure:\n // - browserServer.on('close'): fires when Chrome process exits\n // - browser.on('disconnected'): fires when Playwright connection is lost\n // - CDP Target.targetDestroyed: fires when any target (page/context) is destroyed\n let disconnectHandled = false;\n const handleDisconnect = () => {\n if (disconnectHandled) return;\n disconnectHandled = true;\n this.handleBrowserDisconnected(threadId);\n };\n\n // Listen for browser server close (fires when Chrome process exits)\n browserServer.on('close', handleDisconnect);\n // Listen for browser connection lost\n browser.on('disconnected', handleDisconnect);\n\n // Use browser-level CDP session to watch for ALL target destruction\n // Page-level CDP session only sees events for that specific page, but CLI creates its own pages\n // Browser-level session sees all targets across all contexts\n try {\n const browserCdpSession = await browser.newBrowserCDPSession();\n // Enable target discovery to get notified of all targets\n await browserCdpSession.send('Target.setDiscoverTargets', { discover: true });\n\n browserCdpSession.on('Target.targetDestroyed', async () => {\n // When a target is destroyed, check if any page targets remain\n // browser.isConnected() stays true because browserServer keeps Chrome alive,\n // so we need to check for actual page targets instead\n try {\n const { targetInfos } = (await browserCdpSession.send('Target.getTargets')) as {\n targetInfos: Array<{ type: string; url: string }>;\n };\n // Filter to actual page targets (not background pages, service workers, etc.)\n const pageTargets = targetInfos.filter(\n t => t.type === 'page' && !t.url.startsWith('chrome://') && !t.url.startsWith('devtools://'),\n );\n if (pageTargets.length === 0) {\n handleDisconnect();\n }\n } catch {\n // CDP session dead, browser definitely closed\n handleDisconnect();\n }\n });\n\n // Also listen for detached event (fires when CDP connection is lost)\n browserCdpSession.on('Inspector.detached', handleDisconnect);\n } catch {\n // Non-fatal: target watching is a reliability enhancement, not required\n this.logger?.debug?.('Failed to set up browser-level CDP target watching');\n }\n\n return { browserServer, browser, context, cdpSession, cdpUrl };\n } catch (error) {\n // Clean up partially initialized resources\n this.logger?.warn?.(`Failed to launch browser for thread ${threadId}: ${error}`);\n await browser?.close().catch(() => {});\n await browserServer?.close().catch(() => {});\n throw error;\n }\n }\n\n /**\n * Get CDP URL for a specific thread.\n */\n getCdpUrlForThread(threadId?: string): string | null {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n return this.getViewerSession(effectiveThreadId)?.cdpUrl ?? null;\n }\n\n /**\n * Get the active page for a thread.\n */\n async getActivePageForThread(threadId?: string): Promise<Page | null> {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n const session = this.getViewerSession(effectiveThreadId);\n\n if (!session?.context) {\n return null;\n }\n\n return this.resolveActivePage(session.context);\n }\n\n /**\n * Resolve the active page from a browser context.\n * Uses last page (most recently opened) with fallback to first page.\n */\n private resolveActivePage(context: BrowserContext): Page | null {\n const pages = context.pages();\n return pages[pages.length - 1] ?? pages[0] ?? null;\n }\n\n /**\n * Get or create a CDP session for the active page in a thread.\n *\n * CDP sessions are page-scoped, so we create a fresh one for the currently active page\n * rather than caching one that may point to a closed or inactive page.\n */\n async getCdpSessionForThread(threadId?: string): Promise<CDPSession | null> {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n const session = this.getViewerSession(effectiveThreadId);\n\n if (!session?.context) {\n return null;\n }\n\n // Check if browser is still connected - if not, trigger cleanup\n if (session.browser && !session.browser.isConnected()) {\n this.handleBrowserDisconnected(effectiveThreadId);\n return null;\n }\n\n const activePage = this.resolveActivePage(session.context);\n\n if (!activePage) {\n return null;\n }\n\n // Check if we have a cached CDP session for the current active page\n const cached = this.inputCdpSessions.get(effectiveThreadId);\n const currentUrl = activePage.url();\n if (cached && cached.pageUrl === currentUrl) {\n // Reuse cached session if same page\n return cached.session;\n }\n\n // Create a new CDP session for the active page\n try {\n const cdpSession = await session.context.newCDPSession(activePage);\n // Cache it for future input events\n this.inputCdpSessions.set(effectiveThreadId, { session: cdpSession, pageUrl: currentUrl });\n return cdpSession;\n } catch {\n // Page may have been closed between getting pages and creating session\n // This often indicates browser was closed - trigger cleanup\n this.inputCdpSessions.delete(effectiveThreadId);\n this.handleBrowserDisconnected(effectiveThreadId);\n return null;\n }\n }\n\n /**\n * Get the browser context for a thread.\n */\n getContextForThread(threadId?: string): BrowserContext | null {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n return this.getViewerSession(effectiveThreadId)?.context ?? null;\n }\n\n /**\n * Create a fresh CDP session for the active page (not cached).\n * Used by screencast which needs fresh sessions on tab switches.\n */\n async createFreshCdpSession(threadId?: string): Promise<CDPSession | null> {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n const session = this.getViewerSession(effectiveThreadId);\n\n if (!session?.context) {\n return null;\n }\n\n const activePage = this.resolveActivePage(session.context);\n if (!activePage) {\n return null;\n }\n\n try {\n return await session.context.newCDPSession(activePage);\n } catch {\n return null;\n }\n }\n\n /**\n * Create a new session for a thread.\n */\n protected async createSession(threadId: string): Promise<BrowserViewerSession> {\n const savedState = this.getSavedBrowserState(threadId);\n const { browserServer, browser, context, cdpSession, cdpUrl } = await this.launchBrowser(threadId);\n\n const session: BrowserViewerSession = {\n threadId,\n createdAt: Date.now(),\n browserState: savedState,\n browserServer,\n browser,\n context,\n cdpSession,\n cdpUrl,\n };\n\n // Store session using consolidated helper\n this.storeSession(session, threadId);\n\n this.logger?.debug?.(`Chrome launched for thread ${threadId}, CDP URL: ${cdpUrl}`);\n\n // Notify callback\n this.onBrowserCreated?.(browser, threadId, cdpUrl);\n\n return session;\n }\n\n /**\n * Discover the actual CDP WebSocket URL from Chrome's DevToolsActivePort file.\n *\n * Playwright's BrowserServer exposes _userDataDirForTest which points to Chrome's\n * user data directory. Chrome writes a DevToolsActivePort file there containing:\n * Line 1: The debugging port number\n * Line 2: The browser WebSocket path (e.g., /devtools/browser/<guid>)\n *\n * This gives us the real CDP URL that external tools like agent-browser can connect to.\n * Returns null if discovery fails - callers should handle this case.\n */\n private discoverCdpUrl(browserServer: BrowserServer): string | null {\n // Access Playwright's internal user data directory\n const userDataDir = (browserServer as BrowserServer & { _userDataDirForTest?: string })._userDataDirForTest;\n\n if (!userDataDir) {\n this.logger?.warn?.('Could not access browser user data directory');\n return null;\n }\n\n const portFilePath = join(userDataDir, 'DevToolsActivePort');\n\n // Chrome may still be writing the file during startup - retry with a short deadline\n const deadline = Date.now() + 1500;\n while (!existsSync(portFilePath) && Date.now() < deadline) {\n // Use Atomics.wait for a non-blocking ~50ms sleep\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);\n }\n\n if (!existsSync(portFilePath)) {\n this.logger?.warn?.('DevToolsActivePort file not found');\n return null;\n }\n\n try {\n const content = readFileSync(portFilePath, 'utf-8').trim().split('\\n');\n const port = content[0];\n const browserPath = content[1];\n\n if (!port || !browserPath) {\n this.logger?.warn?.('Invalid DevToolsActivePort content');\n return null;\n }\n\n const cdpUrl = `ws://127.0.0.1:${port}${browserPath}`;\n this.logger?.debug?.(`Discovered CDP URL from DevToolsActivePort: ${cdpUrl}`);\n return cdpUrl;\n } catch (error) {\n this.logger?.warn?.('Failed to read DevToolsActivePort file:', error);\n return null;\n }\n }\n\n /**\n * Create a shared session by connecting to an existing browser via CDP URL.\n * Used when BrowserViewer is configured with a cdpUrl to connect to an external browser.\n */\n async createSharedSessionFromCdp(cdpUrl: string): Promise<void> {\n if (this.sharedSession) {\n return; // Already created\n }\n await this.connectToCdp(cdpUrl, DEFAULT_THREAD_ID);\n }\n\n /**\n * Create a shared session (for 'shared' scope).\n */\n async createSharedSession(): Promise<void> {\n if (this.sharedSession) {\n return; // Already created\n }\n\n const { browserServer, browser, context, cdpSession, cdpUrl } = await this.launchBrowser(DEFAULT_THREAD_ID);\n\n const session: BrowserViewerSession = {\n threadId: DEFAULT_THREAD_ID,\n createdAt: Date.now(),\n browserServer,\n browser,\n context,\n cdpSession,\n cdpUrl,\n };\n\n // Store session using consolidated helper\n this.storeSession(session, DEFAULT_THREAD_ID);\n\n this.logger?.debug?.(`Shared Chrome launched, CDP URL: ${cdpUrl}`);\n\n // Notify callbacks\n this.onBrowserCreated?.(browser, DEFAULT_THREAD_ID, cdpUrl);\n this.onSessionCreated?.(session);\n }\n\n /**\n * Handle browser disconnection for a thread.\n */\n private handleBrowserDisconnected(threadId: string): void {\n this.logger?.debug?.(`Browser disconnected for thread ${threadId}`);\n\n // Guard against already-closed session (browser.close() triggers 'disconnected')\n if (!this.getViewerSession(threadId)) return;\n\n // Use consolidated helper for state cleanup\n this.clearSessionState(threadId);\n this.onBrowserClosed?.(threadId);\n }\n\n /**\n * Connect to an external browser via CDP URL for screencast.\n *\n * This is used when an agent is using their own external CDP (e.g., browser-use cloud).\n * We connect Playwright to the external browser to enable screencast without launching\n * our own browser.\n *\n * @param cdpUrl - The external CDP WebSocket URL (wss://... or ws://...)\n * @param threadId - Thread ID to associate the session with\n */\n async connectToExternalCdp(cdpUrl: string, threadId: string): Promise<BrowserViewerSession> {\n // Close any existing session for this thread to avoid leaking browser processes\n if (this.getViewerSession(threadId)) {\n if (this.usesSharedSlot(threadId)) {\n await this.closeSharedBrowser();\n } else {\n await this.closeThreadBrowser(threadId);\n }\n }\n\n return this.connectToCdp(cdpUrl, threadId);\n }\n\n /**\n * Connect to a browser via CDP URL and create a session.\n * Shared implementation for createSharedSessionFromCdp and connectToExternalCdp.\n */\n private async connectToCdp(cdpUrl: string, threadId: string): Promise<BrowserViewerSession> {\n const effectiveThreadId = this.usesSharedSlot(threadId) ? DEFAULT_THREAD_ID : threadId;\n this.logger?.debug?.(`Connecting to CDP for thread ${effectiveThreadId}: ${cdpUrl}`);\n\n let browser: Browser | null = null;\n\n try {\n browser = await chromium.connectOverCDP(cdpUrl);\n\n // Get or create context\n const contexts = browser.contexts();\n const context = contexts[0] ?? (await browser.newContext());\n\n // Get or create page\n let pages = context.pages();\n if (pages.length === 0) {\n // Wait briefly for external browser to create a page, or create one\n await new Promise(resolve => setTimeout(resolve, 500));\n pages = context.pages();\n if (pages.length === 0) {\n await context.newPage();\n pages = context.pages();\n }\n }\n\n // Set up CDP session for active page\n const cdpSession = pages[0] ? await context.newCDPSession(pages[0]) : null;\n\n // Set up disconnection handlers - use effectiveThreadId for consistent lifecycle callbacks\n let disconnectHandled = false;\n const handleDisconnect = () => {\n if (disconnectHandled) return;\n disconnectHandled = true;\n this.handleBrowserDisconnected(effectiveThreadId);\n };\n\n // Listen for context close (fires when browser window is closed manually)\n context.on('close', handleDisconnect);\n // Listen for browser connection lost\n browser.on('disconnected', handleDisconnect);\n\n const session: BrowserViewerSession = {\n threadId: effectiveThreadId,\n createdAt: Date.now(),\n browserServer: null, // We don't own the server for external CDP connections\n browser,\n context,\n cdpSession,\n cdpUrl,\n };\n\n this.storeSession(session, threadId);\n this.logger?.debug?.(`Connected to CDP for thread ${effectiveThreadId}`);\n\n // Notify callbacks\n this.onBrowserCreated?.(browser, effectiveThreadId, cdpUrl);\n this.onSessionCreated?.(session);\n\n return session;\n } catch (error) {\n this.logger?.warn?.(`Failed to connect to CDP: ${error}`);\n await browser?.close().catch(() => {});\n throw error;\n }\n }\n\n /**\n * Close a specific thread's browser.\n */\n async closeThreadBrowser(threadId: string): Promise<void> {\n const session = this.threadSessions.get(threadId);\n if (!session) {\n return;\n }\n await this.cleanupSession(session, threadId);\n }\n\n /**\n * Close the shared browser.\n */\n async closeSharedBrowser(): Promise<void> {\n if (!this.sharedSession) {\n return;\n }\n await this.cleanupSession(this.sharedSession, DEFAULT_THREAD_ID);\n }\n\n /**\n * Close all browsers.\n */\n async closeAll(): Promise<void> {\n // Close all thread browsers\n const threadIds = Array.from(this.threadSessions.keys());\n await Promise.all(threadIds.map(id => this.closeThreadBrowser(id)));\n\n // Close shared browser\n await this.closeSharedBrowser();\n }\n\n /**\n * Get the manager for a session.\n * Required by base class.\n */\n protected getManagerForSession(session: ThreadSession): Browser {\n const viewerSession = session as BrowserViewerSession;\n return viewerSession.browser;\n }\n\n /**\n * Get the shared manager.\n * Required by base class.\n */\n protected getSharedManager(): Browser {\n if (!this.sharedSession) {\n throw new Error('Shared browser not launched. Call createSharedSession() first.');\n }\n return this.sharedSession.browser;\n }\n\n /**\n * Destroy a session and clean up resources.\n * Required by base class.\n */\n protected async doDestroySession(session: ThreadSession): Promise<void> {\n const viewerSession = this.getViewerSession(session.threadId);\n if (!viewerSession) {\n return;\n }\n await this.cleanupSession(viewerSession, session.threadId);\n }\n\n /**\n * Check if browser is running for a thread.\n */\n isBrowserRunning(threadId?: string): boolean {\n const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;\n return this.getViewerSession(effectiveThreadId) !== null;\n }\n}\n"],"mappings":"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","names":[]}
{
"name": "@mastra/browser-viewer",
"version": "0.1.1",
"version": "0.1.2-alpha.0",
"description": "Playwright-based browser viewer for Mastra CLI providers",

@@ -27,3 +27,3 @@ "type": "module",

"dependencies": {
"playwright-core": "^1.52.0",
"playwright-core": "^1.60.0",
"typed-emitter": "^2.1.0"

@@ -35,10 +35,10 @@ },

"@vitest/ui": "4.1.5",
"eslint": "^9.27.0",
"eslint": "^10.4.1",
"tsup": "^8.5.0",
"typescript": "^5.9.3",
"typescript": "^6.0.3",
"vitest": "4.1.5",
"zod": "^4.3.6",
"@internal/lint": "0.0.86",
"@internal/types-builder": "0.0.61",
"@mastra/core": "1.28.0"
"zod": "^4.4.3",
"@internal/lint": "0.0.103",
"@internal/types-builder": "0.0.78",
"@mastra/core": "1.42.0-alpha.4"
},

@@ -45,0 +45,0 @@ "peerDependencies": {

+16
-18

@@ -7,8 +7,8 @@ # @mastra/browser-viewer

`@mastra/browser-viewer` provides `BrowserViewer`, which launches Chrome via Playwright and exposes the CDP URL for CLI tools (agent-browser, browser-use, browse-cli) to connect. This gives you:
`@mastra/browser-viewer` provides `BrowserViewer`, which launches Chrome via Playwright and exposes the CDP URL for CLI tools (agent-browser, browser-use, or browse) to connect. This gives you:
- **Full screencast support** — Direct page-level CDP sessions
- **Input injection** — Mouse and keyboard events work correctly
- **Browser lifecycle control** — Browser starts/stops with the server
- **CLI flexibility** — Agent uses skills + workspace commands to drive any CLI
- **Full screencast support**: Direct page-level CDP sessions
- **Input injection**: Mouse and keyboard events work correctly
- **Browser lifecycle control**: Browser starts/stops with the server
- **CLI flexibility**: Agent uses skills + workspace commands to drive any CLI

@@ -19,4 +19,2 @@ ## Installation

npm install @mastra/browser-viewer
# or
pnpm add @mastra/browser-viewer
```

@@ -77,10 +75,10 @@

| Option | Type | Default | Description |
| ---------------- | -------------------------------------------------- | ---------- | ------------------------------------------------ |
| `cli` | `'agent-browser' \| 'browser-use' \| 'browse-cli'` | Required | Which CLI the agent uses |
| `cdpUrl` | `string` | - | Connect to existing browser instead of launching |
| `headless` | `boolean` | `true` | Run browser in headless mode |
| `cdpPort` | `number` | `0` (auto) | Port for Chrome remote debugging |
| `viewport` | `{ width, height }` | `1280x720` | Browser viewport size |
| `executablePath` | `string` | - | Path to Chrome executable |
| Option | Type | Default | Description |
| ---------------- | -------------------------------------------------------------- | ---------- | ------------------------------------------------ |
| `cli` | `'agent-browser' \| 'browser-use' \| 'browse' \| 'browse-cli'` | Required | Which CLI the agent uses |
| `cdpUrl` | `string` | - | Connect to existing browser instead of launching |
| `headless` | `boolean` | `true` | Run browser in headless mode |
| `cdpPort` | `number` | `0` (auto) | Port for Chrome remote debugging |
| `viewport` | `{ width, height }` | `1280x720` | Browser viewport size |
| `executablePath` | `string` | - | Path to Chrome executable |

@@ -97,5 +95,5 @@ ## How It Works

- **agent-browser** — Vercel's browser automation CLI (`--cdp <port>`)
- **browser-use** — Python-based browser automation (`--cdp-url <url>`)
- **browse-cli** — Browserbase's Stagehand CLI (`--ws <url>`)
- **agent-browser**: Vercel's browser automation CLI (`--cdp <port>`)
- **browser-use**: Python-based browser automation (`--cdp-url <url>`)
- **browse**: Browserbase's CLI (`--ws <url>`)

@@ -102,0 +100,0 @@ ## License