hangwin--mcp-chrome
231 行
6.6 KiB
TypeScript
231 行
6.6 KiB
TypeScript
/**
|
|
* Quick Panel Tabs Handler
|
|
*
|
|
* Background service worker bridge for Quick Panel (content script) to:
|
|
* - Enumerate tabs for search suggestions
|
|
* - Activate a selected tab
|
|
* - Close a tab
|
|
*
|
|
* Note: Content scripts cannot access chrome.tabs.* directly.
|
|
*/
|
|
|
|
import {
|
|
BACKGROUND_MESSAGE_TYPES,
|
|
type QuickPanelActivateTabMessage,
|
|
type QuickPanelActivateTabResponse,
|
|
type QuickPanelCloseTabMessage,
|
|
type QuickPanelCloseTabResponse,
|
|
type QuickPanelTabSummary,
|
|
type QuickPanelTabsQueryMessage,
|
|
type QuickPanelTabsQueryResponse,
|
|
} from '@/common/message-types';
|
|
|
|
// ============================================================
|
|
// Constants
|
|
// ============================================================
|
|
|
|
const LOG_PREFIX = '[QuickPanelTabs]';
|
|
|
|
// ============================================================
|
|
// Helpers
|
|
// ============================================================
|
|
|
|
function isValidTabId(value: unknown): value is number {
|
|
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
}
|
|
|
|
function isValidWindowId(value: unknown): value is number {
|
|
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
}
|
|
|
|
function normalizeBoolean(value: unknown): boolean {
|
|
return value === true;
|
|
}
|
|
|
|
function getLastAccessed(tab: chrome.tabs.Tab): number | undefined {
|
|
const anyTab = tab as unknown as { lastAccessed?: unknown };
|
|
const value = anyTab.lastAccessed;
|
|
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
}
|
|
|
|
function safeErrorMessage(err: unknown): string {
|
|
if (err instanceof Error) {
|
|
return err.message || String(err);
|
|
}
|
|
return String(err);
|
|
}
|
|
|
|
/**
|
|
* Convert a chrome.tabs.Tab to our summary format.
|
|
* Returns null if tab is invalid.
|
|
*/
|
|
function toTabSummary(tab: chrome.tabs.Tab): QuickPanelTabSummary | null {
|
|
if (!isValidTabId(tab.id)) return null;
|
|
|
|
const windowId = isValidWindowId(tab.windowId) ? tab.windowId : null;
|
|
if (windowId === null) return null;
|
|
|
|
return {
|
|
tabId: tab.id,
|
|
windowId,
|
|
title: tab.title ?? '',
|
|
url: tab.url ?? '',
|
|
favIconUrl: tab.favIconUrl ?? undefined,
|
|
active: normalizeBoolean(tab.active),
|
|
pinned: normalizeBoolean(tab.pinned),
|
|
audible: normalizeBoolean(tab.audible),
|
|
muted: normalizeBoolean(tab.mutedInfo?.muted),
|
|
index: typeof tab.index === 'number' && Number.isFinite(tab.index) ? tab.index : 0,
|
|
lastAccessed: getLastAccessed(tab),
|
|
};
|
|
}
|
|
|
|
// ============================================================
|
|
// Message Handlers
|
|
// ============================================================
|
|
|
|
async function handleTabsQuery(
|
|
message: QuickPanelTabsQueryMessage,
|
|
sender: chrome.runtime.MessageSender,
|
|
): Promise<QuickPanelTabsQueryResponse> {
|
|
try {
|
|
const includeAllWindows = message.payload?.includeAllWindows ?? true;
|
|
|
|
// Extract current context from sender
|
|
const currentWindowId = isValidWindowId(sender.tab?.windowId) ? sender.tab!.windowId : null;
|
|
const currentTabId = isValidTabId(sender.tab?.id) ? sender.tab!.id : null;
|
|
|
|
// Quick Panel should only be called from content scripts (which have sender.tab)
|
|
// Reject requests without valid sender tab context for security
|
|
if (!includeAllWindows && currentWindowId === null) {
|
|
return {
|
|
success: false,
|
|
error: 'Invalid request: sender tab context required for window-scoped queries',
|
|
};
|
|
}
|
|
|
|
// Build query info based on scope
|
|
const queryInfo: chrome.tabs.QueryInfo = includeAllWindows
|
|
? {}
|
|
: { windowId: currentWindowId! };
|
|
|
|
const tabs = await chrome.tabs.query(queryInfo);
|
|
|
|
// Convert to summaries, filtering out invalid tabs
|
|
const summaries: QuickPanelTabSummary[] = [];
|
|
for (const tab of tabs) {
|
|
const summary = toTabSummary(tab);
|
|
if (summary) {
|
|
summaries.push(summary);
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
tabs: summaries,
|
|
currentTabId,
|
|
currentWindowId,
|
|
};
|
|
} catch (err) {
|
|
console.warn(`${LOG_PREFIX} Error querying tabs:`, err);
|
|
return {
|
|
success: false,
|
|
error: safeErrorMessage(err) || 'Failed to query tabs',
|
|
};
|
|
}
|
|
}
|
|
|
|
async function handleActivateTab(
|
|
message: QuickPanelActivateTabMessage,
|
|
): Promise<QuickPanelActivateTabResponse> {
|
|
try {
|
|
const tabId = message.payload?.tabId;
|
|
const windowId = message.payload?.windowId;
|
|
|
|
if (!isValidTabId(tabId)) {
|
|
return { success: false, error: 'Invalid tabId' };
|
|
}
|
|
|
|
// Focus the window first if provided
|
|
if (isValidWindowId(windowId)) {
|
|
try {
|
|
await chrome.windows.update(windowId, { focused: true });
|
|
} catch {
|
|
// Best-effort: tab activation may still succeed without focusing window.
|
|
}
|
|
}
|
|
|
|
// Activate the tab
|
|
await chrome.tabs.update(tabId, { active: true });
|
|
|
|
return { success: true };
|
|
} catch (err) {
|
|
console.warn(`${LOG_PREFIX} Error activating tab:`, err);
|
|
return {
|
|
success: false,
|
|
error: safeErrorMessage(err) || 'Failed to activate tab',
|
|
};
|
|
}
|
|
}
|
|
|
|
async function handleCloseTab(
|
|
message: QuickPanelCloseTabMessage,
|
|
): Promise<QuickPanelCloseTabResponse> {
|
|
try {
|
|
const tabId = message.payload?.tabId;
|
|
|
|
if (!isValidTabId(tabId)) {
|
|
return { success: false, error: 'Invalid tabId' };
|
|
}
|
|
|
|
await chrome.tabs.remove(tabId);
|
|
|
|
return { success: true };
|
|
} catch (err) {
|
|
console.warn(`${LOG_PREFIX} Error closing tab:`, err);
|
|
return {
|
|
success: false,
|
|
error: safeErrorMessage(err) || 'Failed to close tab',
|
|
};
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Initialization
|
|
// ============================================================
|
|
|
|
let initialized = false;
|
|
|
|
/**
|
|
* Initialize the Quick Panel Tabs handler.
|
|
* Safe to call multiple times - subsequent calls are no-ops.
|
|
*/
|
|
export function initQuickPanelTabsHandler(): void {
|
|
if (initialized) return;
|
|
initialized = true;
|
|
|
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
// Tabs query
|
|
if (message?.type === BACKGROUND_MESSAGE_TYPES.QUICK_PANEL_TABS_QUERY) {
|
|
handleTabsQuery(message as QuickPanelTabsQueryMessage, sender).then(sendResponse);
|
|
return true; // Will respond asynchronously
|
|
}
|
|
|
|
// Tab activate
|
|
if (message?.type === BACKGROUND_MESSAGE_TYPES.QUICK_PANEL_TAB_ACTIVATE) {
|
|
handleActivateTab(message as QuickPanelActivateTabMessage).then(sendResponse);
|
|
return true;
|
|
}
|
|
|
|
// Tab close
|
|
if (message?.type === BACKGROUND_MESSAGE_TYPES.QUICK_PANEL_TAB_CLOSE) {
|
|
handleCloseTab(message as QuickPanelCloseTabMessage).then(sendResponse);
|
|
return true;
|
|
}
|
|
|
|
return false; // Not handled by this listener
|
|
});
|
|
|
|
console.debug(`${LOG_PREFIX} Initialized`);
|
|
}
|