Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const clientSettings: ClientSettings = {
diffIgnoreWhitespace: true,
favorites: [],
providerModelPreferences: {},
previewDisableWebSecurity: false,
sidebarProjectGroupingMode: "repository_path",
sidebarProjectGroupingOverrides: {
"environment-1:/tmp/project-a": "separate",
Expand Down
15 changes: 14 additions & 1 deletion apps/web/src/browser/HostedBrowserWebview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useCallback, useEffect, useRef, useState } from "react";

import { previewBridge } from "~/components/preview/previewBridge";
import { usePreviewBridge } from "~/components/preview/usePreviewBridge";
import { getClientSettings, useClientSettingsHydrated } from "~/hooks/useSettings";
import { cn } from "~/lib/utils";

import { stopBrowserRecording, useActiveBrowserRecordingTabId } from "./browserRecording";
Expand Down Expand Up @@ -43,6 +44,14 @@ export function HostedBrowserWebview(props: {
const { threadRef, tabId, initialUrl, viewport, zoomFactor } = props;
const config = usePreviewWebviewConfig(threadRef.environmentId);
const [initialSrc] = useState(() => initialUrl ?? "about:blank");
// Freeze once hydration lands so a startup/session-restore webview reads the
// persisted value, not the default `false` snapshot. Electron reads the
// attribute only at guest attach, so the value stays frozen afterwards.
const settingsHydrated = useClientSettingsHydrated();
const [disableWebSecurity, setDisableWebSecurity] = useState<boolean | null>(null);
if (disableWebSecurity === null && settingsHydrated) {
setDisableWebSecurity(getClientSettings().previewDisableWebSecurity);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Webview registration skipped after delay

High Severity

The webview now waits for client settings hydration before rendering. This means the registerWebview effect, whose dependencies (config, tabId) don't track the webview's presence, can run when the element is absent and not re-run when it appears. This prevents registerWebview and its listeners from attaching, breaking navigation and desktopOverlay.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b0042ad. Configure here.

const tabLeaseRef = useRef<AcquiredDesktopTab | null>(null);
const wrapperRef = useRef<HTMLDivElement | null>(null);
const webviewRef = useRef<ElectronWebview | null>(null);
Expand Down Expand Up @@ -174,7 +183,7 @@ export function HostedBrowserWebview(props: {
wrapper.scrollTo({ left: 0, top: 0 });
}, [tabId, viewport._tag, viewportHeight, viewportWidth]);

if (!config) return null;
if (!config || disableWebSecurity === null) return null;

const wrapperStyle = resolveHostedBrowserWebviewWrapperStyle({
active,
Expand Down Expand Up @@ -206,6 +215,10 @@ export function HostedBrowserWebview(props: {
partition={config.partition}
webpreferences={config.webPreferences}
{...(config.preloadUrl ? { preload: config.preloadUrl } : {})}
{...(disableWebSecurity
? // string, not boolean: React omits boolean values on this unknown attribute
{ disablewebsecurity: "true" as unknown as boolean }
: {})}
data-preview-tab={tabId}
data-preview-viewport-mode={effectiveViewport._tag}
data-preview-viewport-key={browserViewportSettingKey(effectiveViewport)}
Expand Down
34 changes: 33 additions & 1 deletion apps/web/src/components/preview/PreviewMoreMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@
import { Minus, MoreVertical, Plus as PlusIcon, RotateCcw } from "lucide-react";

import { Button } from "~/components/ui/button";
import { Menu, MenuItem, MenuPopup, MenuSeparator, MenuTrigger } from "~/components/ui/menu";
import {
Menu,
MenuCheckboxItem,
MenuItem,
MenuPopup,
MenuSeparator,
MenuTrigger,
} from "~/components/ui/menu";
import { stackedThreadToast, toastManager } from "~/components/ui/toast";
import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings";

import { previewBridge } from "./previewBridge";

Expand Down Expand Up @@ -37,6 +46,8 @@ export function PreviewMoreMenu({
deviceToolbarVisible,
onToggleDeviceToolbar,
}: Props) {
const disableWebSecurity = useClientSettings((settings) => settings.previewDisableWebSecurity);
const updateClientSettings = useUpdateClientSettings();
if (!previewBridge) return null;
const bridge = previewBridge;
const tabDisabled = !tabId || !hasWebContents;
Expand Down Expand Up @@ -127,6 +138,27 @@ export function PreviewMoreMenu({
<MenuItem onClick={() => void bridge.clearCache().catch(() => undefined)}>
Clear cache
</MenuItem>
<MenuSeparator />
<MenuCheckboxItem
variant="switch"
closeOnClick={false}
checked={disableWebSecurity}
onCheckedChange={(checked) => {
const enabled = Boolean(checked);
updateClientSettings({ previewDisableWebSecurity: enabled });
toastManager.add(
stackedThreadToast({
type: "info",
title: enabled ? "Browser WebSecurity disabled" : "Browser WebSecurity enabled",
description: enabled
? "CORS is off for preview tabs opened from now on. Reopen this tab to apply."
: "Applies to preview tabs opened from now on. Reopen this tab to apply.",
}),
);
}}
>
Disable browser WebSecurity
</MenuCheckboxItem>
</MenuPopup>
</Menu>
);
Expand Down
2 changes: 2 additions & 0 deletions packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const ClientSettingsSchema = Schema.Struct({
modelOrder: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
}),
).pipe(Schema.withDecodingDefault(Effect.succeed({}))),
previewDisableWebSecurity: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
sidebarProjectGroupingMode: SidebarProjectGroupingMode.pipe(
Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE)),
),
Expand Down Expand Up @@ -559,6 +560,7 @@ export const ClientSettingsPatch = Schema.Struct({
}),
),
),
previewDisableWebSecurity: Schema.optionalKey(Schema.Boolean),
sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode),
sidebarProjectGroupingOverrides: Schema.optionalKey(
Schema.Record(TrimmedNonEmptyString, SidebarProjectGroupingMode),
Expand Down
Loading