diff --git a/CHANGELOG.md b/CHANGELOG.md index d97bd0cf9..45839520d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ Docs: https://docs.clawd.bot - Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467) - Exec approvals: persist allowlist entry ids to keep macOS allowlist rows stable. (#1521) Thanks @ngutman. - MS Teams (plugin): remove `.default` suffix from Graph scopes to avoid double-appending. (#1507) Thanks @Evizero. +- Browser: keep extension relay tabs controllable when the extension reuses a session id after switching tabs. (#1160) ## 2026.1.22 diff --git a/src/browser/extension-relay.test.ts b/src/browser/extension-relay.test.ts index 181f02b77..e1cd67eed 100644 --- a/src/browser/extension-relay.test.ts +++ b/src/browser/extension-relay.test.ts @@ -247,4 +247,71 @@ describe("chrome extension relay server", () => { cdp.close(); ext.close(); }, 15_000); + + it("rebroadcasts attach when a session id is reused for a new target", async () => { + const port = await getFreePort(); + cdpUrl = `http://127.0.0.1:${port}`; + await ensureChromeExtensionRelayServer({ cdpUrl }); + + const ext = new WebSocket(`ws://127.0.0.1:${port}/extension`); + await waitForOpen(ext); + + const cdp = new WebSocket(`ws://127.0.0.1:${port}/cdp`); + await waitForOpen(cdp); + const q = createMessageQueue(cdp); + + ext.send( + JSON.stringify({ + method: "forwardCDPEvent", + params: { + method: "Target.attachedToTarget", + params: { + sessionId: "shared-session", + targetInfo: { + targetId: "t1", + type: "page", + title: "First", + url: "https://example.com", + }, + waitingForDebugger: false, + }, + }, + }), + ); + + const first = JSON.parse(await q.next()) as { method?: string; params?: unknown }; + expect(first.method).toBe("Target.attachedToTarget"); + expect(JSON.stringify(first.params ?? {})).toContain("t1"); + + ext.send( + JSON.stringify({ + method: "forwardCDPEvent", + params: { + method: "Target.attachedToTarget", + params: { + sessionId: "shared-session", + targetInfo: { + targetId: "t2", + type: "page", + title: "Second", + url: "https://example.org", + }, + waitingForDebugger: false, + }, + }, + }), + ); + + const received: Array<{ method?: string; params?: unknown }> = []; + received.push(JSON.parse(await q.next()) as never); + received.push(JSON.parse(await q.next()) as never); + + const detached = received.find((m) => m.method === "Target.detachedFromTarget"); + const attached = received.find((m) => m.method === "Target.attachedToTarget"); + expect(JSON.stringify(detached?.params ?? {})).toContain("t1"); + expect(JSON.stringify(attached?.params ?? {})).toContain("t2"); + + cdp.close(); + ext.close(); + }); }); diff --git a/src/browser/extension-relay.ts b/src/browser/extension-relay.ts index 00602a451..f93ff0abe 100644 --- a/src/browser/extension-relay.ts +++ b/src/browser/extension-relay.ts @@ -477,13 +477,23 @@ export async function ensureChromeExtensionRelayServer(opts: { const targetType = attached?.targetInfo?.type ?? "page"; if (targetType !== "page") return; if (attached?.sessionId && attached?.targetInfo?.targetId) { - const already = connectedTargets.has(attached.sessionId); + const prev = connectedTargets.get(attached.sessionId); + const nextTargetId = attached.targetInfo.targetId; + const prevTargetId = prev?.targetId; + const changedTarget = Boolean(prev && prevTargetId && prevTargetId !== nextTargetId); connectedTargets.set(attached.sessionId, { sessionId: attached.sessionId, - targetId: attached.targetInfo.targetId, + targetId: nextTargetId, targetInfo: attached.targetInfo, }); - if (!already) { + if (changedTarget && prevTargetId) { + broadcastToCdpClients({ + method: "Target.detachedFromTarget", + params: { sessionId: attached.sessionId, targetId: prevTargetId }, + sessionId: attached.sessionId, + }); + } + if (!prev || changedTarget) { broadcastToCdpClients({ method, params, sessionId }); } return;