Add 260+ Claude Code skills from skills.sh
Complete collection of AI agent skills including: - Frontend Development (Vue, React, Next.js, Three.js) - Backend Development (NestJS, FastAPI, Node.js) - Mobile Development (React Native, Expo) - Testing (E2E, frontend, webapp) - DevOps (GitHub Actions, CI/CD) - Marketing (SEO, copywriting, analytics) - Security (binary analysis, vulnerability scanning) - And many more... Synchronized from: https://skills.sh/ Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
211
dev-browser/extension/services/CDPRouter.ts
Normal file
211
dev-browser/extension/services/CDPRouter.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* CDPRouter - Routes CDP commands to the correct tab.
|
||||
*/
|
||||
|
||||
import type { Logger } from "../utils/logger";
|
||||
import type { TabManager } from "./TabManager";
|
||||
import type { ExtensionCommandMessage, TabInfo } from "../utils/types";
|
||||
|
||||
export interface CDPRouterDeps {
|
||||
logger: Logger;
|
||||
tabManager: TabManager;
|
||||
}
|
||||
|
||||
export class CDPRouter {
|
||||
private logger: Logger;
|
||||
private tabManager: TabManager;
|
||||
private devBrowserGroupId: number | null = null;
|
||||
|
||||
constructor(deps: CDPRouterDeps) {
|
||||
this.logger = deps.logger;
|
||||
this.tabManager = deps.tabManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or creates the "Dev Browser" tab group, returning its ID.
|
||||
*/
|
||||
private async getOrCreateDevBrowserGroup(tabId: number): Promise<number> {
|
||||
// If we have a cached group ID, verify it still exists
|
||||
if (this.devBrowserGroupId !== null) {
|
||||
try {
|
||||
await chrome.tabGroups.get(this.devBrowserGroupId);
|
||||
// Group exists, add tab to it
|
||||
await chrome.tabs.group({ tabIds: [tabId], groupId: this.devBrowserGroupId });
|
||||
return this.devBrowserGroupId;
|
||||
} catch {
|
||||
// Group no longer exists, reset cache
|
||||
this.devBrowserGroupId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new group with this tab
|
||||
const groupId = await chrome.tabs.group({ tabIds: [tabId] });
|
||||
await chrome.tabGroups.update(groupId, {
|
||||
title: "Dev Browser",
|
||||
color: "blue",
|
||||
});
|
||||
this.devBrowserGroupId = groupId;
|
||||
return groupId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming CDP command from the relay.
|
||||
*/
|
||||
async handleCommand(msg: ExtensionCommandMessage): Promise<unknown> {
|
||||
if (msg.method !== "forwardCDPCommand") return;
|
||||
|
||||
let targetTabId: number | undefined;
|
||||
let targetTab: TabInfo | undefined;
|
||||
|
||||
// Find target tab by sessionId
|
||||
if (msg.params.sessionId) {
|
||||
const found = this.tabManager.getBySessionId(msg.params.sessionId);
|
||||
if (found) {
|
||||
targetTabId = found.tabId;
|
||||
targetTab = found.tab;
|
||||
}
|
||||
}
|
||||
|
||||
// Check child sessions (iframes, workers)
|
||||
if (!targetTab && msg.params.sessionId) {
|
||||
const parentTabId = this.tabManager.getParentTabId(msg.params.sessionId);
|
||||
if (parentTabId) {
|
||||
targetTabId = parentTabId;
|
||||
targetTab = this.tabManager.get(parentTabId);
|
||||
this.logger.debug(
|
||||
"Found parent tab for child session:",
|
||||
msg.params.sessionId,
|
||||
"tabId:",
|
||||
parentTabId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Find by targetId in params
|
||||
if (
|
||||
!targetTab &&
|
||||
msg.params.params &&
|
||||
typeof msg.params.params === "object" &&
|
||||
"targetId" in msg.params.params
|
||||
) {
|
||||
const found = this.tabManager.getByTargetId(msg.params.params.targetId as string);
|
||||
if (found) {
|
||||
targetTabId = found.tabId;
|
||||
targetTab = found.tab;
|
||||
}
|
||||
}
|
||||
|
||||
const debuggee = targetTabId ? { tabId: targetTabId } : undefined;
|
||||
|
||||
// Handle special commands
|
||||
switch (msg.params.method) {
|
||||
case "Runtime.enable": {
|
||||
if (!debuggee) {
|
||||
throw new Error(
|
||||
`No debuggee found for Runtime.enable (sessionId: ${msg.params.sessionId})`
|
||||
);
|
||||
}
|
||||
// Disable and re-enable to reset state
|
||||
try {
|
||||
await chrome.debugger.sendCommand(debuggee, "Runtime.disable");
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
return await chrome.debugger.sendCommand(debuggee, "Runtime.enable", msg.params.params);
|
||||
}
|
||||
|
||||
case "Target.createTarget": {
|
||||
const url = (msg.params.params?.url as string) || "about:blank";
|
||||
this.logger.debug("Creating new tab with URL:", url);
|
||||
const tab = await chrome.tabs.create({ url, active: false });
|
||||
if (!tab.id) throw new Error("Failed to create tab");
|
||||
|
||||
// Add tab to "Dev Browser" group
|
||||
await this.getOrCreateDevBrowserGroup(tab.id);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
const targetInfo = await this.tabManager.attach(tab.id);
|
||||
return { targetId: targetInfo.targetId };
|
||||
}
|
||||
|
||||
case "Target.closeTarget": {
|
||||
if (!targetTabId) {
|
||||
this.logger.log(`Target not found: ${msg.params.params?.targetId}`);
|
||||
return { success: false };
|
||||
}
|
||||
await chrome.tabs.remove(targetTabId);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
case "Target.activateTarget": {
|
||||
if (!targetTabId) {
|
||||
this.logger.log(`Target not found for activation: ${msg.params.params?.targetId}`);
|
||||
return {};
|
||||
}
|
||||
await chrome.tabs.update(targetTabId, { active: true });
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
if (!debuggee || !targetTab) {
|
||||
throw new Error(
|
||||
`No tab found for method ${msg.params.method} sessionId: ${msg.params.sessionId}`
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.debug("CDP command:", msg.params.method, "for tab:", targetTabId);
|
||||
|
||||
const debuggerSession: chrome.debugger.DebuggerSession = {
|
||||
...debuggee,
|
||||
sessionId: msg.params.sessionId !== targetTab.sessionId ? msg.params.sessionId : undefined,
|
||||
};
|
||||
|
||||
return await chrome.debugger.sendCommand(debuggerSession, msg.params.method, msg.params.params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle debugger events from Chrome.
|
||||
*/
|
||||
handleDebuggerEvent(
|
||||
source: chrome.debugger.DebuggerSession,
|
||||
method: string,
|
||||
params: unknown,
|
||||
sendMessage: (msg: unknown) => void
|
||||
): void {
|
||||
const tab = source.tabId ? this.tabManager.get(source.tabId) : undefined;
|
||||
if (!tab) return;
|
||||
|
||||
this.logger.debug("Forwarding CDP event:", method, "from tab:", source.tabId);
|
||||
|
||||
// Track child sessions
|
||||
if (
|
||||
method === "Target.attachedToTarget" &&
|
||||
params &&
|
||||
typeof params === "object" &&
|
||||
"sessionId" in params
|
||||
) {
|
||||
const sessionId = (params as { sessionId: string }).sessionId;
|
||||
this.tabManager.trackChildSession(sessionId, source.tabId!);
|
||||
}
|
||||
|
||||
if (
|
||||
method === "Target.detachedFromTarget" &&
|
||||
params &&
|
||||
typeof params === "object" &&
|
||||
"sessionId" in params
|
||||
) {
|
||||
const sessionId = (params as { sessionId: string }).sessionId;
|
||||
this.tabManager.untrackChildSession(sessionId);
|
||||
}
|
||||
|
||||
sendMessage({
|
||||
method: "forwardCDPEvent",
|
||||
params: {
|
||||
sessionId: source.sessionId || tab.sessionId,
|
||||
method,
|
||||
params,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
214
dev-browser/extension/services/ConnectionManager.ts
Normal file
214
dev-browser/extension/services/ConnectionManager.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* ConnectionManager - Manages WebSocket connection to relay server.
|
||||
*/
|
||||
|
||||
import type { Logger } from "../utils/logger";
|
||||
import type { ExtensionCommandMessage, ExtensionResponseMessage } from "../utils/types";
|
||||
|
||||
const RELAY_URL = "ws://localhost:9222/extension";
|
||||
const RECONNECT_INTERVAL = 3000;
|
||||
|
||||
export interface ConnectionManagerDeps {
|
||||
logger: Logger;
|
||||
onMessage: (message: ExtensionCommandMessage) => Promise<unknown>;
|
||||
onDisconnect: () => void;
|
||||
}
|
||||
|
||||
export class ConnectionManager {
|
||||
private ws: WebSocket | null = null;
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private shouldMaintain = false;
|
||||
private logger: Logger;
|
||||
private onMessage: (message: ExtensionCommandMessage) => Promise<unknown>;
|
||||
private onDisconnect: () => void;
|
||||
|
||||
constructor(deps: ConnectionManagerDeps) {
|
||||
this.logger = deps.logger;
|
||||
this.onMessage = deps.onMessage;
|
||||
this.onDisconnect = deps.onDisconnect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WebSocket is open (may be stale if server crashed).
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
return this.ws?.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate connection by checking if server is reachable.
|
||||
* More reliable than isConnected() as it detects server crashes.
|
||||
*/
|
||||
async checkConnection(): Promise<boolean> {
|
||||
if (!this.isConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify server is actually reachable
|
||||
try {
|
||||
const response = await fetch("http://localhost:9222", {
|
||||
method: "HEAD",
|
||||
signal: AbortSignal.timeout(1000),
|
||||
});
|
||||
return response.ok;
|
||||
} catch {
|
||||
// Server unreachable - close stale socket
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
this.onDisconnect();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the relay server.
|
||||
*/
|
||||
send(message: unknown): void {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this.ws.send(JSON.stringify(message));
|
||||
} catch (error) {
|
||||
console.debug("Error sending message:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start maintaining connection (auto-reconnect).
|
||||
*/
|
||||
startMaintaining(): void {
|
||||
this.shouldMaintain = true;
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
|
||||
this.tryConnect().catch(() => {});
|
||||
this.reconnectTimer = setTimeout(() => this.startMaintaining(), RECONNECT_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop connection maintenance.
|
||||
*/
|
||||
stopMaintaining(): void {
|
||||
this.shouldMaintain = false;
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from relay and stop maintaining connection.
|
||||
*/
|
||||
disconnect(): void {
|
||||
this.stopMaintaining();
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
this.onDisconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure connection is established, waiting if needed.
|
||||
*/
|
||||
async ensureConnected(): Promise<void> {
|
||||
if (this.isConnected()) return;
|
||||
|
||||
await this.tryConnect();
|
||||
|
||||
if (!this.isConnected()) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await this.tryConnect();
|
||||
}
|
||||
|
||||
if (!this.isConnected()) {
|
||||
throw new Error("Could not connect to relay server");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to connect to relay server once.
|
||||
*/
|
||||
private async tryConnect(): Promise<void> {
|
||||
if (this.isConnected()) return;
|
||||
|
||||
// Check if server is available
|
||||
try {
|
||||
await fetch("http://localhost:9222", { method: "HEAD" });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug("Connecting to relay server...");
|
||||
const socket = new WebSocket(RELAY_URL);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("Connection timeout"));
|
||||
}, 5000);
|
||||
|
||||
socket.onopen = () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
};
|
||||
|
||||
socket.onerror = () => {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error("WebSocket connection failed"));
|
||||
};
|
||||
|
||||
socket.onclose = (event) => {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`WebSocket closed: ${event.reason || event.code}`));
|
||||
};
|
||||
});
|
||||
|
||||
this.ws = socket;
|
||||
this.setupSocketHandlers(socket);
|
||||
this.logger.log("Connected to relay server");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up WebSocket event handlers.
|
||||
*/
|
||||
private setupSocketHandlers(socket: WebSocket): void {
|
||||
socket.onmessage = async (event: MessageEvent) => {
|
||||
let message: ExtensionCommandMessage;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
} catch (error) {
|
||||
this.logger.debug("Error parsing message:", error);
|
||||
this.send({
|
||||
error: { code: -32700, message: "Parse error" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const response: ExtensionResponseMessage = { id: message.id };
|
||||
try {
|
||||
response.result = await this.onMessage(message);
|
||||
} catch (error) {
|
||||
this.logger.debug("Error handling command:", error);
|
||||
response.error = (error as Error).message;
|
||||
}
|
||||
this.send(response);
|
||||
};
|
||||
|
||||
socket.onclose = (event: CloseEvent) => {
|
||||
this.logger.debug("Connection closed:", event.code, event.reason);
|
||||
this.ws = null;
|
||||
this.onDisconnect();
|
||||
if (this.shouldMaintain) {
|
||||
this.startMaintaining();
|
||||
}
|
||||
};
|
||||
|
||||
socket.onerror = (event: Event) => {
|
||||
this.logger.debug("WebSocket error:", event);
|
||||
};
|
||||
}
|
||||
}
|
||||
28
dev-browser/extension/services/StateManager.ts
Normal file
28
dev-browser/extension/services/StateManager.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* StateManager - Manages extension active/inactive state with persistence.
|
||||
*/
|
||||
|
||||
const STORAGE_KEY = "devBrowserActiveState";
|
||||
|
||||
export interface ExtensionState {
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export class StateManager {
|
||||
/**
|
||||
* Get the current extension state.
|
||||
* Defaults to inactive if no state is stored.
|
||||
*/
|
||||
async getState(): Promise<ExtensionState> {
|
||||
const result = await chrome.storage.local.get(STORAGE_KEY);
|
||||
const state = result[STORAGE_KEY] as ExtensionState | undefined;
|
||||
return state ?? { isActive: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the extension state.
|
||||
*/
|
||||
async setState(state: ExtensionState): Promise<void> {
|
||||
await chrome.storage.local.set({ [STORAGE_KEY]: state });
|
||||
}
|
||||
}
|
||||
218
dev-browser/extension/services/TabManager.ts
Normal file
218
dev-browser/extension/services/TabManager.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* TabManager - Manages tab state and debugger attachment.
|
||||
*/
|
||||
|
||||
import type { TabInfo, TargetInfo } from "../utils/types";
|
||||
import type { Logger } from "../utils/logger";
|
||||
|
||||
export type SendMessageFn = (message: unknown) => void;
|
||||
|
||||
export interface TabManagerDeps {
|
||||
logger: Logger;
|
||||
sendMessage: SendMessageFn;
|
||||
}
|
||||
|
||||
export class TabManager {
|
||||
private tabs = new Map<number, TabInfo>();
|
||||
private childSessions = new Map<string, number>(); // sessionId -> parentTabId
|
||||
private nextSessionId = 1;
|
||||
private logger: Logger;
|
||||
private sendMessage: SendMessageFn;
|
||||
|
||||
constructor(deps: TabManagerDeps) {
|
||||
this.logger = deps.logger;
|
||||
this.sendMessage = deps.sendMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tab info by session ID.
|
||||
*/
|
||||
getBySessionId(sessionId: string): { tabId: number; tab: TabInfo } | undefined {
|
||||
for (const [tabId, tab] of this.tabs) {
|
||||
if (tab.sessionId === sessionId) {
|
||||
return { tabId, tab };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tab info by target ID.
|
||||
*/
|
||||
getByTargetId(targetId: string): { tabId: number; tab: TabInfo } | undefined {
|
||||
for (const [tabId, tab] of this.tabs) {
|
||||
if (tab.targetId === targetId) {
|
||||
return { tabId, tab };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parent tab ID for a child session (iframe, worker).
|
||||
*/
|
||||
getParentTabId(sessionId: string): number | undefined {
|
||||
return this.childSessions.get(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tab info by tab ID.
|
||||
*/
|
||||
get(tabId: number): TabInfo | undefined {
|
||||
return this.tabs.get(tabId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tab is tracked.
|
||||
*/
|
||||
has(tabId: number): boolean {
|
||||
return this.tabs.has(tabId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set tab info (used for intermediate states like "connecting").
|
||||
*/
|
||||
set(tabId: number, info: TabInfo): void {
|
||||
this.tabs.set(tabId, info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a child session (iframe, worker).
|
||||
*/
|
||||
trackChildSession(sessionId: string, parentTabId: number): void {
|
||||
this.logger.debug("Child target attached:", sessionId, "for tab:", parentTabId);
|
||||
this.childSessions.set(sessionId, parentTabId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Untrack a child session.
|
||||
*/
|
||||
untrackChildSession(sessionId: string): void {
|
||||
this.logger.debug("Child target detached:", sessionId);
|
||||
this.childSessions.delete(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach debugger to a tab and register it.
|
||||
*/
|
||||
async attach(tabId: number): Promise<TargetInfo> {
|
||||
const debuggee = { tabId };
|
||||
|
||||
this.logger.debug("Attaching debugger to tab:", tabId);
|
||||
await chrome.debugger.attach(debuggee, "1.3");
|
||||
|
||||
const result = (await chrome.debugger.sendCommand(debuggee, "Target.getTargetInfo")) as {
|
||||
targetInfo: TargetInfo;
|
||||
};
|
||||
|
||||
const targetInfo = result.targetInfo;
|
||||
const sessionId = `pw-tab-${this.nextSessionId++}`;
|
||||
|
||||
this.tabs.set(tabId, {
|
||||
sessionId,
|
||||
targetId: targetInfo.targetId,
|
||||
state: "connected",
|
||||
});
|
||||
|
||||
// Notify relay of new target
|
||||
this.sendMessage({
|
||||
method: "forwardCDPEvent",
|
||||
params: {
|
||||
method: "Target.attachedToTarget",
|
||||
params: {
|
||||
sessionId,
|
||||
targetInfo: { ...targetInfo, attached: true },
|
||||
waitingForDebugger: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log("Tab attached:", tabId, "sessionId:", sessionId, "url:", targetInfo.url);
|
||||
return targetInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach a tab and clean up.
|
||||
*/
|
||||
detach(tabId: number, shouldDetachDebugger: boolean): void {
|
||||
const tab = this.tabs.get(tabId);
|
||||
if (!tab) return;
|
||||
|
||||
this.logger.debug("Detaching tab:", tabId);
|
||||
|
||||
this.sendMessage({
|
||||
method: "forwardCDPEvent",
|
||||
params: {
|
||||
method: "Target.detachedFromTarget",
|
||||
params: { sessionId: tab.sessionId, targetId: tab.targetId },
|
||||
},
|
||||
});
|
||||
|
||||
this.tabs.delete(tabId);
|
||||
|
||||
// Clean up child sessions
|
||||
for (const [childSessionId, parentTabId] of this.childSessions) {
|
||||
if (parentTabId === tabId) {
|
||||
this.childSessions.delete(childSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDetachDebugger) {
|
||||
chrome.debugger.detach({ tabId }).catch((err) => {
|
||||
this.logger.debug("Error detaching debugger:", err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle debugger detach event from Chrome.
|
||||
*/
|
||||
handleDebuggerDetach(tabId: number): void {
|
||||
if (!this.tabs.has(tabId)) return;
|
||||
|
||||
const tab = this.tabs.get(tabId);
|
||||
if (tab) {
|
||||
this.sendMessage({
|
||||
method: "forwardCDPEvent",
|
||||
params: {
|
||||
method: "Target.detachedFromTarget",
|
||||
params: { sessionId: tab.sessionId, targetId: tab.targetId },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up child sessions
|
||||
for (const [childSessionId, parentTabId] of this.childSessions) {
|
||||
if (parentTabId === tabId) {
|
||||
this.childSessions.delete(childSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
this.tabs.delete(tabId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all tabs and child sessions.
|
||||
*/
|
||||
clear(): void {
|
||||
this.tabs.clear();
|
||||
this.childSessions.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach all tabs (used on disconnect).
|
||||
*/
|
||||
detachAll(): void {
|
||||
for (const tabId of this.tabs.keys()) {
|
||||
chrome.debugger.detach({ tabId }).catch(() => {});
|
||||
}
|
||||
this.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tab IDs.
|
||||
*/
|
||||
getAllTabIds(): number[] {
|
||||
return Array.from(this.tabs.keys());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user