Skip to main content
Glama
tabs.ts5.92 kB
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; }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/andytango/puppeteer-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server