diff --git a/README.md b/README.md
index c9db724..98f45fa 100644
--- a/README.md
+++ b/README.md
@@ -60,6 +60,22 @@ veb select "#country" "US"
# Hover over elements
veb hover "#menu"
+# Tab management
+veb tab new # Open new tab
+veb tab list # List all tabs
+veb tab 0 # Switch to tab 0
+veb tab close # Close current tab
+veb tab close 1 # Close tab 1
+
+# Window management
+veb window new # Open new window
+
+# Session management (isolate multiple agents)
+veb --session agent1 open example.com
+veb --session agent2 open google.com
+veb session list # List active sessions
+VEB_SESSION=agent1 veb eval "document.title"
+
# Close browser (stops daemon)
veb close
```
@@ -96,12 +112,43 @@ veb runs a background daemon that keeps the browser open between commands. The f
| `scroll
[amount]` | Scroll page |
| `hover ` | Hover over element |
| `select ` | Select dropdown option |
+| `tab new` | Open new tab |
+| `tab list` | List all tabs |
+| `tab ` | Switch to tab |
+| `tab close [index]` | Close tab |
+| `window new` | Open new window |
+| `session` | Show current session |
+| `session list` | List active sessions |
| `close` | Close browser |
+## Sessions
+
+Sessions allow multiple agents to use veb simultaneously without interfering with each other. Each session runs its own isolated browser instance.
+
+```bash
+# Using --session flag
+veb --session agent1 open https://site-a.com
+veb --session agent2 open https://site-b.com
+
+# Using environment variable
+export VEB_SESSION=agent1
+veb open https://example.com
+veb click "#button"
+
+# List all running sessions
+veb session list
+
+# Close a specific session
+veb --session agent1 close
+```
+
+Sessions are identified by name. If no session is specified, the "default" session is used.
+
## Options
| Option | Description |
|--------|-------------|
+| `--session ` | Use isolated browser session |
| `--json` | Output raw JSON |
| `--full, -f` | Full page screenshot |
| `--text, -t` | Wait for text |
diff --git a/src/actions.ts b/src/actions.ts
index 8f0d90a..1f154ec 100644
--- a/src/actions.ts
+++ b/src/actions.ts
@@ -14,10 +14,17 @@ import type {
SelectCommand,
HoverCommand,
ContentCommand,
+ TabSwitchCommand,
+ TabCloseCommand,
+ WindowNewCommand,
NavigateData,
ScreenshotData,
EvaluateData,
ContentData,
+ TabListData,
+ TabNewData,
+ TabSwitchData,
+ TabCloseData,
} from './types.js';
import { successResponse, errorResponse } from './protocol.js';
@@ -63,6 +70,16 @@ export async function executeCommand(
return await handleContent(command, browser);
case 'close':
return await handleClose(command, browser);
+ case 'tab_new':
+ return await handleTabNew(command, browser);
+ case 'tab_list':
+ return await handleTabList(command, browser);
+ case 'tab_switch':
+ return await handleTabSwitch(command, browser);
+ case 'tab_close':
+ return await handleTabClose(command, browser);
+ case 'window_new':
+ return await handleWindowNew(command, browser);
default: {
// TypeScript narrows to never here, but we handle it for safety
const unknownCommand = command as { id: string; action: string };
@@ -308,3 +325,50 @@ async function handleClose(
await browser.close();
return successResponse(command.id, { closed: true });
}
+
+async function handleTabNew(
+ command: Command & { action: 'tab_new' },
+ browser: BrowserManager
+): Promise> {
+ const result = await browser.newTab();
+ return successResponse(command.id, result);
+}
+
+async function handleTabList(
+ command: Command & { action: 'tab_list' },
+ browser: BrowserManager
+): Promise> {
+ const tabs = await browser.listTabs();
+ return successResponse(command.id, {
+ tabs,
+ active: browser.getActiveIndex(),
+ });
+}
+
+async function handleTabSwitch(
+ command: TabSwitchCommand,
+ browser: BrowserManager
+): Promise> {
+ const result = browser.switchTo(command.index);
+ const page = browser.getPage();
+ return successResponse(command.id, {
+ ...result,
+ title: await page.title(),
+ });
+}
+
+async function handleTabClose(
+ command: TabCloseCommand,
+ browser: BrowserManager
+): Promise> {
+ const result = await browser.closeTab(command.index);
+ return successResponse(command.id, result);
+}
+
+async function handleWindowNew(
+ command: WindowNewCommand,
+ browser: BrowserManager
+): Promise> {
+ const result = await browser.newWindow(command.viewport);
+ return successResponse(command.id, result);
+}
diff --git a/src/browser.ts b/src/browser.ts
index fec8910..8af9af5 100644
--- a/src/browser.ts
+++ b/src/browser.ts
@@ -1,45 +1,51 @@
import { chromium, firefox, webkit, type Browser, type BrowserContext, type Page } from 'playwright';
-import type { BrowserState, LaunchCommand } from './types.js';
+import type { LaunchCommand } from './types.js';
/**
- * Manages the Playwright browser lifecycle
+ * Manages the Playwright browser lifecycle with multiple tabs/windows
*/
export class BrowserManager {
- private state: BrowserState = {
- browser: null,
- context: null,
- page: null,
- };
+ private browser: Browser | null = null;
+ private contexts: BrowserContext[] = [];
+ private pages: Page[] = [];
+ private activePageIndex: number = 0;
/**
* Check if browser is launched
*/
isLaunched(): boolean {
- return this.state.browser !== null;
+ return this.browser !== null;
}
/**
- * Get the current page, throws if not launched
+ * Get the current active page, throws if not launched
*/
getPage(): Page {
- if (!this.state.page) {
+ if (this.pages.length === 0) {
throw new Error('Browser not launched. Call launch first.');
}
- return this.state.page;
+ return this.pages[this.activePageIndex];
+ }
+
+ /**
+ * Get all pages
+ */
+ getPages(): Page[] {
+ return this.pages;
+ }
+
+ /**
+ * Get current page index
+ */
+ getActiveIndex(): number {
+ return this.activePageIndex;
}
/**
* Get the current browser instance
*/
getBrowser(): Browser | null {
- return this.state.browser;
- }
-
- /**
- * Get the current context
- */
- getContext(): BrowserContext | null {
- return this.state.context;
+ return this.browser;
}
/**
@@ -47,7 +53,7 @@ export class BrowserManager {
*/
async launch(options: LaunchCommand): Promise {
// Close existing browser if any
- if (this.state.browser) {
+ if (this.browser) {
await this.close();
}
@@ -60,36 +66,138 @@ export class BrowserManager {
: chromium;
// Launch browser
- this.state.browser = await launcher.launch({
+ this.browser = await launcher.launch({
headless: options.headless ?? true,
});
// Create context with viewport
- this.state.context = await this.state.browser.newContext({
+ const context = await this.browser.newContext({
viewport: options.viewport ?? { width: 1280, height: 720 },
});
+ this.contexts.push(context);
// Create initial page
- this.state.page = await this.state.context.newPage();
+ const page = await context.newPage();
+ this.pages.push(page);
+ this.activePageIndex = 0;
+ }
+
+ /**
+ * Create a new tab in the current context
+ */
+ async newTab(): Promise<{ index: number; total: number }> {
+ if (!this.browser || this.contexts.length === 0) {
+ throw new Error('Browser not launched');
+ }
+
+ const context = this.contexts[0]; // Use first context for tabs
+ const page = await context.newPage();
+ this.pages.push(page);
+ this.activePageIndex = this.pages.length - 1;
+
+ return { index: this.activePageIndex, total: this.pages.length };
+ }
+
+ /**
+ * Create a new window (new context)
+ */
+ async newWindow(viewport?: { width: number; height: number }): Promise<{ index: number; total: number }> {
+ if (!this.browser) {
+ throw new Error('Browser not launched');
+ }
+
+ const context = await this.browser.newContext({
+ viewport: viewport ?? { width: 1280, height: 720 },
+ });
+ this.contexts.push(context);
+
+ const page = await context.newPage();
+ this.pages.push(page);
+ this.activePageIndex = this.pages.length - 1;
+
+ return { index: this.activePageIndex, total: this.pages.length };
+ }
+
+ /**
+ * Switch to a specific tab/page by index
+ */
+ switchTo(index: number): { index: number; url: string; title: string } {
+ if (index < 0 || index >= this.pages.length) {
+ throw new Error(`Invalid tab index: ${index}. Available: 0-${this.pages.length - 1}`);
+ }
+
+ this.activePageIndex = index;
+ const page = this.pages[index];
+
+ return {
+ index: this.activePageIndex,
+ url: page.url(),
+ title: '', // Title requires async, will be fetched separately
+ };
+ }
+
+ /**
+ * Close a specific tab/page
+ */
+ async closeTab(index?: number): Promise<{ closed: number; remaining: number }> {
+ const targetIndex = index ?? this.activePageIndex;
+
+ if (targetIndex < 0 || targetIndex >= this.pages.length) {
+ throw new Error(`Invalid tab index: ${targetIndex}`);
+ }
+
+ if (this.pages.length === 1) {
+ throw new Error('Cannot close the last tab. Use "close" to close the browser.');
+ }
+
+ const page = this.pages[targetIndex];
+ await page.close();
+ this.pages.splice(targetIndex, 1);
+
+ // Adjust active index if needed
+ if (this.activePageIndex >= this.pages.length) {
+ this.activePageIndex = this.pages.length - 1;
+ } else if (this.activePageIndex > targetIndex) {
+ this.activePageIndex--;
+ }
+
+ return { closed: targetIndex, remaining: this.pages.length };
+ }
+
+ /**
+ * List all tabs with their info
+ */
+ async listTabs(): Promise> {
+ const tabs = await Promise.all(
+ this.pages.map(async (page, index) => ({
+ index,
+ url: page.url(),
+ title: await page.title().catch(() => ''),
+ active: index === this.activePageIndex,
+ }))
+ );
+ return tabs;
}
/**
* Close the browser and clean up
*/
async close(): Promise {
- if (this.state.page) {
- await this.state.page.close().catch(() => {});
- this.state.page = null;
+ for (const page of this.pages) {
+ await page.close().catch(() => {});
+ }
+ this.pages = [];
+
+ for (const context of this.contexts) {
+ await context.close().catch(() => {});
+ }
+ this.contexts = [];
+
+ if (this.browser) {
+ await this.browser.close().catch(() => {});
+ this.browser = null;
}
- if (this.state.context) {
- await this.state.context.close().catch(() => {});
- this.state.context = null;
- }
-
- if (this.state.browser) {
- await this.state.browser.close().catch(() => {});
- this.state.browser = null;
- }
+ this.activePageIndex = 0;
}
}
diff --git a/src/client.ts b/src/client.ts
index 2adba88..fc2ec0b 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -3,7 +3,7 @@ import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import * as path from 'path';
import * as fs from 'fs';
-import { getSocketPath, isDaemonRunning } from './daemon.js';
+import { getSocketPath, isDaemonRunning, setSession, getSession } from './daemon.js';
import type { Response } from './types.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -14,6 +14,8 @@ export function setDebug(enabled: boolean): void {
DEBUG = enabled;
}
+export { setSession, getSession };
+
function debug(...args: unknown[]): void {
if (DEBUG) {
console.error('[debug]', ...args);
@@ -41,7 +43,8 @@ async function waitForSocket(maxAttempts = 30): Promise {
* Ensure daemon is running, start if not
*/
export async function ensureDaemon(): Promise {
- debug('Checking if daemon is running...');
+ const session = getSession();
+ debug(`Checking if daemon is running for session "${session}"...`);
if (isDaemonRunning()) {
debug('Daemon already running');
return;
@@ -52,7 +55,7 @@ export async function ensureDaemon(): Promise {
const child = spawn(process.execPath, [daemonPath], {
detached: true,
stdio: 'ignore',
- env: { ...process.env, VEB_DAEMON: '1' },
+ env: { ...process.env, VEB_DAEMON: '1', VEB_SESSION: session },
});
child.unref();
@@ -62,7 +65,7 @@ export async function ensureDaemon(): Promise {
throw new Error('Failed to start daemon');
}
- debug('Daemon started');
+ debug(`Daemon started for session "${session}"`);
}
/**
diff --git a/src/daemon.ts b/src/daemon.ts
index ba1f21e..d68fb75 100644
--- a/src/daemon.ts
+++ b/src/daemon.ts
@@ -6,48 +6,67 @@ import { BrowserManager } from './browser.js';
import { parseCommand, serializeResponse, errorResponse } from './protocol.js';
import { executeCommand } from './actions.js';
-const SOCKET_PATH = path.join(os.tmpdir(), 'veb.sock');
-const PID_FILE = path.join(os.tmpdir(), 'veb.pid');
+// Session support - each session gets its own socket/pid
+let currentSession = process.env.VEB_SESSION || 'default';
/**
- * Get the socket path
+ * Set the current session
*/
-export function getSocketPath(): string {
- return SOCKET_PATH;
+export function setSession(session: string): void {
+ currentSession = session;
}
/**
- * Get the PID file path
+ * Get the current session
*/
-export function getPidFile(): string {
- return PID_FILE;
+export function getSession(): string {
+ return currentSession;
}
/**
- * Check if daemon is running
+ * Get the socket path for the current session
*/
-export function isDaemonRunning(): boolean {
- if (!fs.existsSync(PID_FILE)) return false;
+export function getSocketPath(session?: string): string {
+ const sess = session ?? currentSession;
+ return path.join(os.tmpdir(), `veb-${sess}.sock`);
+}
+
+/**
+ * Get the PID file path for the current session
+ */
+export function getPidFile(session?: string): string {
+ const sess = session ?? currentSession;
+ return path.join(os.tmpdir(), `veb-${sess}.pid`);
+}
+
+/**
+ * Check if daemon is running for the current session
+ */
+export function isDaemonRunning(session?: string): boolean {
+ const pidFile = getPidFile(session);
+ if (!fs.existsSync(pidFile)) return false;
try {
- const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
// Check if process exists
process.kill(pid, 0);
return true;
} catch {
// Process doesn't exist, clean up stale files
- cleanupSocket();
+ cleanupSocket(session);
return false;
}
}
/**
- * Clean up socket and PID file
+ * Clean up socket and PID file for the current session
*/
-export function cleanupSocket(): void {
+export function cleanupSocket(session?: string): void {
+ const socketPath = getSocketPath(session);
+ const pidFile = getPidFile(session);
try {
- if (fs.existsSync(SOCKET_PATH)) fs.unlinkSync(SOCKET_PATH);
- if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
+ if (fs.existsSync(socketPath)) fs.unlinkSync(socketPath);
+ if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
} catch {
// Ignore cleanup errors
}
@@ -121,10 +140,13 @@ export async function startDaemon(): Promise {
});
});
- // Write PID file before listening
- fs.writeFileSync(PID_FILE, process.pid.toString());
+ const socketPath = getSocketPath();
+ const pidFile = getPidFile();
- server.listen(SOCKET_PATH, () => {
+ // Write PID file before listening
+ fs.writeFileSync(pidFile, process.pid.toString());
+
+ server.listen(socketPath, () => {
// Daemon is ready
});
diff --git a/src/index.ts b/src/index.ts
index 90cbac2..9f5b8b2 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,7 +1,40 @@
#!/usr/bin/env node
-import { send, setDebug } from './client.js';
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+import { send, setDebug, setSession, getSession } from './client.js';
import type { Response } from './types.js';
+/**
+ * List all active veb sessions
+ */
+function listSessions(): string[] {
+ const tmpDir = os.tmpdir();
+ try {
+ const files = fs.readdirSync(tmpDir);
+ const sessions: string[] = [];
+
+ for (const file of files) {
+ const match = file.match(/^veb-(.+)\.pid$/);
+ if (match) {
+ const pidFile = path.join(tmpDir, file);
+ try {
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
+ // Check if process is still running
+ process.kill(pid, 0);
+ sessions.push(match[1]);
+ } catch {
+ // Process not running, ignore
+ }
+ }
+ }
+
+ return sessions;
+ } catch {
+ return [];
+ }
+}
+
// ANSI colors
const colors = {
reset: '\x1b[0m',
@@ -39,7 +72,19 @@ ${c('yellow', 'Commands:')}
${c('cyan', 'select')} Select dropdown option
${c('cyan', 'close')} Close browser and stop daemon
+${c('yellow', 'Tab/Window Commands:')}
+ ${c('cyan', 'tab new')} Open a new tab
+ ${c('cyan', 'tab list')} List all open tabs
+ ${c('cyan', 'tab')} Switch to tab by index
+ ${c('cyan', 'tab close')} [index] Close tab (current if no index)
+ ${c('cyan', 'window new')} Open a new window
+
+${c('yellow', 'Session Commands:')}
+ ${c('cyan', 'session')} Show current session name
+ ${c('cyan', 'session list')} List all active sessions
+
${c('yellow', 'Options:')}
+ --session Use isolated browser session (or VEB_SESSION env)
--json Output raw JSON (for agents)
--selector, -s Target specific element
--text, -t Wait for text instead of selector
@@ -57,6 +102,9 @@ ${c('yellow', 'Examples:')}
veb extract "table" --json
veb eval "document.title"
veb scroll down 500
+ veb tab new
+ veb tab list
+ veb tab 0
`);
}
@@ -101,8 +149,24 @@ function printResponse(response: Response, jsonMode: boolean): void {
console.log(c('green', '✓'), 'Done');
} else if (data.launched) {
console.log(c('green', '✓'), 'Browser launched');
- } else if (data.closed) {
+ } else if (data.closed === true) {
console.log(c('green', '✓'), 'Browser closed');
+ } else if (data.tabs) {
+ // Tab list
+ const tabs = data.tabs as Array<{ index: number; url: string; title: string; active: boolean }>;
+ tabs.forEach(tab => {
+ const marker = tab.active ? c('green', '→') : ' ';
+ const idx = c('cyan', `[${tab.index}]`);
+ const title = tab.title || c('dim', '(untitled)');
+ console.log(`${marker} ${idx} ${title}`);
+ if (tab.url) console.log(c('dim', ` ${tab.url}`));
+ });
+ } else if (data.index !== undefined && data.total !== undefined) {
+ // Tab new / window new
+ console.log(c('green', '✓'), `Tab ${data.index} created (${data.total} total)`);
+ } else if (data.remaining !== undefined) {
+ // Tab close
+ console.log(c('green', '✓'), `Tab closed (${data.remaining} remaining)`);
} else {
console.log(c('green', '✓'), JSON.stringify(data));
}
@@ -117,6 +181,13 @@ async function main(): Promise {
setDebug(true);
}
+ // Handle session - check --session flag first, then env var
+ const sessionIdx = args.findIndex(a => a === '--session');
+ if (sessionIdx !== -1 && args[sessionIdx + 1]) {
+ setSession(args[sessionIdx + 1]);
+ }
+ // VEB_SESSION env var is already handled by daemon.ts default
+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
printHelp();
process.exit(0);
@@ -132,6 +203,7 @@ async function main(): Promise {
// Check if previous arg was a flag that takes a value
const prev = args[i - 1];
if (prev === '--selector' || prev === '-s') return false;
+ if (prev === '--session') return false;
return true;
});
const command = cleanArgs[0];
@@ -292,6 +364,66 @@ async function main(): Promise {
break;
}
+ case 'tab': {
+ const subCmd = cleanArgs[1];
+
+ if (subCmd === 'new') {
+ cmd = { id, action: 'tab_new' };
+ } else if (subCmd === 'list' || subCmd === 'ls') {
+ cmd = { id, action: 'tab_list' };
+ } else if (subCmd === 'close') {
+ const tabIndex = cleanArgs[2] !== undefined ? parseInt(cleanArgs[2], 10) : undefined;
+ cmd = { id, action: 'tab_close', index: tabIndex };
+ } else if (subCmd !== undefined) {
+ // Assume it's a tab index to switch to
+ const tabIndex = parseInt(subCmd, 10);
+ if (isNaN(tabIndex)) {
+ console.error(c('red', 'Error:'), `Invalid tab command or index: ${subCmd}`);
+ process.exit(1);
+ }
+ cmd = { id, action: 'tab_switch', index: tabIndex };
+ } else {
+ // No subcommand - list tabs
+ cmd = { id, action: 'tab_list' };
+ }
+ break;
+ }
+
+ case 'window': {
+ const subCmd = cleanArgs[1];
+
+ if (subCmd === 'new') {
+ cmd = { id, action: 'window_new' };
+ } else {
+ console.error(c('red', 'Error:'), 'Usage: veb window new');
+ process.exit(1);
+ }
+ break;
+ }
+
+ case 'session': {
+ const subCmd = cleanArgs[1];
+
+ if (subCmd === 'list' || subCmd === 'ls') {
+ const sessions = listSessions();
+ const currentSession = getSession();
+
+ if (sessions.length === 0) {
+ console.log(c('dim', 'No active sessions'));
+ } else {
+ sessions.forEach(sess => {
+ const marker = sess === currentSession ? c('green', '→') : ' ';
+ console.log(`${marker} ${c('cyan', sess)}`);
+ });
+ }
+ process.exit(0);
+ } else {
+ // Show current session
+ console.log(c('cyan', getSession()));
+ process.exit(0);
+ }
+ }
+
default:
console.error(c('red', 'Error:'), `Unknown command: ${command}`);
console.error(c('dim', 'Run veb --help for usage'));
diff --git a/src/protocol.ts b/src/protocol.ts
index 75349ea..92848e3 100644
--- a/src/protocol.ts
+++ b/src/protocol.ts
@@ -101,6 +101,33 @@ const closeSchema = baseCommandSchema.extend({
action: z.literal('close'),
});
+// Tab/Window schemas
+const tabNewSchema = baseCommandSchema.extend({
+ action: z.literal('tab_new'),
+});
+
+const tabListSchema = baseCommandSchema.extend({
+ action: z.literal('tab_list'),
+});
+
+const tabSwitchSchema = baseCommandSchema.extend({
+ action: z.literal('tab_switch'),
+ index: z.number().nonnegative(),
+});
+
+const tabCloseSchema = baseCommandSchema.extend({
+ action: z.literal('tab_close'),
+ index: z.number().nonnegative().optional(),
+});
+
+const windowNewSchema = baseCommandSchema.extend({
+ action: z.literal('window_new'),
+ viewport: z.object({
+ width: z.number().positive(),
+ height: z.number().positive(),
+ }).optional(),
+});
+
// Union schema for all commands
const commandSchema = z.discriminatedUnion('action', [
launchSchema,
@@ -117,6 +144,11 @@ const commandSchema = z.discriminatedUnion('action', [
hoverSchema,
contentSchema,
closeSchema,
+ tabNewSchema,
+ tabListSchema,
+ tabSwitchSchema,
+ tabCloseSchema,
+ windowNewSchema,
]);
// Parse result type
diff --git a/src/types.ts b/src/types.ts
index 663a956..9a5503b 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -97,6 +97,30 @@ export interface CloseCommand extends BaseCommand {
action: 'close';
}
+// Tab/Window commands
+export interface TabNewCommand extends BaseCommand {
+ action: 'tab_new';
+}
+
+export interface TabListCommand extends BaseCommand {
+ action: 'tab_list';
+}
+
+export interface TabSwitchCommand extends BaseCommand {
+ action: 'tab_switch';
+ index: number;
+}
+
+export interface TabCloseCommand extends BaseCommand {
+ action: 'tab_close';
+ index?: number;
+}
+
+export interface WindowNewCommand extends BaseCommand {
+ action: 'window_new';
+ viewport?: { width: number; height: number };
+}
+
// Union of all command types
export type Command =
| LaunchCommand
@@ -112,7 +136,12 @@ export type Command =
| SelectCommand
| HoverCommand
| ContentCommand
- | CloseCommand;
+ | CloseCommand
+ | TabNewCommand
+ | TabListCommand
+ | TabSwitchCommand
+ | TabCloseCommand
+ | WindowNewCommand;
// Response types
export interface SuccessResponse {
@@ -152,6 +181,34 @@ export interface ContentData {
html: string;
}
+export interface TabInfo {
+ index: number;
+ url: string;
+ title: string;
+ active: boolean;
+}
+
+export interface TabListData {
+ tabs: TabInfo[];
+ active: number;
+}
+
+export interface TabNewData {
+ index: number;
+ total: number;
+}
+
+export interface TabSwitchData {
+ index: number;
+ url: string;
+ title: string;
+}
+
+export interface TabCloseData {
+ closed: number;
+ remaining: number;
+}
+
// Browser state
export interface BrowserState {
browser: Browser | null;