2026-01-27 17:21:32 +08:00
|
|
|
/**
|
|
|
|
|
* background logics for TabsController
|
|
|
|
|
*/
|
|
|
|
|
import type { TabAction } from './TabsController'
|
|
|
|
|
|
2026-02-11 19:51:19 +08:00
|
|
|
const PREFIX = '[TabsController.background]'
|
|
|
|
|
|
2026-03-30 19:48:52 +08:00
|
|
|
const debug = console.debug.bind(console, `\x1b[90m${PREFIX}\x1b[0m`)
|
2026-02-11 19:51:19 +08:00
|
|
|
|
2026-07-02 02:11:32 +08:00
|
|
|
/**
|
|
|
|
|
* Resolve active tab.
|
|
|
|
|
*
|
|
|
|
|
* - `tabs.query({ active: true })` does not work in multi-window scenarios.
|
|
|
|
|
* - Extension pages (side panel, hub tab) can resolve their own windowId.
|
|
|
|
|
* We just find the active tab within that window.
|
|
|
|
|
* - Content scripts (PAGE_AGENT_EXT) can't self-report a windowId.
|
|
|
|
|
* Chrome populates `sender.tab` for every content-script message,
|
|
|
|
|
* which is the tab hosting the script.
|
|
|
|
|
*/
|
|
|
|
|
async function resolveActiveTab(
|
|
|
|
|
payload: { windowId?: number } | undefined,
|
|
|
|
|
sender: chrome.runtime.MessageSender
|
|
|
|
|
): Promise<chrome.tabs.Tab> {
|
|
|
|
|
const windowId = payload?.windowId
|
|
|
|
|
|
|
|
|
|
if (windowId != null) {
|
|
|
|
|
debug('get_active_tab: resolving via caller-reported windowId', windowId)
|
|
|
|
|
const [tab] = await chrome.tabs.query({ active: true, windowId })
|
|
|
|
|
if (!tab) throw new Error(`No active tab found in window ${windowId}.`)
|
|
|
|
|
return tab
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sender.tab) {
|
|
|
|
|
debug('get_active_tab: resolving via sender.tab (content script)', sender.tab.id)
|
|
|
|
|
return sender.tab
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
'Cannot resolve active tab: caller reported no windowId and is not a content script (no sender.tab).'
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 17:21:32 +08:00
|
|
|
export function handleTabControlMessage(
|
|
|
|
|
message: { type: 'TAB_CONTROL'; action: TabAction; payload: any },
|
|
|
|
|
sender: chrome.runtime.MessageSender,
|
|
|
|
|
sendResponse: (response: unknown) => void
|
2026-01-29 18:37:42 +08:00
|
|
|
): true | undefined {
|
2026-01-27 17:21:32 +08:00
|
|
|
const { action, payload } = message
|
|
|
|
|
|
|
|
|
|
switch (action as TabAction) {
|
|
|
|
|
case 'get_active_tab': {
|
2026-07-02 02:11:32 +08:00
|
|
|
debug('get_active_tab', payload)
|
|
|
|
|
resolveActiveTab(payload, sender)
|
|
|
|
|
.then((tab) => {
|
|
|
|
|
debug('get_active_tab: success', tab)
|
|
|
|
|
sendResponse({ success: true, tab })
|
2026-01-27 17:21:32 +08:00
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
sendResponse({ error: error instanceof Error ? error.message : String(error) })
|
|
|
|
|
})
|
|
|
|
|
return true // async response
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'get_tab_info': {
|
2026-02-11 19:51:19 +08:00
|
|
|
debug('get_tab_info', payload)
|
2026-01-27 17:21:32 +08:00
|
|
|
chrome.tabs
|
|
|
|
|
.get(payload.tabId)
|
|
|
|
|
.then((tab) => {
|
2026-02-11 19:51:19 +08:00
|
|
|
debug('get_tab_info: success', tab)
|
|
|
|
|
sendResponse(tab)
|
2026-01-27 17:21:32 +08:00
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
sendResponse({ error: error instanceof Error ? error.message : String(error) })
|
|
|
|
|
})
|
|
|
|
|
return true // async response
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'open_new_tab': {
|
2026-02-11 19:51:19 +08:00
|
|
|
debug('open_new_tab', payload)
|
2026-01-27 17:21:32 +08:00
|
|
|
chrome.tabs
|
2026-07-02 02:11:32 +08:00
|
|
|
.create({ url: payload.url, windowId: payload.windowId, active: false })
|
2026-01-27 17:21:32 +08:00
|
|
|
.then((newTab) => {
|
2026-02-11 19:51:19 +08:00
|
|
|
debug('open_new_tab: success', newTab)
|
2026-03-16 20:51:32 +08:00
|
|
|
sendResponse({ success: true, tabId: newTab.id })
|
2026-01-27 17:21:32 +08:00
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
sendResponse({ error: error instanceof Error ? error.message : String(error) })
|
|
|
|
|
})
|
|
|
|
|
return true // async response
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'create_tab_group': {
|
2026-02-11 19:51:19 +08:00
|
|
|
debug('create_tab_group', payload)
|
2026-01-27 17:21:32 +08:00
|
|
|
chrome.tabs
|
2026-03-30 20:24:24 +08:00
|
|
|
.group({ tabIds: payload.tabIds, createProperties: { windowId: payload.windowId } })
|
2026-01-27 17:21:32 +08:00
|
|
|
.then((groupId) => {
|
2026-02-11 19:51:19 +08:00
|
|
|
debug('create_tab_group: success', groupId)
|
2026-01-27 17:21:32 +08:00
|
|
|
sendResponse({ success: true, groupId })
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
2026-02-11 19:51:19 +08:00
|
|
|
console.error(PREFIX, 'Failed to create tab group', error)
|
2026-01-27 17:21:32 +08:00
|
|
|
sendResponse({ error: error instanceof Error ? error.message : String(error) })
|
|
|
|
|
})
|
|
|
|
|
return true // async response
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'update_tab_group': {
|
2026-02-11 19:51:19 +08:00
|
|
|
debug('update_tab_group', payload)
|
2026-01-27 17:21:32 +08:00
|
|
|
chrome.tabGroups
|
|
|
|
|
.update(payload.groupId, payload.properties)
|
|
|
|
|
.then(() => {
|
|
|
|
|
sendResponse({ success: true })
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
sendResponse({ error: error instanceof Error ? error.message : String(error) })
|
|
|
|
|
})
|
|
|
|
|
return true // async response
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'add_tab_to_group': {
|
2026-02-11 19:51:19 +08:00
|
|
|
debug('add_tab_to_group', payload)
|
2026-01-27 17:21:32 +08:00
|
|
|
chrome.tabs
|
|
|
|
|
.group({ tabIds: payload.tabId, groupId: payload.groupId })
|
|
|
|
|
.then(() => {
|
|
|
|
|
sendResponse({ success: true })
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
sendResponse({ error: error instanceof Error ? error.message : String(error) })
|
|
|
|
|
})
|
|
|
|
|
return true // async response
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'close_tab': {
|
2026-02-11 19:51:19 +08:00
|
|
|
debug('close_tab', payload)
|
2026-01-27 17:21:32 +08:00
|
|
|
chrome.tabs
|
|
|
|
|
.remove(payload.tabId)
|
|
|
|
|
.then(() => {
|
|
|
|
|
sendResponse({ success: true })
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
sendResponse({ error: error instanceof Error ? error.message : String(error) })
|
|
|
|
|
})
|
|
|
|
|
return true // async response
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 20:18:13 +08:00
|
|
|
case 'get_window_tabs': {
|
2026-03-30 20:24:24 +08:00
|
|
|
debug('get_window_tabs', payload)
|
2026-03-27 20:18:13 +08:00
|
|
|
chrome.tabs
|
2026-03-30 20:24:24 +08:00
|
|
|
.query({ windowId: payload.windowId })
|
2026-03-27 20:18:13 +08:00
|
|
|
.then((tabs) => {
|
|
|
|
|
sendResponse({ success: true, tabs })
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
sendResponse({ error: error instanceof Error ? error.message : String(error) })
|
|
|
|
|
})
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 17:21:32 +08:00
|
|
|
default:
|
|
|
|
|
sendResponse({ error: `Unknown action: ${action}` })
|
2026-01-29 18:37:42 +08:00
|
|
|
return
|
2026-01-27 17:21:32 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-10 21:59:58 +08:00
|
|
|
|
2026-03-30 21:40:16 +08:00
|
|
|
const tabEventPorts = new Set<chrome.runtime.Port>()
|
|
|
|
|
|
|
|
|
|
function broadcastTabEvent(message: object) {
|
|
|
|
|
for (const port of tabEventPorts) {
|
|
|
|
|
port.postMessage(message)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Port-based tab events: agents connect via `chrome.runtime.connect({ name: 'tab-events' })`
|
|
|
|
|
* and receive tab change events through the port. Works for both extension pages and content scripts.
|
|
|
|
|
*/
|
|
|
|
|
export function setupTabEventsPort() {
|
|
|
|
|
chrome.runtime.onConnect.addListener((port) => {
|
|
|
|
|
if (port.name !== 'tab-events') return
|
|
|
|
|
|
|
|
|
|
debug('port connected', port.sender?.tab?.id ?? port.sender?.url)
|
|
|
|
|
tabEventPorts.add(port)
|
|
|
|
|
|
|
|
|
|
port.onDisconnect.addListener(() => {
|
|
|
|
|
debug('port disconnected')
|
|
|
|
|
tabEventPorts.delete(port)
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-02-11 19:51:19 +08:00
|
|
|
|
2026-02-10 21:59:58 +08:00
|
|
|
chrome.tabs.onCreated.addListener((tab) => {
|
2026-03-30 21:40:16 +08:00
|
|
|
broadcastTabEvent({ action: 'created', payload: { tab } })
|
2026-02-10 21:59:58 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
|
2026-03-30 21:40:16 +08:00
|
|
|
broadcastTabEvent({ action: 'removed', payload: { tabId, removeInfo } })
|
2026-02-11 19:51:19 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
2026-03-30 21:40:16 +08:00
|
|
|
broadcastTabEvent({ action: 'updated', payload: { tabId, changeInfo, tab } })
|
2026-02-10 21:59:58 +08:00
|
|
|
})
|
|
|
|
|
}
|