fix: query playwriter tab group by title instead of caching ID

- Remove global playwriterGroupId cache that could become stale
- Query chrome.tabGroups by title on each sync instead
- Use batch chrome.tabs.ungroup() with array of IDs instead of loops
- Fixes issue where group wasn't created after debugger detach/reattach
This commit is contained in:
Tommy D. Rossi
2026-01-04 16:09:32 +01:00
parent b4689f390f
commit 8e62b657cc
3 changed files with 567 additions and 570 deletions
+26 -29
View File
@@ -14,7 +14,6 @@ function sleep(ms: number): Promise<void> {
let childSessions: Map<string, number> = new Map() let childSessions: Map<string, number> = new Map()
let nextSessionId = 1 let nextSessionId = 1
let playwriterGroupId: number | null = null
let tabGroupQueue: Promise<void> = Promise.resolve() let tabGroupQueue: Promise<void> = Promise.resolve()
class ConnectionManager { class ConnectionManager {
@@ -427,63 +426,60 @@ async function syncTabGroup(): Promise<void> {
.filter(([_, info]) => info.state === 'connected') .filter(([_, info]) => info.state === 'connected')
.map(([tabId]) => tabId) .map(([tabId]) => tabId)
// Always query by title - no cached ID that can go stale
const existingGroups = await chrome.tabGroups.query({ title: 'playwriter' }) const existingGroups = await chrome.tabGroups.query({ title: 'playwriter' })
// Check for no connected tabs FIRST, before setting playwriterGroupId // If no connected tabs, clear any existing playwriter groups
// This prevents a race condition where onTabUpdated sees playwriterGroupId !== null
if (connectedTabIds.length === 0) { if (connectedTabIds.length === 0) {
for (const group of existingGroups) { for (const group of existingGroups) {
const tabsInGroup = await chrome.tabs.query({ groupId: group.id }) const tabsInGroup = await chrome.tabs.query({ groupId: group.id })
for (const tab of tabsInGroup) { const tabIdsToUngroup = tabsInGroup.map((t) => t.id).filter((id): id is number => id !== undefined)
if (tab.id) { if (tabIdsToUngroup.length > 0) {
await chrome.tabs.ungroup(tab.id) await chrome.tabs.ungroup(tabIdsToUngroup)
}
} }
logger.debug('Cleared playwriter group:', group.id) logger.debug('Cleared playwriter group:', group.id)
} }
playwriterGroupId = null
return return
} }
// Consolidate duplicate groups into one
let groupId: number | undefined = existingGroups[0]?.id
if (existingGroups.length > 1) { if (existingGroups.length > 1) {
const [keep, ...duplicates] = existingGroups const [keep, ...duplicates] = existingGroups
groupId = keep.id
for (const group of duplicates) { for (const group of duplicates) {
const tabsInDupe = await chrome.tabs.query({ groupId: group.id }) const tabsInDupe = await chrome.tabs.query({ groupId: group.id })
for (const tab of tabsInDupe) { const tabIdsToUngroup = tabsInDupe.map((t) => t.id).filter((id): id is number => id !== undefined)
if (tab.id) { if (tabIdsToUngroup.length > 0) {
await chrome.tabs.ungroup(tab.id) await chrome.tabs.ungroup(tabIdsToUngroup)
}
} }
logger.debug('Removed duplicate playwriter group:', group.id) logger.debug('Removed duplicate playwriter group:', group.id)
} }
playwriterGroupId = keep.id
} else if (existingGroups.length === 1) {
playwriterGroupId = existingGroups[0].id
} }
const allTabs = await chrome.tabs.query({}) const allTabs = await chrome.tabs.query({})
const tabsInGroup = allTabs.filter((t) => t.groupId === playwriterGroupId && t.id !== undefined) const tabsInGroup = allTabs.filter((t) => t.groupId === groupId && t.id !== undefined)
const tabIdsInGroup = new Set(tabsInGroup.map((t) => t.id!)) const tabIdsInGroup = new Set(tabsInGroup.map((t) => t.id!))
const tabsToAdd = connectedTabIds.filter((id) => !tabIdsInGroup.has(id)) const tabsToAdd = connectedTabIds.filter((id) => !tabIdsInGroup.has(id))
const tabsToRemove = Array.from(tabIdsInGroup).filter((id) => !connectedTabIds.includes(id)) const tabsToRemove = Array.from(tabIdsInGroup).filter((id) => !connectedTabIds.includes(id))
for (const tabId of tabsToRemove) { if (tabsToRemove.length > 0) {
try { try {
await chrome.tabs.ungroup(tabId) await chrome.tabs.ungroup(tabsToRemove)
logger.debug('Removed tab from group:', tabId) logger.debug('Removed tabs from group:', tabsToRemove)
} catch (e: any) { } catch (e: any) {
logger.debug('Failed to ungroup tab:', tabId, e.message) logger.debug('Failed to ungroup tabs:', tabsToRemove, e.message)
} }
} }
if (tabsToAdd.length > 0) { if (tabsToAdd.length > 0) {
if (playwriterGroupId === null) { if (groupId === undefined) {
playwriterGroupId = await chrome.tabs.group({ tabIds: tabsToAdd }) const newGroupId = await chrome.tabs.group({ tabIds: tabsToAdd })
await chrome.tabGroups.update(playwriterGroupId, { title: 'playwriter', color: 'green' }) await chrome.tabGroups.update(newGroupId, { title: 'playwriter', color: 'green' })
logger.debug('Created tab group:', playwriterGroupId, 'with tabs:', tabsToAdd) logger.debug('Created tab group:', newGroupId, 'with tabs:', tabsToAdd)
} else { } else {
await chrome.tabs.group({ tabIds: tabsToAdd, groupId: playwriterGroupId }) await chrome.tabs.group({ tabIds: tabsToAdd, groupId })
logger.debug('Added tabs to existing group:', tabsToAdd) logger.debug('Added tabs to existing group:', tabsToAdd)
} }
} }
@@ -876,7 +872,6 @@ async function toggleExtensionForActiveTab(): Promise<{ isConnected: boolean; st
async function disconnectEverything(): Promise<void> { async function disconnectEverything(): Promise<void> {
// Queue disconnect operation to serialize with other tab group operations // Queue disconnect operation to serialize with other tab group operations
tabGroupQueue = tabGroupQueue.then(async () => { tabGroupQueue = tabGroupQueue.then(async () => {
playwriterGroupId = null
const { tabs } = store.getState() const { tabs } = store.getState()
for (const tabId of tabs.keys()) { for (const tabId of tabs.keys()) {
await disconnectTab(tabId) await disconnectTab(tabId)
@@ -1164,12 +1159,14 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.groupId !== undefined) { if (changeInfo.groupId !== undefined) {
// Queue tab group operations to serialize with syncTabGroup and disconnectEverything // Queue tab group operations to serialize with syncTabGroup and disconnectEverything
tabGroupQueue = tabGroupQueue.then(async () => { tabGroupQueue = tabGroupQueue.then(async () => {
// Re-check conditions after queue - state may have changed // Query for playwriter group by title - no stale cached ID
if (playwriterGroupId === null) { const existingGroups = await chrome.tabGroups.query({ title: 'playwriter' })
const groupId = existingGroups[0]?.id
if (groupId === undefined) {
return return
} }
const { tabs } = store.getState() const { tabs } = store.getState()
if (changeInfo.groupId === playwriterGroupId) { if (changeInfo.groupId === groupId) {
if (!tabs.has(tabId) && !isRestrictedUrl(tab.url)) { if (!tabs.has(tabId) && !isRestrictedUrl(tab.url)) {
logger.debug('Tab manually added to playwriter group:', tabId) logger.debug('Tab manually added to playwriter group:', tabId)
await connectTab(tabId) await connectTab(tabId)
@@ -31,7 +31,7 @@
- generic [ref=e55]: - generic [ref=e55]:
- text: "Google offered in:" - text: "Google offered in:"
- link [ref=e56] [cursor=pointer]: - link [ref=e56] [cursor=pointer]:
- /url: https://www.google.com/setprefs?sig=0_SUYEzrSi8XZY6OXPbGayYpDBmR0%3D&hl=it&source=homepage&sa=X&ved=0ahUKEwjv3IDE0fGRAxXk2ckDHd3hHMQQ2ZgBCBU - /url: https://www.google.com/setprefs?sig=0_46HrevRB93iEOCE9jEsq18qNx-Q%3D&hl=it&source=homepage&sa=X&ved=0ahUKEwjGpZr4lPKRAxXcFTQIHQEtATMQ2ZgBCBU
- text: Italiano - text: Italiano
- contentinfo [ref=e58]: - contentinfo [ref=e58]:
- generic [ref=e59]: Italy - generic [ref=e59]: Italy
File diff suppressed because it is too large Load Diff