diff --git a/packages/extension/src/agent/MultiPageAgent.ts b/packages/extension/src/agent/MultiPageAgent.ts index f2e7b0a..aa768bc 100644 --- a/packages/extension/src/agent/MultiPageAgent.ts +++ b/packages/extension/src/agent/MultiPageAgent.ts @@ -11,13 +11,18 @@ function detectLanguage(): 'en-US' | 'zh-CN' { return lang.startsWith('zh') ? 'zh-CN' : 'en-US' } +interface MultiPageAgentConfig extends AgentConfig { + includeInitialTab?: boolean + experimentalIncludeAllTabs?: boolean +} + /** * MultiPageAgent * - use with extension * - can be used from a side panel or a content script */ export class MultiPageAgent extends PageAgentCore { - constructor(config: AgentConfig & { includeInitialTab?: boolean }) { + constructor(config: MultiPageAgentConfig) { // multi page controller const tabsController = new TabsController() const pageController = new RemotePageController(tabsController) @@ -31,8 +36,8 @@ export class MultiPageAgent extends PageAgentCore { `Default working language: **${targetLanguage}**` ) - // include initial tab for controlling const includeInitialTab = config.includeInitialTab ?? true + const experimentalIncludeAllTabs = config.experimentalIncludeAllTabs ?? false /** * When the agent is in side-panel and user closed the side-panel. @@ -50,7 +55,7 @@ export class MultiPageAgent extends PageAgentCore { customSystemPrompt: systemPrompt, onBeforeTask: async (agent) => { - await tabsController.init(agent.task, includeInitialTab) + await tabsController.init(agent.task, { includeInitialTab, experimentalIncludeAllTabs }) heartBeatInterval = window.setInterval(() => { chrome.storage.local.set({ diff --git a/packages/extension/src/agent/TabsController.background.ts b/packages/extension/src/agent/TabsController.background.ts index 39c628a..43e5ade 100644 --- a/packages/extension/src/agent/TabsController.background.ts +++ b/packages/extension/src/agent/TabsController.background.ts @@ -114,6 +114,19 @@ export function handleTabControlMessage( 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: sendResponse({ error: `Unknown action: ${action}` }) return diff --git a/packages/extension/src/agent/TabsController.ts b/packages/extension/src/agent/TabsController.ts index 46fabf6..6fe2c1f 100644 --- a/packages/extension/src/agent/TabsController.ts +++ b/packages/extension/src/agent/TabsController.ts @@ -28,16 +28,19 @@ export class TabsController extends EventTarget { private tabs: TabMeta[] = [] private initialTabId: number | null = null private tabGroupId: number | null = null + private experimentalIncludeAllTabs = false private task: string = '' - async init(task: string, includeInitialTab: boolean = true) { - debug('init', task, includeInitialTab) + async init(task: string, options: TabsInitOptions = {}) { + const { includeInitialTab = true, experimentalIncludeAllTabs = false } = options + debug('init', task, options) this.task = task this.tabs = [] this.currentTabId = null this.tabGroupId = null this.initialTabId = null + this.experimentalIncludeAllTabs = experimentalIncludeAllTabs const result = await sendMessage({ type: 'TAB_CONTROL', @@ -50,14 +53,34 @@ export class TabsController extends EventTarget { 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({ type: 'TAB_CONTROL', action: 'get_tab_info', payload: { tabId: this.initialTabId }, }) - if (isContentScriptAllowed(info.url)) { + if (isContentScriptAllowed(info.url) && !info.pinned) { this.currentTabId = this.initialTabId this.tabs.push({ @@ -76,14 +99,13 @@ export class TabsController extends EventTarget { const tabChangeHandler = (message: any): void => { if (message.type !== 'TAB_CHANGE') { - // throw new Error(`[TabsController]: Invalid message type: ${message.type}`) return } if (message.action === 'created') { const tab = message.payload.tab as chrome.tabs.Tab - if (tab.groupId === this.tabGroupId && tab.id != null) { - // Tab created in our controlled group + const shouldTrack = this.experimentalIncludeAllTabs || tab.groupId === this.tabGroupId + if (shouldTrack && tab.id != null) { if (!this.tabs.find((t) => t.id === tab.id)) { 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 = | 'get_active_tab' | 'get_tab_info' @@ -302,6 +329,7 @@ export type TabAction = | 'add_tab_to_group' | 'close_tab' | 'get_tab_title' + | 'get_window_tabs' interface TabMeta { id: number diff --git a/packages/extension/src/agent/useAgent.ts b/packages/extension/src/agent/useAgent.ts index f5596a5..97d0a39 100644 --- a/packages/extension/src/agent/useAgent.ts +++ b/packages/extension/src/agent/useAgent.ts @@ -21,6 +21,7 @@ export interface AdvancedConfig { maxSteps?: number systemInstruction?: string experimentalLlmsTxt?: boolean + experimentalIncludeAllTabs?: boolean disableNamedToolChoice?: boolean } diff --git a/packages/extension/src/entrypoints/main-world.ts b/packages/extension/src/entrypoints/main-world.ts index fd93e68..c7946e8 100644 --- a/packages/extension/src/entrypoints/main-world.ts +++ b/packages/extension/src/entrypoints/main-world.ts @@ -13,6 +13,9 @@ export interface ExecuteConfig { */ includeInitialTab?: boolean + /** Control all unpinned tabs in the window instead of only the tab group. */ + experimentalIncludeAllTabs?: boolean + onStatusChange?: (status: AgentStatus) => void onActivity?: (activity: AgentActivity) => void onHistoryUpdate?: (history: HistoricalEvent[]) => void @@ -87,6 +90,7 @@ export default defineUnlistedScript(() => { model: config.model, apiKey: config.apiKey, includeInitialTab: config.includeInitialTab, + experimentalIncludeAllTabs: config.experimentalIncludeAllTabs, }, }, },