From 5378598d96cffc2ce9544dfbd5db573300f7180e Mon Sep 17 00:00:00 2001 From: mathofdynamic Date: Sat, 29 Aug 2026 14:11:39 +0330 Subject: [PATCH 1/9] feat(desktop): add configurable Windows Acrylic backdrop --- .../desktop/src/ipc/methods/clientSettings.ts | 3 + .../settings/DesktopClientSettings.test.ts | 1 + apps/desktop/src/window/DesktopWindow.test.ts | 23 +++ apps/desktop/src/window/DesktopWindow.ts | 144 +++++++++++++++++- .../components/settings/SettingsPanels.tsx | 40 ++++- .../src/components/settings/settingsSearch.ts | 13 ++ apps/web/src/index.css | 30 ++++ apps/web/src/routes/__root.tsx | 7 +- docs/README.md | 1 + docs/user/desktop-appearance.md | 17 +++ packages/contracts/src/settings.test.ts | 9 ++ packages/contracts/src/settings.ts | 5 + 12 files changed, 281 insertions(+), 12 deletions(-) create mode 100644 docs/user/desktop-appearance.md diff --git a/apps/desktop/src/ipc/methods/clientSettings.ts b/apps/desktop/src/ipc/methods/clientSettings.ts index dd0625759e94..77b759a836f7 100644 --- a/apps/desktop/src/ipc/methods/clientSettings.ts +++ b/apps/desktop/src/ipc/methods/clientSettings.ts @@ -4,6 +4,7 @@ import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import * as DesktopClientSettings from "../../settings/DesktopClientSettings.ts"; +import * as DesktopWindow from "../../window/DesktopWindow.ts"; import * as IpcChannels from "../channels.ts"; import * as DesktopIpc from "../DesktopIpc.ts"; @@ -24,5 +25,7 @@ export const setClientSettings = DesktopIpc.makeIpcMethod({ handler: Effect.fn("desktop.ipc.clientSettings.set")(function* (settings) { const clientSettings = yield* DesktopClientSettings.DesktopClientSettings; yield* clientSettings.set(settings); + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.syncAppearance; }), }); diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index e32a5e2d0807..12c9b31b7131 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -25,6 +25,7 @@ const clientSettings: ClientSettings = { dismissedProviderUpdateNotificationKeys: [], diffIgnoreWhitespace: true, environmentIdentificationMode: "artwork", + desktopBackdropEnabled: true, favorites: [], fontFamilyCode: "", fontFamilyComposer: "", diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index 036eddd8db78..a11493dd9c19 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -104,6 +104,7 @@ function makeFakeBrowserWindow() { }), restore: vi.fn(), setBackgroundColor: vi.fn(), + setBackgroundMaterial: vi.fn(), setAutoHideCursor: vi.fn(), setTitle: vi.fn(), setTitleBarOverlay: vi.fn(), @@ -394,6 +395,28 @@ const makeSplashScenario = (createOutcomes: readonly (Electron.BrowserWindow | n }); describe("DesktopWindow", () => { + it("uses transparent Acrylic-ready window options only on Windows", () => { + assert.deepEqual(DesktopWindow.getWindowBackdropOptions("win32", true), { + backgroundColor: "#00000000", + backgroundMaterial: "acrylic", + frame: false, + roundedCorners: true, + thickFrame: true, + transparent: true, + }); + assert.deepEqual(DesktopWindow.getWindowBackdropOptions("win32", true, false), { + backgroundColor: "#0a0a0a", + backgroundMaterial: "none", + frame: false, + roundedCorners: true, + thickFrame: true, + transparent: true, + }); + assert.deepEqual(DesktopWindow.getWindowBackdropOptions("darwin", true), { + backgroundColor: "#0a0a0a", + }); + }); + it("restores bounds only when the window fits within a connected display", () => { const persistedBounds = { x: 2040, y: 80, width: 1320, height: 880 }; const displays = [ diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index 56411711eb6c..48170e159d28 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -34,6 +34,8 @@ const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linu const TITLEBAR_LIGHT_SYMBOL_COLOR = "#1f2937"; const TITLEBAR_DARK_SYMBOL_COLOR = "#f8fafc"; const MAIN_WINDOW_BOUNDS_PERSIST_DEBOUNCE_MS = 500; +const WINDOWS_TRANSPARENT_BACKGROUND_COLOR = "#00000000"; +const WINDOWS_ACRYLIC_MATERIAL = "acrylic" as const; const DEVELOPMENT_LOAD_RETRY_DELAYS_MS = [100, 250, 500, 1_000, 2_000] as const; // Renderer crash (usually V8 OOM on long sessions) recovery: reload after a // short delay, at most MAX_ATTEMPTS times per rolling WINDOW so a renderer @@ -129,6 +131,80 @@ function getInitialWindowBackgroundColor(shouldUseDarkColors: boolean): string { return shouldUseDarkColors ? "#0a0a0a" : "#ffffff"; } +const windowsWithAcrylicBackdrop = new WeakSet(); +const windowsWithoutAcrylicBackdrop = new WeakSet(); + +export function getWindowBackdropOptions( + platform: NodeJS.Platform, + shouldUseDarkColors: boolean, + desktopBackdropEnabled = true, +): Pick< + Electron.BrowserWindowConstructorOptions, + | "backgroundColor" + | "backgroundMaterial" + | "frame" + | "roundedCorners" + | "thickFrame" + | "transparent" +> { + if (platform !== "win32") { + return { backgroundColor: getInitialWindowBackgroundColor(shouldUseDarkColors) }; + } + + return { + backgroundColor: desktopBackdropEnabled + ? WINDOWS_TRANSPARENT_BACKGROUND_COLOR + : getInitialWindowBackgroundColor(shouldUseDarkColors), + backgroundMaterial: desktopBackdropEnabled ? WINDOWS_ACRYLIC_MATERIAL : "none", + frame: false, + roundedCorners: true, + thickFrame: true, + transparent: true, + }; +} + +function applyWindowsBackdrop( + window: Electron.BrowserWindow, + platform: NodeJS.Platform, + shouldUseDarkColors: boolean, + desktopBackdropEnabled: boolean, +): Effect.Effect { + if (platform !== "win32") { + return Effect.void; + } + + return Effect.try({ + try: () => { + window.setBackgroundMaterial(desktopBackdropEnabled ? WINDOWS_ACRYLIC_MATERIAL : "none"); + if (desktopBackdropEnabled) { + windowsWithAcrylicBackdrop.add(window); + windowsWithoutAcrylicBackdrop.delete(window); + window.setBackgroundColor(WINDOWS_TRANSPARENT_BACKGROUND_COLOR); + } else { + windowsWithAcrylicBackdrop.delete(window); + windowsWithoutAcrylicBackdrop.add(window); + window.setBackgroundColor(getInitialWindowBackgroundColor(shouldUseDarkColors)); + } + }, + catch: (cause) => cause, + }).pipe( + Effect.catchCause((cause) => + Effect.gen(function* () { + windowsWithAcrylicBackdrop.delete(window); + windowsWithoutAcrylicBackdrop.add(window); + try { + window.setBackgroundColor(getInitialWindowBackgroundColor(shouldUseDarkColors)); + } catch { + // Preserve the original backdrop failure; window creation must stay best effort. + } + yield* logWindowWarning("Windows backdrop material unavailable; using solid background", { + cause, + }); + }), + ), + ); +} + type DisplayBounds = Pick; function windowFitsWithinDisplay( @@ -171,8 +247,12 @@ export function resolveInitialMainWindowBounds( // A self-contained "Connecting to WSL" splash, shown immediately in wsl-only // mode while the WSL backend (which serves the renderer) cold-boots. Inlined as // a data URL so it needs no bundled asset and no backend — pure CSS, no JS. -function buildConnectingSplashDataUrl(shouldUseDarkColors: boolean): string { - const background = getInitialWindowBackgroundColor(shouldUseDarkColors); +function buildConnectingSplashDataUrl( + shouldUseDarkColors: boolean, + platform: NodeJS.Platform, +): string { + const background = + platform === "win32" ? "transparent" : getInitialWindowBackgroundColor(shouldUseDarkColors); const label = shouldUseDarkColors ? "#9ca3af" : "#6b7280"; const accent = shouldUseDarkColors ? "#f8fafc" : "#1f2937"; const track = shouldUseDarkColors ? "rgba(248,250,252,0.18)" : "rgba(31,41,55,0.18)"; @@ -232,13 +312,25 @@ function syncWindowAppearance( window: Electron.BrowserWindow, shouldUseDarkColors: boolean, platform: NodeJS.Platform, + desktopBackdropEnabled: boolean, ): Effect.Effect { - return Effect.sync(() => { + return Effect.gen(function* () { if (window.isDestroyed()) { return; } - window.setBackgroundColor(getInitialWindowBackgroundColor(shouldUseDarkColors)); + if (platform === "win32") { + if (desktopBackdropEnabled && windowsWithAcrylicBackdrop.has(window)) { + window.setBackgroundColor(WINDOWS_TRANSPARENT_BACKGROUND_COLOR); + } else if (!desktopBackdropEnabled && windowsWithoutAcrylicBackdrop.has(window)) { + window.setBackgroundColor(getInitialWindowBackgroundColor(shouldUseDarkColors)); + } else { + yield* applyWindowsBackdrop(window, platform, shouldUseDarkColors, desktopBackdropEnabled); + } + } else { + window.setBackgroundColor(getInitialWindowBackgroundColor(shouldUseDarkColors)); + } + const { titleBarOverlay } = getWindowTitleBarOptions(shouldUseDarkColors, platform); if (typeof titleBarOverlay === "object") { window.setTitleBarOverlay(titleBarOverlay); @@ -286,6 +378,14 @@ export const make = Effect.gen(function* () { const context = yield* Effect.context(); const runFork = Effect.runForkWith(context); const runPromise = Effect.runPromiseWith(context); + const getDesktopBackdropEnabled = clientSettings.get.pipe( + Effect.map((settings) => + Option.match(settings, { + onNone: () => DEFAULT_CLIENT_SETTINGS.desktopBackdropEnabled, + onSome: (value) => value.desktopBackdropEnabled, + }), + ), + ); let flushMainWindowBounds: Effect.Effect = Effect.void; const dismissConnectingSplash = Effect.gen(function* () { @@ -323,6 +423,7 @@ export const make = Effect.gen(function* () { const iconPaths = yield* assets.iconPaths; const iconOption = getIconOption(iconPaths, environment.platform); const shouldUseDarkColors = yield* electronTheme.shouldUseDarkColors; + const desktopBackdropEnabled = yield* getDesktopBackdropEnabled; const persistedSettings = yield* desktopSettings.get; const persistedBounds = persistedSettings.mainWindowBounds; const displayBoundsResult = yield* Effect.sync(() => { @@ -353,7 +454,11 @@ export const make = Effect.gen(function* () { show: false, autoHideMenuBar: true, ...(environment.platform === "darwin" ? { disableAutoHideCursor: true } : {}), - backgroundColor: getInitialWindowBackgroundColor(shouldUseDarkColors), + ...getWindowBackdropOptions( + environment.platform, + shouldUseDarkColors, + desktopBackdropEnabled, + ), ...iconOption, title: environment.displayName, ...getWindowTitleBarOptions(shouldUseDarkColors, environment.platform), @@ -371,6 +476,12 @@ export const make = Effect.gen(function* () { webviewTag: true, }, }); + yield* applyWindowsBackdrop( + window, + environment.platform, + shouldUseDarkColors, + desktopBackdropEnabled, + ); if (environment.platform === "darwin") { window.setAutoHideCursor(false); @@ -795,6 +906,7 @@ export const make = Effect.gen(function* () { if (Option.isSome(existingWindow)) return; const shouldUseDarkColors = yield* electronTheme.shouldUseDarkColors; + const desktopBackdropEnabled = yield* getDesktopBackdropEnabled; const splash = yield* electronWindow.create({ width: 360, height: 220, @@ -806,7 +918,11 @@ export const make = Effect.gen(function* () { center: true, show: false, skipTaskbar: false, - backgroundColor: getInitialWindowBackgroundColor(shouldUseDarkColors), + ...getWindowBackdropOptions( + environment.platform, + shouldUseDarkColors, + desktopBackdropEnabled, + ), title: environment.displayName, webPreferences: { contextIsolation: true, @@ -823,7 +939,13 @@ export const make = Effect.gen(function* () { splash.show(); } }); - void splash.loadURL(buildConnectingSplashDataUrl(shouldUseDarkColors)); + yield* applyWindowsBackdrop( + splash, + environment.platform, + shouldUseDarkColors, + desktopBackdropEnabled, + ); + void splash.loadURL(buildConnectingSplashDataUrl(shouldUseDarkColors, environment.platform)); yield* logWindowInfo("connecting splash shown"); }).pipe( // The splash is best-effort UX — never let it fail startup. @@ -910,8 +1032,14 @@ export const make = Effect.gen(function* () { }), syncAppearance: Effect.gen(function* () { const shouldUseDarkColors = yield* electronTheme.shouldUseDarkColors; + const desktopBackdropEnabled = yield* getDesktopBackdropEnabled; yield* electronWindow.syncAllAppearance((window) => - syncWindowAppearance(window, shouldUseDarkColors, environment.platform), + syncWindowAppearance( + window, + shouldUseDarkColors, + environment.platform, + desktopBackdropEnabled, + ), ); }).pipe(Effect.withSpan("desktop.window.syncAppearance")), }); diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index b486a0eefea4..513ac52a4862 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -78,7 +78,7 @@ import { sortProviderInstanceEntries, } from "../../providerInstances"; import { ensureLocalApi, readLocalApi } from "../../localApi"; -import { isMacPlatform } from "../../lib/utils"; +import { isMacPlatform, isWindowsPlatform } from "../../lib/utils"; import { primaryServerObservabilityAtom, primaryServerProvidersAtom } from "../../state/server"; import { useProjects } from "../../state/entities"; import { useArchivedThreadSnapshots } from "../../lib/archivedThreadsState"; @@ -481,6 +481,9 @@ export function useSettingsRestore(onRestored?: () => void) { ? ["Contrast"] : []), ...(settings.glassOpacity !== DEFAULT_UNIFIED_SETTINGS.glassOpacity ? ["Glass opacity"] : []), + ...(settings.desktopBackdropEnabled !== DEFAULT_UNIFIED_SETTINGS.desktopBackdropEnabled + ? ["Desktop background blur"] + : []), ...(settings.environmentIdentificationMode !== DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode ? ["Environment identification"] @@ -574,6 +577,7 @@ export function useSettingsRestore(onRestored?: () => void) { settings.fontSizePrompt, settings.fontSizeTerminal, settings.glassOpacity, + settings.desktopBackdropEnabled, settings.enableLegacyTokenStreaming, settings.enableProviderUpdateChecks, settings.sidebarAutoSettleAfterDays, @@ -659,6 +663,7 @@ export function useSettingsRestore(onRestored?: () => void) { showSkillsInSlashMenu: DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu, environmentIdentificationMode: DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode, glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity, + desktopBackdropEnabled: DEFAULT_UNIFIED_SETTINGS.desktopBackdropEnabled, sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount, sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode, sidebarAutoSettleAfterDays: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays, @@ -997,6 +1002,8 @@ export function AppearanceSettingsPanel() { const environmentStageLabel = useEnvironmentStageLabel(); const showEnvironmentIdentification = resolveEnvironmentIdentificationPillLabel(environmentStageLabel) !== null; + const showDesktopBackdropSetting = + isElectron && typeof navigator !== "undefined" && isWindowsPlatform(navigator.platform); const glassOpacityRatio = (settings.glassOpacity - MIN_GLASS_OPACITY) / (MAX_GLASS_OPACITY - MIN_GLASS_OPACITY); const glassOpacitySliderStyle = { @@ -1080,7 +1087,7 @@ export function AppearanceSettingsPanel() { + {showDesktopBackdropSetting ? ( + + updateSettings({ + desktopBackdropEnabled: DEFAULT_UNIFIED_SETTINGS.desktopBackdropEnabled, + }) + } + /> + ) : null + } + control={ + + updateSettings({ desktopBackdropEnabled: Boolean(checked) }) + } + aria-label="Desktop background blur" + /> + } + /> + ) : null} + {showEnvironmentIdentification ? ( (isElectron || item.desktopOnly !== true) && + (item.windowsOnly !== true || (isElectron && isWindows)) && normalizeSearchText(item.title).includes(normalizedQuery), ); } diff --git a/apps/web/src/index.css b/apps/web/src/index.css index d43475f901cc..79752390020f 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -2018,6 +2018,36 @@ body { .electron-windows { --desktop-window-right-resize-inset: 6px; + --desktop-window-canvas: color-mix(in srgb, var(--background) var(--glass-opacity), transparent); + --desktop-window-sidebar: color-mix(in srgb, var(--sidebar) var(--glass-opacity), transparent); +} + +html.electron-windows[data-desktop-backdrop="off"] { + --desktop-window-canvas: var(--background); + --desktop-window-sidebar: var(--sidebar); +} + +/* Electron's Windows Acrylic backdrop is native to the BrowserWindow. The + renderer must leave the window and its primary app surfaces translucent so + that the native material can reach the entire client area. Reuse the + existing glass opacity preference; web, macOS, Linux, and mobile keep their + current surface behavior. */ +html.electron-windows, +html.electron-windows body, +html.electron-windows #root, +html.electron-windows [data-slot="sidebar-wrapper"] { + background-color: transparent !important; +} + +html.electron-windows .bg-background, +html.electron-windows [data-slot="sidebar-inset"] { + background-color: var(--desktop-window-canvas) !important; +} + +html.electron-windows .bg-sidebar, +html.electron-windows [data-app-sidebar], +html.electron-windows [data-slot="sidebar-inner"] { + background-color: var(--desktop-window-sidebar) !important; } /* App-chrome grain. Baked into each surface's own background (behind diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 7c715dff9e95..3e70858a0114 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -166,10 +166,13 @@ function ContrastAppearanceSync() { function GlassAppearanceSync() { const glassOpacity = useClientSettings((settings) => settings.glassOpacity); + const desktopBackdropEnabled = useClientSettings((settings) => settings.desktopBackdropEnabled); useEffect(() => { - document.documentElement.style.setProperty("--glass-opacity", `${glassOpacity}%`); - }, [glassOpacity]); + const root = document.documentElement; + root.style.setProperty("--glass-opacity", `${glassOpacity}%`); + root.dataset.desktopBackdrop = desktopBackdropEnabled ? "on" : "off"; + }, [desktopBackdropEnabled, glassOpacity]); return null; } diff --git a/docs/README.md b/docs/README.md index a0e26dffb74c..cda9d858bdea 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,6 +8,7 @@ - [Organizing threads](./user/thread-sidebar.md) - [Review usage](./user/usage.md) - [Customize a project icon](./user/project-settings.md) +- [Desktop appearance](./user/desktop-appearance.md) - [Mobile appearance](./user/mobile-appearance.md) - [Remote access](./user/remote-access.md) - [Keeping app and server in sync](./user/updating.md) diff --git a/docs/user/desktop-appearance.md b/docs/user/desktop-appearance.md new file mode 100644 index 000000000000..3594c25d82f8 --- /dev/null +++ b/docs/user/desktop-appearance.md @@ -0,0 +1,17 @@ +# Desktop appearance + +On Windows, T3 Code can blur the desktop behind the app using the native Windows Acrylic +material. + +To enable or disable it: + +1. Open **Settings**. +2. Select **Appearance**. +3. Turn **Desktop background blur** on or off. + +The change applies immediately. **Glass opacity** controls how much of the Acrylic backdrop is +visible through the app surfaces. On Windows versions that do not support Acrylic, T3 Code uses a +solid surface instead. + +The setting is available only in the Windows desktop app. Web, macOS, Linux, and mobile clients +keep their existing appearance behavior. diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index 32851e735a34..40208be4713a 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -82,6 +82,15 @@ describe("ClientSettings glass opacity", () => { }); }); +describe("ClientSettings desktop backdrop", () => { + it("defaults the Windows desktop blur on and accepts the patch", () => { + expect(decodeClientSettings({}).desktopBackdropEnabled).toBe(true); + expect( + decodeClientSettingsPatch({ desktopBackdropEnabled: false }).desktopBackdropEnabled, + ).toBe(false); + }); +}); + describe("ClientSettings appearance contrast", () => { it("defaults to the theme's original contrast", () => { expect(decodeClientSettings({}).appearanceContrast).toBe(100); diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 7e670229704d..8e9d340ecd58 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -75,6 +75,7 @@ export const GlassOpacity = Schema.Int.check( ); export type GlassOpacity = typeof GlassOpacity.Type; export const DEFAULT_GLASS_OPACITY: GlassOpacity = 80; +export const DEFAULT_DESKTOP_BACKDROP_ENABLED = true; export const MIN_APPEARANCE_CONTRAST = 50; export const MAX_APPEARANCE_CONTRAST = 200; @@ -178,6 +179,9 @@ export const ClientSettingsSchema = Schema.Struct({ glassOpacity: GlassOpacity.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_GLASS_OPACITY)), ), + desktopBackdropEnabled: Schema.Boolean.pipe( + Schema.withDecodingDefault(Effect.succeed(DEFAULT_DESKTOP_BACKDROP_ENABLED)), + ), fontSizeInterface: InterfaceFontSize.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_INTERFACE_FONT_SIZE)), ), @@ -910,6 +914,7 @@ export const ClientSettingsPatch = Schema.Struct({ diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean), environmentIdentificationMode: Schema.optionalKey(EnvironmentIdentificationMode), glassOpacity: Schema.optionalKey(GlassOpacity), + desktopBackdropEnabled: Schema.optionalKey(Schema.Boolean), fontSizeInterface: Schema.optionalKey(InterfaceFontSize), fontSizePrompt: Schema.optionalKey(PromptFontSize), fontSizeCode: Schema.optionalKey(CodeFontSize), From 05b75690d0e94d5811425daaa874304f1ecf2b1c Mon Sep 17 00:00:00 2001 From: mathofdynamic Date: Sun, 30 Aug 2026 10:11:33 +0330 Subject: [PATCH 2/9] fix(desktop): scope Acrylic backdrop to app surfaces --- apps/desktop/src/ipc/channels.ts | 1 + apps/desktop/src/preload.ts | 11 ++++ apps/desktop/src/window/DesktopWindow.test.ts | 50 +++++++++++++++---- apps/desktop/src/window/DesktopWindow.ts | 50 ++++++++++++++++--- apps/web/src/components/ChatView.tsx | 4 +- apps/web/src/components/DiffPanel.tsx | 2 +- apps/web/src/components/DiffPanelShell.tsx | 4 +- .../src/components/NoActiveThreadState.tsx | 2 +- apps/web/src/components/SplashScreen.tsx | 2 +- .../src/components/ThreadTerminalDrawer.tsx | 4 +- .../src/components/auth/AuthSurfaceShell.tsx | 2 +- .../components/auth/PairingRouteSurface.tsx | 6 +-- .../src/components/files/FileBrowserPanel.tsx | 2 +- .../src/components/files/FilePreviewPanel.tsx | 4 +- .../components/preview/PreviewPanelShell.tsx | 2 +- .../components/preview/PreviewUnreachable.tsx | 2 +- .../src/components/preview/PreviewView.tsx | 2 +- .../pullRequest/PullRequestDetailPanel.tsx | 2 +- .../pullRequest/PullRequestGhosts.tsx | 2 +- .../settings/ProjectSettingsPanel.tsx | 2 +- apps/web/src/components/ui/sidebar.tsx | 2 +- apps/web/src/components/usage/UsagePage.tsx | 2 +- apps/web/src/index.css | 39 ++++++++++----- apps/web/src/routes/__root.tsx | 16 ++++-- apps/web/src/routes/_chat.index.tsx | 4 +- apps/web/src/routes/_chat.pull-requests.tsx | 4 +- apps/web/src/routes/settings.tsx | 2 +- packages/contracts/src/ipc.ts | 2 + 28 files changed, 165 insertions(+), 62 deletions(-) diff --git a/apps/desktop/src/ipc/channels.ts b/apps/desktop/src/ipc/channels.ts index c4ef82ec8cb7..7445be07efce 100644 --- a/apps/desktop/src/ipc/channels.ts +++ b/apps/desktop/src/ipc/channels.ts @@ -8,6 +8,7 @@ export const PROBE_REMOTE_EDITORS_CHANNEL = "desktop:probe-remote-editors"; export const MENU_ACTION_CHANNEL = "desktop:menu-action"; export const QUIT_SHORTCUT_CHANNEL = "desktop:quit-shortcut"; export const GET_WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:get-window-fullscreen-state"; +export const WINDOW_BACKDROP_STATE_CHANNEL = "desktop:window-backdrop-state"; export const WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:window-fullscreen-state"; export const UPDATE_STATE_CHANNEL = "desktop:update-state"; export const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state"; diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index d1313ff2e767..9be9394a10eb 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -151,6 +151,17 @@ contextBridge.exposeInMainWorld("desktopBridge", { ipcRenderer.removeListener(IpcChannels.WINDOW_FULLSCREEN_STATE_CHANNEL, wrappedListener); }; }, + onWindowBackdropStateChange: (listener) => { + const wrappedListener = (_event: Electron.IpcRendererEvent, enabled: unknown) => { + if (typeof enabled !== "boolean") return; + listener(enabled); + }; + + ipcRenderer.on(IpcChannels.WINDOW_BACKDROP_STATE_CHANNEL, wrappedListener); + return () => { + ipcRenderer.removeListener(IpcChannels.WINDOW_BACKDROP_STATE_CHANNEL, wrappedListener); + }; + }, getUpdateState: () => ipcRenderer.invoke(IpcChannels.UPDATE_GET_STATE_CHANNEL), setUpdateChannel: (channel) => ipcRenderer.invoke(IpcChannels.UPDATE_SET_CHANNEL_CHANNEL, channel), diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index a11493dd9c19..db7c9e107751 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -127,6 +127,8 @@ function makeFakeBrowserWindow() { send: webContents.send, setZoomLevel: webContents.setZoomLevel, setBackgroundThrottling: webContents.setBackgroundThrottling, + setBackgroundColor: window.setBackgroundColor, + setBackgroundMaterial: window.setBackgroundMaterial, setAutoHideCursor: window.setAutoHideCursor, webContentsListeners, windowListeners, @@ -177,17 +179,20 @@ const electronThemeLayer = Layer.succeed(ElectronTheme.ElectronTheme, { onUpdated: () => Effect.void, } satisfies ElectronTheme.ElectronTheme["Service"]); -const desktopEnvironmentLayer = DesktopEnvironment.layer(environmentInput).pipe( - Layer.provide( - Layer.mergeAll( - NodeServices.layer, - DesktopConfig.layerTest({ - T3CODE_PORT: "3773", - VITE_DEV_SERVER_URL: "http://127.0.0.1:5733", - }), +const makeDesktopEnvironmentLayer = (platform: NodeJS.Platform = environmentInput.platform) => + DesktopEnvironment.layer({ ...environmentInput, platform }).pipe( + Layer.provide( + Layer.mergeAll( + NodeServices.layer, + DesktopConfig.layerTest({ + T3CODE_PORT: "3773", + VITE_DEV_SERVER_URL: "http://127.0.0.1:5733", + }), + ), ), - ), -); + ); + +const desktopEnvironmentLayer = makeDesktopEnvironmentLayer(); const desktopWindowBoundsEquivalence = Schema.toEquivalence( DesktopAppSettings.DesktopWindowBoundsSchema, @@ -206,6 +211,7 @@ function makeTestLayer(input: { ) => Effect.Effect; readonly openedExternalUrls?: unknown[]; readonly previewZoomReapplies?: number[]; + readonly platform?: NodeJS.Platform; }) { let desktopSettings = input.desktopSettings ?? DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS; const desktopAppSettingsLayer = Layer.succeed(DesktopAppSettings.DesktopAppSettings, { @@ -264,7 +270,7 @@ function makeTestLayer(input: { Layer.provide( Layer.mergeAll( desktopAssetsLayer, - desktopEnvironmentLayer, + makeDesktopEnvironmentLayer(input.platform), desktopAppSettingsLayer, desktopClientSettingsLayer, desktopServerExposureLayer, @@ -417,6 +423,28 @@ describe("DesktopWindow", () => { }); }); + it.effect("does not apply the Windows backdrop to unowned auxiliary windows", () => + Effect.gen(function* () { + const fakeWindow = makeFakeBrowserWindow(); + const createCount = yield* Ref.make(0); + const mainWindow = yield* Ref.make>(Option.none()); + const layer = makeTestLayer({ + window: fakeWindow.window, + createCount, + mainWindow, + platform: "win32", + }); + + yield* Effect.gen(function* () { + const desktopWindow = yield* DesktopWindow.DesktopWindow; + yield* desktopWindow.syncAppearance; + }).pipe(Effect.provide(layer)); + + assert.equal(fakeWindow.setBackgroundMaterial.mock.calls.length, 0); + assert.equal(fakeWindow.setBackgroundColor.mock.calls.length, 0); + }), + ); + it("restores bounds only when the window fits within a connected display", () => { const persistedBounds = { x: 2040, y: 80, width: 1320, height: 880 }; const displays = [ diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index 48170e159d28..e3bda31d51b9 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -21,6 +21,7 @@ import * as ElectronWindow from "../electron/ElectronWindow.ts"; import { MENU_ACTION_CHANNEL, QUIT_SHORTCUT_CHANNEL, + WINDOW_BACKDROP_STATE_CHANNEL, WINDOW_FULLSCREEN_STATE_CHANNEL, } from "../ipc/channels.ts"; import * as PreviewManager from "../preview/Manager.ts"; @@ -133,6 +134,32 @@ function getInitialWindowBackgroundColor(shouldUseDarkColors: boolean): string { const windowsWithAcrylicBackdrop = new WeakSet(); const windowsWithoutAcrylicBackdrop = new WeakSet(); +const windowsManagedForBackdrop = new WeakSet(); +const windowsWithBackdropStateListener = new WeakSet(); + +function sendWindowBackdropState(window: Electron.BrowserWindow): void { + if (window.isDestroyed()) return; + + try { + window.webContents.send(WINDOW_BACKDROP_STATE_CHANNEL, windowsWithAcrylicBackdrop.has(window)); + } catch { + // The renderer may not be ready yet. The did-finish-load listener retries it. + } +} + +function registerWindowBackdropStateSync(window: Electron.BrowserWindow): void { + if (!windowsWithBackdropStateListener.has(window)) { + try { + window.webContents.on("did-finish-load", () => sendWindowBackdropState(window)); + windowsWithBackdropStateListener.add(window); + } catch { + // Native appearance must remain best effort if a test double or old shell + // does not expose the renderer event surface. + } + } + + sendWindowBackdropState(window); +} export function getWindowBackdropOptions( platform: NodeJS.Platform, @@ -173,6 +200,8 @@ function applyWindowsBackdrop( return Effect.void; } + windowsManagedForBackdrop.add(window); + return Effect.try({ try: () => { window.setBackgroundMaterial(desktopBackdropEnabled ? WINDOWS_ACRYLIC_MATERIAL : "none"); @@ -188,6 +217,7 @@ function applyWindowsBackdrop( }, catch: (cause) => cause, }).pipe( + Effect.andThen(Effect.sync(() => registerWindowBackdropStateSync(window))), Effect.catchCause((cause) => Effect.gen(function* () { windowsWithAcrylicBackdrop.delete(window); @@ -197,6 +227,7 @@ function applyWindowsBackdrop( } catch { // Preserve the original backdrop failure; window creation must stay best effort. } + registerWindowBackdropStateSync(window); yield* logWindowWarning("Windows backdrop material unavailable; using solid background", { cause, }); @@ -320,12 +351,19 @@ function syncWindowAppearance( } if (platform === "win32") { - if (desktopBackdropEnabled && windowsWithAcrylicBackdrop.has(window)) { - window.setBackgroundColor(WINDOWS_TRANSPARENT_BACKGROUND_COLOR); - } else if (!desktopBackdropEnabled && windowsWithoutAcrylicBackdrop.has(window)) { - window.setBackgroundColor(getInitialWindowBackgroundColor(shouldUseDarkColors)); - } else { - yield* applyWindowsBackdrop(window, platform, shouldUseDarkColors, desktopBackdropEnabled); + if (windowsManagedForBackdrop.has(window)) { + if (desktopBackdropEnabled && windowsWithAcrylicBackdrop.has(window)) { + window.setBackgroundColor(WINDOWS_TRANSPARENT_BACKGROUND_COLOR); + } else if (!desktopBackdropEnabled && windowsWithoutAcrylicBackdrop.has(window)) { + window.setBackgroundColor(getInitialWindowBackgroundColor(shouldUseDarkColors)); + } else { + yield* applyWindowsBackdrop( + window, + platform, + shouldUseDarkColors, + desktopBackdropEnabled, + ); + } } } else { window.setBackgroundColor(getInitialWindowBackgroundColor(shouldUseDarkColors)); diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 42e618500481..6c123f7cdb44 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -6849,7 +6849,7 @@ function ChatViewContent(props: ChatViewProps) { composerBannerItems.length > 0 || Boolean(threadSyncPhase && !activeEnvironmentUnavailable); return ( -
+
{rightPanelOpen && !shouldUseRightPanelSheet ? panelLayoutControls : null}
{!rightPanelOpen ? panelLayoutControls : null} ) : ( <> -
+
{isSelectedPatchTruncated && (

This diff was truncated because it exceeded the preview limit. The changes shown are diff --git a/apps/web/src/components/DiffPanelShell.tsx b/apps/web/src/components/DiffPanelShell.tsx index a9b7cf542e02..c12ab61c1697 100644 --- a/apps/web/src/components/DiffPanelShell.tsx +++ b/apps/web/src/components/DiffPanelShell.tsx @@ -28,7 +28,7 @@ export function DiffPanelShell(props: { return (

-
+
{isElectron ? ( No active thread diff --git a/apps/web/src/components/SplashScreen.tsx b/apps/web/src/components/SplashScreen.tsx index a0b593a95078..5d09f75875e2 100644 --- a/apps/web/src/components/SplashScreen.tsx +++ b/apps/web/src/components/SplashScreen.tsx @@ -1,6 +1,6 @@ export function SplashScreen() { return ( -
+
T3 Code
diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index abd9bf9edfd5..b85a321a968a 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -1392,7 +1392,7 @@ export default function ThreadTerminalDrawer({