maxBytes)
- throw new Error(`Selected attachments exceed the ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MB limit`)
+ if (info.size > maxBytes) throw new Error(message)
const bytes = Buffer.allocUnsafe(info.size)
let offset = 0
while (offset < info.size) {
diff --git a/packages/desktop/src/main/background-image.test.ts b/packages/desktop/src/main/background-image.test.ts
new file mode 100644
index 000000000000..1f892df56f8d
--- /dev/null
+++ b/packages/desktop/src/main/background-image.test.ts
@@ -0,0 +1,56 @@
+import { afterEach, expect, test } from "bun:test"
+import { mkdtemp, rm, truncate, writeFile } from "node:fs/promises"
+import { join } from "node:path"
+import { tmpdir } from "node:os"
+import { clearBackgroundImage, findBackgroundImage, saveBackgroundImage } from "./background-image"
+
+const directories: string[] = []
+
+afterEach(async () => {
+ await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })))
+})
+
+test("persists and clears a selected image", async () => {
+ const directory = await mkdtemp(join(tmpdir(), "opencode-background-"))
+ directories.push(directory)
+ const source = join(directory, "source.png")
+ await writeFile(source, new Uint8Array([1, 2, 3]))
+
+ expect((await saveBackgroundImage(directory, source))?.mime).toBe("image/png")
+ expect((await findBackgroundImage(directory))?.path).toBe(join(directory, "background-image.png"))
+
+ await clearBackgroundImage(directory)
+ expect(await findBackgroundImage(directory)).toBeUndefined()
+})
+
+test("rejects unsupported image formats", async () => {
+ const directory = await mkdtemp(join(tmpdir(), "opencode-background-"))
+ directories.push(directory)
+ const source = join(directory, "source.svg")
+ await writeFile(source, "
")
+
+ expect(saveBackgroundImage(directory, source)).rejects.toThrow("Unsupported background image format")
+})
+
+test("replaces an existing image", async () => {
+ const directory = await mkdtemp(join(tmpdir(), "opencode-background-"))
+ directories.push(directory)
+ const first = join(directory, "first.png")
+ const second = join(directory, "second.webp")
+ await Promise.all([writeFile(first, new Uint8Array([1])), writeFile(second, new Uint8Array([2]))])
+
+ await saveBackgroundImage(directory, first)
+ await saveBackgroundImage(directory, second)
+
+ expect((await findBackgroundImage(directory))?.mime).toBe("image/webp")
+})
+
+test("rejects images larger than 20 MB", async () => {
+ const directory = await mkdtemp(join(tmpdir(), "opencode-background-"))
+ directories.push(directory)
+ const source = join(directory, "large.png")
+ await writeFile(source, "")
+ await truncate(source, 20 * 1024 * 1024 + 1)
+
+ expect(saveBackgroundImage(directory, source)).rejects.toThrow("Background images must be 20 MB or smaller")
+})
diff --git a/packages/desktop/src/main/background-image.ts b/packages/desktop/src/main/background-image.ts
new file mode 100644
index 000000000000..32076dc1519e
--- /dev/null
+++ b/packages/desktop/src/main/background-image.ts
@@ -0,0 +1,50 @@
+import { randomUUID } from "node:crypto"
+import { mkdir, rename, rm, stat, writeFile } from "node:fs/promises"
+import { extname, join } from "node:path"
+import { MAX_ATTACHMENT_BYTES, readBoundedFile } from "./attachment-picker"
+
+const name = "background-image"
+const mime = {
+ ".avif": "image/avif",
+ ".bmp": "image/bmp",
+ ".gif": "image/gif",
+ ".jpeg": "image/jpeg",
+ ".jpg": "image/jpeg",
+ ".png": "image/png",
+ ".webp": "image/webp",
+} as const
+
+export const backgroundImageExtensions = Object.keys(mime).map((extension) => extension.slice(1))
+
+export async function findBackgroundImage(directory: string) {
+ const candidates = await Promise.all(
+ Object.entries(mime).map(async ([extension, contentType]) => {
+ const path = join(directory, `${name}${extension}`)
+ const info = await stat(path).catch(() => undefined)
+ if (!info?.isFile() || info.size > MAX_ATTACHMENT_BYTES) return
+ return { path, mime: contentType, revision: `${info.mtimeMs}-${info.size}`, modified: info.mtimeMs }
+ }),
+ )
+ return candidates.filter((candidate) => candidate !== undefined).sort((a, b) => b.modified - a.modified)[0]
+}
+
+export async function saveBackgroundImage(directory: string, path: string) {
+ const extension = extname(path).toLowerCase() as keyof typeof mime
+ if (!mime[extension]) throw new Error("Unsupported background image format")
+ const data = await readBoundedFile(path, MAX_ATTACHMENT_BYTES, "Background images must be 20 MB or smaller")
+ await mkdir(directory, { recursive: true })
+ const target = join(directory, `${name}${extension}`)
+ const temporary = join(directory, `.${name}-${randomUUID()}`)
+ await writeFile(temporary, new Uint8Array(data))
+ await rename(temporary, target).finally(() => rm(temporary, { force: true }))
+ await Promise.all(
+ Object.keys(mime)
+ .filter((candidate) => candidate !== extension)
+ .map((candidate) => rm(join(directory, `${name}${candidate}`), { force: true })),
+ )
+ return findBackgroundImage(directory)
+}
+
+export async function clearBackgroundImage(directory: string) {
+ await Promise.all(Object.keys(mime).map((extension) => rm(join(directory, `${name}${extension}`), { force: true })))
+}
diff --git a/packages/desktop/src/main/ipc.ts b/packages/desktop/src/main/ipc.ts
index 1103401c492b..d9c6e30b29db 100644
--- a/packages/desktop/src/main/ipc.ts
+++ b/packages/desktop/src/main/ipc.ts
@@ -8,6 +8,12 @@ import type { DesktopMenuAction } from "@opencode-ai/app/desktop-menu"
import type { FatalRendererError, ServerReadyData, TitlebarTheme } from "../preload/types"
import { runDesktopMenuAction } from "./desktop-menu-actions"
import { assertAttachmentBudget, createPickedFileAuthorizations } from "./attachment-picker"
+import {
+ backgroundImageExtensions,
+ clearBackgroundImage,
+ findBackgroundImage,
+ saveBackgroundImage,
+} from "./background-image"
import { getStore, removeStoreFileIfEmpty } from "./store"
import { getPinchZoomEnabled, getWindowID, setPinchZoomEnabled, setTitlebar, updateTitlebar } from "./windows"
import type { UpdaterController } from "./updater-controller"
@@ -80,6 +86,29 @@ export function registerIpcHandlers(deps: Deps) {
ipcMain.handle("updater-check", () => deps.updater.check())
ipcMain.handle("updater-install", () => deps.updater.install())
ipcMain.handle("set-background-color", (_event: IpcMainInvokeEvent, color: string) => deps.setBackgroundColor(color))
+ const backgroundImageState = async () => {
+ const image = await findBackgroundImage(app.getPath("userData"))
+ return image ? { revision: image.revision } : null
+ }
+ const publishBackgroundImage = (state: { revision: string } | null) => {
+ BrowserWindow.getAllWindows().forEach((win) => win.webContents.send("background-image-changed", state))
+ return state
+ }
+ ipcMain.handle("load-background-image", backgroundImageState)
+ ipcMain.handle("select-background-image", async () => {
+ const result = await dialog.showOpenDialog({
+ properties: ["openFile"],
+ title: "Choose a background image",
+ filters: [{ name: "Images", extensions: backgroundImageExtensions }],
+ })
+ if (result.canceled || !result.filePaths[0]) return null
+ await saveBackgroundImage(app.getPath("userData"), result.filePaths[0])
+ return publishBackgroundImage(await backgroundImageState())
+ })
+ ipcMain.handle("clear-background-image", async () => {
+ await clearBackgroundImage(app.getPath("userData"))
+ publishBackgroundImage(null)
+ })
ipcMain.handle("export-debug-logs", () => deps.exportDebugLogs())
ipcMain.handle("record-fatal-renderer-error", (_event: IpcMainInvokeEvent, error: FatalRendererError) =>
deps.recordFatalRendererError(error),
diff --git a/packages/desktop/src/main/windows.ts b/packages/desktop/src/main/windows.ts
index 693690c9c455..61ce34f1954b 100644
--- a/packages/desktop/src/main/windows.ts
+++ b/packages/desktop/src/main/windows.ts
@@ -9,6 +9,7 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path"
import { fileURLToPath, pathToFileURL } from "node:url"
import type { TitlebarTheme } from "../preload/types"
import { exportDebugLogs, write as writeLog } from "./logging"
+import { findBackgroundImage } from "./background-image"
import { getStore, removeStoreFile } from "./store"
import { PINCH_ZOOM_ENABLED_KEY, WINDOW_IDS_KEY } from "./store-keys"
import { createUnresponsiveSampler } from "./unresponsive"
@@ -259,6 +260,12 @@ export function registerRendererProtocol() {
return new Response("Not found", { status: 404 })
}
+ if (url.pathname === "/background-image") {
+ const image = await findBackgroundImage(app.getPath("userData"))
+ if (!image) return new Response("Not found", { status: 404 })
+ return net.fetch(pathToFileURL(image.path).toString())
+ }
+
const file = resolve(rendererRoot, `.${decodeURIComponent(url.pathname)}`)
const rel = relative(rendererRoot, file)
if (rel.startsWith("..") || isAbsolute(rel)) {
diff --git a/packages/desktop/src/preload/index.ts b/packages/desktop/src/preload/index.ts
index d14dd0d00a22..e41ae550a89d 100644
--- a/packages/desktop/src/preload/index.ts
+++ b/packages/desktop/src/preload/index.ts
@@ -120,6 +120,14 @@ const api: ElectronAPI = {
setTitlebar: (theme) => ipcRenderer.invoke("set-titlebar", theme),
runDesktopMenuAction: (action) => ipcRenderer.invoke("run-desktop-menu-action", action),
setBackgroundColor: (color: string) => ipcRenderer.invoke("set-background-color", color),
+ loadBackgroundImage: () => ipcRenderer.invoke("load-background-image"),
+ selectBackgroundImage: () => ipcRenderer.invoke("select-background-image"),
+ clearBackgroundImage: () => ipcRenderer.invoke("clear-background-image"),
+ onBackgroundImageChanged: (cb) => {
+ const handler = (_: unknown, image: Parameters
[0]) => cb(image)
+ ipcRenderer.on("background-image-changed", handler)
+ return () => ipcRenderer.removeListener("background-image-changed", handler)
+ },
exportDebugLogs: () => ipcRenderer.invoke("export-debug-logs"),
recordFatalRendererError: (error) => ipcRenderer.invoke("record-fatal-renderer-error", error),
}
diff --git a/packages/desktop/src/preload/types.ts b/packages/desktop/src/preload/types.ts
index da10f7008def..350b8d62820d 100644
--- a/packages/desktop/src/preload/types.ts
+++ b/packages/desktop/src/preload/types.ts
@@ -41,6 +41,10 @@ export type FatalRendererError = {
os?: string
}
+export type BackgroundImage = {
+ revision: string
+}
+
export type ElectronAPI = {
killSidecar: () => Promise
installCli: () => Promise
@@ -103,6 +107,10 @@ export type ElectronAPI = {
setTitlebar: (theme: TitlebarTheme) => Promise
runDesktopMenuAction: (action: DesktopMenuAction) => Promise
setBackgroundColor: (color: string) => Promise
+ loadBackgroundImage: () => Promise
+ selectBackgroundImage: () => Promise
+ clearBackgroundImage: () => Promise
+ onBackgroundImageChanged: (cb: (image: BackgroundImage | null) => void) => () => void
exportDebugLogs: () => Promise
recordFatalRendererError: (error: FatalRendererError) => Promise
}
diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx
index 13c122cb2ecb..e178173d010c 100644
--- a/packages/desktop/src/renderer/index.tsx
+++ b/packages/desktop/src/renderer/index.tsx
@@ -112,6 +112,21 @@ function DesktopMemoryRouter(props: BaseRouterProps & { windowID: string }) {
const createPlatform = (windowState: DesktopWindowState): Platform => {
const attachmentPaths = new WeakMap()
+ const [backgroundImage, setBackgroundImage] = createSignal(false)
+ const applyBackgroundImage = (image: Awaited>) => {
+ setBackgroundImage(!!image)
+ document.documentElement.toggleAttribute("data-background-image", !!image)
+ if (image) {
+ document.documentElement.style.setProperty(
+ "--desktop-background-image",
+ `url("oc://renderer/background-image?revision=${encodeURIComponent(image.revision)}")`,
+ )
+ return true
+ }
+ document.documentElement.style.removeProperty("--desktop-background-image")
+ return false
+ }
+ window.api.onBackgroundImageChanged(applyBackgroundImage)
const os = (() => {
const ua = navigator.userAgent
if (ua.includes("Mac")) return "macos"
@@ -310,6 +325,23 @@ const createPlatform = (windowState: DesktopWindowState): Platform => {
type: "image/png",
})
},
+
+ async loadBackgroundImage() {
+ return applyBackgroundImage(await window.api.loadBackgroundImage())
+ },
+
+ async selectBackgroundImage() {
+ const image = await window.api.selectBackgroundImage()
+ if (!image) return backgroundImage()
+ return applyBackgroundImage(image)
+ },
+
+ async clearBackgroundImage() {
+ await window.api.clearBackgroundImage()
+ applyBackgroundImage(null)
+ },
+
+ backgroundImage,
}
}
@@ -329,6 +361,7 @@ function LoadingSplash() {
function DesktopRoot(props: { windowState: DesktopWindowState }) {
const platform = createPlatform(props.windowState)
+ void platform.loadBackgroundImage?.()
const loadLocale = async () => {
const current = await platform.storage?.("opencode.global.dat").getItem("language")
const legacy = current ? undefined : await platform.storage?.().getItem("language.v1")
diff --git a/packages/desktop/src/renderer/styles.css b/packages/desktop/src/renderer/styles.css
index e69de29bb2d1..0b485ed0f337 100644
--- a/packages/desktop/src/renderer/styles.css
+++ b/packages/desktop/src/renderer/styles.css
@@ -0,0 +1,18 @@
+html[data-background-image] body {
+ background-image: linear-gradient(rgb(0 0 0 / 18%), rgb(0 0 0 / 18%)), var(--desktop-background-image);
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: cover;
+}
+
+html[data-background-image] [data-background-surface="shell"] {
+ background-color: transparent !important;
+}
+
+html[data-background-image] [data-background-surface="content"] {
+ background-color: color-mix(in srgb, var(--background-base) 76%, transparent) !important;
+}
+
+html[data-background-image] [data-background-surface="panel"] {
+ background-color: color-mix(in srgb, var(--v2-background-bg-base) 70%, transparent) !important;
+}
From 364e2d9f03412710e79f79a1d5d06f7545e91eb6 Mon Sep 17 00:00:00 2001
From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Date: Mon, 20 Jul 2026 16:19:59 +0000
Subject: [PATCH 2/8] feat(app): support web image backgrounds
---
packages/app/src/context/platform.tsx | 8 ++--
packages/app/src/entry.tsx | 37 +++++++++++++++++++
packages/app/src/i18n/ar.ts | 2 +-
packages/app/src/i18n/br.ts | 2 +-
packages/app/src/i18n/bs.ts | 2 +-
packages/app/src/i18n/da.ts | 2 +-
packages/app/src/i18n/de.ts | 2 +-
packages/app/src/i18n/en.ts | 2 +-
packages/app/src/i18n/es.ts | 2 +-
packages/app/src/i18n/fr.ts | 2 +-
packages/app/src/i18n/ja.ts | 2 +-
packages/app/src/i18n/ko.ts | 2 +-
packages/app/src/i18n/no.ts | 2 +-
packages/app/src/i18n/pl.ts | 2 +-
packages/app/src/i18n/ru.ts | 2 +-
packages/app/src/i18n/th.ts | 2 +-
packages/app/src/i18n/tr.ts | 2 +-
packages/app/src/i18n/uk.ts | 2 +-
packages/app/src/i18n/zh.ts | 2 +-
packages/app/src/i18n/zht.ts | 2 +-
packages/app/src/index.css | 24 ++++++++++++
.../app/src/utils/web-background-image.ts | 33 +++++++++++++++++
packages/desktop/src/renderer/index.tsx | 4 +-
packages/desktop/src/renderer/styles.css | 18 ---------
24 files changed, 118 insertions(+), 42 deletions(-)
create mode 100644 packages/app/src/utils/web-background-image.ts
diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx
index f511e94ec60f..c9effb2fc127 100644
--- a/packages/app/src/context/platform.tsx
+++ b/packages/app/src/context/platform.tsx
@@ -112,16 +112,16 @@ type PlatformBase = {
/** Read image from clipboard (desktop only) */
readClipboardImage?(): Promise
- /** Load and apply the saved desktop background image. */
+ /** Load and apply the saved app background image. */
loadBackgroundImage?(): Promise
- /** Whether the desktop currently has a background image. */
+ /** Whether the app currently has a background image. */
backgroundImage?: Accessor
- /** Select and apply a desktop background image. */
+ /** Select and apply an app background image. */
selectBackgroundImage?(): Promise
- /** Clear the saved desktop background image. */
+ /** Clear the saved app background image. */
clearBackgroundImage?(): Promise
/** Export collected diagnostic logs (desktop only) */
diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx
index c41c1bb4b63e..96c092bf6b60 100644
--- a/packages/app/src/entry.tsx
+++ b/packages/app/src/entry.tsx
@@ -1,6 +1,7 @@
// @refresh reload
import * as Sentry from "@sentry/solid"
+import { createSignal } from "solid-js"
import { render } from "solid-js/web"
import { AppBaseProviders, AppInterface } from "@/app"
import { type Platform, PlatformProvider } from "@/context/platform"
@@ -8,6 +9,12 @@ import { dict as en } from "@/i18n/en"
import { dict as zh } from "@/i18n/zh"
import { handleNotificationClick } from "@/utils/notification-click"
import { authFromToken } from "@/utils/server"
+import {
+ clearWebBackgroundImage,
+ loadWebBackgroundImage,
+ saveWebBackgroundImage,
+ selectWebBackgroundImage,
+} from "@/utils/web-background-image"
import pkg from "../package.json"
import { ServerConnection } from "./context/server"
@@ -119,6 +126,21 @@ const clearAuthToken = () => {
history.replaceState(null, "", location.pathname + (params.size ? `?${params}` : "") + location.hash)
}
+const [backgroundImage, setBackgroundImage] = createSignal(false)
+let backgroundImageUrl: string | undefined
+const applyBackgroundImage = (image: Blob | null) => {
+ if (backgroundImageUrl) URL.revokeObjectURL(backgroundImageUrl)
+ backgroundImageUrl = image ? URL.createObjectURL(image) : undefined
+ setBackgroundImage(!!backgroundImageUrl)
+ document.documentElement.toggleAttribute("data-background-image", !!backgroundImageUrl)
+ if (backgroundImageUrl) {
+ document.documentElement.style.setProperty("--app-background-image", `url("${backgroundImageUrl}")`)
+ return true
+ }
+ document.documentElement.style.removeProperty("--app-background-image")
+ return false
+}
+
const platform: Platform = {
platform: "web",
version: pkg.version,
@@ -132,8 +154,23 @@ const platform: Platform = {
return stored ? ServerConnection.Key.make(stored) : null
},
setDefaultServer: writeDefaultServerUrl,
+ backgroundImage,
+ async loadBackgroundImage() {
+ return applyBackgroundImage(await loadWebBackgroundImage())
+ },
+ async selectBackgroundImage() {
+ const file = await selectWebBackgroundImage()
+ if (!file) return backgroundImage()
+ return applyBackgroundImage(await saveWebBackgroundImage(file))
+ },
+ async clearBackgroundImage() {
+ await clearWebBackgroundImage()
+ applyBackgroundImage(null)
+ },
}
+void platform.loadBackgroundImage?.()
+
if (import.meta.env.VITE_SENTRY_DSN) {
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts
index ca8d0b1fecc1..397fcbd7ffa0 100644
--- a/packages/app/src/i18n/ar.ts
+++ b/packages/app/src/i18n/ar.ts
@@ -705,7 +705,7 @@ export const dict = {
"settings.general.row.terminalFont.title": "خط الطرفية",
"settings.general.row.terminalFont.description": "خصّص الخط المستخدم في الطرفية",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "خط الواجهة",
diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts
index 49a95f5cc802..019221d36c73 100644
--- a/packages/app/src/i18n/br.ts
+++ b/packages/app/src/i18n/br.ts
@@ -714,7 +714,7 @@ export const dict = {
"settings.general.row.terminalFont.title": "Fonte do terminal",
"settings.general.row.terminalFont.description": "Personalize a fonte usada no terminal",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "Fonte da interface",
diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts
index 1961072084e3..463e58f0a8e4 100644
--- a/packages/app/src/i18n/bs.ts
+++ b/packages/app/src/i18n/bs.ts
@@ -779,7 +779,7 @@ export const dict = {
"settings.general.row.terminalFont.title": "Font terminala",
"settings.general.row.terminalFont.description": "Prilagodite font koji se koristi u terminalu",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "UI font",
diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts
index 7b068e3da683..2d9b339bba68 100644
--- a/packages/app/src/i18n/da.ts
+++ b/packages/app/src/i18n/da.ts
@@ -774,7 +774,7 @@ export const dict = {
"settings.general.row.terminalFont.title": "Terminalskrifttype",
"settings.general.row.terminalFont.description": "Tilpas den skrifttype, der bruges i terminalen",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "UI-skrifttype",
diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts
index 82d87e20db7f..cdf3f0467002 100644
--- a/packages/app/src/i18n/de.ts
+++ b/packages/app/src/i18n/de.ts
@@ -725,7 +725,7 @@ export const dict = {
"settings.general.row.terminalFont.title": "Terminalschriftart",
"settings.general.row.terminalFont.description": "Passe die im Terminal verwendete Schriftart an",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "UI-Schriftart",
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index 42680e12026c..f99d5b61afce 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -863,7 +863,7 @@ export const dict = {
"settings.general.row.theme.title": "Theme",
"settings.general.row.theme.description": "Customise how OpenCode is themed.",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.font.title": "Code Font",
diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts
index cccdb2e9f0e3..91c3b9bf9cd4 100644
--- a/packages/app/src/i18n/es.ts
+++ b/packages/app/src/i18n/es.ts
@@ -782,7 +782,7 @@ export const dict = {
"settings.general.row.terminalFont.title": "Fuente del terminal",
"settings.general.row.terminalFont.description": "Personaliza la fuente utilizada en el terminal",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "Fuente de la interfaz",
diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts
index 249e04ff274b..67b13eb091e6 100644
--- a/packages/app/src/i18n/fr.ts
+++ b/packages/app/src/i18n/fr.ts
@@ -721,7 +721,7 @@ export const dict = {
"settings.general.row.terminalFont.title": "Police du terminal",
"settings.general.row.terminalFont.description": "Personnalisez la police utilisée dans le terminal",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "Police de l'interface",
diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts
index 7dfd031f73c2..79d036cb9a2b 100644
--- a/packages/app/src/i18n/ja.ts
+++ b/packages/app/src/i18n/ja.ts
@@ -710,7 +710,7 @@ export const dict = {
"settings.general.row.terminalFont.title": "ターミナルのフォント",
"settings.general.row.terminalFont.description": "ターミナルで使用するフォントをカスタマイズ",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "UIフォント",
diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts
index e6ecae4c9f94..610ad04ca384 100644
--- a/packages/app/src/i18n/ko.ts
+++ b/packages/app/src/i18n/ko.ts
@@ -581,7 +581,7 @@ export const dict = {
"settings.general.row.terminalFont.title": "터미널 글꼴",
"settings.general.row.terminalFont.description": "터미널에서 사용할 글꼴을 설정합니다",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "UI 글꼴",
diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts
index 8ab2b32424c7..3d1484fd7a0d 100644
--- a/packages/app/src/i18n/no.ts
+++ b/packages/app/src/i18n/no.ts
@@ -655,7 +655,7 @@ export const dict = {
"settings.general.row.terminalFont.title": "Terminalskrift",
"settings.general.row.terminalFont.description": "Tilpass skrifttypen som brukes i terminalen",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "UI-skrift",
diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts
index 8f01e698b96d..1a4a35de1c98 100644
--- a/packages/app/src/i18n/pl.ts
+++ b/packages/app/src/i18n/pl.ts
@@ -715,7 +715,7 @@ export const dict = {
"settings.general.row.terminalFont.title": "Czcionka terminala",
"settings.general.row.terminalFont.description": "Dostosuj czcionkę używaną w terminalu",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "Czcionka interfejsu",
diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts
index 3e2518b5be34..16465b75a6fa 100644
--- a/packages/app/src/i18n/ru.ts
+++ b/packages/app/src/i18n/ru.ts
@@ -779,7 +779,7 @@ export const dict = {
"settings.general.row.terminalFont.title": "Шрифт терминала",
"settings.general.row.terminalFont.description": "Настройте шрифт, используемый в терминале",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "Шрифт интерфейса",
diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts
index 352b2e01a3f2..865579ef74ab 100644
--- a/packages/app/src/i18n/th.ts
+++ b/packages/app/src/i18n/th.ts
@@ -772,7 +772,7 @@ export const dict = {
"settings.general.row.terminalFont.title": "ฟอนต์เทอร์มินัล",
"settings.general.row.terminalFont.description": "ปรับแต่งฟอนต์ที่ใช้ในเทอร์มินัล",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "ฟอนต์ UI",
diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts
index fb13377fd468..c733af6cde1f 100644
--- a/packages/app/src/i18n/tr.ts
+++ b/packages/app/src/i18n/tr.ts
@@ -785,7 +785,7 @@ export const dict = {
"settings.general.row.terminalFont.title": "Terminal yazı tipi",
"settings.general.row.terminalFont.description": "Terminalde kullanılan yazı tipini özelleştirin",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "Arayüz Yazı Tipi",
diff --git a/packages/app/src/i18n/uk.ts b/packages/app/src/i18n/uk.ts
index 4c35ca9c39fa..1b558dd8d24a 100644
--- a/packages/app/src/i18n/uk.ts
+++ b/packages/app/src/i18n/uk.ts
@@ -871,7 +871,7 @@ export const dict = {
"settings.general.row.terminalFont.title": "Шрифт термінала",
"settings.general.row.terminalFont.description": "Налаштуйте шрифт, який використовується в терміналі",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "Шрифт інтерфейсу",
diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts
index 84eeae984311..cfb686b71af3 100644
--- a/packages/app/src/i18n/zh.ts
+++ b/packages/app/src/i18n/zh.ts
@@ -769,7 +769,7 @@ export const dict = {
"settings.general.row.terminalFont.title": "终端字体",
"settings.general.row.terminalFont.description": "自定义终端使用的字体",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "界面字体",
diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts
index 5e5be498bb51..8a1c6fdfa180 100644
--- a/packages/app/src/i18n/zht.ts
+++ b/packages/app/src/i18n/zht.ts
@@ -764,7 +764,7 @@ export const dict = {
"settings.general.row.terminalFont.title": "終端機字型",
"settings.general.row.terminalFont.description": "自訂終端機使用的字型",
"settings.general.row.backgroundImage.title": "Background image",
- "settings.general.row.backgroundImage.description": "Choose an image for the desktop background.",
+ "settings.general.row.backgroundImage.description": "Choose an image for the app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "介面字型",
diff --git a/packages/app/src/index.css b/packages/app/src/index.css
index 78a9e103a7f5..08da436ae966 100644
--- a/packages/app/src/index.css
+++ b/packages/app/src/index.css
@@ -3,6 +3,30 @@
@import "@opencode-ai/ui/v2/styles/tailwind.css";
@import "tw-animate-css";
+html[data-background-image] body {
+ background-image: linear-gradient(rgb(0 0 0 / 12%), rgb(0 0 0 / 12%)), var(--app-background-image);
+ background-color: transparent !important;
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: cover;
+}
+
+html[data-background-image] #root {
+ background-color: transparent !important;
+}
+
+html[data-background-image] [data-background-surface="shell"] {
+ background-color: transparent !important;
+}
+
+html[data-background-image] [data-background-surface="content"] {
+ background-color: color-mix(in srgb, var(--background-base) 68%, transparent) !important;
+}
+
+html[data-background-image] [data-background-surface="panel"] {
+ background-color: color-mix(in srgb, var(--v2-background-bg-base) 56%, transparent) !important;
+}
+
@font-face {
font-family: "JetBrainsMono Nerd Font Mono";
src: url("/assets/JetBrainsMonoNerdFontMono-Regular.woff2") format("woff2");
diff --git a/packages/app/src/utils/web-background-image.ts b/packages/app/src/utils/web-background-image.ts
new file mode 100644
index 000000000000..89e497cf1718
--- /dev/null
+++ b/packages/app/src/utils/web-background-image.ts
@@ -0,0 +1,33 @@
+const cacheName = "opencode-background-image-v1"
+const maxBytes = 20 * 1024 * 1024
+
+function key() {
+ return new URL("/__opencode/background-image", location.origin).toString()
+}
+
+export async function loadWebBackgroundImage() {
+ const response = await (await caches.open(cacheName)).match(key())
+ return response?.blob() ?? null
+}
+
+export async function saveWebBackgroundImage(file: File) {
+ if (!file.type.startsWith("image/")) throw new Error("Unsupported background image format")
+ if (file.size > maxBytes) throw new Error("Background images must be 20 MB or smaller")
+ await (await caches.open(cacheName)).put(key(), new Response(file, { headers: { "Content-Type": file.type } }))
+ return file
+}
+
+export async function clearWebBackgroundImage() {
+ await (await caches.open(cacheName)).delete(key())
+}
+
+export function selectWebBackgroundImage() {
+ return new Promise((resolve) => {
+ const input = document.createElement("input")
+ input.type = "file"
+ input.accept = "image/avif,image/bmp,image/gif,image/jpeg,image/png,image/webp"
+ input.onchange = () => resolve(input.files?.[0] ?? null)
+ input.oncancel = () => resolve(null)
+ input.click()
+ })
+}
diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx
index e178173d010c..55f164d8e8ba 100644
--- a/packages/desktop/src/renderer/index.tsx
+++ b/packages/desktop/src/renderer/index.tsx
@@ -118,12 +118,12 @@ const createPlatform = (windowState: DesktopWindowState): Platform => {
document.documentElement.toggleAttribute("data-background-image", !!image)
if (image) {
document.documentElement.style.setProperty(
- "--desktop-background-image",
+ "--app-background-image",
`url("oc://renderer/background-image?revision=${encodeURIComponent(image.revision)}")`,
)
return true
}
- document.documentElement.style.removeProperty("--desktop-background-image")
+ document.documentElement.style.removeProperty("--app-background-image")
return false
}
window.api.onBackgroundImageChanged(applyBackgroundImage)
diff --git a/packages/desktop/src/renderer/styles.css b/packages/desktop/src/renderer/styles.css
index 0b485ed0f337..e69de29bb2d1 100644
--- a/packages/desktop/src/renderer/styles.css
+++ b/packages/desktop/src/renderer/styles.css
@@ -1,18 +0,0 @@
-html[data-background-image] body {
- background-image: linear-gradient(rgb(0 0 0 / 18%), rgb(0 0 0 / 18%)), var(--desktop-background-image);
- background-position: center;
- background-repeat: no-repeat;
- background-size: cover;
-}
-
-html[data-background-image] [data-background-surface="shell"] {
- background-color: transparent !important;
-}
-
-html[data-background-image] [data-background-surface="content"] {
- background-color: color-mix(in srgb, var(--background-base) 76%, transparent) !important;
-}
-
-html[data-background-image] [data-background-surface="panel"] {
- background-color: color-mix(in srgb, var(--v2-background-bg-base) 70%, transparent) !important;
-}
From d881c1c4764da4f3af4607dcaf49a85958f5839f Mon Sep 17 00:00:00 2001
From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Date: Mon, 20 Jul 2026 16:53:40 +0000
Subject: [PATCH 3/8] fix(app): make background prompt translucent
---
packages/app/src/components/prompt-input.tsx | 1 +
packages/app/src/index.css | 5 +++++
2 files changed, 6 insertions(+)
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index a820067a4e74..2811247c5ee4 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/packages/app/src/components/prompt-input.tsx
@@ -1615,6 +1615,7 @@ export const PromptInput: Component = (props) => {
Date: Mon, 20 Jul 2026 23:43:41 +0000
Subject: [PATCH 4/8] fix(app): refine background contrast
---
packages/app/src/components/prompt-project-selector.tsx | 2 ++
.../src/components/session/session-new-design-view.tsx | 2 +-
packages/app/src/index.css | 8 +++++++-
3 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/packages/app/src/components/prompt-project-selector.tsx b/packages/app/src/components/prompt-project-selector.tsx
index 45d0371fda58..ff0fc920bc4c 100644
--- a/packages/app/src/components/prompt-project-selector.tsx
+++ b/packages/app/src/components/prompt-project-selector.tsx
@@ -446,6 +446,7 @@ export function PromptProjectAddButton(props: { controller: PromptProjectControl
return (