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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"]>;
|
||||
|
||||
@@ -1488,6 +1488,17 @@ export const ar: TranslationResources = {
|
||||
},
|
||||
general: {
|
||||
title: "عام",
|
||||
browserData: {
|
||||
title: "بيانات المتصفح",
|
||||
siteData: "ملفات تعريف الارتباط وبيانات المواقع",
|
||||
description: "تتشارك علامات تبويب المتصفح تسجيلات الدخول وبيانات المواقع عبر Paseo.",
|
||||
clear: "مسح بيانات المتصفح",
|
||||
clearing: "جارٍ المسح...",
|
||||
confirmTitle: "هل تريد مسح بيانات المتصفح؟",
|
||||
confirmMessage: "سيتم تسجيل خروجك من المواقع وإعادة تحميل علامات تبويب المتصفح المفتوحة.",
|
||||
success: "تم مسح بيانات المتصفح.",
|
||||
error: "تعذر مسح بيانات المتصفح.",
|
||||
},
|
||||
defaultSend: {
|
||||
label: "إرسال افتراضي",
|
||||
descriptions: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -1505,6 +1505,17 @@ export const ja: TranslationResources = {
|
||||
},
|
||||
general: {
|
||||
title: "一般",
|
||||
browserData: {
|
||||
title: "ブラウザーデータ",
|
||||
siteData: "Cookie とサイトデータ",
|
||||
description: "ブラウザータブ間でログイン情報とサイトデータが共有されます。",
|
||||
clear: "ブラウザーデータを消去",
|
||||
clearing: "消去中...",
|
||||
confirmTitle: "ブラウザーデータを消去しますか?",
|
||||
confirmMessage: "サイトからログアウトし、開いているブラウザータブを再読み込みします。",
|
||||
success: "ブラウザーデータを消去しました。",
|
||||
error: "ブラウザーデータを消去できませんでした。",
|
||||
},
|
||||
defaultSend: {
|
||||
label: "デフォルトの送信",
|
||||
descriptions: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user