import type { Page, Browser } from 'puppeteer';
import type { TabInfo } from './types.js';
import { getBrowser } from './browser.js';
import { Result, ok, err, tabNotFound, noActiveTab, browserNotLaunched } from './errors.js';
/**
* Internal tab state
*/
interface TabState {
tabs: Map<string, Page>;
activeTabId: string | null;
}
const state: TabState = {
tabs: new Map(),
activeTabId: null,
};
/**
* Generate a unique tab ID
*/
function generateTabId(): string {
const suffix = Math.random().toString(36).substring(2, 8);
return `tab_${suffix}`;
}
/**
* Initialize tabs from existing browser pages
*/
async function initializeFromBrowser(browser: Browser): Promise<void> {
const pages = await browser.pages();
// Register existing pages
for (const page of pages) {
if (!isPageTracked(page)) {
const tabId = generateTabId();
registerPage(tabId, page);
}
}
// Set active tab if none set
if (!state.activeTabId && state.tabs.size > 0) {
const firstTabId = state.tabs.keys().next().value;
if (firstTabId) {
state.activeTabId = firstTabId;
}
}
}
/**
* Check if a page is already tracked
*/
function isPageTracked(page: Page): boolean {
for (const trackedPage of state.tabs.values()) {
if (trackedPage === page) {
return true;
}
}
return false;
}
/**
* Register a page with a tab ID
*/
function registerPage(tabId: string, page: Page): void {
state.tabs.set(tabId, page);
// Handle page close
page.on('close', () => {
state.tabs.delete(tabId);
if (state.activeTabId === tabId) {
// Switch to another tab
const remaining = state.tabs.keys().next();
state.activeTabId = remaining.done ? null : remaining.value;
}
});
}
/**
* Ensure browser and tabs are initialized
*/
async function ensureInitialized(): Promise<Result<Browser>> {
const browserResult = await getBrowser();
if (!browserResult.success) {
return browserResult;
}
if (state.tabs.size === 0) {
await initializeFromBrowser(browserResult.data);
}
return browserResult;
}
/**
* Create a new tab
* @param url Optional URL to navigate to
* @returns Result with tab info
*/
export async function createTab(url?: string): Promise<Result<TabInfo>> {
const browserResult = await ensureInitialized();
if (!browserResult.success) {
return browserResult;
}
try {
const page = await browserResult.data.newPage();
const tabId = generateTabId();
registerPage(tabId, page);
state.activeTabId = tabId;
if (url) {
await page.goto(url);
}
return ok({
id: tabId,
url: page.url(),
title: await page.title(),
isActive: true,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return err(browserNotLaunched());
}
}
/**
* Close a tab
* @param tabId Tab ID to close (uses active tab if not specified)
*/
export async function closeTab(tabId?: string): Promise<Result<void>> {
const targetId = tabId ?? state.activeTabId;
if (!targetId) {
return err(noActiveTab());
}
const page = state.tabs.get(targetId);
if (!page) {
return err(tabNotFound(targetId));
}
try {
await page.close();
// Page close handler will update state
return ok(undefined);
} catch (error) {
// Page may already be closed
state.tabs.delete(targetId);
if (state.activeTabId === targetId) {
const remaining = state.tabs.keys().next();
state.activeTabId = remaining.done ? null : remaining.value;
}
return ok(undefined);
}
}
/**
* Get a tab by ID
* @param tabId Tab ID (uses active tab if not specified)
*/
export function getTab(tabId?: string): Page | undefined {
const targetId = tabId ?? state.activeTabId;
if (!targetId) {
return undefined;
}
return state.tabs.get(targetId);
}
/**
* Get the active tab
*/
export function getActiveTab(): Page | undefined {
if (!state.activeTabId) {
return undefined;
}
return state.tabs.get(state.activeTabId);
}
/**
* Get the active tab ID
*/
export function getActiveTabId(): string | undefined {
return state.activeTabId ?? undefined;
}
/**
* List all tabs
*/
export async function listTabs(): Promise<TabInfo[]> {
await ensureInitialized();
const tabs: TabInfo[] = [];
for (const [id, page] of state.tabs.entries()) {
try {
tabs.push({
id,
url: page.url(),
title: await page.title(),
isActive: id === state.activeTabId,
});
} catch {
// Page may be closed, skip it
state.tabs.delete(id);
}
}
return tabs;
}
/**
* Switch to a specific tab
* @param tabId Tab ID to switch to
*/
export async function switchTab(tabId: string): Promise<Result<TabInfo>> {
const page = state.tabs.get(tabId);
if (!page) {
return err(tabNotFound(tabId));
}
state.activeTabId = tabId;
try {
// Bring page to front
await page.bringToFront();
return ok({
id: tabId,
url: page.url(),
title: await page.title(),
isActive: true,
});
} catch (error) {
return err(tabNotFound(tabId));
}
}
/**
* Get page for tool operation
* @param tabId Optional tab ID
* @returns Result with page
*/
export async function getPageForOperation(tabId?: string): Promise<Result<Page>> {
await ensureInitialized();
const page = getTab(tabId);
if (!page) {
if (tabId) {
return err(tabNotFound(tabId));
}
return err(noActiveTab());
}
return ok(page);
}
/**
* Close all tabs and cleanup
*/
export async function closeAllTabs(): Promise<void> {
for (const page of state.tabs.values()) {
try {
await page.close();
} catch {
// Ignore close errors
}
}
state.tabs.clear();
state.activeTabId = null;
}
/**
* Get tab count
*/
export function getTabCount(): number {
return state.tabs.size;
}