fix: lost control of tab when window inactive
This commit is contained in:
@@ -11,7 +11,7 @@ import type {
|
|||||||
ScrollHorizontallyOptions,
|
ScrollHorizontallyOptions,
|
||||||
ScrollOptions,
|
ScrollOptions,
|
||||||
} from '../messaging/protocol'
|
} from '../messaging/protocol'
|
||||||
import { rpcClient } from '../messaging/rpc'
|
import { type RPCClient, createRPCClient } from '../messaging/rpc'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RemotePageController is a proxy that implements the PageController interface.
|
* 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.
|
* though events in the remote context are not currently bridged.
|
||||||
*/
|
*/
|
||||||
export class RemotePageController extends EventTarget {
|
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 =======
|
// ======= State Queries =======
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current page URL
|
* Get current page URL
|
||||||
*/
|
*/
|
||||||
async getCurrentUrl(): Promise<string> {
|
async getCurrentUrl(): Promise<string> {
|
||||||
return rpcClient.getCurrentUrl()
|
return this.rpc.getCurrentUrl()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get last tree update timestamp
|
* Get last tree update timestamp
|
||||||
*/
|
*/
|
||||||
async getLastUpdateTime(): Promise<number> {
|
async getLastUpdateTime(): Promise<number> {
|
||||||
return rpcClient.getLastUpdateTime()
|
return this.rpc.getLastUpdateTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get structured browser state for LLM consumption.
|
* Get structured browser state for LLM consumption.
|
||||||
*/
|
*/
|
||||||
async getBrowserState(): Promise<BrowserState> {
|
async getBrowserState(): Promise<BrowserState> {
|
||||||
return rpcClient.getBrowserState()
|
return this.rpc.getBrowserState()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======= DOM Tree Operations =======
|
// ======= DOM Tree Operations =======
|
||||||
@@ -50,14 +62,14 @@ export class RemotePageController extends EventTarget {
|
|||||||
* Update DOM tree, returns simplified HTML for LLM.
|
* Update DOM tree, returns simplified HTML for LLM.
|
||||||
*/
|
*/
|
||||||
async updateTree(): Promise<string> {
|
async updateTree(): Promise<string> {
|
||||||
return rpcClient.updateTree()
|
return this.rpc.updateTree()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up all element highlights
|
* Clean up all element highlights
|
||||||
*/
|
*/
|
||||||
async cleanUpHighlights(): Promise<void> {
|
async cleanUpHighlights(): Promise<void> {
|
||||||
return rpcClient.cleanUpHighlights()
|
return this.rpc.cleanUpHighlights()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======= Element Actions =======
|
// ======= Element Actions =======
|
||||||
@@ -66,42 +78,42 @@ export class RemotePageController extends EventTarget {
|
|||||||
* Click element by index
|
* Click element by index
|
||||||
*/
|
*/
|
||||||
async clickElement(index: number): Promise<ActionResult> {
|
async clickElement(index: number): Promise<ActionResult> {
|
||||||
return rpcClient.clickElement(index)
|
return this.rpc.clickElement(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Input text into element by index
|
* Input text into element by index
|
||||||
*/
|
*/
|
||||||
async inputText(index: number, text: string): Promise<ActionResult> {
|
async inputText(index: number, text: string): Promise<ActionResult> {
|
||||||
return rpcClient.inputText(index, text)
|
return this.rpc.inputText(index, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select dropdown option by index and option text
|
* Select dropdown option by index and option text
|
||||||
*/
|
*/
|
||||||
async selectOption(index: number, optionText: string): Promise<ActionResult> {
|
async selectOption(index: number, optionText: string): Promise<ActionResult> {
|
||||||
return rpcClient.selectOption(index, optionText)
|
return this.rpc.selectOption(index, optionText)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scroll vertically
|
* Scroll vertically
|
||||||
*/
|
*/
|
||||||
async scroll(options: ScrollOptions): Promise<ActionResult> {
|
async scroll(options: ScrollOptions): Promise<ActionResult> {
|
||||||
return rpcClient.scroll(options)
|
return this.rpc.scroll(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scroll horizontally
|
* Scroll horizontally
|
||||||
*/
|
*/
|
||||||
async scrollHorizontally(options: ScrollHorizontallyOptions): Promise<ActionResult> {
|
async scrollHorizontally(options: ScrollHorizontallyOptions): Promise<ActionResult> {
|
||||||
return rpcClient.scrollHorizontally(options)
|
return this.rpc.scrollHorizontally(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute arbitrary JavaScript on the page
|
* Execute arbitrary JavaScript on the page
|
||||||
*/
|
*/
|
||||||
async executeJavascript(script: string): Promise<ActionResult> {
|
async executeJavascript(script: string): Promise<ActionResult> {
|
||||||
return rpcClient.executeJavascript(script)
|
return this.rpc.executeJavascript(script)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======= Mask Operations =======
|
// ======= Mask Operations =======
|
||||||
@@ -110,21 +122,21 @@ export class RemotePageController extends EventTarget {
|
|||||||
* Show the visual mask overlay.
|
* Show the visual mask overlay.
|
||||||
*/
|
*/
|
||||||
async showMask(): Promise<void> {
|
async showMask(): Promise<void> {
|
||||||
return rpcClient.showMask()
|
return this.rpc.showMask()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide the visual mask overlay.
|
* Hide the visual mask overlay.
|
||||||
*/
|
*/
|
||||||
async hideMask(): Promise<void> {
|
async hideMask(): Promise<void> {
|
||||||
return rpcClient.hideMask()
|
return this.rpc.hideMask()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispose and clean up resources
|
* Dispose and clean up resources
|
||||||
*/
|
*/
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
rpcClient.dispose().catch(() => {
|
this.rpc.dispose().catch(() => {
|
||||||
// Ignore errors on dispose
|
// Ignore errors on dispose
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,96 +13,102 @@ import type {
|
|||||||
} from './protocol'
|
} from './protocol'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the active tab ID for the current sidepanel context.
|
* Create an RPC client bound to a specific tab.
|
||||||
* In MV3, we need to explicitly target the 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<number> {
|
export function createRPCClient(tabIdPromise: Promise<number>): RPCClient {
|
||||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
|
return {
|
||||||
if (!tab?.id) {
|
// State queries
|
||||||
throw new Error('No active tab found')
|
async getCurrentUrl(): Promise<string> {
|
||||||
|
const tabId = await tabIdPromise
|
||||||
|
return pageControllerRPC.sendMessage('rpc:getCurrentUrl', undefined, tabId)
|
||||||
|
},
|
||||||
|
|
||||||
|
async getLastUpdateTime(): Promise<number> {
|
||||||
|
const tabId = await tabIdPromise
|
||||||
|
return pageControllerRPC.sendMessage('rpc:getLastUpdateTime', undefined, tabId)
|
||||||
|
},
|
||||||
|
|
||||||
|
async getBrowserState(): Promise<BrowserState> {
|
||||||
|
const tabId = await tabIdPromise
|
||||||
|
return pageControllerRPC.sendMessage('rpc:getBrowserState', undefined, tabId)
|
||||||
|
},
|
||||||
|
|
||||||
|
// DOM operations
|
||||||
|
async updateTree(): Promise<string> {
|
||||||
|
const tabId = await tabIdPromise
|
||||||
|
return pageControllerRPC.sendMessage('rpc:updateTree', undefined, tabId)
|
||||||
|
},
|
||||||
|
|
||||||
|
async cleanUpHighlights(): Promise<void> {
|
||||||
|
const tabId = await tabIdPromise
|
||||||
|
return pageControllerRPC.sendMessage('rpc:cleanUpHighlights', undefined, tabId)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Element actions
|
||||||
|
async clickElement(index: number): Promise<ActionResult> {
|
||||||
|
const tabId = await tabIdPromise
|
||||||
|
return pageControllerRPC.sendMessage('rpc:clickElement', index, tabId)
|
||||||
|
},
|
||||||
|
|
||||||
|
async inputText(index: number, text: string): Promise<ActionResult> {
|
||||||
|
const tabId = await tabIdPromise
|
||||||
|
return pageControllerRPC.sendMessage('rpc:inputText', { index, text }, tabId)
|
||||||
|
},
|
||||||
|
|
||||||
|
async selectOption(index: number, optionText: string): Promise<ActionResult> {
|
||||||
|
const tabId = await tabIdPromise
|
||||||
|
return pageControllerRPC.sendMessage('rpc:selectOption', { index, optionText }, tabId)
|
||||||
|
},
|
||||||
|
|
||||||
|
async scroll(options: ScrollOptions): Promise<ActionResult> {
|
||||||
|
const tabId = await tabIdPromise
|
||||||
|
return pageControllerRPC.sendMessage('rpc:scroll', options, tabId)
|
||||||
|
},
|
||||||
|
|
||||||
|
async scrollHorizontally(options: ScrollHorizontallyOptions): Promise<ActionResult> {
|
||||||
|
const tabId = await tabIdPromise
|
||||||
|
return pageControllerRPC.sendMessage('rpc:scrollHorizontally', options, tabId)
|
||||||
|
},
|
||||||
|
|
||||||
|
async executeJavascript(script: string): Promise<ActionResult> {
|
||||||
|
const tabId = await tabIdPromise
|
||||||
|
return pageControllerRPC.sendMessage('rpc:executeJavascript', script, tabId)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Mask operations
|
||||||
|
async showMask(): Promise<void> {
|
||||||
|
const tabId = await tabIdPromise
|
||||||
|
return pageControllerRPC.sendMessage('rpc:showMask', undefined, tabId)
|
||||||
|
},
|
||||||
|
|
||||||
|
async hideMask(): Promise<void> {
|
||||||
|
const tabId = await tabIdPromise
|
||||||
|
return pageControllerRPC.sendMessage('rpc:hideMask', undefined, tabId)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
async dispose(): Promise<void> {
|
||||||
|
const tabId = await tabIdPromise
|
||||||
|
return pageControllerRPC.sendMessage('rpc:dispose', undefined, tabId)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return tab.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export interface RPCClient {
|
||||||
* RPC client for calling PageController methods in ContentScript.
|
getCurrentUrl(): Promise<string>
|
||||||
* Each method sends a message and waits for the response.
|
getLastUpdateTime(): Promise<number>
|
||||||
*/
|
getBrowserState(): Promise<BrowserState>
|
||||||
export const rpcClient = {
|
updateTree(): Promise<string>
|
||||||
// State queries
|
cleanUpHighlights(): Promise<void>
|
||||||
async getCurrentUrl(): Promise<string> {
|
clickElement(index: number): Promise<ActionResult>
|
||||||
const tabId = await getActiveTabId()
|
inputText(index: number, text: string): Promise<ActionResult>
|
||||||
return pageControllerRPC.sendMessage('rpc:getCurrentUrl', undefined, tabId)
|
selectOption(index: number, optionText: string): Promise<ActionResult>
|
||||||
},
|
scroll(options: ScrollOptions): Promise<ActionResult>
|
||||||
|
scrollHorizontally(options: ScrollHorizontallyOptions): Promise<ActionResult>
|
||||||
async getLastUpdateTime(): Promise<number> {
|
executeJavascript(script: string): Promise<ActionResult>
|
||||||
const tabId = await getActiveTabId()
|
showMask(): Promise<void>
|
||||||
return pageControllerRPC.sendMessage('rpc:getLastUpdateTime', undefined, tabId)
|
hideMask(): Promise<void>
|
||||||
},
|
dispose(): Promise<void>
|
||||||
|
|
||||||
async getBrowserState(): Promise<BrowserState> {
|
|
||||||
const tabId = await getActiveTabId()
|
|
||||||
return pageControllerRPC.sendMessage('rpc:getBrowserState', undefined, tabId)
|
|
||||||
},
|
|
||||||
|
|
||||||
// DOM operations
|
|
||||||
async updateTree(): Promise<string> {
|
|
||||||
const tabId = await getActiveTabId()
|
|
||||||
return pageControllerRPC.sendMessage('rpc:updateTree', undefined, tabId)
|
|
||||||
},
|
|
||||||
|
|
||||||
async cleanUpHighlights(): Promise<void> {
|
|
||||||
const tabId = await getActiveTabId()
|
|
||||||
return pageControllerRPC.sendMessage('rpc:cleanUpHighlights', undefined, tabId)
|
|
||||||
},
|
|
||||||
|
|
||||||
// Element actions
|
|
||||||
async clickElement(index: number): Promise<ActionResult> {
|
|
||||||
const tabId = await getActiveTabId()
|
|
||||||
return pageControllerRPC.sendMessage('rpc:clickElement', index, tabId)
|
|
||||||
},
|
|
||||||
|
|
||||||
async inputText(index: number, text: string): Promise<ActionResult> {
|
|
||||||
const tabId = await getActiveTabId()
|
|
||||||
return pageControllerRPC.sendMessage('rpc:inputText', { index, text }, tabId)
|
|
||||||
},
|
|
||||||
|
|
||||||
async selectOption(index: number, optionText: string): Promise<ActionResult> {
|
|
||||||
const tabId = await getActiveTabId()
|
|
||||||
return pageControllerRPC.sendMessage('rpc:selectOption', { index, optionText }, tabId)
|
|
||||||
},
|
|
||||||
|
|
||||||
async scroll(options: ScrollOptions): Promise<ActionResult> {
|
|
||||||
const tabId = await getActiveTabId()
|
|
||||||
return pageControllerRPC.sendMessage('rpc:scroll', options, tabId)
|
|
||||||
},
|
|
||||||
|
|
||||||
async scrollHorizontally(options: ScrollHorizontallyOptions): Promise<ActionResult> {
|
|
||||||
const tabId = await getActiveTabId()
|
|
||||||
return pageControllerRPC.sendMessage('rpc:scrollHorizontally', options, tabId)
|
|
||||||
},
|
|
||||||
|
|
||||||
async executeJavascript(script: string): Promise<ActionResult> {
|
|
||||||
const tabId = await getActiveTabId()
|
|
||||||
return pageControllerRPC.sendMessage('rpc:executeJavascript', script, tabId)
|
|
||||||
},
|
|
||||||
|
|
||||||
// Mask operations
|
|
||||||
async showMask(): Promise<void> {
|
|
||||||
const tabId = await getActiveTabId()
|
|
||||||
return pageControllerRPC.sendMessage('rpc:showMask', undefined, tabId)
|
|
||||||
},
|
|
||||||
|
|
||||||
async hideMask(): Promise<void> {
|
|
||||||
const tabId = await getActiveTabId()
|
|
||||||
return pageControllerRPC.sendMessage('rpc:hideMask', undefined, tabId)
|
|
||||||
},
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
async dispose(): Promise<void> {
|
|
||||||
const tabId = await getActiveTabId()
|
|
||||||
return pageControllerRPC.sendMessage('rpc:dispose', undefined, tabId)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RPCClient = typeof rpcClient
|
|
||||||
|
|||||||
Reference in New Issue
Block a user