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,
@@ -740,10 +740,10 @@ export function BrowserPane({
const residentWebview = takeResidentBrowserWebview(browserId) as ElectronWebview | null;
const webview = residentWebview ?? (document.createElement("webview") as ElectronWebview);
webviewRef.current = webview;
void getDesktopHost()?.browser?.registerWorkspaceBrowser?.({ browserId, workspaceId });
if (!residentWebview) {
prepareBrowserWebview(webview, {
browserId,
workspaceId,
initialUrl: initialUnsafeNavigationMessage ? "about:blank" : initialUrlRef.current,
});
}
@@ -1,5 +1,6 @@
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
type BrowserWebviewProfileHost,
clearResidentBrowserWebviewsForTests,
ensureResidentBrowserWebview,
prepareBrowserWebview,
@@ -9,6 +10,32 @@ import {
} from "./browser-webview-resident";
const RESIDENT_HOST_ID = "paseo-browser-resident-webviews";
const attachedBrowsers: Array<{
browserId: string;
workspaceId: string;
webContentsId: number;
}> = [];
const profileHost: BrowserWebviewProfileHost = {
profilePartition: "persist:paseo-browser",
registerAttachedBrowser: async (input) => {
attachedBrowsers.push(input);
},
};
function ensureTestBrowser(input: {
browserId: string;
workspaceId: string;
url: string;
}): HTMLElement | null {
return ensureResidentBrowserWebview({ ...input, profileHost });
}
function prepareTestBrowser(
webview: HTMLElement,
input: { browserId: string; workspaceId: string; initialUrl?: string | null },
): void {
prepareBrowserWebview(webview, { ...input, profileHost });
}
function residentHost(): HTMLElement {
const host = document.getElementById(RESIDENT_HOST_ID);
@@ -44,6 +71,10 @@ function expectResidentWebviewParking(webview: HTMLElement): void {
}
describe("resident browser webviews", () => {
beforeEach(() => {
attachedBrowsers.length = 0;
});
afterEach(() => {
clearResidentBrowserWebviewsForTests();
});
@@ -65,15 +96,16 @@ describe("resident browser webviews", () => {
});
it("creates a resident webview for an agent-created unfocused tab", () => {
const webview = ensureResidentBrowserWebview({
const webview = ensureTestBrowser({
browserId: "browser-agent",
workspaceId: "workspace-agent",
url: "https://example.com",
});
expect(webview).not.toBeNull();
expect(webview?.isConnected).toBe(true);
expect(webview?.getAttribute("data-paseo-browser-id")).toBe("browser-agent");
expect(webview?.getAttribute("partition")).toBe("persist:paseo-browser-browser-agent");
expect(webview?.getAttribute("partition")).toBe("persist:paseo-browser");
expect((webview as HTMLUnknownElement & { src?: string })?.src).toContain(
"https://example.com",
);
@@ -81,6 +113,34 @@ describe("resident browser webviews", () => {
expectResidentWebviewParking(webview as HTMLElement);
});
it("shares one profile and registers attached guests with explicit identity", () => {
const firstWebview = ensureTestBrowser({
browserId: "browser-first",
workspaceId: "workspace-a",
url: "https://example.com/first",
});
const secondWebview = ensureTestBrowser({
browserId: "browser-second",
workspaceId: "workspace-b",
url: "https://example.com/second",
});
if (!firstWebview || !secondWebview) {
throw new Error("Expected resident webviews");
}
Object.assign(firstWebview, { getWebContentsId: () => 101 });
Object.assign(secondWebview, { getWebContentsId: () => 202 });
firstWebview.dispatchEvent(new Event("did-attach"));
secondWebview.dispatchEvent(new Event("did-attach"));
expect(firstWebview.getAttribute("partition")).toBe("persist:paseo-browser");
expect(secondWebview.getAttribute("partition")).toBe("persist:paseo-browser");
expect(attachedBrowsers).toEqual([
{ browserId: "browser-first", workspaceId: "workspace-a", webContentsId: 101 },
{ browserId: "browser-second", workspaceId: "workspace-b", webContentsId: 202 },
]);
});
it("normalizes an existing resident host back to permanent parking", () => {
const staleHost = document.createElement("div");
staleHost.id = RESIDENT_HOST_ID;
@@ -91,8 +151,9 @@ describe("resident browser webviews", () => {
staleHost.style.display = "none";
document.body.appendChild(staleHost);
const webview = ensureResidentBrowserWebview({
const webview = ensureTestBrowser({
browserId: "browser-stale-host",
workspaceId: "workspace-stale-host",
url: "https://example.com",
});
@@ -111,8 +172,9 @@ describe("resident browser webviews", () => {
staleHost.style.display = "none";
const staleWebview = document.createElement("webview");
prepareBrowserWebview(staleWebview, {
prepareTestBrowser(staleWebview, {
browserId: "browser-stale-child",
workspaceId: "workspace-stale-child",
initialUrl: "https://example.com",
});
staleWebview.style.display = "none";
@@ -123,8 +185,9 @@ describe("resident browser webviews", () => {
staleHost.appendChild(staleWebview);
document.body.appendChild(staleHost);
const webview = ensureResidentBrowserWebview({
const webview = ensureTestBrowser({
browserId: "browser-stale-child",
workspaceId: "workspace-stale-child",
url: "https://example.com/agent",
});
@@ -135,12 +198,14 @@ describe("resident browser webviews", () => {
});
it("parks resident webviews as an overlapping stack", () => {
const firstWebview = ensureResidentBrowserWebview({
const firstWebview = ensureTestBrowser({
browserId: "browser-first",
workspaceId: "workspace-stack",
url: "https://example.com/first",
});
const secondWebview = ensureResidentBrowserWebview({
const secondWebview = ensureTestBrowser({
browserId: "browser-second",
workspaceId: "workspace-stack",
url: "https://example.com/second",
});
@@ -152,8 +217,9 @@ describe("resident browser webviews", () => {
});
it("moves a resident webview into a visible pane without recreating the node", () => {
const webview = ensureResidentBrowserWebview({
const webview = ensureTestBrowser({
browserId: "browser-visible",
workspaceId: "workspace-visible",
url: "https://example.com",
});
@@ -170,15 +236,17 @@ describe("resident browser webviews", () => {
it("returns an existing visible pane webview instead of creating a resident duplicate", () => {
const visibleHost = document.createElement("div");
const visibleWebview = document.createElement("webview");
prepareBrowserWebview(visibleWebview, {
prepareTestBrowser(visibleWebview, {
browserId: "browser-visible-pane",
workspaceId: "workspace-visible-pane",
initialUrl: "https://example.com",
});
visibleHost.appendChild(visibleWebview);
document.body.appendChild(visibleHost);
const webview = ensureResidentBrowserWebview({
const webview = ensureTestBrowser({
browserId: "browser-visible-pane",
workspaceId: "workspace-visible-pane",
url: "https://example.com/agent",
});
@@ -187,8 +255,9 @@ describe("resident browser webviews", () => {
});
it("removes a resident webview when its browser tab closes", () => {
const webview = ensureResidentBrowserWebview({
const webview = ensureTestBrowser({
browserId: "browser-closed",
workspaceId: "workspace-closed",
url: "https://example.com",
});
@@ -1,3 +1,9 @@
import {
getDesktopHost,
type DesktopAttachedBrowserRegistration,
type DesktopBrowserBridge,
} from "@/desktop/host";
const RESIDENT_BROWSER_HOST_ID = "paseo-browser-resident-webviews";
const BROWSER_ID_ATTRIBUTE = "data-paseo-browser-id";
const RESIDENT_VIEWPORT_WIDTH = 1280;
@@ -8,6 +14,62 @@ const residentWebviewSizesByBrowserId = new Map<string, { width: number; height:
interface BrowserWebviewElement extends HTMLElement {
src: string;
getWebContentsId(): number;
}
interface BrowserWebviewIdentity {
browserId: string;
workspaceId: string;
}
export interface BrowserWebviewProfileHost {
profilePartition: string;
registerAttachedBrowser(input: DesktopAttachedBrowserRegistration): Promise<void>;
}
function isAttachedBrowserBridge(
browser: DesktopBrowserBridge | undefined,
): browser is BrowserWebviewProfileHost {
return (
browser !== undefined &&
typeof browser.profilePartition === "string" &&
browser.profilePartition.startsWith("persist:") &&
typeof browser.registerAttachedBrowser === "function"
);
}
function getBrowserBridge(override?: BrowserWebviewProfileHost): BrowserWebviewProfileHost {
if (override) {
return override;
}
const browser = getDesktopHost()?.browser;
if (!isAttachedBrowserBridge(browser)) {
throw new Error("Electron browser profile bridge is unavailable");
}
return browser;
}
function registerBrowserWhenAttached(
webview: BrowserWebviewElement,
identity: BrowserWebviewIdentity,
browser: BrowserWebviewProfileHost,
): void {
webview.addEventListener(
"did-attach",
() => {
const webContentsId = webview.getWebContentsId();
void browser
.registerAttachedBrowser({
browserId: identity.browserId,
workspaceId: identity.workspaceId,
webContentsId,
})
.catch((error) => {
console.error("[browser-webview] attached registration failed", error);
});
},
{ once: true },
);
}
function trimNonEmpty(value: string | null | undefined): string | null {
@@ -104,21 +166,30 @@ function clearResidentWebviewParkingStyle(webview: HTMLElement): void {
export function prepareBrowserWebview(
webview: HTMLElement,
input: { browserId: string; initialUrl?: string | null },
input: {
browserId: string;
workspaceId: string;
initialUrl?: string | null;
profileHost?: BrowserWebviewProfileHost;
},
): void {
const browser = getBrowserBridge(input.profileHost);
webview.setAttribute(BROWSER_ID_ATTRIBUTE, input.browserId);
webview.setAttribute("partition", `persist:paseo-browser-${input.browserId}`);
webview.setAttribute("partition", browser.profilePartition);
webview.setAttribute("allowpopups", "true");
webview.setAttribute("spellcheck", "false");
webview.setAttribute("autosize", "on");
if (input.initialUrl) {
(webview as BrowserWebviewElement).src = input.initialUrl;
}
registerBrowserWhenAttached(webview as BrowserWebviewElement, input, browser);
}
export function ensureResidentBrowserWebview(input: {
browserId: string;
workspaceId: string;
url: string;
profileHost?: BrowserWebviewProfileHost;
}): HTMLElement | null {
const browserId = trimNonEmpty(input.browserId);
if (!browserId) {
@@ -144,7 +215,12 @@ export function ensureResidentBrowserWebview(input: {
}
const webview = ownerDocument.createElement("webview") as BrowserWebviewElement;
prepareBrowserWebview(webview, { browserId, initialUrl: input.url });
prepareBrowserWebview(webview, {
browserId,
workspaceId: input.workspaceId,
initialUrl: input.url,
profileHost: input.profileHost,
});
releaseResidentBrowserWebview(browserId, webview);
return webview;
}
@@ -0,0 +1,80 @@
import { useCallback, useRef, useState } from "react";
import { Text, View } from "react-native";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { useToast } from "@/contexts/toast-context";
import { getDesktopHost } from "@/desktop/host";
import { SettingsSection } from "@/screens/settings/settings-section";
import { useBrowserStore } from "@/stores/browser-store";
import { settingsStyles } from "@/styles/settings";
import { confirmDialog } from "@/utils/confirm-dialog";
export function BrowserDataSection() {
const { t } = useTranslation();
const toast = useToast();
const clearInFlightRef = useRef(false);
const [isClearing, setIsClearing] = useState(false);
const handleClear = useCallback(async () => {
if (clearInFlightRef.current) {
return;
}
clearInFlightRef.current = true;
setIsClearing(true);
try {
const confirmed = await confirmDialog({
title: t("settings.general.browserData.confirmTitle"),
message: t("settings.general.browserData.confirmMessage"),
confirmLabel: t("settings.general.browserData.clear"),
cancelLabel: t("common.actions.cancel"),
destructive: true,
});
if (!confirmed) {
return;
}
const clearProfile = getDesktopHost()?.browser?.clearProfile;
if (!clearProfile) {
throw new Error("Electron browser profile bridge is unavailable");
}
await clearProfile(Object.keys(useBrowserStore.getState().browsersById));
toast.show(t("settings.general.browserData.success"), { variant: "success" });
} catch {
toast.error(t("settings.general.browserData.error"));
} finally {
clearInFlightRef.current = false;
setIsClearing(false);
}
}, [t, toast]);
const clearButtonLabel = isClearing
? t("settings.general.browserData.clearing")
: t("settings.general.browserData.clear");
return (
<SettingsSection title={t("settings.general.browserData.title")}>
<View style={settingsStyles.card}>
<View style={settingsStyles.row}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>
{t("settings.general.browserData.siteData")}
</Text>
<Text style={settingsStyles.rowHint}>
{t("settings.general.browserData.description")}
</Text>
</View>
<Button
variant="destructive"
size="sm"
loading={isClearing}
disabled={isClearing}
onPress={handleClear}
>
{clearButtonLabel}
</Button>
</View>
</View>
</SettingsSection>
);
}
+9 -2
View File
@@ -128,15 +128,22 @@ export interface DesktopBrowserNewTabRequestEvent {
url: string;
}
export interface DesktopAttachedBrowserRegistration {
browserId: string;
workspaceId: string;
webContentsId: number;
}
export interface DesktopBrowserBridge {
registerWorkspaceBrowser?: (input: { browserId: string; workspaceId: string }) => Promise<void>;
readonly profilePartition?: string;
registerAttachedBrowser?: (input: DesktopAttachedBrowserRegistration) => Promise<void>;
unregisterWorkspaceBrowser?: (browserId: string) => Promise<void>;
setWorkspaceActiveBrowser?: (input: {
workspaceId: string;
browserId: string | null;
}) => Promise<void>;
openDevTools?: (browserId: string) => Promise<unknown>;
clearPartition?: (browserId: string) => Promise<void>;
clearProfile?: (legacyBrowserIds: string[]) => Promise<void>;
executeAutomationCommand?: (
request: BrowserAutomationExecuteRequest,
) => Promise<BrowserAutomationExecuteResponse["payload"]>;
+11
View File
@@ -1488,6 +1488,17 @@ export const ar: TranslationResources = {
},
general: {
title: "عام",
browserData: {
title: "بيانات المتصفح",
siteData: "ملفات تعريف الارتباط وبيانات المواقع",
description: "تتشارك علامات تبويب المتصفح تسجيلات الدخول وبيانات المواقع عبر Paseo.",
clear: "مسح بيانات المتصفح",
clearing: "جارٍ المسح...",
confirmTitle: "هل تريد مسح بيانات المتصفح؟",
confirmMessage: "سيتم تسجيل خروجك من المواقع وإعادة تحميل علامات تبويب المتصفح المفتوحة.",
success: "تم مسح بيانات المتصفح.",
error: "تعذر مسح بيانات المتصفح.",
},
defaultSend: {
label: "إرسال افتراضي",
descriptions: {
+11
View File
@@ -1496,6 +1496,17 @@ export const en = {
},
general: {
title: "General",
browserData: {
title: "Browser data",
siteData: "Cookies and site data",
description: "Browser tabs share sign-ins and site data across Paseo.",
clear: "Clear browser data",
clearing: "Clearing...",
confirmTitle: "Clear browser data?",
confirmMessage: "Sites will be signed out and open browser tabs will reload.",
success: "Browser data cleared.",
error: "Couldn't clear browser data.",
},
defaultSend: {
label: "Default send",
descriptions: {
+13
View File
@@ -1527,6 +1527,19 @@ export const es: TranslationResources = {
},
general: {
title: "General",
browserData: {
title: "Datos del navegador",
siteData: "Cookies y datos de sitios",
description:
"Las pestañas del navegador comparten inicios de sesión y datos de sitios en Paseo.",
clear: "Borrar datos del navegador",
clearing: "Borrando...",
confirmTitle: "¿Borrar los datos del navegador?",
confirmMessage:
"Se cerrarán las sesiones de los sitios y se recargarán las pestañas abiertas del navegador.",
success: "Datos del navegador borrados.",
error: "No se pudieron borrar los datos del navegador.",
},
defaultSend: {
label: "Envío predeterminado",
descriptions: {
+12
View File
@@ -1530,6 +1530,18 @@ export const fr: TranslationResources = {
},
general: {
title: "Général",
browserData: {
title: "Données du navigateur",
siteData: "Cookies et données des sites",
description:
"Les onglets du navigateur partagent les connexions et les données des sites dans Paseo.",
clear: "Effacer les données du navigateur",
clearing: "Effacement...",
confirmTitle: "Effacer les données du navigateur ?",
confirmMessage: "Vous serez déconnecté des sites et les onglets ouverts seront rechargés.",
success: "Données du navigateur effacées.",
error: "Impossible d'effacer les données du navigateur.",
},
defaultSend: {
label: "Envoi par défaut",
descriptions: {
+11
View File
@@ -1505,6 +1505,17 @@ export const ja: TranslationResources = {
},
general: {
title: "一般",
browserData: {
title: "ブラウザーデータ",
siteData: "Cookie とサイトデータ",
description: "ブラウザータブ間でログイン情報とサイトデータが共有されます。",
clear: "ブラウザーデータを消去",
clearing: "消去中...",
confirmTitle: "ブラウザーデータを消去しますか?",
confirmMessage: "サイトからログアウトし、開いているブラウザータブを再読み込みします。",
success: "ブラウザーデータを消去しました。",
error: "ブラウザーデータを消去できませんでした。",
},
defaultSend: {
label: "デフォルトの送信",
descriptions: {
+12
View File
@@ -1513,6 +1513,18 @@ export const ptBR: TranslationResources = {
},
general: {
title: "Geral",
browserData: {
title: "Dados do navegador",
siteData: "Cookies e dados de sites",
description: "As abas do navegador compartilham logins e dados de sites no Paseo.",
clear: "Limpar dados do navegador",
clearing: "Limpando...",
confirmTitle: "Limpar dados do navegador?",
confirmMessage:
"Você será desconectado dos sites e as abas abertas do navegador serão recarregadas.",
success: "Dados do navegador limpos.",
error: "Não foi possível limpar os dados do navegador.",
},
defaultSend: {
label: "Envio padrão",
descriptions: {
+12
View File
@@ -1519,6 +1519,18 @@ export const ru: TranslationResources = {
},
general: {
title: "Общий",
browserData: {
title: "Данные браузера",
siteData: "Файлы cookie и данные сайтов",
description: "Вкладки браузера используют общие данные входа и данные сайтов в Paseo.",
clear: "Очистить данные браузера",
clearing: "Очистка...",
confirmTitle: "Очистить данные браузера?",
confirmMessage:
"На сайтах будет выполнен выход, а открытые вкладки браузера перезагрузятся.",
success: "Данные браузера очищены.",
error: "Не удалось очистить данные браузера.",
},
defaultSend: {
label: "Отправка по умолчанию",
descriptions: {
+11
View File
@@ -1472,6 +1472,17 @@ export const zhCN: TranslationResources = {
},
general: {
title: "通用",
browserData: {
title: "浏览器数据",
siteData: "Cookie 和网站数据",
description: "浏览器标签页在 Paseo 中共享登录状态和网站数据。",
clear: "清除浏览器数据",
clearing: "正在清除...",
confirmTitle: "清除浏览器数据?",
confirmMessage: "网站帐号将退出登录,打开的浏览器标签页将重新加载。",
success: "浏览器数据已清除。",
error: "无法清除浏览器数据。",
},
defaultSend: {
label: "默认发送",
descriptions: {
+12 -8
View File
@@ -70,6 +70,7 @@ import { CommunityLinks } from "@/components/community-links";
import { SegmentedControl } from "@/components/ui/segmented-control";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section";
import { BrowserDataSection } from "@/desktop/components/browser-data-section";
import { IntegrationsSection } from "@/desktop/components/integrations-section";
import { isElectronRuntime } from "@/desktop/host";
import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater";
@@ -1385,14 +1386,17 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
switch (view.section) {
case "general":
return (
<GeneralSection
settings={settings}
isDesktopApp={isDesktopApp}
handleSendBehaviorChange={handleSendBehaviorChange}
handleServiceUrlBehaviorChange={handleServiceUrlBehaviorChange}
handleLanguageChange={handleLanguageChange}
handleTerminalScrollbackLinesChange={handleTerminalScrollbackLinesChange}
/>
<>
<GeneralSection
settings={settings}
isDesktopApp={isDesktopApp}
handleSendBehaviorChange={handleSendBehaviorChange}
handleServiceUrlBehaviorChange={handleServiceUrlBehaviorChange}
handleLanguageChange={handleLanguageChange}
handleTerminalScrollbackLinesChange={handleTerminalScrollbackLinesChange}
/>
{isDesktopApp ? <BrowserDataSection /> : null}
</>
);
case "appearance":
return <AppearanceSection />;
@@ -1970,7 +1970,7 @@ function WorkspaceScreenContent({
const { browserId } = input.target;
useBrowserStore.getState().removeBrowser(browserId);
removeResidentBrowserWebview(browserId);
void getDesktopHost()?.browser?.clearPartition?.(browserId);
void getDesktopHost()?.browser?.unregisterWorkspaceBrowser?.(browserId);
}
closeWorkspaceTab(persistenceKey, normalizedTabId);
},