Lifecycle Hooks
Lifecycle hooks let your plugin react to what's happening in the page — when the adapter activates, when the user navigates, when a tool starts or finishes. They're practical, not abstract: most plugins only need onActivate and onDeactivate to manage event listeners. The rest are there when you need them. All hooks are optional methods on the OpenTabsPlugin base class — implement only the ones you need. Because the scaffolded tsconfig.json enables noImplicitOverride, implementations must use the override keyword — omitting it causes a TS4114 type error at build time.
Import
import { OpenTabsPlugin } from '@opentabs-dev/plugin-sdk';Hook Lifecycle
Hooks fire at specific points in the adapter's lifetime. Here's the order:
| Phase | Hook | When it fires |
|---|---|---|
| Registration | onActivate() | Once, after the adapter is registered on globalThis.__openTabs.adapters. |
| Navigation | onNavigate(url) | On in-page URL changes (pushState, replaceState, popstate, hashchange). |
| Tool start | onToolInvocationStart(toolName) | Before each tool.handle() execution. |
| Tool end | onToolInvocationEnd(toolName, success, durationMs) | After each tool.handle() completes (success or failure). |
| Removal | onDeactivate() | When the adapter is being removed — on plugin update, plugin uninstall, or when the tab navigates away from a matching URL. |
| Re-injection | teardown() | After onDeactivate(), before the new adapter version is injected. Fires on plugin update (re-injection) and plugin uninstall. |
All hooks run in the page context. Errors in hooks are caught and logged — they never prevent adapter registration or tool execution.
onActivate()
Called once after the adapter is successfully registered. Use it to set up page-level event listeners, observers, or other resources that should exist for the adapter's lifetime.
onActivate?(): void;class MyPlugin extends OpenTabsPlugin {
private cleanup?: () => void;
override onActivate() {
// Set up a keyboard shortcut listener
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
// Handle escape key
}
};
document.addEventListener('keydown', handler);
this.cleanup = () => document.removeEventListener('keydown', handler);
}
override onDeactivate() {
this.cleanup?.();
}
// ... abstract properties and methods
}onDeactivate()
Called when the adapter is being removed — on plugin update, plugin uninstall, or when the tab navigates away from a matching URL. Does not fire when the tab closes (the browser destroys the JavaScript context before cleanup code can run). Use it to clean up resources set up in onActivate().
onDeactivate?(): void;teardown()
Called by the platform before re-injection on plugin update. Use it to clean up event listeners, timers, or global state set up by the previous adapter version so the new adapter version starts from a clean slate.
teardown?(): void;When both onDeactivate and teardown are defined, onDeactivate fires first, then teardown. teardown() fires on plugin update (re-injection) and plugin uninstall. onDeactivate() additionally fires when the tab navigates away from a matching URL. Neither hook fires when the tab closes — the browser destroys the JavaScript context before any cleanup code can run.
teardown() is optional — plugins that do not set up persistent side effects (such as window properties or setInterval timers) can omit it. Because the scaffolded tsconfig.json enables noImplicitOverride, implementations must use the override keyword.
class MyPlugin extends OpenTabsPlugin {
private _pollInterval?: ReturnType<typeof setInterval>;
override onActivate() {
// Expose a global handle and start polling
(window as Record<string, unknown>).__myPlugin = { active: true };
this._pollInterval = setInterval(() => {
// poll for updates
}, 5000);
}
override teardown() {
// Clean up before the new adapter version is injected
clearInterval(this._pollInterval);
delete (window as Record<string, unknown>).__myPlugin;
}
// ... abstract properties and methods
}onNavigate(url)
Called on in-page URL changes — pushState, replaceState, popstate, and hashchange events. Not called on full page loads (the adapter is re-injected on full navigation). If the plugin does not implement this method, no navigation listeners are set up.
onNavigate?(url: string): void;| Parameter | Type | Description |
|---|---|---|
url | string | The new URL after navigation (window.location.href). |
class MyPlugin extends OpenTabsPlugin {
private currentProjectId: string | null = null;
override onNavigate(url: string) {
// Track which project the user is viewing
const match = url.match(/\/projects\/(\w+)/);
this.currentProjectId = match?.[1] ?? null;
}
// ... abstract properties and methods
}onToolInvocationStart(toolName)
Called before each tool.handle() execution. Receives the unprefixed tool name (e.g., 'send_message', not 'slack_send_message').
onToolInvocationStart?(toolName: string): void;| Parameter | Type | Description |
|---|---|---|
toolName | string | The unprefixed tool name. |
class MyPlugin extends OpenTabsPlugin {
override onToolInvocationStart(toolName: string) {
log.info(`Starting tool: ${toolName}`);
}
// ... abstract properties and methods
}onToolInvocationEnd(toolName, success, durationMs)
Called after each tool.handle() completes, whether it succeeded or threw. Receives the tool name, a success boolean, and the wall-clock duration in milliseconds.
onToolInvocationEnd?(toolName: string, success: boolean, durationMs: number): void;| Parameter | Type | Description |
|---|---|---|
toolName | string | The unprefixed tool name. |
success | boolean | true if handle() resolved, false if it threw. |
durationMs | number | Wall-clock time of tool.handle() in milliseconds. |
class MyPlugin extends OpenTabsPlugin {
override onToolInvocationEnd(toolName: string, success: boolean, durationMs: number) {
if (!success) {
log.warn(`Tool ${toolName} failed after ${durationMs}ms`);
} else if (durationMs > 5000) {
log.warn(`Tool ${toolName} took ${durationMs}ms — consider adding progress reporting`);
}
}
// ... abstract properties and methods
}Full Example
A plugin that uses all lifecycle hooks to track state and log telemetry:
import { OpenTabsPlugin, log } from '@opentabs-dev/plugin-sdk';
import type { ToolDefinition } from '@opentabs-dev/plugin-sdk';
class AnalyticsPlugin extends OpenTabsPlugin {
readonly name = 'analytics';
readonly description = 'Analytics dashboard tools';
override readonly displayName = 'Analytics';
readonly urlPatterns = ['*://analytics.example.com/*'];
readonly tools: ToolDefinition[] = [/* ... */];
private currentPage: string | null = null;
private toolCount = 0;
async isReady() {
return document.querySelector('.app-loaded') !== null;
}
override onActivate() {
log.info('Analytics adapter activated');
this.currentPage = window.location.pathname;
(window as Record<string, unknown>).__analyticsAdapter = { name: this.name };
}
override onDeactivate() {
log.info('Analytics adapter deactivated', {
totalToolCalls: this.toolCount,
});
}
override teardown() {
// Remove the global handle before the new adapter version is injected
delete (window as Record<string, unknown>).__analyticsAdapter;
}
override onNavigate(url: string) {
const path = new URL(url).pathname;
log.debug('Navigated', { from: this.currentPage, to: path });
this.currentPage = path;
}
override onToolInvocationStart(toolName: string) {
log.debug(`Tool starting: ${toolName}`, { page: this.currentPage });
}
override onToolInvocationEnd(toolName: string, success: boolean, durationMs: number) {
this.toolCount++;
log.info(`Tool completed: ${toolName}`, { success, durationMs });
}
}Wiring
Lifecycle hooks are wired automatically by the opentabs-plugin build command in the generated adapter IIFE. Plugin authors only need to implement the methods on their OpenTabsPlugin subclass — no manual registration is required.
Last Updated: 10 Mar, 2026