diff --git a/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx b/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx index 1f8484f8e4..0c9d5aba59 100644 --- a/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx +++ b/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx @@ -698,6 +698,31 @@ describe('PreviewPane console state', () => { profile: 'macmini' }) }) + + // #101880: guest window.print() segfaults the macOS native print panel — + // the pane stubs print in every guest document so the panel is never built. + it('stubs window.print in the guest on dom-ready', async () => { + let rendered!: ReturnType + await act(async () => { + rendered = render( + + ) + }) + + const webview = rendered.container.querySelector('webview') as HTMLElement & Record + const executeJavaScript = vi.fn(async (_code: string) => undefined) + + Object.assign(webview, { executeJavaScript }) + + act(() => { + webview.dispatchEvent(new Event('dom-ready')) + }) + + expect(executeJavaScript).toHaveBeenCalledOnce() + expect(String(executeJavaScript.mock.calls[0]?.[0])).toContain('window.print') + }) }) describe('PreviewPane guest external handoff', () => { diff --git a/apps/desktop/src/app/chat/right-rail/preview-pane.tsx b/apps/desktop/src/app/chat/right-rail/preview-pane.tsx index 958e9cc1b9..7dd0d85be0 100644 --- a/apps/desktop/src/app/chat/right-rail/preview-pane.tsx +++ b/apps/desktop/src/app/chat/right-rail/preview-pane.tsx @@ -187,6 +187,20 @@ function isModuleMimeError(message: string): boolean { return lower.includes('failed to load module script') && lower.includes('mime type') } +/** + * #101880: a guest page's `window.print()` (e.g. a Google Doc's Print + * button) reaches the macOS native print panel, whose construction + * segfaults inside PrintCore — the whole app dies before any dialog + * appears. Stub `print` in the guest so the native panel is never built; + * the warn surfaces in the preview console via the existing pipe. The + * `__hermesPrintGuard` flag keeps re-arms idempotent. Full print-to-PDF + * routing is the follow-up; this stops the crash. + */ +const PREVIEW_PRINT_GUARD_SCRIPT = + '(function(){if(window.__hermesPrintGuard)return;window.__hermesPrintGuard=true;' + + 'window.print=function(){console.warn("[Hermes] Printing is disabled in the in-app preview. ' + + 'Open the page in your browser to print.");};})()' + function PreviewLoadError({ consoleHeight = 0, error, @@ -1175,6 +1189,15 @@ export function PreviewPane({ notePage() } + // #101880: arm the print guard for every new guest document — a fresh + // load gets a fresh window object, so the stub must be re-injected. + const armPrintGuard = () => { + void webview.executeJavaScript?.(PREVIEW_PRINT_GUARD_SCRIPT)?.catch(() => { + // Guest tore down mid-arm (a navigation raced the injection) — the + // next dom-ready arms it again. + }) + } + // The WEBVIEW is the source of truth for DevTools, not our click handler: // closing the DevTools window itself fires devtools-closed with no click, // and the glyph was left stuck "on" when we tracked it locally. @@ -1268,6 +1291,8 @@ export function PreviewPane({ // SPAs title themselves long after the load settles, and a route change // renames the page without navigating at all. webview.addEventListener('page-title-updated', notePage) + // #101880: never let a guest reach the native print panel. + webview.addEventListener('dom-ready', armPrintGuard) host.appendChild(webview) webviewRef.current = webview @@ -1284,6 +1309,7 @@ export function PreviewPane({ webview.removeEventListener('did-start-loading', onStart) webview.removeEventListener('did-stop-loading', onStop) webview.removeEventListener('page-title-updated', notePage) + webview.removeEventListener('dom-ready', armPrintGuard) webview.remove() setAnnotate(session => (session.mode ? { ...endAnnotateMode(session), stack: emptyAnnotateStack() } : session)) }