summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorLuke Parker <[email protected]>2026-03-12 16:38:56 +1000
committerGitHub <[email protected]>2026-03-12 16:38:56 +1000
commitd481f64bdeaca91226e66c0e7888c7a10ba631f7 (patch)
tree8700aaad0363342015ffc49cb962620186e1f7f0
parent54e7baa6cfff86627e5555842560b4a20e4be424 (diff)
downloadopencode-d481f64bdeaca91226e66c0e7888c7a10ba631f7.tar.gz
opencode-d481f64bdeaca91226e66c0e7888c7a10ba631f7.zip
fix(electron): theme Windows titlebar overlay (#16843)
Co-authored-by: Brendan Allan <[email protected]>
-rw-r--r--packages/app/src/app.tsx9
-rw-r--r--packages/app/src/components/titlebar.tsx4
-rw-r--r--packages/desktop-electron/src/main/ipc.ts8
-rw-r--r--packages/desktop-electron/src/main/windows.ts35
-rw-r--r--packages/desktop-electron/src/preload/index.ts1
-rw-r--r--packages/desktop-electron/src/preload/types.ts4
-rw-r--r--packages/ui/src/theme/context.tsx15
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<void>
+ }
}
}
@@ -115,7 +118,11 @@ export function AppBaseProviders(props: ParentProps) {
return (
<MetaProvider>
<Font />
- <ThemeProvider>
+ <ThemeProvider
+ onThemeApplied={(_, mode) => {
+ void window.api?.setTitlebar?.({ mode })
+ }}
+ >
<LanguageProvider>
<UiI18nBridge>
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
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() {
>
<div id="opencode-titlebar-right" class="flex items-center gap-1 shrink-0 justify-end" />
<Show when={windows()}>
- <div class="w-6 shrink-0" />
+ {!tauriApi() && <div class="w-36 shrink-0" />}
<div data-tauri-decorum-tb class="flex flex-row" />
</Show>
</div>
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<TitlebarTheme> = {}) {
+ const mode = theme.mode ?? tone()
+ return {
+ color: "#00000000",
+ symbolColor: mode === "dark" ? "white" : "black",
+ height: 40,
+ }
+}
+
+export function setTitlebar(win: BrowserWindow, theme: Partial<TitlebarTheme> = {}) {
+ if (process.platform !== "win32") return
+ win.setTitleBarOverlay(overlay(theme))
+}
+
export function setDockIcon() {
if (process.platform !== "darwin") return
app.dock?.setIcon(nativeImage.createFromPath(join(iconsDir(), "[email protected]")))
@@ -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<void>
@@ -57,6 +60,7 @@ export type ElectronAPI = {
relaunch: () => void
getZoomFactor: () => Promise<number>
setZoomFactor: (factor: number) => Promise<void>
+ setTitlebar: (theme: TitlebarTheme) => Promise<void>
loadingWindowComplete: () => void
runUpdater: (alertOnFail: boolean) => Promise<void>
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<string, DesktopTheme>,
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)
}
},
}