diff --git a/extension/src/background.ts b/extension/src/background.ts index 4d9de55..b170555 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -9,6 +9,7 @@ let ws: WebSocket | null = null let childSessions: Map = new Map() let nextSessionId = 1 let playwriterGroupId: number | null = null +let syncTabGroupQueue: Promise = Promise.resolve() const store = createStore(() => ({ tabs: new Map(), @@ -95,43 +96,72 @@ function sendMessage(message: any): void { } } -async function addTabToGroup(tabId: number): Promise { +async function syncTabGroup(): Promise { try { - if (playwriterGroupId !== null) { - const existingGroups = await chrome.tabGroups.query({ title: 'playwriter' }) - if (!existingGroups.some((g) => g.id === playwriterGroupId)) { - playwriterGroupId = null + const connectedTabIds = Array.from(store.getState().tabs.entries()) + .filter(([_, info]) => info.state === 'connected') + .map(([tabId]) => tabId) + + const existingGroups = await chrome.tabGroups.query({ title: 'playwriter' }) + + if (existingGroups.length > 1) { + const [keep, ...duplicates] = existingGroups + for (const group of duplicates) { + const tabsInDupe = await chrome.tabs.query({ groupId: group.id }) + for (const tab of tabsInDupe) { + if (tab.id) { + await chrome.tabs.ungroup(tab.id) + } + } + logger.debug('Removed duplicate playwriter group:', group.id) + } + playwriterGroupId = keep.id + } else if (existingGroups.length === 1) { + playwriterGroupId = existingGroups[0].id + } + + if (connectedTabIds.length === 0) { + for (const group of existingGroups) { + const tabsInGroup = await chrome.tabs.query({ groupId: group.id }) + for (const tab of tabsInGroup) { + if (tab.id) { + await chrome.tabs.ungroup(tab.id) + } + } + logger.debug('Cleared playwriter group:', group.id) + } + playwriterGroupId = null + return + } + + const allTabs = await chrome.tabs.query({}) + const tabsInGroup = allTabs.filter((t) => t.groupId === playwriterGroupId && t.id !== undefined) + const tabIdsInGroup = new Set(tabsInGroup.map((t) => t.id!)) + + const tabsToAdd = connectedTabIds.filter((id) => !tabIdsInGroup.has(id)) + const tabsToRemove = Array.from(tabIdsInGroup).filter((id) => !connectedTabIds.includes(id)) + + for (const tabId of tabsToRemove) { + try { + await chrome.tabs.ungroup(tabId) + logger.debug('Removed tab from group:', tabId) + } catch (e: any) { + logger.debug('Failed to ungroup tab:', tabId, e.message) } } - if (playwriterGroupId === null) { - playwriterGroupId = await chrome.tabs.group({ tabIds: [tabId] }) - await chrome.tabGroups.update(playwriterGroupId, { title: 'playwriter', color: 'green' }) - logger.debug('Created tab group:', playwriterGroupId) - } else { - await chrome.tabs.group({ tabIds: [tabId], groupId: playwriterGroupId }) - logger.debug('Added tab to existing group:', tabId) - } - } catch (error: any) { - logger.debug('Failed to add tab to group:', error.message) - } -} - -async function removeTabFromGroup(tabId: number): Promise { - try { - await chrome.tabs.ungroup(tabId) - logger.debug('Removed tab from group:', tabId) - - if (playwriterGroupId !== null) { - const groups = await chrome.tabGroups.query({ title: 'playwriter' }) - const group = groups.find((g) => g.id === playwriterGroupId) - if (!group) { - playwriterGroupId = null - logger.debug('Tab group was deleted') + if (tabsToAdd.length > 0) { + if (playwriterGroupId === null) { + playwriterGroupId = await chrome.tabs.group({ tabIds: tabsToAdd }) + await chrome.tabGroups.update(playwriterGroupId, { title: 'playwriter', color: 'green' }) + logger.debug('Created tab group:', playwriterGroupId, 'with tabs:', tabsToAdd) + } else { + await chrome.tabs.group({ tabIds: tabsToAdd, groupId: playwriterGroupId }) + logger.debug('Added tabs to existing group:', tabsToAdd) } } } catch (error: any) { - logger.debug('Failed to remove tab from group:', error.message) + logger.debug('Failed to sync tab group:', error.message) } } @@ -340,7 +370,6 @@ async function attachTab(tabId: number): Promise { }) logger.debug('Tab attached successfully:', tabId, 'sessionId:', sessionId, 'targetId:', targetInfo.targetId) - await addTabToGroup(tabId) return targetInfo } @@ -353,8 +382,6 @@ function detachTab(tabId: number, shouldDetachDebugger: boolean): void { logger.debug('Detaching tab:', tabId, 'sessionId:', tab.sessionId, 'shouldDetach:', shouldDetachDebugger) - void removeTabFromGroup(tabId) - sendMessage({ method: 'forwardCDPEvent', params: { @@ -397,7 +424,6 @@ function closeConnection(reason: string): void { store.setState({ tabs: new Map(), connectionState: 'disconnected', errorText: undefined }) childSessions.clear() - playwriterGroupId = null if (ws) { ws.close(1000, reason) @@ -420,7 +446,6 @@ function handleConnectionClose(reason: string, code: number): void { } childSessions.clear() - playwriterGroupId = null ws = null if (reason === 'Extension Replaced' || code === 4001) { @@ -851,9 +876,19 @@ chrome.runtime.onInstalled.addListener((details) => { } }) -store.subscribe(async () => { - logger.log(store.getState()) - await updateIcons() +function serializeTabs(tabs: Map): string { + return JSON.stringify(Array.from(tabs.entries())) +} + +store.subscribe((state, prevState) => { + logger.log(state) + void updateIcons() + const tabsChanged = serializeTabs(state.tabs) !== serializeTabs(prevState.tabs) + if (tabsChanged) { + syncTabGroupQueue = syncTabGroupQueue.then(syncTabGroup).catch((e) => { + logger.debug('syncTabGroup error:', e) + }) + } }) logger.debug(`Using relay URL: ${RELAY_URL}`)