feat(ext): option to control all tabs

This commit is contained in:
Simon
2026-03-27 20:18:13 +08:00
parent 2322d6c46b
commit e1fede1194
5 changed files with 61 additions and 10 deletions

View File

@@ -11,13 +11,18 @@ function detectLanguage(): 'en-US' | 'zh-CN' {
return lang.startsWith('zh') ? 'zh-CN' : 'en-US' return lang.startsWith('zh') ? 'zh-CN' : 'en-US'
} }
interface MultiPageAgentConfig extends AgentConfig {
includeInitialTab?: boolean
experimentalIncludeAllTabs?: boolean
}
/** /**
* MultiPageAgent * MultiPageAgent
* - use with extension * - use with extension
* - can be used from a side panel or a content script * - can be used from a side panel or a content script
*/ */
export class MultiPageAgent extends PageAgentCore { export class MultiPageAgent extends PageAgentCore {
constructor(config: AgentConfig & { includeInitialTab?: boolean }) { constructor(config: MultiPageAgentConfig) {
// multi page controller // multi page controller
const tabsController = new TabsController() const tabsController = new TabsController()
const pageController = new RemotePageController(tabsController) const pageController = new RemotePageController(tabsController)
@@ -31,8 +36,8 @@ export class MultiPageAgent extends PageAgentCore {
`Default working language: **${targetLanguage}**` `Default working language: **${targetLanguage}**`
) )
// include initial tab for controlling
const includeInitialTab = config.includeInitialTab ?? true const includeInitialTab = config.includeInitialTab ?? true
const experimentalIncludeAllTabs = config.experimentalIncludeAllTabs ?? false
/** /**
* When the agent is in side-panel and user closed the side-panel. * When the agent is in side-panel and user closed the side-panel.
@@ -50,7 +55,7 @@ export class MultiPageAgent extends PageAgentCore {
customSystemPrompt: systemPrompt, customSystemPrompt: systemPrompt,
onBeforeTask: async (agent) => { onBeforeTask: async (agent) => {
await tabsController.init(agent.task, includeInitialTab) await tabsController.init(agent.task, { includeInitialTab, experimentalIncludeAllTabs })
heartBeatInterval = window.setInterval(() => { heartBeatInterval = window.setInterval(() => {
chrome.storage.local.set({ chrome.storage.local.set({

View File

@@ -114,6 +114,19 @@ export function handleTabControlMessage(
return true // async response return true // async response
} }
case 'get_window_tabs': {
debug('get_window_tabs')
chrome.tabs
.query({ currentWindow: true })
.then((tabs) => {
sendResponse({ success: true, tabs })
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
})
return true
}
default: default:
sendResponse({ error: `Unknown action: ${action}` }) sendResponse({ error: `Unknown action: ${action}` })
return return

View File

@@ -28,16 +28,19 @@ export class TabsController extends EventTarget {
private tabs: TabMeta[] = [] private tabs: TabMeta[] = []
private initialTabId: number | null = null private initialTabId: number | null = null
private tabGroupId: number | null = null private tabGroupId: number | null = null
private experimentalIncludeAllTabs = false
private task: string = '' private task: string = ''
async init(task: string, includeInitialTab: boolean = true) { async init(task: string, options: TabsInitOptions = {}) {
debug('init', task, includeInitialTab) const { includeInitialTab = true, experimentalIncludeAllTabs = false } = options
debug('init', task, options)
this.task = task this.task = task
this.tabs = [] this.tabs = []
this.currentTabId = null this.currentTabId = null
this.tabGroupId = null this.tabGroupId = null
this.initialTabId = null this.initialTabId = null
this.experimentalIncludeAllTabs = experimentalIncludeAllTabs
const result = await sendMessage({ const result = await sendMessage({
type: 'TAB_CONTROL', type: 'TAB_CONTROL',
@@ -50,14 +53,34 @@ export class TabsController extends EventTarget {
throw new Error('Failed to get initial tab ID') throw new Error('Failed to get initial tab ID')
} }
if (includeInitialTab) { if (experimentalIncludeAllTabs) {
const allTabs = await sendMessage({
type: 'TAB_CONTROL',
action: 'get_window_tabs',
})
for (const tab of allTabs.tabs as chrome.tabs.Tab[]) {
if (tab.id && !tab.pinned && isContentScriptAllowed(tab.url)) {
this.tabs.push({
id: tab.id,
isInitial: tab.id === this.initialTabId,
url: tab.url,
title: tab.title,
status: tab.status,
})
}
}
if (this.tabs.find((t) => t.id === this.initialTabId)) {
this.currentTabId = this.initialTabId
await this.createTabGroup([this.initialTabId])
}
} else if (includeInitialTab) {
const info = await sendMessage({ const info = await sendMessage({
type: 'TAB_CONTROL', type: 'TAB_CONTROL',
action: 'get_tab_info', action: 'get_tab_info',
payload: { tabId: this.initialTabId }, payload: { tabId: this.initialTabId },
}) })
if (isContentScriptAllowed(info.url)) { if (isContentScriptAllowed(info.url) && !info.pinned) {
this.currentTabId = this.initialTabId this.currentTabId = this.initialTabId
this.tabs.push({ this.tabs.push({
@@ -76,14 +99,13 @@ export class TabsController extends EventTarget {
const tabChangeHandler = (message: any): void => { const tabChangeHandler = (message: any): void => {
if (message.type !== 'TAB_CHANGE') { if (message.type !== 'TAB_CHANGE') {
// throw new Error(`[TabsController]: Invalid message type: ${message.type}`)
return return
} }
if (message.action === 'created') { if (message.action === 'created') {
const tab = message.payload.tab as chrome.tabs.Tab const tab = message.payload.tab as chrome.tabs.Tab
if (tab.groupId === this.tabGroupId && tab.id != null) { const shouldTrack = this.experimentalIncludeAllTabs || tab.groupId === this.tabGroupId
// Tab created in our controlled group if (shouldTrack && tab.id != null) {
if (!this.tabs.find((t) => t.id === tab.id)) { if (!this.tabs.find((t) => t.id === tab.id)) {
this.tabs.push({ id: tab.id, isInitial: false }) this.tabs.push({ id: tab.id, isInitial: false })
} }
@@ -293,6 +315,11 @@ export class TabsController extends EventTarget {
} }
} }
export interface TabsInitOptions {
includeInitialTab?: boolean
experimentalIncludeAllTabs?: boolean
}
export type TabAction = export type TabAction =
| 'get_active_tab' | 'get_active_tab'
| 'get_tab_info' | 'get_tab_info'
@@ -302,6 +329,7 @@ export type TabAction =
| 'add_tab_to_group' | 'add_tab_to_group'
| 'close_tab' | 'close_tab'
| 'get_tab_title' | 'get_tab_title'
| 'get_window_tabs'
interface TabMeta { interface TabMeta {
id: number id: number

View File

@@ -21,6 +21,7 @@ export interface AdvancedConfig {
maxSteps?: number maxSteps?: number
systemInstruction?: string systemInstruction?: string
experimentalLlmsTxt?: boolean experimentalLlmsTxt?: boolean
experimentalIncludeAllTabs?: boolean
disableNamedToolChoice?: boolean disableNamedToolChoice?: boolean
} }

View File

@@ -13,6 +13,9 @@ export interface ExecuteConfig {
*/ */
includeInitialTab?: boolean includeInitialTab?: boolean
/** Control all unpinned tabs in the window instead of only the tab group. */
experimentalIncludeAllTabs?: boolean
onStatusChange?: (status: AgentStatus) => void onStatusChange?: (status: AgentStatus) => void
onActivity?: (activity: AgentActivity) => void onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void onHistoryUpdate?: (history: HistoricalEvent[]) => void
@@ -87,6 +90,7 @@ export default defineUnlistedScript(() => {
model: config.model, model: config.model,
apiKey: config.apiKey, apiKey: config.apiKey,
includeInitialTab: config.includeInitialTab, includeInitialTab: config.includeInitialTab,
experimentalIncludeAllTabs: config.experimentalIncludeAllTabs,
}, },
}, },
}, },