nicer logging of returned output

This commit is contained in:
Tommy D. Rossi
2026-01-26 12:46:36 +01:00
parent 905288a790
commit 4fa618134d
+112 -96
View File
@@ -61,6 +61,7 @@ const EXTENSION_NOT_CONNECTED_ERROR = `The Playwriter Chrome extension is not co
const NO_TABS_ENABLED_ERROR = `No browser tabs have Playwriter enabled. Click the extension icon on at least one tab to enable it.` const NO_TABS_ENABLED_ERROR = `No browser tabs have Playwriter enabled. Click the extension icon on at least one tab to enable it.`
const MAX_LOGS_PER_PAGE = 5000 const MAX_LOGS_PER_PAGE = 5000
const DEFAULT_PLAYWRIGHT_TIMEOUT = 10_000
const ALLOWED_MODULES = new Set([ const ALLOWED_MODULES = new Set([
'path', 'node:path', 'path', 'node:path',
@@ -113,22 +114,28 @@ function isRegExp(value: any): value is RegExp {
) )
} }
function isPromise(value: any): value is Promise<unknown> {
return typeof value === 'object' && value !== null && typeof value.then === 'function'
}
export class PlaywrightExecutor { export class PlaywrightExecutor {
private isConnected = false private isConnected = false
private page: Page | null = null private page: Page | null = null
private browser: Browser | null = null private browser: Browser | null = null
private context: BrowserContext | null = null private context: BrowserContext | null = null
private userState: Record<string, any> = {} private userState: Record<string, any> = {}
private browserLogs: Map<string, string[]> = new Map() private browserLogs: Map<string, string[]> = new Map()
private lastSnapshots: WeakMap<Page, string> = new WeakMap() private lastSnapshots: WeakMap<Page, string> = new WeakMap()
private cdpSessionCache: WeakMap<Page, CDPSession> = new WeakMap() private cdpSessionCache: WeakMap<Page, CDPSession> = new WeakMap()
private scopedFs: ScopedFS private scopedFs: ScopedFS
private sandboxedRequire: NodeRequire private sandboxedRequire: NodeRequire
private cdpConfig: CdpConfig private cdpConfig: CdpConfig
private logger: ExecutorLogger private logger: ExecutorLogger
constructor(options: ExecutorOptions) { constructor(options: ExecutorOptions) {
this.cdpConfig = options.cdpConfig this.cdpConfig = options.cdpConfig
this.logger = options.logger || { log: console.log, error: console.error } this.logger = options.logger || { log: console.log, error: console.error }
@@ -136,7 +143,7 @@ export class PlaywrightExecutor {
this.scopedFs = new ScopedFS(options.cwd ? [options.cwd, '/tmp', os.tmpdir()] : undefined) this.scopedFs = new ScopedFS(options.cwd ? [options.cwd, '/tmp', os.tmpdir()] : undefined)
this.sandboxedRequire = this.createSandboxedRequire(require) this.sandboxedRequire = this.createSandboxedRequire(require)
} }
private createSandboxedRequire(originalRequire: NodeRequire): NodeRequire { private createSandboxedRequire(originalRequire: NodeRequire): NodeRequire {
const scopedFs = this.scopedFs const scopedFs = this.scopedFs
const sandboxedRequire = ((id: string) => { const sandboxedRequire = ((id: string) => {
@@ -153,15 +160,15 @@ export class PlaywrightExecutor {
} }
return originalRequire(id) return originalRequire(id)
}) as NodeRequire }) as NodeRequire
sandboxedRequire.resolve = originalRequire.resolve sandboxedRequire.resolve = originalRequire.resolve
sandboxedRequire.cache = originalRequire.cache sandboxedRequire.cache = originalRequire.cache
sandboxedRequire.extensions = originalRequire.extensions sandboxedRequire.extensions = originalRequire.extensions
sandboxedRequire.main = originalRequire.main sandboxedRequire.main = originalRequire.main
return sandboxedRequire return sandboxedRequire
} }
private async setDeviceScaleFactorForMacOS(context: BrowserContext): Promise<void> { private async setDeviceScaleFactorForMacOS(context: BrowserContext): Promise<void> {
if (os.platform() !== 'darwin') { if (os.platform() !== 'darwin') {
return return
@@ -172,7 +179,7 @@ export class PlaywrightExecutor {
} }
options.deviceScaleFactor = 2 options.deviceScaleFactor = 2
} }
private async preserveSystemColorScheme(context: BrowserContext): Promise<void> { private async preserveSystemColorScheme(context: BrowserContext): Promise<void> {
const options = (context as any)._options const options = (context as any)._options
if (!options) { if (!options) {
@@ -182,38 +189,38 @@ export class PlaywrightExecutor {
options.reducedMotion = 'no-override' options.reducedMotion = 'no-override'
options.forcedColors = 'no-override' options.forcedColors = 'no-override'
} }
private clearUserState() { private clearUserState() {
Object.keys(this.userState).forEach((key) => delete this.userState[key]) Object.keys(this.userState).forEach((key) => delete this.userState[key])
} }
private clearConnectionState() { private clearConnectionState() {
this.isConnected = false this.isConnected = false
this.browser = null this.browser = null
this.page = null this.page = null
this.context = null this.context = null
} }
private setupPageConsoleListener(page: Page) { private setupPageConsoleListener(page: Page) {
const targetId = (page as any)._guid as string | undefined const targetId = (page as any)._guid as string | undefined
if (!targetId) { if (!targetId) {
return return
} }
if (!this.browserLogs.has(targetId)) { if (!this.browserLogs.has(targetId)) {
this.browserLogs.set(targetId, []) this.browserLogs.set(targetId, [])
} }
page.on('framenavigated', (frame) => { page.on('framenavigated', (frame) => {
if (frame === page.mainFrame()) { if (frame === page.mainFrame()) {
this.browserLogs.set(targetId, []) this.browserLogs.set(targetId, [])
} }
}) })
page.on('close', () => { page.on('close', () => {
this.browserLogs.delete(targetId) this.browserLogs.delete(targetId)
}) })
page.on('console', (msg) => { page.on('console', (msg) => {
try { try {
const logEntry = `[${msg.type()}] ${msg.text()}` const logEntry = `[${msg.type()}] ${msg.text()}`
@@ -230,7 +237,7 @@ export class PlaywrightExecutor {
} }
}) })
} }
private async checkExtensionStatus(): Promise<{ connected: boolean; activeTargets: number }> { private async checkExtensionStatus(): Promise<{ connected: boolean; activeTargets: number }> {
const { host = '127.0.0.1', port = 19988 } = this.cdpConfig const { host = '127.0.0.1', port = 19988 } = this.cdpConfig
try { try {
@@ -245,58 +252,60 @@ export class PlaywrightExecutor {
return { connected: false, activeTargets: 0 } return { connected: false, activeTargets: 0 }
} }
} }
private async ensureConnection(): Promise<{ browser: Browser; page: Page }> { private async ensureConnection(): Promise<{ browser: Browser; page: Page }> {
if (this.isConnected && this.browser && this.page) { if (this.isConnected && this.browser && this.page) {
return { browser: this.browser, page: this.page } return { browser: this.browser, page: this.page }
} }
// Check extension status first to provide better error messages // Check extension status first to provide better error messages
const extensionStatus = await this.checkExtensionStatus() const extensionStatus = await this.checkExtensionStatus()
if (!extensionStatus.connected) { if (!extensionStatus.connected) {
throw new Error(EXTENSION_NOT_CONNECTED_ERROR) throw new Error(EXTENSION_NOT_CONNECTED_ERROR)
} }
// Generate a fresh unique URL for each Playwright connection // Generate a fresh unique URL for each Playwright connection
const cdpUrl = getCdpUrl(this.cdpConfig) const cdpUrl = getCdpUrl(this.cdpConfig)
const browser = await chromium.connectOverCDP(cdpUrl) const browser = await chromium.connectOverCDP(cdpUrl)
browser.on('disconnected', () => { browser.on('disconnected', () => {
this.logger.log('Browser disconnected, clearing connection state') this.logger.log('Browser disconnected, clearing connection state')
this.clearConnectionState() this.clearConnectionState()
}) })
const contexts = browser.contexts() const contexts = browser.contexts()
const context = contexts.length > 0 ? contexts[0] : await browser.newContext() const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
context.on('page', (page) => { context.on('page', (page) => {
this.setupPageConsoleListener(page) this.setupPageConsoleListener(page)
}) })
const pages = context.pages() const pages = context.pages()
if (pages.length === 0) { if (pages.length === 0) {
throw new Error(NO_TABS_ENABLED_ERROR) throw new Error(NO_TABS_ENABLED_ERROR)
} }
const page = pages[0] const page = pages[0]
context.setDefaultTimeout(DEFAULT_PLAYWRIGHT_TIMEOUT)
context.pages().forEach((p) => this.setupPageConsoleListener(p)) context.pages().forEach((p) => this.setupPageConsoleListener(p))
await this.preserveSystemColorScheme(context) await this.preserveSystemColorScheme(context)
await this.setDeviceScaleFactorForMacOS(context) await this.setDeviceScaleFactorForMacOS(context)
this.browser = browser this.browser = browser
this.page = page this.page = page
this.context = context this.context = context
this.isConnected = true this.isConnected = true
return { browser, page } return { browser, page }
} }
private async getCurrentPage(timeout = 10000): Promise<Page> { private async getCurrentPage(timeout = 10000): Promise<Page> {
if (this.page && !this.page.isClosed()) { if (this.page && !this.page.isClosed()) {
return this.page return this.page
} }
if (this.browser) { if (this.browser) {
const contexts = this.browser.contexts() const contexts = this.browser.contexts()
if (contexts.length > 0) { if (contexts.length > 0) {
@@ -309,10 +318,10 @@ export class PlaywrightExecutor {
} }
} }
} }
throw new Error(NO_TABS_ENABLED_ERROR) throw new Error(NO_TABS_ENABLED_ERROR)
} }
async reset(): Promise<{ page: Page; context: BrowserContext }> { async reset(): Promise<{ page: Page; context: BrowserContext }> {
if (this.browser) { if (this.browser) {
try { try {
@@ -321,54 +330,56 @@ export class PlaywrightExecutor {
this.logger.error('Error closing browser:', e) this.logger.error('Error closing browser:', e)
} }
} }
this.clearConnectionState() this.clearConnectionState()
this.clearUserState() this.clearUserState()
// Check extension status first to provide better error messages // Check extension status first to provide better error messages
const extensionStatus = await this.checkExtensionStatus() const extensionStatus = await this.checkExtensionStatus()
if (!extensionStatus.connected) { if (!extensionStatus.connected) {
throw new Error(EXTENSION_NOT_CONNECTED_ERROR) throw new Error(EXTENSION_NOT_CONNECTED_ERROR)
} }
// Generate a fresh unique URL for each Playwright connection // Generate a fresh unique URL for each Playwright connection
const cdpUrl = getCdpUrl(this.cdpConfig) const cdpUrl = getCdpUrl(this.cdpConfig)
const browser = await chromium.connectOverCDP(cdpUrl) const browser = await chromium.connectOverCDP(cdpUrl)
browser.on('disconnected', () => { browser.on('disconnected', () => {
this.logger.log('Browser disconnected, clearing connection state') this.logger.log('Browser disconnected, clearing connection state')
this.clearConnectionState() this.clearConnectionState()
}) })
const contexts = browser.contexts() const contexts = browser.contexts()
const context = contexts.length > 0 ? contexts[0] : await browser.newContext() const context = contexts.length > 0 ? contexts[0] : await browser.newContext()
context.on('page', (page) => { context.on('page', (page) => {
this.setupPageConsoleListener(page) this.setupPageConsoleListener(page)
}) })
const pages = context.pages() const pages = context.pages()
if (pages.length === 0) { if (pages.length === 0) {
throw new Error(NO_TABS_ENABLED_ERROR) throw new Error(NO_TABS_ENABLED_ERROR)
} }
const page = pages[0] const page = pages[0]
context.setDefaultTimeout(DEFAULT_PLAYWRIGHT_TIMEOUT)
context.pages().forEach((p) => this.setupPageConsoleListener(p)) context.pages().forEach((p) => this.setupPageConsoleListener(p))
await this.preserveSystemColorScheme(context) await this.preserveSystemColorScheme(context)
await this.setDeviceScaleFactorForMacOS(context) await this.setDeviceScaleFactorForMacOS(context)
this.browser = browser this.browser = browser
this.page = page this.page = page
this.context = context this.context = context
this.isConnected = true this.isConnected = true
return { page, context } return { page, context }
} }
async execute(code: string, timeout = 10000): Promise<ExecuteResult> { async execute(code: string, timeout = 10000): Promise<ExecuteResult> {
const consoleLogs: Array<{ method: string; args: any[] }> = [] const consoleLogs: Array<{ method: string; args: any[] }> = []
const formatConsoleLogs = (logs: Array<{ method: string; args: any[] }>, prefix = 'Console output') => { const formatConsoleLogs = (logs: Array<{ method: string; args: any[] }>, prefix = 'Console output') => {
if (logs.length === 0) { if (logs.length === 0) {
return '' return ''
@@ -384,14 +395,14 @@ export class PlaywrightExecutor {
}) })
return text + '\n' return text + '\n'
} }
try { try {
await this.ensureConnection() await this.ensureConnection()
const page = await this.getCurrentPage(timeout) const page = await this.getCurrentPage(timeout)
const context = this.context || page.context() const context = this.context || page.context()
this.logger.log('Executing code:', code) this.logger.log('Executing code:', code)
const customConsole = { const customConsole = {
log: (...args: any[]) => { consoleLogs.push({ method: 'log', args }) }, log: (...args: any[]) => { consoleLogs.push({ method: 'log', args }) },
info: (...args: any[]) => { consoleLogs.push({ method: 'info', args }) }, info: (...args: any[]) => { consoleLogs.push({ method: 'info', args }) },
@@ -399,7 +410,7 @@ export class PlaywrightExecutor {
error: (...args: any[]) => { consoleLogs.push({ method: 'error', args }) }, error: (...args: any[]) => { consoleLogs.push({ method: 'error', args }) },
debug: (...args: any[]) => { consoleLogs.push({ method: 'debug', args }) }, debug: (...args: any[]) => { consoleLogs.push({ method: 'debug', args }) },
} }
const accessibilitySnapshot = async (options: { const accessibilitySnapshot = async (options: {
page: Page page: Page
search?: string | RegExp search?: string | RegExp
@@ -414,7 +425,7 @@ export class PlaywrightExecutor {
const sanitizedStr = rawStr.toWellFormed?.() ?? rawStr const sanitizedStr = rawStr.toWellFormed?.() ?? rawStr
// Apply format transformation // Apply format transformation
const snapshotStr = formatSnapshot(sanitizedStr, format, this.logger) const snapshotStr = formatSnapshot(sanitizedStr, format, this.logger)
if (showDiffSinceLastCall) { if (showDiffSinceLastCall) {
const previousSnapshot = this.lastSnapshots.get(targetPage) const previousSnapshot = this.lastSnapshots.get(targetPage)
if (!previousSnapshot) { if (!previousSnapshot) {
@@ -427,13 +438,13 @@ export class PlaywrightExecutor {
} }
return patch return patch
} }
this.lastSnapshots.set(targetPage, snapshotStr) this.lastSnapshots.set(targetPage, snapshotStr)
if (!search) { if (!search) {
return snapshotStr return snapshotStr
} }
const lines = snapshotStr.split('\n') const lines = snapshotStr.split('\n')
const matchIndices: number[] = [] const matchIndices: number[] = []
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
@@ -444,11 +455,11 @@ export class PlaywrightExecutor {
if (matchIndices.length >= 10) break if (matchIndices.length >= 10) break
} }
} }
if (matchIndices.length === 0) { if (matchIndices.length === 0) {
return 'No matches found' return 'No matches found'
} }
const CONTEXT_LINES = 5 const CONTEXT_LINES = 5
const includedLines = new Set<number>() const includedLines = new Set<number>()
for (const idx of matchIndices) { for (const idx of matchIndices) {
@@ -458,7 +469,7 @@ export class PlaywrightExecutor {
includedLines.add(i) includedLines.add(i)
} }
} }
const sortedIndices = [...includedLines].sort((a, b) => a - b) const sortedIndices = [...includedLines].sort((a, b) => a - b)
const result: string[] = [] const result: string[] = []
for (let i = 0; i < sortedIndices.length; i++) { for (let i = 0; i < sortedIndices.length; i++) {
@@ -472,7 +483,7 @@ export class PlaywrightExecutor {
} }
throw new Error('accessibilitySnapshot is not available on this page') throw new Error('accessibilitySnapshot is not available on this page')
} }
const getLocatorStringForElement = async (element: any) => { const getLocatorStringForElement = async (element: any) => {
if (!element || typeof element.evaluate !== 'function') { if (!element || typeof element.evaluate !== 'function') {
throw new Error('getLocatorStringForElement: argument must be a Playwright Locator or ElementHandle') throw new Error('getLocatorStringForElement: argument must be a Playwright Locator or ElementHandle')
@@ -492,17 +503,17 @@ export class PlaywrightExecutor {
return toLocator(result.selector, 'javascript') return toLocator(result.selector, 'javascript')
}) })
} }
const getPageTargetId = async (p: Page): Promise<string> => { const getPageTargetId = async (p: Page): Promise<string> => {
const guid = (p as any)._guid const guid = (p as any)._guid
if (guid) return guid if (guid) return guid
throw new Error('Could not get page identifier: _guid not available') throw new Error('Could not get page identifier: _guid not available')
} }
const getLatestLogs = async (options?: { page?: Page; count?: number; search?: string | RegExp }) => { const getLatestLogs = async (options?: { page?: Page; count?: number; search?: string | RegExp }) => {
const { page: filterPage, count, search } = options || {} const { page: filterPage, count, search } = options || {}
let allLogs: string[] = [] let allLogs: string[] = []
if (filterPage) { if (filterPage) {
const targetId = await getPageTargetId(filterPage) const targetId = await getPageTargetId(filterPage)
const pageLogs = this.browserLogs.get(targetId) || [] const pageLogs = this.browserLogs.get(targetId) || []
@@ -512,7 +523,7 @@ export class PlaywrightExecutor {
allLogs.push(...pageLogs) allLogs.push(...pageLogs)
} }
} }
if (search) { if (search) {
const matchIndices: number[] = [] const matchIndices: number[] = []
for (let i = 0; i < allLogs.length; i++) { for (let i = 0; i < allLogs.length; i++) {
@@ -520,7 +531,7 @@ export class PlaywrightExecutor {
const isMatch = typeof search === 'string' ? log.includes(search) : isRegExp(search) && search.test(log) const isMatch = typeof search === 'string' ? log.includes(search) : isRegExp(search) && search.test(log)
if (isMatch) matchIndices.push(i) if (isMatch) matchIndices.push(i)
} }
const CONTEXT_LINES = 5 const CONTEXT_LINES = 5
const includedIndices = new Set<number>() const includedIndices = new Set<number>()
for (const idx of matchIndices) { for (const idx of matchIndices) {
@@ -530,7 +541,7 @@ export class PlaywrightExecutor {
includedIndices.add(i) includedIndices.add(i)
} }
} }
const sortedIndices = [...includedIndices].sort((a, b) => a - b) const sortedIndices = [...includedIndices].sort((a, b) => a - b)
const result: string[] = [] const result: string[] = []
for (let i = 0; i < sortedIndices.length; i++) { for (let i = 0; i < sortedIndices.length; i++) {
@@ -542,14 +553,14 @@ export class PlaywrightExecutor {
} }
allLogs = result allLogs = result
} }
return count !== undefined ? allLogs.slice(-count) : allLogs return count !== undefined ? allLogs.slice(-count) : allLogs
} }
const clearAllLogs = () => { const clearAllLogs = () => {
this.browserLogs.clear() this.browserLogs.clear()
} }
const getCDPSession = async (options: { page: Page }) => { const getCDPSession = async (options: { page: Page }) => {
const cached = this.cdpSessionCache.get(options.page) const cached = this.cdpSessionCache.get(options.page)
if (cached) return cached if (cached) return cached
@@ -559,22 +570,22 @@ export class PlaywrightExecutor {
this.cdpSessionCache.set(options.page, session) this.cdpSessionCache.set(options.page, session)
return session return session
} }
const createDebugger = (options: { cdp: ICDPSession }) => new Debugger(options) const createDebugger = (options: { cdp: ICDPSession }) => new Debugger(options)
const createEditor = (options: { cdp: ICDPSession }) => new Editor(options) const createEditor = (options: { cdp: ICDPSession }) => new Editor(options)
const getStylesForLocatorFn = async (options: { locator: any }) => { const getStylesForLocatorFn = async (options: { locator: any }) => {
const cdp = await getCDPSession({ page: options.locator.page() }) const cdp = await getCDPSession({ page: options.locator.page() })
return getStylesForLocator({ locator: options.locator, cdp }) return getStylesForLocator({ locator: options.locator, cdp })
} }
const getReactSourceFn = async (options: { locator: any }) => { const getReactSourceFn = async (options: { locator: any }) => {
const cdp = await getCDPSession({ page: options.locator.page() }) const cdp = await getCDPSession({ page: options.locator.page() })
return getReactSource({ locator: options.locator, cdp }) return getReactSource({ locator: options.locator, cdp })
} }
const screenshotCollector: ScreenshotResult[] = [] const screenshotCollector: ScreenshotResult[] = []
const screenshotWithAccessibilityLabelsFn = async (options: { page: Page; interactiveOnly?: boolean }) => { const screenshotWithAccessibilityLabelsFn = async (options: { page: Page; interactiveOnly?: boolean }) => {
return screenshotWithAccessibilityLabels({ return screenshotWithAccessibilityLabels({
...options, ...options,
@@ -585,9 +596,9 @@ export class PlaywrightExecutor {
}, },
}) })
} }
const self = this const self = this
let vmContextObj: any = { let vmContextObj: any = {
page, page,
context, context,
@@ -616,54 +627,59 @@ export class PlaywrightExecutor {
import: (specifier: string) => import(specifier), import: (specifier: string) => import(specifier),
...usefulGlobals, ...usefulGlobals,
} }
const vmContext = vm.createContext(vmContextObj) const vmContext = vm.createContext(vmContextObj)
const wrappedCode = `(async () => { ${code} })()` const wrappedCode = `(async () => { ${code} })()`
const hasExplicitReturn = /\breturn\b/.test(code)
const result = await Promise.race([ const result = await Promise.race([
vm.runInContext(wrappedCode, vmContext, { timeout, displayErrors: true }), vm.runInContext(wrappedCode, vmContext, { timeout, displayErrors: true }),
new Promise((_, reject) => setTimeout(() => reject(new CodeExecutionTimeoutError(timeout)), timeout)), new Promise((_, reject) => setTimeout(() => reject(new CodeExecutionTimeoutError(timeout)), timeout)),
]) ])
let responseText = formatConsoleLogs(consoleLogs) let responseText = formatConsoleLogs(consoleLogs)
if (result !== undefined) { // Only show return value if user explicitly used return
responseText += 'Return value:\n' if (hasExplicitReturn) {
if (typeof result === 'string') { const resolvedResult = isPromise(result) ? await result : result
responseText += result if (resolvedResult !== undefined) {
} else { const formatted = util.inspect(resolvedResult, { depth: 4, colors: false, maxArrayLength: 100, breakLength: 80 })
responseText += JSON.stringify(result, null, 2) if (formatted.trim()) {
responseText += `[return value] ${formatted}\n`
}
} }
} else if (consoleLogs.length === 0) {
responseText += 'Code executed successfully (no output)'
} }
if (!responseText.trim()) {
responseText = 'Code executed successfully (no output)'
}
for (const screenshot of screenshotCollector) { for (const screenshot of screenshotCollector) {
responseText += `\nScreenshot saved to: ${screenshot.path}\n` responseText += `\nScreenshot saved to: ${screenshot.path}\n`
responseText += `Labels shown: ${screenshot.labelCount}\n\n` responseText += `Labels shown: ${screenshot.labelCount}\n\n`
responseText += `Accessibility snapshot:\n${screenshot.snapshot}\n` responseText += `Accessibility snapshot:\n${screenshot.snapshot}\n`
} }
const MAX_LENGTH = 6000 const MAX_LENGTH = 6000
let finalText = responseText.trim() let finalText = responseText.trim()
if (finalText.length > MAX_LENGTH) { if (finalText.length > MAX_LENGTH) {
finalText = finalText.slice(0, MAX_LENGTH) + finalText = finalText.slice(0, MAX_LENGTH) +
`\n\n[Truncated to ${MAX_LENGTH} characters. Better manage your logs or paginate them to read the full logs]` `\n\n[Truncated to ${MAX_LENGTH} characters. Better manage your logs or paginate them to read the full logs]`
} }
const images = screenshotCollector.map((s) => ({ data: s.base64, mimeType: s.mimeType })) const images = screenshotCollector.map((s) => ({ data: s.base64, mimeType: s.mimeType }))
return { text: finalText, images, isError: false } return { text: finalText, images, isError: false }
} catch (error: any) { } catch (error: any) {
const errorStack = error.stack || error.message const errorStack = error.stack || error.message
const isTimeoutError = error instanceof CodeExecutionTimeoutError || error.name === 'TimeoutError' const isTimeoutError = error instanceof CodeExecutionTimeoutError || error.name === 'TimeoutError'
this.logger.error('Error in execute:', errorStack) this.logger.error('Error in execute:', errorStack)
const logsText = formatConsoleLogs(consoleLogs, 'Console output (before error)') const logsText = formatConsoleLogs(consoleLogs, 'Console output (before error)')
const resetHint = isTimeoutError ? '' : const resetHint = isTimeoutError ? '' :
'\n\n[HINT: If this is an internal Playwright error, page/browser closed, or connection issue, call reset to reconnect.]' '\n\n[HINT: If this is an internal Playwright error, page/browser closed, or connection issue, call reset to reconnect.]'
return { return {
text: `${logsText}\nError executing code: ${error.message}\n${errorStack}${resetHint}`, text: `${logsText}\nError executing code: ${error.message}\n${errorStack}${resetHint}`,
images: [], images: [],
@@ -671,7 +687,7 @@ export class PlaywrightExecutor {
} }
} }
} }
/** Get info about current connection state */ /** Get info about current connection state */
getStatus(): { connected: boolean; pageUrl: string | null; pagesCount: number } { getStatus(): { connected: boolean; pageUrl: string | null; pagesCount: number } {
return { return {
@@ -680,7 +696,7 @@ export class PlaywrightExecutor {
pagesCount: this.context?.pages().length || 0, pagesCount: this.context?.pages().length || 0,
} }
} }
/** Get keys of user-defined state */ /** Get keys of user-defined state */
getStateKeys(): string[] { getStateKeys(): string[] {
return Object.keys(this.userState) return Object.keys(this.userState)
@@ -694,12 +710,12 @@ export class ExecutorManager {
private executors = new Map<string, PlaywrightExecutor>() private executors = new Map<string, PlaywrightExecutor>()
private cdpConfig: CdpConfig private cdpConfig: CdpConfig
private logger: ExecutorLogger private logger: ExecutorLogger
constructor(options: { cdpConfig: CdpConfig; logger?: ExecutorLogger }) { constructor(options: { cdpConfig: CdpConfig; logger?: ExecutorLogger }) {
this.cdpConfig = options.cdpConfig this.cdpConfig = options.cdpConfig
this.logger = options.logger || { log: console.log, error: console.error } this.logger = options.logger || { log: console.log, error: console.error }
} }
getExecutor(sessionId: string, cwd?: string): PlaywrightExecutor { getExecutor(sessionId: string, cwd?: string): PlaywrightExecutor {
let executor = this.executors.get(sessionId) let executor = this.executors.get(sessionId)
if (!executor) { if (!executor) {
@@ -712,11 +728,11 @@ export class ExecutorManager {
} }
return executor return executor
} }
deleteExecutor(sessionId: string): boolean { deleteExecutor(sessionId: string): boolean {
return this.executors.delete(sessionId) return this.executors.delete(sessionId)
} }
listSessions(): Array<{ id: string; stateKeys: string[] }> { listSessions(): Array<{ id: string; stateKeys: string[] }> {
return [...this.executors.entries()].map(([id, executor]) => { return [...this.executors.entries()].map(([id, executor]) => {
return { return {