From d481f64bdeaca91226e66c0e7888c7a10ba631f7 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:38:56 +1000 Subject: [PATCH] fix(electron): theme Windows titlebar overlay (#16843) Co-authored-by: Brendan Allan --- packages/app/src/app.tsx | 9 ++++- packages/app/src/components/titlebar.tsx | 4 +-- packages/desktop-electron/src/main/ipc.ts | 8 ++++- packages/desktop-electron/src/main/windows.ts | 35 +++++++++++++------ .../desktop-electron/src/preload/index.ts | 1 + .../desktop-electron/src/preload/types.ts | 4 +++ packages/ui/src/theme/context.tsx | 15 +++++--- 7 files changed, 56 insertions(+), 20 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 52a1dac6a..2790e7d3c 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -62,6 +62,9 @@ declare global { deepLinks?: string[] wsl?: boolean } + api?: { + setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise + } } } @@ -115,7 +118,11 @@ export function AppBaseProviders(props: ParentProps) { return ( - + { + void window.api?.setTitlebar?.({ mode }) + }} + > }> diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index b45f81150..3e2374f43 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -1,4 +1,4 @@ -import { createEffect, createMemo, Show, untrack } from "solid-js" +import { createEffect, createMemo, onCleanup, Show, untrack } from "solid-js" import { createStore } from "solid-js/store" import { useLocation, useNavigate, useParams } from "@solidjs/router" import { IconButton } from "@opencode-ai/ui/icon-button" @@ -282,7 +282,7 @@ export function Titlebar() { >
-
+ {!tauriApi() &&
}
diff --git a/packages/desktop-electron/src/main/ipc.ts b/packages/desktop-electron/src/main/ipc.ts index bbb5379bb..c0d8773c2 100644 --- a/packages/desktop-electron/src/main/ipc.ts +++ b/packages/desktop-electron/src/main/ipc.ts @@ -2,8 +2,9 @@ import { execFile } from "node:child_process" import { BrowserWindow, Notification, app, clipboard, dialog, ipcMain, shell } from "electron" import type { IpcMainEvent, IpcMainInvokeEvent } from "electron" -import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types" +import type { InitStep, ServerReadyData, SqliteMigrationProgress, TitlebarTheme, WslConfig } from "../preload/types" import { getStore } from "./store" +import { setTitlebar } from "./windows" type Deps = { killSidecar: () => void @@ -161,6 +162,11 @@ export function registerIpcHandlers(deps: Deps) { ipcMain.handle("get-zoom-factor", (event: IpcMainInvokeEvent) => event.sender.getZoomFactor()) ipcMain.handle("set-zoom-factor", (event: IpcMainInvokeEvent, factor: number) => event.sender.setZoomFactor(factor)) + ipcMain.handle("set-titlebar", (event: IpcMainInvokeEvent, theme: TitlebarTheme) => { + const win = BrowserWindow.fromWebContents(event.sender) + if (!win) return + setTitlebar(win, theme) + }) } export function sendSqliteMigrationProgress(win: BrowserWindow, progress: SqliteMigrationProgress) { diff --git a/packages/desktop-electron/src/main/windows.ts b/packages/desktop-electron/src/main/windows.ts index 9178457f8..d4ec5ac79 100644 --- a/packages/desktop-electron/src/main/windows.ts +++ b/packages/desktop-electron/src/main/windows.ts @@ -1,7 +1,8 @@ import windowState from "electron-window-state" -import { app, BrowserWindow, nativeImage } from "electron" +import { app, BrowserWindow, nativeImage, nativeTheme } from "electron" import { dirname, join } from "node:path" import { fileURLToPath } from "node:url" +import type { TitlebarTheme } from "../preload/types" type Globals = { updaterEnabled: boolean @@ -20,6 +21,24 @@ function iconPath() { return join(iconsDir(), `icon.${ext}`) } +function tone() { + return nativeTheme.shouldUseDarkColors ? "dark" : "light" +} + +function overlay(theme: Partial = {}) { + const mode = theme.mode ?? tone() + return { + color: "#00000000", + symbolColor: mode === "dark" ? "white" : "black", + height: 40, + } +} + +export function setTitlebar(win: BrowserWindow, theme: Partial = {}) { + if (process.platform !== "win32") return + win.setTitleBarOverlay(overlay(theme)) +} + export function setDockIcon() { if (process.platform !== "darwin") return app.dock?.setIcon(nativeImage.createFromPath(join(iconsDir(), "128x128@2x.png"))) @@ -31,6 +50,7 @@ export function createMainWindow(globals: Globals) { defaultHeight: 800, }) + const mode = tone() const win = new BrowserWindow({ x: state.x, y: state.y, @@ -49,11 +69,7 @@ export function createMainWindow(globals: Globals) { ? { frame: false, titleBarStyle: "hidden" as const, - titleBarOverlay: { - color: "transparent", - symbolColor: "#999", - height: 40, - }, + titleBarOverlay: overlay({ mode }), } : {}), webPreferences: { @@ -71,6 +87,7 @@ export function createMainWindow(globals: Globals) { } export function createLoadingWindow(globals: Globals) { + const mode = tone() const win = new BrowserWindow({ width: 640, height: 480, @@ -83,11 +100,7 @@ export function createLoadingWindow(globals: Globals) { ? { frame: false, titleBarStyle: "hidden" as const, - titleBarOverlay: { - color: "transparent", - symbolColor: "#999", - height: 40, - }, + titleBarOverlay: overlay({ mode }), } : {}), webPreferences: { diff --git a/packages/desktop-electron/src/preload/index.ts b/packages/desktop-electron/src/preload/index.ts index a6520ab42..c1ed9afd2 100644 --- a/packages/desktop-electron/src/preload/index.ts +++ b/packages/desktop-electron/src/preload/index.ts @@ -57,6 +57,7 @@ const api: ElectronAPI = { relaunch: () => ipcRenderer.send("relaunch"), getZoomFactor: () => ipcRenderer.invoke("get-zoom-factor"), setZoomFactor: (factor) => ipcRenderer.invoke("set-zoom-factor", factor), + setTitlebar: (theme) => ipcRenderer.invoke("set-titlebar", theme), loadingWindowComplete: () => ipcRenderer.send("loading-window-complete"), runUpdater: (alertOnFail) => ipcRenderer.invoke("run-updater", alertOnFail), checkUpdate: () => ipcRenderer.invoke("check-update"), diff --git a/packages/desktop-electron/src/preload/types.ts b/packages/desktop-electron/src/preload/types.ts index af5410f5f..43bdf1e6c 100644 --- a/packages/desktop-electron/src/preload/types.ts +++ b/packages/desktop-electron/src/preload/types.ts @@ -10,6 +10,9 @@ export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { export type WslConfig = { enabled: boolean } export type LinuxDisplayBackend = "wayland" | "auto" +export type TitlebarTheme = { + mode: "light" | "dark" +} export type ElectronAPI = { killSidecar: () => Promise @@ -57,6 +60,7 @@ export type ElectronAPI = { relaunch: () => void getZoomFactor: () => Promise setZoomFactor: (factor: number) => Promise + setTitlebar: (theme: TitlebarTheme) => Promise loadingWindowComplete: () => void runUpdater: (alertOnFail: boolean) => Promise checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }> diff --git a/packages/ui/src/theme/context.tsx b/packages/ui/src/theme/context.tsx index cda967697..ad82a088d 100644 --- a/packages/ui/src/theme/context.tsx +++ b/packages/ui/src/theme/context.tsx @@ -77,7 +77,7 @@ function cacheThemeVariants(theme: DesktopTheme, themeId: string) { export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ name: "Theme", - init: (props: { defaultTheme?: string }) => { + init: (props: { defaultTheme?: string; onThemeApplied?: (theme: DesktopTheme, mode: "light" | "dark") => void }) => { const [store, setStore] = createStore({ themes: DEFAULT_THEMES as Record, themeId: normalize(props.defaultTheme) ?? "oc-2", @@ -119,10 +119,15 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ } }) + const applyTheme = (theme: DesktopTheme, themeId: string, mode: "light" | "dark") => { + applyThemeCss(theme, themeId, mode) + props.onThemeApplied?.(theme, mode) + } + createEffect(() => { const theme = store.themes[store.themeId] if (theme) { - applyThemeCss(theme, store.themeId, store.mode) + applyTheme(theme, store.themeId, store.mode) } }) @@ -171,7 +176,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ ? getSystemMode() : store.previewScheme : store.mode - applyThemeCss(theme, next, previewMode) + applyTheme(theme, next, previewMode) }, previewColorScheme: (scheme: ColorScheme) => { setStore("previewScheme", scheme) @@ -179,7 +184,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ const id = store.previewThemeId ?? store.themeId const theme = store.themes[id] if (theme) { - applyThemeCss(theme, id, previewMode) + applyTheme(theme, id, previewMode) } }, commitPreview: () => { @@ -197,7 +202,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ setStore("previewScheme", null) const theme = store.themes[store.themeId] if (theme) { - applyThemeCss(theme, store.themeId, store.mode) + applyTheme(theme, store.themeId, store.mode) } }, }