diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index 1b7ffde46..c6fca36d5 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -159,7 +159,7 @@ const effectMinDuration =
(e: Effect.Effect) =>
Effect.all([e, Effect.sleep(duration)], { concurrency: "unbounded" }).pipe(Effect.map((v) => v[0]))
-function ConnectionGate(props: ParentProps) {
+function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
const server = useServer()
const checkServerHealth = useCheckServerHealth()
@@ -168,21 +168,23 @@ function ConnectionGate(props: ParentProps) {
// performs repeated health check with a grace period for
// non-http connections, otherwise fails instantly
const [startupHealthCheck, healthCheckActions] = createResource(() =>
- Effect.gen(function* () {
- if (!server.current) return true
- const { http, type } = server.current
+ props.disableHealthCheck
+ ? true
+ : Effect.gen(function* () {
+ if (!server.current) return true
+ const { http, type } = server.current
- while (true) {
- const res = yield* Effect.promise(() => checkServerHealth(http))
- if (res.healthy) return true
- if (checkMode() === "background" || type === "http") return false
- }
- }).pipe(
- effectMinDuration(checkMode() === "blocking" ? "1.2 seconds" : 0),
- Effect.timeoutOrElse({ duration: "10 seconds", onTimeout: () => Effect.succeed(false) }),
- Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
- Effect.runPromise,
- ),
+ while (true) {
+ const res = yield* Effect.promise(() => checkServerHealth(http))
+ if (res.healthy) return true
+ if (checkMode() === "background" || type === "http") return false
+ }
+ }).pipe(
+ effectMinDuration(checkMode() === "blocking" ? "1.2 seconds" : 0),
+ Effect.timeoutOrElse({ duration: "10 seconds", onTimeout: () => Effect.succeed(false) }),
+ Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
+ Effect.runPromise,
+ ),
)
return (
@@ -261,10 +263,11 @@ export function AppInterface(props: {
defaultServer: ServerConnection.Key
servers?: Array
router?: Component
+ disableHealthCheck?: boolean
}) {
return (
-
+
(
-
+
),
diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts
index 64c2eb10f..484e4feb2 100644
--- a/packages/desktop-electron/src/main/index.ts
+++ b/packages/desktop-electron/src/main/index.ts
@@ -5,7 +5,7 @@ import { createServer } from "node:net"
import { homedir } from "node:os"
import { join } from "node:path"
import type { Event } from "electron"
-import { app, type BrowserWindow, dialog } from "electron"
+import { app, BrowserWindow, dialog } from "electron"
import pkg from "electron-updater"
const APP_NAMES: Record = {
@@ -32,7 +32,7 @@ import { initLogging } from "./logging"
import { parseMarkdown } from "./markdown"
import { createMenu } from "./menu"
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
-import { createLoadingWindow, createMainWindow, setDockIcon } from "./windows"
+import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows"
const initEmitter = new EventEmitter()
let initStep: InitStep = { phase: "server_waiting" }
@@ -156,12 +156,9 @@ async function initialize() {
const globals = {
updaterEnabled: UPDATER_ENABLED,
- wsl: getWslConfig().enabled,
deepLinks: pendingDeepLinks,
}
- wireMenu()
-
if (needsMigration) {
const show = await Promise.race([loadingTask.then(() => false), delay(1_000).then(() => true)])
if (show) {
@@ -178,6 +175,7 @@ async function initialize() {
}
mainWindow = createMainWindow(globals)
+ wireMenu()
overlay?.close()
}
@@ -231,6 +229,7 @@ registerIpcHandlers({
runUpdater: async (alertOnFail) => checkForUpdates(alertOnFail),
checkUpdate: async () => checkUpdate(),
installUpdate: async () => installUpdate(),
+ setBackgroundColor: (color) => setBackgroundColor(color),
})
function killSidecar() {
diff --git a/packages/desktop-electron/src/main/ipc.ts b/packages/desktop-electron/src/main/ipc.ts
index c0d8773c2..71b3c3395 100644
--- a/packages/desktop-electron/src/main/ipc.ts
+++ b/packages/desktop-electron/src/main/ipc.ts
@@ -24,6 +24,7 @@ type Deps = {
runUpdater: (alertOnFail: boolean) => Promise | void
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
installUpdate: () => Promise | void
+ setBackgroundColor: (color: string) => void
}
export function registerIpcHandlers(deps: Deps) {
@@ -53,6 +54,7 @@ export function registerIpcHandlers(deps: Deps) {
ipcMain.handle("run-updater", (_event: IpcMainInvokeEvent, alertOnFail: boolean) => deps.runUpdater(alertOnFail))
ipcMain.handle("check-update", () => deps.checkUpdate())
ipcMain.handle("install-update", () => deps.installUpdate())
+ ipcMain.handle("set-background-color", (_event: IpcMainInvokeEvent, color: string) => deps.setBackgroundColor(color))
ipcMain.handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => {
const store = getStore(name)
const value = store.get(key)
@@ -140,6 +142,8 @@ export function registerIpcHandlers(deps: Deps) {
new Notification({ title, body }).show()
})
+ ipcMain.handle("get-window-count", () => BrowserWindow.getAllWindows().length)
+
ipcMain.handle("get-window-focused", (event: IpcMainInvokeEvent) => {
const win = BrowserWindow.fromWebContents(event.sender)
return win?.isFocused() ?? false
diff --git a/packages/desktop-electron/src/main/menu.ts b/packages/desktop-electron/src/main/menu.ts
index 53707ba7f..d8997be31 100644
--- a/packages/desktop-electron/src/main/menu.ts
+++ b/packages/desktop-electron/src/main/menu.ts
@@ -1,6 +1,7 @@
import { BrowserWindow, Menu, shell } from "electron"
import { UPDATER_ENABLED } from "./constants"
+import { createMainWindow } from "./windows"
type Deps = {
trigger: (id: string) => void
@@ -48,6 +49,11 @@ export function createMenu(deps: Deps) {
submenu: [
{ label: "New Session", accelerator: "Shift+Cmd+S", click: () => deps.trigger("session.new") },
{ label: "Open Project...", accelerator: "Cmd+O", click: () => deps.trigger("project.open") },
+ {
+ label: "New Window",
+ accelerator: "Cmd+Shift+N",
+ click: () => createMainWindow({ updaterEnabled: UPDATER_ENABLED }),
+ },
{ type: "separator" },
{ role: "close" },
],
diff --git a/packages/desktop-electron/src/main/windows.ts b/packages/desktop-electron/src/main/windows.ts
index d4ec5ac79..0b7783f28 100644
--- a/packages/desktop-electron/src/main/windows.ts
+++ b/packages/desktop-electron/src/main/windows.ts
@@ -6,12 +6,21 @@ import type { TitlebarTheme } from "../preload/types"
type Globals = {
updaterEnabled: boolean
- wsl: boolean
deepLinks?: string[]
}
const root = dirname(fileURLToPath(import.meta.url))
+let backgroundColor: string | undefined
+
+export function setBackgroundColor(color: string) {
+ backgroundColor = color
+}
+
+export function getBackgroundColor(): string | undefined {
+ return backgroundColor
+}
+
function iconsDir() {
return app.isPackaged ? join(process.resourcesPath, "icons") : join(root, "../../resources/icons")
}
@@ -59,6 +68,7 @@ export function createMainWindow(globals: Globals) {
show: true,
title: "OpenCode",
icon: iconPath(),
+ backgroundColor,
...(process.platform === "darwin"
? {
titleBarStyle: "hidden" as const,
@@ -95,6 +105,7 @@ export function createLoadingWindow(globals: Globals) {
center: true,
show: true,
icon: iconPath(),
+ backgroundColor,
...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const } : {}),
...(process.platform === "win32"
? {
@@ -131,7 +142,6 @@ function injectGlobals(win: BrowserWindow, globals: Globals) {
const deepLinks = globals.deepLinks ?? []
const data = {
updaterEnabled: globals.updaterEnabled,
- wsl: globals.wsl,
deepLinks: Array.isArray(deepLinks) ? deepLinks.splice(0) : deepLinks,
}
void win.webContents.executeJavaScript(
diff --git a/packages/desktop-electron/src/preload/index.ts b/packages/desktop-electron/src/preload/index.ts
index c1ed9afd2..296fcb2f1 100644
--- a/packages/desktop-electron/src/preload/index.ts
+++ b/packages/desktop-electron/src/preload/index.ts
@@ -28,6 +28,7 @@ const api: ElectronAPI = {
storeKeys: (name) => ipcRenderer.invoke("store-keys", name),
storeLength: (name) => ipcRenderer.invoke("store-length", name),
+ getWindowCount: () => ipcRenderer.invoke("get-window-count"),
onSqliteMigrationProgress: (cb) => {
const handler = (_: unknown, progress: SqliteMigrationProgress) => cb(progress)
ipcRenderer.on("sqlite-migration-progress", handler)
@@ -62,6 +63,7 @@ const api: ElectronAPI = {
runUpdater: (alertOnFail) => ipcRenderer.invoke("run-updater", alertOnFail),
checkUpdate: () => ipcRenderer.invoke("check-update"),
installUpdate: () => ipcRenderer.invoke("install-update"),
+ setBackgroundColor: (color: string) => ipcRenderer.invoke("set-background-color", color),
}
contextBridge.exposeInMainWorld("api", api)
diff --git a/packages/desktop-electron/src/preload/types.ts b/packages/desktop-electron/src/preload/types.ts
index ae4ca213d..100508fcd 100644
--- a/packages/desktop-electron/src/preload/types.ts
+++ b/packages/desktop-electron/src/preload/types.ts
@@ -36,6 +36,7 @@ export type ElectronAPI = {
storeKeys: (name: string) => Promise
storeLength: (name: string) => Promise
+ getWindowCount: () => Promise
onSqliteMigrationProgress: (cb: (progress: SqliteMigrationProgress) => void) => () => void
onMenuCommand: (cb: (id: string) => void) => () => void
onDeepLink: (cb: (urls: string[]) => void) => () => void
@@ -66,4 +67,5 @@ export type ElectronAPI = {
runUpdater: (alertOnFail: boolean) => Promise
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
installUpdate: () => Promise
+ setBackgroundColor: (color: string) => Promise
}
diff --git a/packages/desktop-electron/src/renderer/index.tsx b/packages/desktop-electron/src/renderer/index.tsx
index e313d5594..6719c04be 100644
--- a/packages/desktop-electron/src/renderer/index.tsx
+++ b/packages/desktop-electron/src/renderer/index.tsx
@@ -10,14 +10,15 @@ import {
useCommand,
} from "@opencode-ai/app"
import type { AsyncStorage } from "@solid-primitives/storage"
-import { createResource, onCleanup, onMount, Show } from "solid-js"
-import { render } from "solid-js/web"
import { MemoryRouter } from "@solidjs/router"
+import { createEffect, createResource, onCleanup, onMount, Show } from "solid-js"
+import { render } from "solid-js/web"
import pkg from "../../package.json"
import { initI18n, t } from "./i18n"
import { UPDATER_ENABLED } from "./updater"
import { webviewZoom } from "./webview-zoom"
import "./styles.css"
+import { useTheme } from "@opencode-ai/ui/theme"
const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
@@ -226,7 +227,9 @@ const createPlatform = (): Platform => {
const image = await window.api.readClipboardImage().catch(() => null)
if (!image) return null
const blob = new Blob([image.buffer], { type: "image/png" })
- return new File([blob], `pasted-image-${Date.now()}.png`, { type: "image/png" })
+ return new File([blob], `pasted-image-${Date.now()}.png`, {
+ type: "image/png",
+ })
},
}
}
@@ -240,6 +243,8 @@ listenForDeepLinks()
render(() => {
const platform = createPlatform()
+ const [windowCount] = createResource(() => window.api.getWindowCount())
+
// Fetch sidecar credentials (available immediately, before health check)
const [sidecar] = createResource(() => window.api.awaitInitialization(() => undefined))
@@ -276,6 +281,18 @@ render(() => {
function Inner() {
const cmd = useCommand()
menuTrigger = (id) => cmd.trigger(id)
+
+ const theme = useTheme()
+
+ createEffect(() => {
+ theme.themeId()
+ theme.mode()
+ const bg = getComputedStyle(document.documentElement).getPropertyValue("--background-base").trim()
+ if (bg) {
+ void window.api.setBackgroundColor(bg)
+ }
+ })
+
return null
}
@@ -289,13 +306,14 @@ render(() => {
return (
-
+
{(_) => {
return (
1}
>
diff --git a/packages/desktop/src/menu.ts b/packages/desktop/src/menu.ts
index de6a1d6a7..9005dd702 100644
--- a/packages/desktop/src/menu.ts
+++ b/packages/desktop/src/menu.ts
@@ -1,12 +1,11 @@
import { Menu, MenuItem, PredefinedMenuItem, Submenu } from "@tauri-apps/api/menu"
+import { openUrl } from "@tauri-apps/plugin-opener"
import { type as ostype } from "@tauri-apps/plugin-os"
import { relaunch } from "@tauri-apps/plugin-process"
-import { openUrl } from "@tauri-apps/plugin-opener"
-
-import { runUpdater, UPDATER_ENABLED } from "./updater"
+import { commands } from "./bindings"
import { installCli } from "./cli"
import { initI18n, t } from "./i18n"
-import { commands } from "./bindings"
+import { runUpdater, UPDATER_ENABLED } from "./updater"
export async function createMenu(trigger: (id: string) => void) {
if (ostype() !== "macos") return
diff --git a/packages/ui/src/theme/context.tsx b/packages/ui/src/theme/context.tsx
index ad82a088d..9808c8e84 100644
--- a/packages/ui/src/theme/context.tsx
+++ b/packages/ui/src/theme/context.tsx
@@ -1,9 +1,9 @@
-import { onMount, onCleanup, createEffect } from "solid-js"
+import { createEffect, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
-import type { DesktopTheme } from "./types"
-import { resolveThemeVariant, themeToCss } from "./resolve"
-import { DEFAULT_THEMES } from "./default-themes"
import { createSimpleContext } from "../context/helper"
+import { DEFAULT_THEMES } from "./default-themes"
+import { resolveThemeVariant, themeToCss } from "./resolve"
+import type { DesktopTheme } from "./types"
export type ColorScheme = "light" | "dark" | "system"
@@ -87,6 +87,14 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
previewScheme: null as ColorScheme | null,
})
+ window.addEventListener("storage", (e) => {
+ if (e.key === STORAGE_KEYS.THEME_ID && e.newValue) setStore("themeId", e.newValue)
+ if (e.key === STORAGE_KEYS.COLOR_SCHEME && e.newValue) {
+ setStore("colorScheme", e.newValue as ColorScheme)
+ setStore("mode", e.newValue === "system" ? getSystemMode() : (e.newValue as any))
+ }
+ })
+
onMount(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
const handler = () => {