Keep browser input from submitting Paseo prompts (#1982)

* fix(desktop): keep browser input out of the composer

Unhandled webview keys could be redispatched into the active host window, allowing agent Enter to submit a draft prompt. Give pages first refusal for ordinary shortcuts and contain automation at the guest boundary.

* fix(desktop): tighten browser shortcut validation

* fix(desktop): respect browser shortcut ownership

* fix(desktop): retain browser keys across windows

* fix(desktop): scope browser webviews by host

* Reshape browser shortcut ownership

Use the browser webview registry as the single guest identity authority, scope browser operations to their host window, publish chord continuations only while pending, and restore focus to the originating browser after command center dismissal.

* Fix host-scoped browser shortcut follow-ups

* Fix browser keyboard review follow-ups

* Fix browser keyboard lifecycle regressions

* Fix browser shortcut review regressions

* Fix desktop browser review regressions

* Fix browser shortcut frame regressions

* Fix rebase integration regressions

* Preserve browser-native shortcut ownership

* Isolate browser shortcuts and automation by context
This commit is contained in:
Mohamed Boudra
2026-07-17 14:29:19 +08:00
committed by GitHub
parent 9f5f5fce62
commit 04d1ebdce0
32 changed files with 3481 additions and 451 deletions
@@ -6,6 +6,8 @@ import {
isPaseoBrowserWebviewAttach,
preparePaseoBrowserWebContents,
registerAttachedPaseoBrowser,
unregisterPaseoBrowser,
unregisterPaseoBrowserFromHost,
} from "./index.js";
class FakeRenderer {
@@ -82,6 +84,7 @@ describe("browser webview attachment", () => {
expect(registered).toBe(true);
expect(getPaseoBrowserIdForWebContents(guest)).toBe("browser-a");
expect(getPaseoBrowserWorkspaceId("browser-a")).toBe("workspace-a");
unregisterPaseoBrowser("browser-a");
});
test("rejects a guest hosted by another renderer", () => {
@@ -91,7 +94,7 @@ describe("browser webview attachment", () => {
const guest = new FakeBrowserGuest(201, owner, profileSession);
const registered = registerAttachedPaseoBrowser({
browserId: "browser-a",
browserId: "browser-rejected-owner",
workspaceId: "workspace-a",
webContentsId: guest.id,
sender: claimant,
@@ -109,7 +112,7 @@ describe("browser webview attachment", () => {
const guest = new FakeBrowserGuest(301, renderer, {});
const registered = registerAttachedPaseoBrowser({
browserId: "browser-a",
browserId: "browser-rejected-profile",
workspaceId: "workspace-a",
webContentsId: guest.id,
sender: renderer,
@@ -151,12 +154,43 @@ describe("browser webview attachment", () => {
expect(getPaseoBrowserIdForWebContents(firstGuest)).toBe("browser-first");
expect(getPaseoBrowserIdForWebContents(secondGuest)).toBe("browser-second");
unregisterPaseoBrowser("browser-first");
unregisterPaseoBrowser("browser-second");
});
test("unregisters the same browser only from its requesting host", () => {
const profileSession = {};
const firstRenderer = new FakeRenderer(11);
const secondRenderer = new FakeRenderer(22);
const firstGuest = new FakeBrowserGuest(501, firstRenderer, profileSession);
const secondGuest = new FakeBrowserGuest(502, secondRenderer, profileSession);
for (const [renderer, guest] of [
[firstRenderer, firstGuest],
[secondRenderer, secondGuest],
] as const) {
registerAttachedPaseoBrowser({
browserId: "browser-shared-hosts",
workspaceId: "workspace-shared",
webContentsId: guest.id,
sender: renderer,
profileSession,
findWebContents: () => guest,
});
}
unregisterPaseoBrowserFromHost(firstRenderer.id, "browser-shared-hosts");
expect(getPaseoBrowserIdForWebContents(firstGuest)).toBeNull();
expect(getPaseoBrowserIdForWebContents(secondGuest)).toBe("browser-shared-hosts");
expect(getPaseoBrowserWorkspaceId("browser-shared-hosts")).toBe("workspace-shared");
unregisterPaseoBrowser("browser-shared-hosts");
});
test("prepares throttling once and removes registration when the guest is destroyed", () => {
const profileSession = {};
const renderer = new FakeRenderer(1);
const guest = new FakeBrowserGuest(501, renderer, profileSession);
const renderer = new FakeRenderer(31);
const guest = new FakeBrowserGuest(601, renderer, profileSession);
preparePaseoBrowserWebContents(guest);
registerAttachedPaseoBrowser({
browserId: "browser-cleanup",
@@ -47,15 +47,18 @@ export function isPaseoBrowserWebviewAttach(input: { src?: string; partition?: s
}
export function listRegisteredPaseoBrowserIds(): string[] {
return browserRegistry
.listBrowserIds()
.filter((browserId) => getPaseoBrowserWebContents(browserId));
return browserRegistry.listBrowserIds();
}
export function getPaseoBrowserWebviewRegistry(): PaseoBrowserWebviewRegistry {
return browserRegistry;
}
export function preparePaseoBrowserWebContents(contents: RegisteredBrowserWebContents): void {
const webContentsId = contents.id;
contents.setBackgroundThrottling(false);
contents.once("destroyed", () => {
browserRegistry.unregisterWebContents(contents.id);
browserRegistry.unregisterWebContents(webContentsId);
});
}
@@ -73,6 +76,7 @@ export function registerAttachedPaseoBrowser(input: RegisterAttachedBrowserInput
browserRegistry.registerWebContents({
webContentsId: input.webContentsId,
browserId: input.browserId,
hostWebContentsId: input.sender.id,
});
browserRegistry.registerWorkspace({
browserId: input.browserId,
@@ -94,17 +98,24 @@ export function unregisterPaseoBrowser(browserId: string): void {
browserRegistry.unregisterBrowser(browserId);
}
export function unregisterPaseoBrowserFromHost(hostWebContentsId: number, browserId: string): void {
browserRegistry.unregisterBrowserFromHost(hostWebContentsId, browserId);
}
export function unregisterPaseoBrowserHost(hostWebContentsId: number): void {
browserRegistry.unregisterHostWebContents(hostWebContentsId);
}
export function getPaseoBrowserWorkspaceId(browserId: string): string | null {
return browserRegistry.getWorkspaceId(browserId);
}
export function listRegisteredPaseoBrowserIdsForWorkspace(workspaceId: string): string[] {
return browserRegistry
.listBrowserIdsForWorkspace(workspaceId)
.filter((browserId) => getPaseoBrowserWebContents(browserId));
return browserRegistry.listBrowserIdsForWorkspace(workspaceId);
}
export function setWorkspaceActivePaseoBrowserId(input: {
hostWebContentsId: number;
workspaceId: string;
browserId: string | null;
}): void {
@@ -112,11 +123,24 @@ export function setWorkspaceActivePaseoBrowserId(input: {
}
export function getWorkspaceActivePaseoBrowserId(workspaceId: string): string | null {
return browserRegistry.getWorkspaceActiveBrowserId(workspaceId);
return browserRegistry.getMostRecentActiveBrowserIdForWorkspace(workspaceId);
}
export function getPaseoBrowserWebContents(browserId: string): WebContents | null {
const contentsId = browserRegistry.getWebContentsIdForBrowser(browserId);
export function getWorkspaceActivePaseoBrowserIdForHostWindow(
workspaceId: string,
hostWebContentsId: number,
): string | null {
return browserRegistry.getActiveBrowserIdForWorkspaceInHostWindow(hostWebContentsId, workspaceId);
}
export function getPaseoBrowserWebContentsForHostWindow(
browserId: string,
hostWebContentsId: number,
): WebContents | null {
const contentsId = browserRegistry.getWebContentsIdForBrowserInHostWindow(
hostWebContentsId,
browserId,
);
if (contentsId === null) {
return null;
}
@@ -128,9 +152,26 @@ export function getPaseoBrowserWebContents(browserId: string): WebContents | nul
return null;
}
export function getMostRecentWorkspaceActivePaseoBrowserWebContents(): WebContents | null {
const browserId = browserRegistry.getMostRecentWorkspaceActiveBrowserId();
return browserId ? getPaseoBrowserWebContents(browserId) : null;
export function getActivePaseoBrowserWebContentsForHostWindow(
hostWebContentsId: number,
): WebContents | null {
const browserId = browserRegistry.getActiveBrowserIdForHostWindow(hostWebContentsId);
if (!browserId) {
return null;
}
const contentsId = browserRegistry.getWebContentsIdForBrowserInHostWindow(
hostWebContentsId,
browserId,
);
if (contentsId === null) {
return null;
}
const contents = allWebContents.fromId(contentsId);
if (contents && !contents.isDestroyed()) {
return contents;
}
browserRegistry.unregisterWebContents(contentsId);
return null;
}
function preventUnsafeBrowserWebviewNavigation(
@@ -2,42 +2,312 @@ import { describe, expect, it } from "vitest";
import { PaseoBrowserWebviewRegistry } from "./registry.js";
describe("PaseoBrowserWebviewRegistry", () => {
it("keeps one authoritative webContents target per browserId", () => {
it("keeps one authoritative webContents target per host and browser", () => {
const registry = new PaseoBrowserWebviewRegistry();
registry.registerWebContents({ webContentsId: 1, browserId: "browser-a" });
registry.registerWebContents({
webContentsId: 1,
browserId: "browser-a",
hostWebContentsId: 101,
});
registry.registerWorkspace({ browserId: "browser-a", workspaceId: "workspace-a" });
registry.setWorkspaceActiveBrowser({ workspaceId: "workspace-a", browserId: "browser-a" });
registry.registerWebContents({ webContentsId: 2, browserId: "browser-a" });
registry.setWorkspaceActiveBrowser({
hostWebContentsId: 101,
workspaceId: "workspace-a",
browserId: "browser-a",
});
registry.registerWebContents({
webContentsId: 2,
browserId: "browser-a",
hostWebContentsId: 101,
});
expect(registry.getBrowserIdForWebContents(1)).toBeNull();
expect(registry.getBrowserIdForWebContents(2)).toBe("browser-a");
expect(registry.getWebContentsIdForBrowser("browser-a")).toBe(2);
expect(registry.getRegistrationForWebContents(2)).toEqual({
browserId: "browser-a",
hostWebContentsId: 101,
});
expect(registry.getWebContentsIdForBrowserInHostWindow(101, "browser-a")).toBe(2);
expect(registry.getWorkspaceId("browser-a")).toBe("workspace-a");
expect(registry.getWorkspaceActiveBrowserId("workspace-a")).toBe("browser-a");
expect(registry.getActiveBrowserIdForHostWindow(101)).toBe("browser-a");
});
it("ignores stale destroy events after a duplicate browserId moved", () => {
const registry = new PaseoBrowserWebviewRegistry();
registry.registerWebContents({ webContentsId: 1, browserId: "browser-a" });
registry.registerWebContents({ webContentsId: 2, browserId: "browser-a" });
registry.registerWebContents({
webContentsId: 1,
browserId: "browser-a",
hostWebContentsId: 101,
});
registry.registerWebContents({
webContentsId: 2,
browserId: "browser-a",
hostWebContentsId: 101,
});
registry.unregisterWebContents(1);
expect(registry.getWebContentsIdForBrowser("browser-a")).toBe(2);
expect(registry.getWebContentsIdForBrowserInHostWindow(101, "browser-a")).toBe(2);
});
it("keeps one browser identity per webContents target", () => {
it("returns the active browser only from the requested host window", () => {
const registry = new PaseoBrowserWebviewRegistry();
registry.registerWebContents({ webContentsId: 1, browserId: "browser-a" });
registry.registerWorkspace({ browserId: "browser-a", workspaceId: "workspace-a" });
registry.setWorkspaceActiveBrowser({ workspaceId: "workspace-a", browserId: "browser-a" });
registry.registerWebContents({ webContentsId: 1, browserId: "browser-b" });
registry.registerWebContents({
webContentsId: 11,
browserId: "browser-first-window",
hostWebContentsId: 101,
});
registry.registerWebContents({
webContentsId: 22,
browserId: "browser-second-window",
hostWebContentsId: 202,
});
registry.setWorkspaceActiveBrowser({
hostWebContentsId: 101,
workspaceId: "workspace-a",
browserId: "browser-first-window",
});
registry.setWorkspaceActiveBrowser({
hostWebContentsId: 202,
workspaceId: "workspace-a",
browserId: "browser-second-window",
});
expect(registry.getWebContentsIdForBrowser("browser-a")).toBeNull();
expect(registry.getWorkspaceId("browser-a")).toBeNull();
expect(registry.getWorkspaceActiveBrowserId("workspace-a")).toBeNull();
expect(registry.getBrowserIdForWebContents(1)).toBe("browser-b");
expect(registry.getActiveBrowserIdForHostWindow(101)).toBe("browser-first-window");
expect(registry.getActiveBrowserIdForHostWindow(202)).toBe("browser-second-window");
expect(registry.getActiveBrowserIdForWorkspaceInHostWindow(101, "workspace-a")).toBe(
"browser-first-window",
);
expect(registry.getActiveBrowserIdForWorkspaceInHostWindow(202, "workspace-a")).toBe(
"browser-second-window",
);
});
it("keeps active updates and clears inside their owning host window", () => {
const registry = new PaseoBrowserWebviewRegistry();
registry.registerWebContents({
webContentsId: 11,
browserId: "browser-first-window",
hostWebContentsId: 101,
});
registry.registerWebContents({
webContentsId: 22,
browserId: "browser-second-window",
hostWebContentsId: 202,
});
registry.setWorkspaceActiveBrowser({
hostWebContentsId: 101,
workspaceId: "workspace-a",
browserId: "browser-first-window",
});
registry.setWorkspaceActiveBrowser({
hostWebContentsId: 202,
workspaceId: "workspace-a",
browserId: "browser-second-window",
});
registry.setWorkspaceActiveBrowser({
hostWebContentsId: 101,
workspaceId: "workspace-a",
browserId: "browser-second-window",
});
registry.setWorkspaceActiveBrowser({
hostWebContentsId: 101,
workspaceId: "workspace-a",
browserId: null,
});
expect(registry.getActiveBrowserIdForHostWindow(101)).toBeNull();
expect(registry.getActiveBrowserIdForHostWindow(202)).toBe("browser-second-window");
});
it("keeps same-browser active references in separate host windows", () => {
const registry = new PaseoBrowserWebviewRegistry();
registry.registerWebContents({
webContentsId: 11,
browserId: "browser-a",
hostWebContentsId: 101,
});
registry.setWorkspaceActiveBrowser({
hostWebContentsId: 101,
workspaceId: "workspace-a",
browserId: "browser-a",
});
registry.registerWebContents({
webContentsId: 22,
browserId: "browser-a",
hostWebContentsId: 202,
});
registry.setWorkspaceActiveBrowser({
hostWebContentsId: 202,
workspaceId: "workspace-a",
browserId: "browser-a",
});
expect(registry.getActiveBrowserIdForHostWindow(101)).toBe("browser-a");
expect(registry.getActiveBrowserIdForHostWindow(202)).toBe("browser-a");
expect(registry.getWebContentsIdForBrowserInHostWindow(101, "browser-a")).toBe(11);
expect(registry.getWebContentsIdForBrowserInHostWindow(202, "browser-a")).toBe(22);
});
it("removes only the closing host's same-browser guest", () => {
const registry = new PaseoBrowserWebviewRegistry();
registry.registerWebContents({
webContentsId: 11,
browserId: "browser-a",
hostWebContentsId: 101,
});
registry.registerWebContents({
webContentsId: 22,
browserId: "browser-a",
hostWebContentsId: 202,
});
registry.unregisterHostWebContents(101);
expect(registry.getRegistrationForWebContents(11)).toBeNull();
expect(registry.getWebContentsIdForBrowserInHostWindow(101, "browser-a")).toBeNull();
expect(registry.getRegistrationForWebContents(22)).toEqual({
browserId: "browser-a",
hostWebContentsId: 202,
});
expect(registry.getWebContentsIdForBrowserInHostWindow(202, "browser-a")).toBe(22);
});
it("unregisters a browser only from the requesting host", () => {
const registry = new PaseoBrowserWebviewRegistry();
registry.registerWebContents({
webContentsId: 11,
browserId: "browser-a",
hostWebContentsId: 101,
});
registry.registerWebContents({
webContentsId: 22,
browserId: "browser-a",
hostWebContentsId: 202,
});
registry.registerWorkspace({ browserId: "browser-a", workspaceId: "workspace-a" });
registry.unregisterBrowserFromHost(101, "browser-a");
expect(registry.getWebContentsIdForBrowserInHostWindow(101, "browser-a")).toBeNull();
expect(registry.getWebContentsIdForBrowserInHostWindow(202, "browser-a")).toBe(22);
expect(registry.getWorkspaceId("browser-a")).toBe("workspace-a");
});
it("keeps another host's active browser when one guest is destroyed", () => {
const registry = new PaseoBrowserWebviewRegistry();
registry.registerWebContents({
webContentsId: 11,
browserId: "browser-a",
hostWebContentsId: 101,
});
registry.registerWebContents({
webContentsId: 22,
browserId: "browser-a",
hostWebContentsId: 202,
});
registry.setWorkspaceActiveBrowser({
hostWebContentsId: 101,
workspaceId: "workspace-a",
browserId: "browser-a",
});
registry.setWorkspaceActiveBrowser({
hostWebContentsId: 202,
workspaceId: "workspace-a",
browserId: "browser-a",
});
registry.unregisterWebContents(11);
expect(registry.getActiveBrowserIdForHostWindow(101)).toBeNull();
expect(registry.getActiveBrowserIdForHostWindow(202)).toBe("browser-a");
expect(registry.getWebContentsIdForBrowserInHostWindow(202, "browser-a")).toBe(22);
});
it("keeps the same-window active selection made before the guest attaches", () => {
const registry = new PaseoBrowserWebviewRegistry();
registry.setWorkspaceActiveBrowser({
hostWebContentsId: 101,
workspaceId: "workspace-a",
browserId: "browser-a",
});
registry.registerWebContents({
webContentsId: 11,
browserId: "browser-a",
hostWebContentsId: 101,
});
expect(registry.getActiveBrowserIdForHostWindow(101)).toBe("browser-a");
});
it("keeps a pre-attach selection when another host attaches the same browser", () => {
const registry = new PaseoBrowserWebviewRegistry();
registry.setWorkspaceActiveBrowser({
hostWebContentsId: 101,
workspaceId: "workspace-a",
browserId: "browser-a",
});
registry.registerWebContents({
webContentsId: 11,
browserId: "browser-a",
hostWebContentsId: 202,
});
expect(registry.getActiveBrowserIdForHostWindow(101)).toBe("browser-a");
expect(registry.getActiveBrowserIdForHostWindow(202)).toBeNull();
});
it("keeps a pre-attach selection when another host tears down the same browser", () => {
const registry = new PaseoBrowserWebviewRegistry();
registry.setWorkspaceActiveBrowser({
hostWebContentsId: 101,
workspaceId: "workspace-a",
browserId: "browser-a",
});
registry.registerWebContents({
webContentsId: 22,
browserId: "browser-a",
hostWebContentsId: 202,
});
registry.unregisterWebContents(22);
registry.registerWebContents({
webContentsId: 11,
browserId: "browser-a",
hostWebContentsId: 101,
});
expect(registry.getActiveBrowserIdForHostWindow(101)).toBe("browser-a");
});
it("reports when another host still owns the same browser", () => {
const registry = new PaseoBrowserWebviewRegistry();
registry.registerWebContents({
webContentsId: 11,
browserId: "browser-a",
hostWebContentsId: 101,
});
registry.registerWebContents({
webContentsId: 22,
browserId: "browser-a",
hostWebContentsId: 202,
});
expect(registry.hasBrowserInOtherHostWindow(101, "browser-a")).toBe(true);
expect(registry.hasBrowserInOtherHostWindow(202, "browser-a")).toBe(true);
expect(registry.hasBrowserInOtherHostWindow(101, "browser-b")).toBe(false);
registry.unregisterWebContents(22);
expect(registry.hasBrowserInOtherHostWindow(101, "browser-a")).toBe(false);
});
});
@@ -3,59 +3,71 @@ export interface BrowserWorkspaceRegistration {
workspaceId: string;
}
export interface BrowserWebContentsRegistration {
browserId: string;
hostWebContentsId: number;
}
export class PaseoBrowserWebviewRegistry {
private readonly browserIdsByWebContentsId = new Map<number, string>();
private readonly webContentsIdsByBrowserId = new Map<string, number>();
private readonly registrationsByWebContentsId = new Map<number, BrowserWebContentsRegistration>();
private readonly webContentsIdsByHostAndBrowserId = new Map<string, number>();
private readonly workspaceIdsByBrowserId = new Map<string, string>();
private readonly activeBrowserIdsByWorkspaceId = new Map<string, string>();
private readonly activeBrowserIdsByHostWindow = new Map<number, Map<string, string>>();
public registerWebContents(input: { webContentsId: number; browserId: string }): void {
const previousBrowserId = this.browserIdsByWebContentsId.get(input.webContentsId) ?? null;
if (
previousBrowserId !== null &&
previousBrowserId !== input.browserId &&
this.webContentsIdsByBrowserId.get(previousBrowserId) === input.webContentsId
) {
this.webContentsIdsByBrowserId.delete(previousBrowserId);
this.workspaceIdsByBrowserId.delete(previousBrowserId);
this.deleteActiveBrowserReferences(previousBrowserId);
public registerWebContents(input: {
webContentsId: number;
browserId: string;
hostWebContentsId: number;
}): void {
const hostBrowserKey = this.hostBrowserKey(input.hostWebContentsId, input.browserId);
const replacedWebContentsId = this.webContentsIdsByHostAndBrowserId.get(hostBrowserKey);
if (replacedWebContentsId !== undefined && replacedWebContentsId !== input.webContentsId) {
this.removeWebContents(replacedWebContentsId, { preserveActiveBrowser: true });
}
if (this.registrationsByWebContentsId.has(input.webContentsId)) {
this.removeWebContents(input.webContentsId);
}
const previousWebContentsId = this.webContentsIdsByBrowserId.get(input.browserId) ?? null;
if (previousWebContentsId !== null && previousWebContentsId !== input.webContentsId) {
this.browserIdsByWebContentsId.delete(previousWebContentsId);
}
this.browserIdsByWebContentsId.set(input.webContentsId, input.browserId);
this.webContentsIdsByBrowserId.set(input.browserId, input.webContentsId);
this.registrationsByWebContentsId.set(input.webContentsId, {
browserId: input.browserId,
hostWebContentsId: input.hostWebContentsId,
});
this.webContentsIdsByHostAndBrowserId.set(hostBrowserKey, input.webContentsId);
}
public unregisterWebContents(webContentsId: number): void {
const browserId = this.browserIdsByWebContentsId.get(webContentsId) ?? null;
if (!browserId) {
if (!this.registrationsByWebContentsId.has(webContentsId)) {
return;
}
this.browserIdsByWebContentsId.delete(webContentsId);
if (this.webContentsIdsByBrowserId.get(browserId) !== webContentsId) {
return;
}
this.webContentsIdsByBrowserId.delete(browserId);
this.workspaceIdsByBrowserId.delete(browserId);
this.deleteActiveBrowserReferences(browserId);
this.removeWebContents(webContentsId);
}
public getBrowserIdForWebContents(webContentsId: number): string | null {
return this.browserIdsByWebContentsId.get(webContentsId) ?? null;
return this.registrationsByWebContentsId.get(webContentsId)?.browserId ?? null;
}
public getWebContentsIdForBrowser(browserId: string): number | null {
return this.webContentsIdsByBrowserId.get(browserId) ?? null;
public getRegistrationForWebContents(
webContentsId: number,
): BrowserWebContentsRegistration | null {
return this.registrationsByWebContentsId.get(webContentsId) ?? null;
}
public getWebContentsIdForBrowserInHostWindow(
hostWebContentsId: number,
browserId: string,
): number | null {
return (
this.webContentsIdsByHostAndBrowserId.get(
this.hostBrowserKey(hostWebContentsId, browserId),
) ?? null
);
}
public listBrowserIds(): string[] {
return Array.from(this.webContentsIdsByBrowserId.keys()).sort();
return Array.from(
new Set(Array.from(this.registrationsByWebContentsId.values(), ({ browserId }) => browserId)),
).sort();
}
public registerWorkspace(input: BrowserWorkspaceRegistration): void {
@@ -63,48 +75,176 @@ export class PaseoBrowserWebviewRegistry {
}
public unregisterBrowser(browserId: string): void {
const webContentsId = this.webContentsIdsByBrowserId.get(browserId) ?? null;
if (webContentsId !== null) {
this.browserIdsByWebContentsId.delete(webContentsId);
this.webContentsIdsByBrowserId.delete(browserId);
for (const [webContentsId, registration] of this.registrationsByWebContentsId) {
if (registration.browserId === browserId) {
this.registrationsByWebContentsId.delete(webContentsId);
this.webContentsIdsByHostAndBrowserId.delete(
this.hostBrowserKey(registration.hostWebContentsId, browserId),
);
}
}
this.workspaceIdsByBrowserId.delete(browserId);
this.deleteActiveBrowserReferences(browserId);
}
public unregisterBrowserFromHost(hostWebContentsId: number, browserId: string): void {
const webContentsId = this.getWebContentsIdForBrowserInHostWindow(hostWebContentsId, browserId);
if (webContentsId !== null) {
this.unregisterWebContents(webContentsId);
}
}
public getWorkspaceId(browserId: string): string | null {
return this.workspaceIdsByBrowserId.get(browserId) ?? null;
}
public hasBrowserInOtherHostWindow(hostWebContentsId: number, browserId: string): boolean {
for (const registration of this.registrationsByWebContentsId.values()) {
if (
registration.browserId === browserId &&
registration.hostWebContentsId !== hostWebContentsId
) {
return true;
}
}
return false;
}
public unregisterHostWebContents(hostWebContentsId: number): void {
for (const [webContentsId, registration] of this.registrationsByWebContentsId) {
if (registration.hostWebContentsId === hostWebContentsId) {
this.unregisterWebContents(webContentsId);
}
}
this.activeBrowserIdsByHostWindow.delete(hostWebContentsId);
}
public listBrowserIdsForWorkspace(workspaceId: string): string[] {
return this.listBrowserIds().filter(
(browserId) => this.workspaceIdsByBrowserId.get(browserId) === workspaceId,
);
}
public setWorkspaceActiveBrowser(input: { workspaceId: string; browserId: string | null }): void {
if (input.browserId) {
this.workspaceIdsByBrowserId.set(input.browserId, input.workspaceId);
this.activeBrowserIdsByWorkspaceId.delete(input.workspaceId);
this.activeBrowserIdsByWorkspaceId.set(input.workspaceId, input.browserId);
public setWorkspaceActiveBrowser(input: {
hostWebContentsId: number;
workspaceId: string;
browserId: string | null;
}): void {
if (input.browserId === null) {
const activeBrowserIdsByWorkspace = this.activeBrowserIdsByHostWindow.get(
input.hostWebContentsId,
);
if (!activeBrowserIdsByWorkspace) {
return;
}
activeBrowserIdsByWorkspace.delete(input.workspaceId);
if (activeBrowserIdsByWorkspace.size === 0) {
this.activeBrowserIdsByHostWindow.delete(input.hostWebContentsId);
}
return;
}
this.activeBrowserIdsByWorkspaceId.delete(input.workspaceId);
if (this.hasBrowser(input.browserId)) {
this.workspaceIdsByBrowserId.set(input.browserId, input.workspaceId);
}
const activeBrowserIdsByWorkspace =
this.activeBrowserIdsByHostWindow.get(input.hostWebContentsId) ?? new Map<string, string>();
activeBrowserIdsByWorkspace.delete(input.workspaceId);
activeBrowserIdsByWorkspace.set(input.workspaceId, input.browserId);
this.activeBrowserIdsByHostWindow.delete(input.hostWebContentsId);
this.activeBrowserIdsByHostWindow.set(input.hostWebContentsId, activeBrowserIdsByWorkspace);
}
public getWorkspaceActiveBrowserId(workspaceId: string): string | null {
return this.activeBrowserIdsByWorkspaceId.get(workspaceId) ?? null;
public getActiveBrowserIdForHostWindow(hostWebContentsId: number): string | null {
return (
Array.from(this.activeBrowserIdsByHostWindow.get(hostWebContentsId)?.values() ?? []).at(-1) ??
null
);
}
public getMostRecentWorkspaceActiveBrowserId(): string | null {
return Array.from(this.activeBrowserIdsByWorkspaceId.values()).at(-1) ?? null;
public getActiveBrowserIdForWorkspaceInHostWindow(
hostWebContentsId: number,
workspaceId: string,
): string | null {
return this.activeBrowserIdsByHostWindow.get(hostWebContentsId)?.get(workspaceId) ?? null;
}
public getMostRecentActiveBrowserIdForWorkspace(workspaceId: string): string | null {
const activeBrowserIdsByHostWindow = Array.from(this.activeBrowserIdsByHostWindow.values());
for (let index = activeBrowserIdsByHostWindow.length - 1; index >= 0; index -= 1) {
const browserId = activeBrowserIdsByHostWindow[index].get(workspaceId);
if (browserId) {
return browserId;
}
}
return null;
}
private deleteActiveBrowserReferences(browserId: string): void {
for (const [workspaceId, activeBrowserId] of this.activeBrowserIdsByWorkspaceId) {
if (activeBrowserId === browserId) {
this.activeBrowserIdsByWorkspaceId.delete(workspaceId);
for (const [hostWebContentsId, activeBrowserIdsByWorkspace] of this
.activeBrowserIdsByHostWindow) {
for (const [workspaceId, activeBrowserId] of activeBrowserIdsByWorkspace) {
if (activeBrowserId === browserId) {
activeBrowserIdsByWorkspace.delete(workspaceId);
}
}
if (activeBrowserIdsByWorkspace.size === 0) {
this.activeBrowserIdsByHostWindow.delete(hostWebContentsId);
}
}
}
private deleteActiveBrowserReferencesInHostWindow(
browserId: string,
hostWebContentsId: number,
): void {
const activeBrowserIdsByWorkspace = this.activeBrowserIdsByHostWindow.get(hostWebContentsId);
if (!activeBrowserIdsByWorkspace) {
return;
}
for (const [workspaceId, activeBrowserId] of activeBrowserIdsByWorkspace) {
if (activeBrowserId === browserId) {
activeBrowserIdsByWorkspace.delete(workspaceId);
}
}
if (activeBrowserIdsByWorkspace.size === 0) {
this.activeBrowserIdsByHostWindow.delete(hostWebContentsId);
}
}
private removeWebContents(
webContentsId: number,
options: { preserveActiveBrowser?: boolean } = {},
): void {
const registration = this.registrationsByWebContentsId.get(webContentsId);
if (!registration) {
return;
}
const { browserId, hostWebContentsId } = registration;
this.registrationsByWebContentsId.delete(webContentsId);
this.webContentsIdsByHostAndBrowserId.delete(this.hostBrowserKey(hostWebContentsId, browserId));
if (
!options.preserveActiveBrowser &&
!this.hasBrowserInHostWindow(browserId, hostWebContentsId)
) {
this.deleteActiveBrowserReferencesInHostWindow(browserId, hostWebContentsId);
}
}
private hasBrowser(browserId: string): boolean {
return Array.from(this.registrationsByWebContentsId.values()).some(
(registration) => registration.browserId === browserId,
);
}
private hasBrowserInHostWindow(browserId: string, hostWebContentsId: number): boolean {
return this.webContentsIdsByHostAndBrowserId.has(
this.hostBrowserKey(hostWebContentsId, browserId),
);
}
private hostBrowserKey(hostWebContentsId: number, browserId: string): string {
return `${hostWebContentsId}:${browserId}`;
}
}