Keep browser sign-ins across tabs and restarts (#2089)

* feat(desktop): share persistent browser data across tabs

Keep browser sign-ins and site storage across tabs and app restarts, with a Settings action to clear the shared profile.

* fix(settings): prevent duplicate browser data clears

* fix(desktop): complete browser data cleanup

Clear legacy per-tab profiles after upgrades and localize the browser data settings in every supported language.

* fix(desktop): close browser profile cleanup races
This commit is contained in:
Mohamed Boudra
2026-07-16 00:40:24 +08:00
committed by GitHub
parent 9423b091b9
commit 328361667f
32 changed files with 1413 additions and 148 deletions
@@ -73,10 +73,7 @@ class FakeDaemonClient {
class FakeBrowserBridge {
public readonly executedRequests: BrowserAutomationExecuteRequest[] = [];
public readonly registeredWorkspaceBrowsers: Array<{ browserId: string; workspaceId: string }> =
[];
public readonly unregisteredWorkspaceBrowsers: string[] = [];
public readonly clearedPartitions: string[] = [];
public readonly activeWorkspaceBrowsers: Array<{
browserId: string | null;
workspaceId: string;
@@ -94,21 +91,10 @@ class FakeBrowserBridge {
return this.response ?? currentListTabsPayload(request.requestId);
};
public registerWorkspaceBrowser = async (input: {
browserId: string;
workspaceId: string;
}): Promise<void> => {
this.registeredWorkspaceBrowsers.push(input);
};
public unregisterWorkspaceBrowser = async (browserId: string): Promise<void> => {
this.unregisteredWorkspaceBrowsers.push(browserId);
};
public clearPartition = async (browserId: string): Promise<void> => {
this.clearedPartitions.push(browserId);
};
public setWorkspaceActiveBrowser = async (input: {
browserId: string | null;
workspaceId: string;
@@ -118,9 +104,17 @@ class FakeBrowserBridge {
}
class FakeResidentBrowser {
public readonly ensuredWebviews: Array<{ browserId: string; url: string }> = [];
public readonly ensuredWebviews: Array<{
browserId: string;
workspaceId: string;
url: string;
}> = [];
public ensure = (input: { browserId: string; url: string }): HTMLElement | null => {
public ensure = (input: {
browserId: string;
workspaceId: string;
url: string;
}): HTMLElement | null => {
this.ensuredWebviews.push(input);
return null;
};
@@ -321,12 +315,13 @@ describe("mountBrowserAutomationHandler", () => {
}),
);
expect(openedTabs[0]?.tabId).not.toBe(previousFocusedTabId);
expect(browser.browser.registeredWorkspaceBrowsers).toEqual([
{ browserId: result.browserId, workspaceId: "wks_workspace_a" },
]);
expect(browser.browser.activeWorkspaceBrowsers).toEqual([]);
expect(browser.resident.ensuredWebviews).toEqual([
{ browserId: result.browserId, url: "https://example.com" },
{
browserId: result.browserId,
workspaceId: "wks_workspace_a",
url: "https://example.com",
},
]);
expect(browser.browser.executedRequests).toEqual([
{
@@ -366,7 +361,10 @@ describe("mountBrowserAutomationHandler", () => {
},
]);
expect(browser.resident.ensuredWebviews).toEqual([
expect.objectContaining({ url: "https://example.com" }),
expect.objectContaining({
workspaceId: "wks_workspace_a",
url: "https://example.com",
}),
]);
});
@@ -440,7 +438,7 @@ describe("mountBrowserAutomationHandler", () => {
});
});
test("browser_close_tab removes the workspace tab, browser record, resident webview, registry entry, and partition", async () => {
test("browser_close_tab removes the workspace tab, browser record, resident webview, and registry entry", async () => {
const browser = new BrowserAutomationHandlerHarness();
const workspaceKey = buildWorkspaceTabPersistenceKey({
serverId: "server-1",
@@ -466,7 +464,6 @@ describe("mountBrowserAutomationHandler", () => {
expect(workspaceBrowserTabs(workspaceKey, result.browserId)).toEqual([]);
expect(useBrowserStore.getState().browsersById[result.browserId]).toBeUndefined();
expect(browser.browser.unregisteredWorkspaceBrowsers).toEqual([result.browserId]);
expect(browser.browser.clearedPartitions).toEqual([result.browserId]);
expect(currentBrowserTabs()).toEqual([]);
});
@@ -258,7 +258,6 @@ async function closeBrowserTabForRequest(params: {
useBrowserStore.getState().removeBrowser(browserId);
removeResidentBrowserWebview(browserId);
await browserHost?.unregisterWorkspaceBrowser?.(browserId);
await browserHost?.clearPartition?.(browserId);
return {
requestId: request.requestId,
@@ -337,10 +336,8 @@ async function openBrowserTabForRequest(params: {
browserId,
});
await browserHost?.registerWorkspaceBrowser?.({ browserId, workspaceId });
if (browserHost?.executeAutomationCommand) {
ensureResidentBrowserWebview({ browserId, url: normalizedUrl });
ensureResidentBrowserWebview({ browserId, workspaceId, url: normalizedUrl });
const registered = await waitForBrowserRegistration({
request,
browserId,