From a93e31a93395953b795e9f5098a6b93c8ee331af Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Mon, 26 Oct 2026 14:40:39 -0700 Subject: [PATCH] perf(renderer): keep modal blur with independent source regions (#33799) * perf(renderer): drop full-window overlay blurs and the idle usage-chip pulse A full-window backdrop-filter is re-blurred on every frame while anything beneath it animates, which held ~370 MB of extra GPU render-pass textures and swapchain buffers while a dialog, sheet, palette or onboarding was open. The 2px scrim blur is barely visible under the black scrim, so keep the dim only (5 points darker to keep the background as recessive) and drop the panel blur that sits under a 96%-opaque background. The status-bar usage chip also pulsed forever before its first fetch (unfocused windows never poll); show it static until a fetch is actually running. * perf(renderer): preserve modal blur with independent source regions * fix(renderer): preserve modal blur recovery and Space coverage * fix(ci): remove stale context-menu import after main merge --------- Co-authored-by: m4air Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- src/renderer/src/App.tsx | 6 +- .../src/app-shell/AppRootSurfaces.tsx | 49 ++++--- src/renderer/src/assets/main.css | 17 +++ .../use-native-chat-context-menu.tsx | 1 - .../components/onboarding/OnboardingFlow.tsx | 2 +- .../status-bar/StatusBarProviderSegment.tsx | 14 +- src/renderer/src/components/ui/command.tsx | 17 +-- src/renderer/src/components/ui/dialog.tsx | 5 +- .../src/components/ui/sheet.fixture.tsx | 23 +++ src/renderer/src/components/ui/sheet.tsx | 5 +- .../worktree-jump-palette-surface.tsx | 4 +- src/renderer/src/space.tsx | 4 +- tests/e2e/modal-scene-blur.spec.ts | 135 ++++++++++++++++++ 13 files changed, 229 insertions(+), 53 deletions(-) create mode 100644 src/renderer/src/components/ui/sheet.fixture.tsx create mode 100644 tests/e2e/modal-scene-blur.spec.ts diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 7ba62c1b49a..e5a0a1d6d62 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect } from 'react' +import { createPortal } from 'react-dom' import { Toaster } from '@/components/ui/sonner' import { TooltipProvider } from '@/components/ui/tooltip' import { ConfirmationDialogProvider } from './components/confirmation-dialog' @@ -108,7 +109,10 @@ function App(): React.JSX.Element { - + {createPortal( + , + document.body + )} diff --git a/src/renderer/src/app-shell/AppRootSurfaces.tsx b/src/renderer/src/app-shell/AppRootSurfaces.tsx index dfc19a7df02..2f6d99b88e3 100644 --- a/src/renderer/src/app-shell/AppRootSurfaces.tsx +++ b/src/renderer/src/app-shell/AppRootSurfaces.tsx @@ -3,6 +3,7 @@ import { OnboardingLanguageLoading } from '../components/onboarding/OnboardingLa import { useLanguageReadiness } from '../i18n/language-readiness' import { NotificationCardStack } from '../components/NotificationCardStack' import { Suspense } from 'react' +import { createPortal } from 'react-dom' import { lazyWithRetry as lazy } from '@/lib/lazy-with-retry' import { translate } from '@/i18n/i18n' import { RecoverableRenderErrorBoundary } from '../components/error-boundaries/RecoverableRenderErrorBoundary' @@ -347,28 +348,32 @@ export function AppRootSurfaces(props: { > - {onboardingGate.onboarding && onboardingGate.shouldRender ? ( - - - {languageReady ? ( - - ) : ( - - )} - - - ) : null} + {onboardingGate.onboarding && onboardingGate.shouldRender + ? createPortal( + + + {languageReady ? ( + + ) : ( + + )} + + , + document.body + ) + : null} {shouldMountDictationController ? ( diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 8ff08f814a7..67352d82a80 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -865,6 +865,23 @@ body:has(> [data-slot='dialog-overlay'], > [data-slot='sheet-overlay']) overflow: hidden; } +/* Blur source regions independently so status-bar animation cannot re-filter the workspace. */ +body:has(> [data-slot='dialog-overlay'], > [data-slot='sheet-overlay'], > [data-onboarding-overlay]) + .app-layout + > :not(.window-controls) { + filter: blur(2px); +} + +body + > :is( + [data-slot='dialog-content'], + [data-slot='command-dialog-content'], + [data-slot='sheet-content'], + [data-onboarding-overlay] + ):has(~ [data-slot='dialog-overlay'], ~ [data-slot='sheet-overlay']) { + filter: blur(2px); +} + /* Why: native windows have a stable content box and need no wake-time viewport reflow. */ html.native-shell, html.native-shell body, diff --git a/src/renderer/src/components/native-chat/use-native-chat-context-menu.tsx b/src/renderer/src/components/native-chat/use-native-chat-context-menu.tsx index f89f5988613..8e595f3a6a6 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-context-menu.tsx +++ b/src/renderer/src/components/native-chat/use-native-chat-context-menu.tsx @@ -30,7 +30,6 @@ import { isEditableTarget } from '@/lib/editable-target' import { findEditablePrimarySelectionPasteTarget } from '@/lib/primary-selection-paste' import { NativeChatCopyOrcaSessionIdMenuItem } from './NativeChatCopyOrcaSessionIdMenuItem' import { NativeChatSelectionQuote } from './NativeChatSelectionQuote' -import { StructuredChatPaneMenuItems } from './StructuredChatPaneMenuItems' import { NativeChatPaneMenuItems } from './NativeChatPaneMenuItems' import { NativeChatStructuredPaneMenuItems } from './NativeChatStructuredPaneMenuItems' import type { NativeChatComposerHandle } from './native-chat-composer-types' diff --git a/src/renderer/src/components/onboarding/OnboardingFlow.tsx b/src/renderer/src/components/onboarding/OnboardingFlow.tsx index 4a0a60f1128..4ed63c3086b 100644 --- a/src/renderer/src/components/onboarding/OnboardingFlow.tsx +++ b/src/renderer/src/components/onboarding/OnboardingFlow.tsx @@ -197,7 +197,7 @@ export default function OnboardingFlow({ return (
{ if (!shouldRequestOnboardingSkipConfirmation(event)) { diff --git a/src/renderer/src/components/status-bar/StatusBarProviderSegment.tsx b/src/renderer/src/components/status-bar/StatusBarProviderSegment.tsx index 09e084ff78f..3896de3e85a 100644 --- a/src/renderer/src/components/status-bar/StatusBarProviderSegment.tsx +++ b/src/renderer/src/components/status-bar/StatusBarProviderSegment.tsx @@ -263,20 +263,12 @@ export function ProviderSegment({ : [] ) - if (!p || p.status === 'idle') { + // Why static: unfocused windows never poll, so a pending chip's pulse would redraw the compositor indefinitely. + if (!p || p.status === 'idle' || (p.status === 'fetching' && !tightest)) { return ( - ··· - - ) - } - - if (p.status === 'fetching' && !tightest) { - return ( - - - ··· + ··· ) } diff --git a/src/renderer/src/components/ui/command.tsx b/src/renderer/src/components/ui/command.tsx index 660d845cda0..980472c78be 100644 --- a/src/renderer/src/components/ui/command.tsx +++ b/src/renderer/src/components/ui/command.tsx @@ -9,6 +9,7 @@ import * as DialogPrimitive from 'radix-ui/dialog' import { cn } from '@/lib/utils' import { handleImeOverlayEscape } from '@/lib/ime-overlay-escape' import { useImeTextFieldProps } from '@/lib/ime-text-field' +import { DialogOverlay } from '@/components/ui/dialog' const commandVariants = cva('flex h-full w-full flex-col overflow-hidden rounded-md', { variants: { @@ -49,6 +50,7 @@ function CommandDialog({ onCloseAutoFocus, contentClassName, overlayClassName, + surface = 'default', commandProps, ...props }: React.ComponentProps & { @@ -59,6 +61,8 @@ function CommandDialog({ onCloseAutoFocus?: (e: Event) => void contentClassName?: string overlayClassName?: string + /** `palette` is the larger full-search surface (worktree jump palette). */ + surface?: 'default' | 'palette' commandProps?: React.ComponentProps }) { const { className: commandClassName, ...commandRootProps } = commandProps ?? {} @@ -66,17 +70,14 @@ function CommandDialog({ return ( - + handleImeOverlayEscape(event)} diff --git a/src/renderer/src/components/ui/dialog.tsx b/src/renderer/src/components/ui/dialog.tsx index 39079fd1521..d4b394f95a6 100644 --- a/src/renderer/src/components/ui/dialog.tsx +++ b/src/renderer/src/components/ui/dialog.tsx @@ -35,9 +35,8 @@ function DialogOverlay({ return ( ( + <> +
+

Workspace content stays live behind the sheet.

+
+ + + + Sheet preview + + The sheet text stays sharp above the blurred workspace. + + + + + + ) +}) diff --git a/src/renderer/src/components/ui/sheet.tsx b/src/renderer/src/components/ui/sheet.tsx index 8005eaf2096..27a5e178154 100644 --- a/src/renderer/src/components/ui/sheet.tsx +++ b/src/renderer/src/components/ui/sheet.tsx @@ -29,9 +29,8 @@ function SheetOverlay({ return ( - +
+ +
{activeModal === 'delete-worktree' ? : null} {activeModal === 'preserved-branch-review' ? : null}
diff --git a/tests/e2e/modal-scene-blur.spec.ts b/tests/e2e/modal-scene-blur.spec.ts new file mode 100644 index 00000000000..7d8b0d846d2 --- /dev/null +++ b/tests/e2e/modal-scene-blur.spec.ts @@ -0,0 +1,135 @@ +import { test, expect } from './helpers/orca-app' +import { openSidebarProjectDialog } from './helpers/sidebar-project-dialog' + +test.use({ seedTestRepo: false, orcaAppExtraEnv: { ORCA_BACKGROUND_LAUNCH: '1' } }) + +for (const theme of ['dark', 'light'] as const) { + test(`modal surfaces blur the live scene and stay sharp (${theme})`, async ({ + electronApp, + orcaPage + }, testInfo) => { + const windows = await electronApp.evaluate(({ BrowserWindow }) => + BrowserWindow.getAllWindows().map((window) => ({ + visible: window.isVisible(), + focused: window.isFocused() + })) + ) + expect(windows.every((window) => !window.visible && !window.focused)).toBe(true) + await orcaPage.evaluate(async (theme) => { + await window.__store?.getState().updateSettings({ theme, uiLanguage: 'en' }) + }, theme) + + const scene = orcaPage.locator('.app-layout > :first-child') + await expect(scene).toHaveCSS('filter', 'none') + await openSidebarProjectDialog(orcaPage) + await expect(scene).toHaveCSS('filter', 'blur(2px)') + await expect(orcaPage.locator('.app-layout')).toHaveCSS('filter', 'none') + const dialog = orcaPage.getByRole('dialog', { name: /Add a project/i }) + await expect(dialog).toHaveCSS('filter', 'none') + await expect(dialog).toHaveCSS('backdrop-filter', 'none') + await expect(orcaPage.locator('[data-slot="dialog-overlay"]')).toHaveCSS( + 'backdrop-filter', + 'none' + ) + await orcaPage.screenshot({ path: testInfo.outputPath(`dialog-blur-${theme}.png`) }) + await orcaPage.keyboard.press('Escape') + await expect(scene).toHaveCSS('filter', 'none') + + await orcaPage.getByRole('button', { name: 'Search worktrees and browser tabs' }).click() + const palette = orcaPage.locator('[data-slot="command-dialog-content"]') + await expect(palette).toBeVisible() + await expect(scene).toHaveCSS('filter', 'blur(2px)') + await expect(palette).toHaveCSS('filter', 'none') + await expect(palette).toHaveCSS('backdrop-filter', 'none') + await orcaPage.keyboard.press('Escape') + await expect(scene).toHaveCSS('filter', 'none') + }) +} + +test('Space window dialogs blur their scene and release it on close', async ({ + electronApp, + orcaPage +}, testInfo) => { + const spaceWindow = electronApp.waitForEvent('window') + await orcaPage.evaluate(async () => { + await window.api.workspaceSpace.openWindow?.() + }) + const spacePage = await spaceWindow + await spacePage.waitForFunction(() => Boolean(window.__store)) + const scene = spacePage.locator('.app-layout > :first-child') + await expect(scene).toHaveCSS('filter', 'none') + await spacePage.evaluate(() => { + window.__store?.getState().openModal('preserved-branch-review', { + branches: [{ worktreeId: 'blur-preview', branchName: 'preview' }] + }) + }) + const dialog = spacePage.getByRole('dialog', { name: 'Review kept branches' }) + await expect(dialog).toBeVisible() + await expect(scene).toHaveCSS('filter', 'blur(2px)') + await expect(dialog).toHaveCSS('filter', 'none') + await expect(dialog).toHaveCSS('backdrop-filter', 'none') + const windows = await electronApp.evaluate(({ BrowserWindow }) => + BrowserWindow.getAllWindows().map((window) => ({ + visible: window.isVisible(), + focused: window.isFocused() + })) + ) + expect(windows.every((window) => !window.visible && !window.focused)).toBe(true) + await spacePage.screenshot({ path: testInfo.outputPath('space-dialog-blur.png') }) + await spacePage.keyboard.press('Escape') + await expect(scene).toHaveCSS('filter', 'none') +}) + +test.describe('onboarding scene blur', () => { + test.use({ dismissOnboarding: false }) + + test('onboarding stays sharp until a nested confirmation opens', async ({ orcaPage }) => { + const scene = orcaPage.locator('.app-layout > :first-child') + const onboarding = orcaPage.locator('[data-onboarding-overlay]') + await expect(onboarding).toBeVisible() + await expect(scene).toHaveCSS('filter', 'blur(2px)') + await expect(onboarding).toHaveCSS('filter', 'none') + await expect(onboarding).toHaveCSS('backdrop-filter', 'none') + await onboarding.getByRole('button').first().focus() + await orcaPage.keyboard.press('Escape') + const confirmation = orcaPage.getByRole('dialog', { name: /Skip onboarding\?/i }) + await expect(confirmation).toBeVisible() + await expect(confirmation).toHaveCSS('filter', 'none') + await expect(onboarding).toHaveCSS('filter', 'blur(2px)') + await confirmation.getByRole('button', { name: /No, keep going/i }).click() + await expect(onboarding).toHaveCSS('filter', 'none') + await expect(scene).toHaveCSS('filter', 'blur(2px)') + }) + + test('onboarding render recovery stays within the viewport', async ({ orcaPage }, testInfo) => { + await expect(orcaPage.locator('[data-onboarding-overlay]')).toBeVisible() + await orcaPage.evaluate(async () => { + const onboarding = await window.api.onboarding.get() + Object.defineProperty(onboarding, 'lastCompletedStep', { + get() { + throw new Error('Injected onboarding render failure') + } + }) + window.dispatchEvent(new CustomEvent('orca:onboarding-reopened', { detail: onboarding })) + }) + + const crashReport = orcaPage.getByRole('dialog').filter({ + has: orcaPage.getByRole('heading', { name: 'Orca hit a recoverable UI error' }) + }) + await crashReport.getByRole('button', { name: 'Close', exact: true }).click() + const recovery = orcaPage.getByRole('alert').filter({ hasText: 'Onboarding hit an error.' }) + const retry = recovery.getByRole('button', { name: 'Retry', exact: true }) + await expect(retry).toBeInViewport() + await orcaPage.screenshot({ path: testInfo.outputPath('onboarding-render-recovery.png') }) + + await orcaPage.evaluate(async () => { + window.dispatchEvent( + new CustomEvent('orca:onboarding-reopened', { detail: await window.api.onboarding.get() }) + ) + }) + await retry.click() + await expect(recovery).toHaveCount(0) + await expect(orcaPage.locator('[data-onboarding-overlay]')).toBeVisible() + await expect(orcaPage.locator('.app-layout > :first-child')).toHaveCSS('filter', 'blur(2px)') + }) +})