From 1a0c533cb4694c8931fd435d7c6349dba5995af3 Mon Sep 17 00:00:00 2001 From: Simon <10131203+gaomeng1900@users.noreply.github.com> Date: Tue, 20 Jan 2026 19:03:15 +0800 Subject: [PATCH] fix: lost control of tab when window inactive --- .../src/agent/RemotePageController.ts | 42 ++-- packages/extension/src/messaging/rpc.ts | 182 +++++++++--------- 2 files changed, 121 insertions(+), 103 deletions(-) diff --git a/packages/extension/src/agent/RemotePageController.ts b/packages/extension/src/agent/RemotePageController.ts index 0566f3f..82e3bf2 100644 --- a/packages/extension/src/agent/RemotePageController.ts +++ b/packages/extension/src/agent/RemotePageController.ts @@ -11,7 +11,7 @@ import type { ScrollHorizontallyOptions, ScrollOptions, } from '../messaging/protocol' -import { rpcClient } from '../messaging/rpc' +import { type RPCClient, createRPCClient } from '../messaging/rpc' /** * RemotePageController is a proxy that implements the PageController interface. @@ -21,27 +21,39 @@ import { rpcClient } from '../messaging/rpc' * though events in the remote context are not currently bridged. */ export class RemotePageController extends EventTarget { + private rpc: RPCClient + + constructor() { + super() + // Capture the active tab ID at construction time to avoid issues when tab loses focus + const tabIdPromise = chrome.tabs.query({ active: true, currentWindow: true }).then(([tab]) => { + if (!tab?.id) throw new Error('No active tab found') + return tab.id + }) + this.rpc = createRPCClient(tabIdPromise) + } + // ======= State Queries ======= /** * Get current page URL */ async getCurrentUrl(): Promise { - return rpcClient.getCurrentUrl() + return this.rpc.getCurrentUrl() } /** * Get last tree update timestamp */ async getLastUpdateTime(): Promise { - return rpcClient.getLastUpdateTime() + return this.rpc.getLastUpdateTime() } /** * Get structured browser state for LLM consumption. */ async getBrowserState(): Promise { - return rpcClient.getBrowserState() + return this.rpc.getBrowserState() } // ======= DOM Tree Operations ======= @@ -50,14 +62,14 @@ export class RemotePageController extends EventTarget { * Update DOM tree, returns simplified HTML for LLM. */ async updateTree(): Promise { - return rpcClient.updateTree() + return this.rpc.updateTree() } /** * Clean up all element highlights */ async cleanUpHighlights(): Promise { - return rpcClient.cleanUpHighlights() + return this.rpc.cleanUpHighlights() } // ======= Element Actions ======= @@ -66,42 +78,42 @@ export class RemotePageController extends EventTarget { * Click element by index */ async clickElement(index: number): Promise { - return rpcClient.clickElement(index) + return this.rpc.clickElement(index) } /** * Input text into element by index */ async inputText(index: number, text: string): Promise { - return rpcClient.inputText(index, text) + return this.rpc.inputText(index, text) } /** * Select dropdown option by index and option text */ async selectOption(index: number, optionText: string): Promise { - return rpcClient.selectOption(index, optionText) + return this.rpc.selectOption(index, optionText) } /** * Scroll vertically */ async scroll(options: ScrollOptions): Promise { - return rpcClient.scroll(options) + return this.rpc.scroll(options) } /** * Scroll horizontally */ async scrollHorizontally(options: ScrollHorizontallyOptions): Promise { - return rpcClient.scrollHorizontally(options) + return this.rpc.scrollHorizontally(options) } /** * Execute arbitrary JavaScript on the page */ async executeJavascript(script: string): Promise { - return rpcClient.executeJavascript(script) + return this.rpc.executeJavascript(script) } // ======= Mask Operations ======= @@ -110,21 +122,21 @@ export class RemotePageController extends EventTarget { * Show the visual mask overlay. */ async showMask(): Promise { - return rpcClient.showMask() + return this.rpc.showMask() } /** * Hide the visual mask overlay. */ async hideMask(): Promise { - return rpcClient.hideMask() + return this.rpc.hideMask() } /** * Dispose and clean up resources */ dispose(): void { - rpcClient.dispose().catch(() => { + this.rpc.dispose().catch(() => { // Ignore errors on dispose }) } diff --git a/packages/extension/src/messaging/rpc.ts b/packages/extension/src/messaging/rpc.ts index 184cd9a..528dcf0 100644 --- a/packages/extension/src/messaging/rpc.ts +++ b/packages/extension/src/messaging/rpc.ts @@ -13,96 +13,102 @@ import type { } from './protocol' /** - * Get the active tab ID for the current sidepanel context. - * In MV3, we need to explicitly target the tab. + * Create an RPC client bound to a specific tab. + * The tabId is captured at creation time to ensure messages are sent to the correct tab + * even if the user switches tabs or the page loses focus. */ -async function getActiveTabId(): Promise { - const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }) - if (!tab?.id) { - throw new Error('No active tab found') +export function createRPCClient(tabIdPromise: Promise): RPCClient { + return { + // State queries + async getCurrentUrl(): Promise { + const tabId = await tabIdPromise + return pageControllerRPC.sendMessage('rpc:getCurrentUrl', undefined, tabId) + }, + + async getLastUpdateTime(): Promise { + const tabId = await tabIdPromise + return pageControllerRPC.sendMessage('rpc:getLastUpdateTime', undefined, tabId) + }, + + async getBrowserState(): Promise { + const tabId = await tabIdPromise + return pageControllerRPC.sendMessage('rpc:getBrowserState', undefined, tabId) + }, + + // DOM operations + async updateTree(): Promise { + const tabId = await tabIdPromise + return pageControllerRPC.sendMessage('rpc:updateTree', undefined, tabId) + }, + + async cleanUpHighlights(): Promise { + const tabId = await tabIdPromise + return pageControllerRPC.sendMessage('rpc:cleanUpHighlights', undefined, tabId) + }, + + // Element actions + async clickElement(index: number): Promise { + const tabId = await tabIdPromise + return pageControllerRPC.sendMessage('rpc:clickElement', index, tabId) + }, + + async inputText(index: number, text: string): Promise { + const tabId = await tabIdPromise + return pageControllerRPC.sendMessage('rpc:inputText', { index, text }, tabId) + }, + + async selectOption(index: number, optionText: string): Promise { + const tabId = await tabIdPromise + return pageControllerRPC.sendMessage('rpc:selectOption', { index, optionText }, tabId) + }, + + async scroll(options: ScrollOptions): Promise { + const tabId = await tabIdPromise + return pageControllerRPC.sendMessage('rpc:scroll', options, tabId) + }, + + async scrollHorizontally(options: ScrollHorizontallyOptions): Promise { + const tabId = await tabIdPromise + return pageControllerRPC.sendMessage('rpc:scrollHorizontally', options, tabId) + }, + + async executeJavascript(script: string): Promise { + const tabId = await tabIdPromise + return pageControllerRPC.sendMessage('rpc:executeJavascript', script, tabId) + }, + + // Mask operations + async showMask(): Promise { + const tabId = await tabIdPromise + return pageControllerRPC.sendMessage('rpc:showMask', undefined, tabId) + }, + + async hideMask(): Promise { + const tabId = await tabIdPromise + return pageControllerRPC.sendMessage('rpc:hideMask', undefined, tabId) + }, + + // Lifecycle + async dispose(): Promise { + const tabId = await tabIdPromise + return pageControllerRPC.sendMessage('rpc:dispose', undefined, tabId) + }, } - return tab.id } -/** - * RPC client for calling PageController methods in ContentScript. - * Each method sends a message and waits for the response. - */ -export const rpcClient = { - // State queries - async getCurrentUrl(): Promise { - const tabId = await getActiveTabId() - return pageControllerRPC.sendMessage('rpc:getCurrentUrl', undefined, tabId) - }, - - async getLastUpdateTime(): Promise { - const tabId = await getActiveTabId() - return pageControllerRPC.sendMessage('rpc:getLastUpdateTime', undefined, tabId) - }, - - async getBrowserState(): Promise { - const tabId = await getActiveTabId() - return pageControllerRPC.sendMessage('rpc:getBrowserState', undefined, tabId) - }, - - // DOM operations - async updateTree(): Promise { - const tabId = await getActiveTabId() - return pageControllerRPC.sendMessage('rpc:updateTree', undefined, tabId) - }, - - async cleanUpHighlights(): Promise { - const tabId = await getActiveTabId() - return pageControllerRPC.sendMessage('rpc:cleanUpHighlights', undefined, tabId) - }, - - // Element actions - async clickElement(index: number): Promise { - const tabId = await getActiveTabId() - return pageControllerRPC.sendMessage('rpc:clickElement', index, tabId) - }, - - async inputText(index: number, text: string): Promise { - const tabId = await getActiveTabId() - return pageControllerRPC.sendMessage('rpc:inputText', { index, text }, tabId) - }, - - async selectOption(index: number, optionText: string): Promise { - const tabId = await getActiveTabId() - return pageControllerRPC.sendMessage('rpc:selectOption', { index, optionText }, tabId) - }, - - async scroll(options: ScrollOptions): Promise { - const tabId = await getActiveTabId() - return pageControllerRPC.sendMessage('rpc:scroll', options, tabId) - }, - - async scrollHorizontally(options: ScrollHorizontallyOptions): Promise { - const tabId = await getActiveTabId() - return pageControllerRPC.sendMessage('rpc:scrollHorizontally', options, tabId) - }, - - async executeJavascript(script: string): Promise { - const tabId = await getActiveTabId() - return pageControllerRPC.sendMessage('rpc:executeJavascript', script, tabId) - }, - - // Mask operations - async showMask(): Promise { - const tabId = await getActiveTabId() - return pageControllerRPC.sendMessage('rpc:showMask', undefined, tabId) - }, - - async hideMask(): Promise { - const tabId = await getActiveTabId() - return pageControllerRPC.sendMessage('rpc:hideMask', undefined, tabId) - }, - - // Lifecycle - async dispose(): Promise { - const tabId = await getActiveTabId() - return pageControllerRPC.sendMessage('rpc:dispose', undefined, tabId) - }, +export interface RPCClient { + getCurrentUrl(): Promise + getLastUpdateTime(): Promise + getBrowserState(): Promise + updateTree(): Promise + cleanUpHighlights(): Promise + clickElement(index: number): Promise + inputText(index: number, text: string): Promise + selectOption(index: number, optionText: string): Promise + scroll(options: ScrollOptions): Promise + scrollHorizontally(options: ScrollHorizontallyOptions): Promise + executeJavascript(script: string): Promise + showMask(): Promise + hideMask(): Promise + dispose(): Promise } - -export type RPCClient = typeof rpcClient