import { ToolExecutor } from '@/common/tool-handler'; import type { ToolResult } from '@/common/tool-handler'; import { TIMEOUTS, ERROR_MESSAGES } from '@/common/constants'; const PING_TIMEOUT_MS = 300; /** * Base class for browser tool executors */ export abstract class BaseBrowserToolExecutor implements ToolExecutor { abstract name: string; abstract execute(args: any): Promise; /** * Inject content script into tab */ protected async injectContentScript( tabId: number, files: string[], injectImmediately = false, world: 'MAIN' | 'ISOLATED' = 'ISOLATED', allFrames: boolean = false, frameIds?: number[], ): Promise { console.log(`Injecting ${files.join(', ')} into tab ${tabId}`); // check if script is already injected try { const pingFrameId = frameIds?.[0]; const response = await Promise.race([ typeof pingFrameId === 'number' ? chrome.tabs.sendMessage( tabId, { action: `${this.name}_ping` }, { frameId: pingFrameId }, ) : chrome.tabs.sendMessage(tabId, { action: `${this.name}_ping` }), new Promise((_, reject) => setTimeout( () => reject(new Error(`${this.name} Ping action to tab ${tabId} timed out`)), PING_TIMEOUT_MS, ), ), ]); if (response && response.status === 'pong') { console.log( `pong received for action '${this.name}' in tab ${tabId}. Assuming script is active.`, ); return; } else { console.warn(`Unexpected ping response in tab ${tabId}:`, response); } } catch (error) { console.error( `ping content script failed: ${error instanceof Error ? error.message : String(error)}`, ); } try { const target: { tabId: number; allFrames?: boolean; frameIds?: number[] } = { tabId }; if (frameIds && frameIds.length > 0) { target.frameIds = frameIds; } else if (allFrames) { target.allFrames = true; } await chrome.scripting.executeScript({ target, files, injectImmediately, world, } as any); console.log(`'${files.join(', ')}' injection successful for tab ${tabId}`); } catch (injectionError) { const errorMessage = injectionError instanceof Error ? injectionError.message : String(injectionError); console.error( `Content script '${files.join(', ')}' injection failed for tab ${tabId}: ${errorMessage}`, ); throw new Error( `${ERROR_MESSAGES.TOOL_EXECUTION_FAILED}: Failed to inject content script in tab ${tabId}: ${errorMessage}`, ); } } /** * Send message to tab */ protected async sendMessageToTab(tabId: number, message: any, frameId?: number): Promise { try { const response = typeof frameId === 'number' ? await chrome.tabs.sendMessage(tabId, message, { frameId }) : await chrome.tabs.sendMessage(tabId, message); if (response && response.error) { throw new Error(String(response.error)); } return response; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error( `Error sending message to tab ${tabId} for action ${message?.action || 'unknown'}: ${errorMessage}`, ); if (error instanceof Error) { throw error; } throw new Error(errorMessage); } } /** * Try to get an existing tab by id. Returns null when not found. */ protected async tryGetTab(tabId?: number): Promise { if (typeof tabId !== 'number') return null; try { return await chrome.tabs.get(tabId); } catch { return null; } } /** * Get the active tab in the current window. Throws when not found. */ protected async getActiveTabOrThrow(): Promise { const [active] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!active || !active.id) throw new Error('Active tab not found'); return active; } /** * Optionally focus window and/or activate tab. Defaults preserve current behavior * when caller sets activate/focus flags explicitly. */ protected async ensureFocus( tab: chrome.tabs.Tab, options: { activate?: boolean; focusWindow?: boolean } = {}, ): Promise { const activate = options.activate === true; const focusWindow = options.focusWindow === true; if (focusWindow && typeof tab.windowId === 'number') { await chrome.windows.update(tab.windowId, { focused: true }); } if (activate && typeof tab.id === 'number') { await chrome.tabs.update(tab.id, { active: true }); } } /** * Get the active tab. When windowId provided, search within that window; otherwise currentWindow. */ protected async getActiveTabInWindow(windowId?: number): Promise { if (typeof windowId === 'number') { const tabs = await chrome.tabs.query({ active: true, windowId }); return tabs && tabs[0] ? tabs[0] : null; } const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); return tabs && tabs[0] ? tabs[0] : null; } /** * Same as getActiveTabInWindow, but throws if not found. */ protected async getActiveTabOrThrowInWindow(windowId?: number): Promise { const tab = await this.getActiveTabInWindow(windowId); if (!tab || !tab.id) throw new Error('Active tab not found'); return tab; } }