@@ -434,6 +434,66 @@ const APP_FORWARDED_SHORTCUTS: ReadonlyArray<{
434434 { key : "w" , meta : true , shift : false , control : false } ,
435435] ) ;
436436
437+ /**
438+ * Protocols a preview page may open in a real popup window.
439+ *
440+ * `about:blank` stays out: Chromium skips browser-side navigation for it, so the
441+ * child copies the guest's `contextIsolation: false` preferences and Electron
442+ * gives no way to override them. Those popups keep loading in the preview tab.
443+ *
444+ * Deliberately not `ElectronShell.parseSafeExternalUrl`: that also admits
445+ * `vscode://vscode-remote/...` deep links, which belong in `shell.openExternal`
446+ * and not in a window spawned by a third-party page in the preview.
447+ */
448+ const POPUP_PROTOCOLS = new Set ( [ "http:" , "https:" ] ) ;
449+
450+ const isPopupUrl = ( rawUrl : string ) : boolean => {
451+ try {
452+ return POPUP_PROTOCOLS . has ( new URL ( rawUrl ) . protocol ) ;
453+ } catch {
454+ return false ;
455+ }
456+ } ;
457+
458+ /**
459+ * Preferences for a popup a preview page opens.
460+ *
461+ * A popup is not a webview attach, so the `will-attach-webview` hardening in
462+ * `DesktopWindow` never sees it, and an unoverridden child would inherit the
463+ * guest's relaxed posture: the picker preload needs `contextIsolation: false`
464+ * to share `globalThis` with the previewed page, and no OAuth provider should
465+ * get that. The window keeps the opener and the guest session either way.
466+ */
467+ const POPUP_WINDOW_OPTIONS = {
468+ webPreferences : {
469+ contextIsolation : true ,
470+ nodeIntegration : false ,
471+ sandbox : true ,
472+ // `preload` is a webPreference too, so an unset one is inherited from the
473+ // guest. Preview guests load Pylon's pick/annotation preload, which imports
474+ // `ipcRenderer` and was written for the trusted preview surface — it has no
475+ // business running on a third-party sign-in page.
476+ preload : "" ,
477+ } ,
478+ } satisfies Electron . BrowserWindowConstructorOptions ;
479+
480+ /**
481+ * Decides what a preview page's `window.open` should do.
482+ *
483+ * `"popup"` opens a real window, which scripted popups need: denying them makes
484+ * `window.open()` return `null` (OAuth SDKs report that as a blocked popup), and
485+ * navigating the preview tab instead destroys the opener the popup has to
486+ * `postMessage` its result back to.
487+ *
488+ * `target="_blank"` links arrive as a tab disposition and keep loading in the
489+ * preview tab, which is what people expect from a link inside a preview.
490+ */
491+ export const previewWindowOpenAction = ( details : {
492+ readonly url : string ;
493+ readonly disposition : Electron . HandlerDetails [ "disposition" ] ;
494+ } ) : "popup" | "navigate" =>
495+ details . disposition === "new-window" && isPopupUrl ( details . url ) ? "popup" : "navigate" ;
496+
437497export const isPreviewRefreshShortcut = ( input : Electron . Input ) : boolean =>
438498 input . type === "keyDown" &&
439499 input . key . toLowerCase ( ) === "r" &&
@@ -1661,6 +1721,12 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
16611721 ] ,
16621722 } ) ;
16631723 } ) ;
1724+ // A popup opens with Electron's default handler, so the page inside it could
1725+ // otherwise spawn native windows without limit. Nothing in an OAuth flow
1726+ // opens a second popup, so the chain stops at the first one.
1727+ const windowCreated = ( window : Electron . BrowserWindow ) : void => {
1728+ window . webContents . setWindowOpenHandler ( ( ) => ( { action : "deny" } ) ) ;
1729+ } ;
16641730 const beforeInput = ( event : Electron . Event , input : Electron . Input ) : void => {
16651731 if ( isPreviewRefreshShortcut ( input ) ) {
16661732 event . preventDefault ( ) ;
@@ -1686,6 +1752,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
16861752 wc . off ( "did-stop-loading" , sync ) ;
16871753 wc . off ( "did-fail-load" , failed as never ) ;
16881754 wc . off ( "audio-state-changed" , audioStateChanged ) ;
1755+ wc . off ( "did-create-window" , windowCreated ) ;
16891756 wc . off ( "before-input-event" , beforeInput ) ;
16901757 wc . ipc . off ( HUMAN_INPUT_CHANNEL , humanInput ) ;
16911758 wc . ipc . off ( MOUSE_NAVIGATE_CHANNEL , mouseNavigate ) ;
@@ -1704,14 +1771,18 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
17041771 wc . on ( "audio-state-changed" , audioStateChanged ) ;
17051772 wc . ipc . on ( HUMAN_INPUT_CHANNEL , humanInput ) ;
17061773 wc . ipc . on ( MOUSE_NAVIGATE_CHANNEL , mouseNavigate ) ;
1707- wc . setWindowOpenHandler ( ( { url } ) => {
1774+ wc . setWindowOpenHandler ( ( details ) => {
1775+ if ( previewWindowOpenAction ( details ) === "popup" ) {
1776+ return { action : "allow" , overrideBrowserWindowOptions : POPUP_WINDOW_OPTIONS } ;
1777+ }
17081778 runFork (
17091779 attemptPromise ( { operation : "openPreviewWindow" , tabId, webContentsId : wc . id } , ( ) =>
1710- wc . loadURL ( url ) ,
1780+ wc . loadURL ( details . url ) ,
17111781 ) . pipe ( Effect . ignore ) ,
17121782 ) ;
17131783 return { action : "deny" } ;
17141784 } ) ;
1785+ wc . on ( "did-create-window" , windowCreated ) ;
17151786 wc . on ( "before-input-event" , beforeInput ) ;
17161787 } ) ;
17171788 yield * Ref . update ( attachedRef , ( attached ) =>
0 commit comments