Compare commits

...
Author SHA1 Message Date
Kit Langtonand𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 c34e505026 chore: remove updated screenshot asset 2026-07-21 20:52:58 +00:00
Kit Langtonand𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 16863ae97b docs: add updated desktop screenshot 2026-07-21 20:52:33 +00:00
Kit Langtonand𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 314ca785df fix(desktop): use requested default background 2026-07-21 20:50:56 +00:00
Kit Langtonand𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 a2697b9b08 chore: remove temporary screenshot asset 2026-07-21 20:32:41 +00:00
Kit Langtonand𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 5de3d73f24 docs: add desktop background screenshot 2026-07-21 20:31:53 +00:00
Kit Langtonand𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 4bf7210512 feat(desktop): add default background 2026-07-21 20:29:33 +00:00
Brendan Allan 4e4729535e fix(app): pad the workspace bar 2026-07-21 00:04:58 +00:00
Brendan Allan 98bb5e3bb4 fix(app): center the workspace bar 2026-07-21 00:02:13 +00:00
Brendan Allan 8eec2c6d57 fix(app): glass the workspace bar 2026-07-20 23:59:10 +00:00
Brendan Allan 6fb632eeb6 fix(app): tune background presentation 2026-07-20 23:47:03 +00:00
Brendan Allan 5452a13e07 fix(app): refine background contrast 2026-07-20 23:43:41 +00:00
Brendan Allan d881c1c476 fix(app): make background prompt translucent 2026-07-20 16:53:40 +00:00
Brendan Allan 364e2d9f03 feat(app): support web image backgrounds 2026-07-20 16:19:59 +00:00
Brendan Allanand𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 35fcf1ed58 feat(desktop): add image backgrounds 2026-07-20 16:10:39 +00:00
43 changed files with 522 additions and 6 deletions
@@ -1615,6 +1615,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<div class="flex flex-col gap-3">
<DockShellForm
data-component={newSession() ? "session-new-composer" : "session-composer"}
data-background-surface="prompt"
onSubmit={handleSubmit}
classList={{
"group/prompt-input min-h-[96px] w-full rounded-xl bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)]": true,
@@ -446,6 +446,7 @@ export function PromptProjectAddButton(props: { controller: PromptProjectControl
return (
<button
data-action="prompt-project"
data-background-surface="project-selector"
type="button"
class="flex h-7 min-w-0 max-w-[160px] items-center gap-1.5 rounded-sm px-2 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint transition-colors hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
onClick={() => props.controller.add()}
@@ -464,6 +465,7 @@ function ProjectTrigger(props: ComponentProps<"button"> & { controller: PromptPr
<button
{...rest}
data-action="prompt-project"
data-background-surface="project-selector"
type="button"
class="flex h-7 min-w-0 max-w-[203px] items-center gap-1.5 rounded-sm px-1.5 transition-colors focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
classList={{
@@ -4,10 +4,14 @@ import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout"
export function NewSessionDesignView(props: { children: JSX.Element }) {
return (
<div data-component="session-new-design" class="relative size-full overflow-hidden bg-v2-background-bg-deep ">
<div
data-component="session-new-design"
data-background-surface="shell"
class="relative size-full overflow-hidden bg-v2-background-bg-deep "
>
<div class="absolute inset-x-0 top-[25.375%] flex justify-center px-6">
<div class={NEW_SESSION_CONTENT_WIDTH}>
<WordmarkV2 class="h-auto w-full text-v2-background-bg-inverse" />
<WordmarkV2 class="h-auto w-full text-v2-background-bg-inverse [&>g>g>g]:!opacity-[0.45]" />
<div class="mt-8">{props.children}</div>
</div>
</div>
@@ -0,0 +1,36 @@
import { createStore } from "solid-js/store"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { showToast } from "@/utils/toast"
export function useSettingsBackgroundImage() {
const platform = usePlatform()
const language = useLanguage()
const [state, setState] = createStore({ busy: false })
const run = async (action: (() => Promise<unknown>) | undefined) => {
if (!action || state.busy) return
setState("busy", true)
try {
await action()
} catch (error) {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: error instanceof Error ? error.message : String(error),
})
} finally {
setState("busy", false)
}
}
return {
available: !!platform.selectBackgroundImage,
active: () => platform.backgroundImage?.() ?? false,
get busy() {
return state.busy
},
select: () => run(platform.selectBackgroundImage),
clear: () => run(platform.clearBackgroundImage),
}
}
@@ -31,6 +31,7 @@ import { decode64 } from "@/utils/base64"
import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
import { Link } from "./link"
import { SettingsList } from "./settings-list"
import { useSettingsBackgroundImage } from "./settings-background-image"
let demoSoundState = {
cleanup: undefined as (() => void) | undefined,
@@ -87,6 +88,7 @@ export const SettingsGeneral: Component = () => {
const language = useLanguage()
const permission = usePermission()
const platform = usePlatform()
const backgroundImage = useSettingsBackgroundImage()
const dialog = useDialog()
const params = useParams()
const settings = useSettings()
@@ -499,6 +501,24 @@ export const SettingsGeneral: Component = () => {
/>
</SettingsRow>
<Show when={backgroundImage.available}>
<SettingsRow
title={language.t("settings.general.row.backgroundImage.title")}
description={language.t("settings.general.row.backgroundImage.description")}
>
<div class="flex items-center gap-2">
<Button size="small" variant="secondary" disabled={backgroundImage.busy} onClick={backgroundImage.select}>
{language.t("settings.general.row.backgroundImage.choose")}
</Button>
<Show when={backgroundImage.active()}>
<Button size="small" variant="ghost" disabled={backgroundImage.busy} onClick={backgroundImage.clear}>
{language.t("settings.general.row.backgroundImage.remove")}
</Button>
</Show>
</div>
</SettingsRow>
</Show>
<SettingsRow
title={language.t("settings.general.row.uiFont.title")}
description={language.t("settings.general.row.uiFont.description")}
@@ -29,6 +29,7 @@ import { Link } from "../link"
import { SettingsListV2 } from "./parts/list"
import { SettingsRowV2 } from "./parts/row"
import { LayoutRetirementNotice, LayoutTransitionToggle } from "./interface-transition"
import { useSettingsBackgroundImage } from "../settings-background-image"
import "./settings-v2.css"
let demoSoundState = {
@@ -88,6 +89,7 @@ export const SettingsGeneralV2: Component<{
const language = useLanguage()
const permission = usePermission()
const platform = usePlatform()
const backgroundImage = useSettingsBackgroundImage()
const dialog = useDialog()
const settings = useSettings()
const serverSync = useServerSync()
@@ -460,6 +462,29 @@ export const SettingsGeneralV2: Component<{
/>
</SettingsRowV2>
<Show when={backgroundImage.available}>
<SettingsRowV2
title={language.t("settings.general.row.backgroundImage.title")}
description={language.t("settings.general.row.backgroundImage.description")}
>
<div class="flex items-center gap-2">
<ButtonV2
size="normal"
variant="neutral"
disabled={backgroundImage.busy}
onClick={backgroundImage.select}
>
{language.t("settings.general.row.backgroundImage.choose")}
</ButtonV2>
<Show when={backgroundImage.active()}>
<ButtonV2 size="normal" variant="ghost" disabled={backgroundImage.busy} onClick={backgroundImage.clear}>
{language.t("settings.general.row.backgroundImage.remove")}
</ButtonV2>
</Show>
</div>
</SettingsRowV2>
</Show>
<SettingsRowV2
title={language.t("settings.general.row.uiFont.title")}
description={language.t("settings.general.row.uiFont.description")}
+1
View File
@@ -227,6 +227,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
return (
<header
data-background-surface="shell"
data-slot={useV2Titlebar() ? "titlebar-v2" : undefined}
classList={{
"shrink-0 relative flex flex-row": true,
+12
View File
@@ -112,6 +112,18 @@ type PlatformBase = {
/** Read image from clipboard (desktop only) */
readClipboardImage?(): Promise<File | null>
/** Load and apply the saved app background image. */
loadBackgroundImage?(): Promise<boolean>
/** Whether the app currently has a background image. */
backgroundImage?: Accessor<boolean>
/** Select and apply an app background image. */
selectBackgroundImage?(): Promise<boolean>
/** Clear the saved app background image. */
clearBackgroundImage?(): Promise<void>
/** Export collected diagnostic logs (desktop only) */
exportDebugLogs?(): Promise<string>
+37
View File
@@ -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,
+4
View File
@@ -704,6 +704,10 @@ export const dict = {
"settings.general.row.font.description": "خصّص الخط المستخدم في كتل التعليمات البرمجية",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "خط الواجهة",
"settings.general.row.uiFont.description": "خصّص الخط المستخدم في الواجهة بأكملها",
"settings.general.row.followup.title": "سلوك المتابعة",
+4
View File
@@ -713,6 +713,10 @@ export const dict = {
"settings.general.row.font.description": "Personalize a fonte usada em blocos de código",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "Fonte da interface",
"settings.general.row.uiFont.description": "Personalize a fonte usada em toda a interface",
"settings.general.row.followup.title": "Comportamento de acompanhamento",
+4
View File
@@ -778,6 +778,10 @@ export const dict = {
"settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "UI font",
"settings.general.row.uiFont.description": "Prilagodi font koji se koristi u cijelom interfejsu",
"settings.general.row.followup.title": "Ponašanje nadovezivanja",
+4
View File
@@ -773,6 +773,10 @@ export const dict = {
"settings.general.row.font.description": "Tilpas skrifttypen, der bruges i kodeblokke",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "UI-skrifttype",
"settings.general.row.uiFont.description": "Tilpas skrifttypen, der bruges i hele brugerfladen",
"settings.general.row.followup.title": "Opfølgningsadfærd",
+4
View File
@@ -724,6 +724,10 @@ export const dict = {
"settings.general.row.font.description": "Die in Codeblöcken verwendete Schriftart anpassen",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "UI-Schriftart",
"settings.general.row.uiFont.description": "Die im gesamten Interface verwendete Schriftart anpassen",
"settings.general.row.followup.title": "Verhalten bei Folgefragen",
+4
View File
@@ -862,6 +862,10 @@ export const dict = {
"settings.general.row.colorScheme.description": "Choose whether OpenCode follows the system, light, or dark theme",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.font.title": "Code Font",
"settings.general.row.font.description": "Customise the font used in code blocks",
"settings.general.row.terminalFont.title": "Terminal Font",
+4
View File
@@ -781,6 +781,10 @@ export const dict = {
"settings.general.row.font.description": "Personaliza la fuente usada en bloques de código",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "Fuente de la interfaz",
"settings.general.row.uiFont.description": "Personaliza la fuente usada en toda la interfaz",
"settings.general.row.followup.title": "Comportamiento de seguimiento",
+4
View File
@@ -720,6 +720,10 @@ export const dict = {
"settings.general.row.font.description": "Personnaliser la police utilisée dans les blocs de code",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "Police de l'interface",
"settings.general.row.uiFont.description": "Personnaliser la police utilisée dans toute l'interface",
"settings.general.row.followup.title": "Comportement de suivi",
+4
View File
@@ -709,6 +709,10 @@ export const dict = {
"settings.general.row.font.description": "コードブロックで使用するフォントをカスタマイズします",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "UIフォント",
"settings.general.row.uiFont.description": "インターフェース全体で使用するフォントをカスタマイズします",
"settings.general.row.followup.title": "フォローアップの動作",
+4
View File
@@ -580,6 +580,10 @@ export const dict = {
"settings.general.row.font.description": "코드 블록에 사용되는 글꼴을 사용자 지정",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "UI 글꼴",
"settings.general.row.uiFont.description": "인터페이스 전반에 사용되는 글꼴을 사용자 지정",
"settings.general.row.followup.title": "후속 조치 동작",
+4
View File
@@ -654,6 +654,10 @@ export const dict = {
"settings.general.row.font.description": "Tilpass skrifttypen som brukes i kodeblokker",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "UI-skrift",
"settings.general.row.uiFont.description": "Tilpass skrifttypen som brukes i hele grensesnittet",
"settings.general.row.followup.title": "Oppfølgingsadferd",
+4
View File
@@ -714,6 +714,10 @@ export const dict = {
"settings.general.row.font.description": "Dostosuj czcionkę używaną w blokach kodu",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "Czcionka interfejsu",
"settings.general.row.uiFont.description": "Dostosuj czcionkę używaną w całym interfejsie",
"settings.general.row.followup.title": "Zachowanie kontynuacji",
+4
View File
@@ -778,6 +778,10 @@ export const dict = {
"settings.general.row.font.description": "Настройте шрифт, используемый в блоках кода",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "Шрифт интерфейса",
"settings.general.row.uiFont.description": "Настройте шрифт, используемый во всем интерфейсе",
"settings.general.row.followup.title": "Поведение уточняющих вопросов",
+4
View File
@@ -771,6 +771,10 @@ export const dict = {
"settings.general.row.font.description": "ปรับแต่งฟอนต์ที่ใช้ในบล็อกโค้ด",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "ฟอนต์ UI",
"settings.general.row.uiFont.description": "ปรับแต่งฟอนต์ที่ใช้ทั่วทั้งอินเทอร์เฟซ",
"settings.general.row.followup.title": "พฤติกรรมการติดตามผล",
+4
View File
@@ -784,6 +784,10 @@ export const dict = {
"settings.general.row.font.description": "Kod bloklarında kullanılan yazı tipini özelleştirin",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "Arayüz Yazı Tipi",
"settings.general.row.uiFont.description": "Arayüz genelinde kullanılan yazı tipini özelleştirin",
"settings.general.row.followup.title": "Takip davranışı",
+4
View File
@@ -870,6 +870,10 @@ export const dict = {
"settings.general.row.font.description": "Налаштуйте шрифт, який використовується в блоках коду",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "Шрифт інтерфейсу",
"settings.general.row.uiFont.description": "Налаштуйте шрифт, який використовується в інтерфейсі",
"settings.general.row.followup.title": "Поведінка продовження",
+4
View File
@@ -768,6 +768,10 @@ export const dict = {
"settings.general.row.font.description": "自定义代码块使用的字体",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "界面字体",
"settings.general.row.uiFont.description": "自定义整个界面使用的字体",
"settings.general.row.followup.title": "跟进消息行为",
+4
View File
@@ -763,6 +763,10 @@ export const dict = {
"settings.general.row.font.description": "自訂程式碼區塊使用的字型",
"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 app background.",
"settings.general.row.backgroundImage.choose": "Choose image",
"settings.general.row.backgroundImage.remove": "Remove",
"settings.general.row.uiFont.title": "介面字型",
"settings.general.row.uiFont.description": "自訂整個介面使用的字型",
"settings.general.row.followup.title": "後續追問行為",
+56
View File
@@ -3,6 +3,62 @@
@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 / 30%), rgb(0 0 0 / 30%)), 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;
}
html[data-background-image] [data-background-surface="prompt"] {
background-color: color-mix(in srgb, var(--v2-background-bg-base) 68%, transparent) !important;
backdrop-filter: blur(16px);
}
html[data-background-image] [data-background-surface="project-selector"] {
background-color: color-mix(in srgb, var(--v2-background-bg-base) 68%, transparent) !important;
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--v2-border-border-base) 70%, transparent);
backdrop-filter: blur(16px);
}
html[data-background-image] [data-background-surface="prompt"] [data-background-surface="project-selector"] {
background-color: transparent !important;
box-shadow: none;
backdrop-filter: none;
}
html[data-background-image] [data-background-surface="workspace-bar"] {
padding-inline: 6px;
padding-block: 2px;
border-radius: 6px;
background-color: color-mix(in srgb, var(--v2-background-bg-base) 68%, transparent) !important;
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--v2-border-border-base) 70%, transparent);
backdrop-filter: blur(16px);
}
html[data-background-image] [data-background-surface="workspace-bar"] [data-background-surface="project-selector"] {
background-color: transparent !important;
box-shadow: none;
backdrop-filter: none;
}
@font-face {
font-family: "JetBrainsMono Nerd Font Mono";
src: url("/assets/JetBrainsMonoNerdFontMono-Regular.woff2") format("woff2");
+4 -1
View File
@@ -598,7 +598,10 @@ export function NewHome() {
}
return (
<div class="rounded-[10px] shadow-[var(--v2-elevation-raised)] m-2 min-h-0 overflow-hidden bg-v2-background-bg-base self-stretch flex-1">
<div
data-background-surface="panel"
class="rounded-[10px] shadow-[var(--v2-elevation-raised)] m-2 min-h-0 overflow-hidden bg-v2-background-bg-base self-stretch flex-1"
>
<ScrollView
class="h-full [container-type:size]"
thumbContainer={sessionThumbTrack}
+1
View File
@@ -26,6 +26,7 @@ export default function NewLayout(props: ParentProps) {
return (
<div
data-background-surface="shell"
class="relative bg-v2-background-bg-deep flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"
style={{
"padding-top": "env(safe-area-inset-top, 0px)",
+5 -1
View File
@@ -2246,7 +2246,10 @@ export default function LegacyLayout(props: ParentProps) {
)
return (
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
<div
data-background-surface="shell"
class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"
>
{autoselecting() ?? ""}
<Titlebar update={titlebarUpdate} />
<Show when={updateVersion() !== undefined}>
@@ -2344,6 +2347,7 @@ export default function LegacyLayout(props: ParentProps) {
}}
>
<main
data-background-surface="content"
classList={{
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
}}
+2
View File
@@ -180,9 +180,11 @@ export default function NewSessionPage() {
/>
<Show when={projectController.selected()}>
<div
data-background-surface={showWorkspaceBar() ? "workspace-bar" : undefined}
class="flex min-h-7 min-w-0 items-center gap-0 text-v2-text-text-faint"
classList={{
"flex-col justify-center sm:flex-row": showWorkspaceBar(),
"w-fit max-w-full self-center": showWorkspaceBar(),
"justify-start": !showWorkspaceBar(),
}}
>
+1
View File
@@ -335,6 +335,7 @@ function SessionRouteFrame(props: ParentProps<{ padded?: boolean }>) {
function SessionPanelFrame(props: ParentProps<{ newLayout: boolean; raised?: boolean }>) {
return (
<div
data-background-surface={props.newLayout ? "panel" : undefined}
classList={{
"flex-1 min-h-0 flex flex-col": true,
"bg-v2-background-bg-base": props.newLayout,
@@ -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<File | null>((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()
})
}
@@ -37,11 +37,18 @@ export function assertAttachmentBudget(files: { size: number }[]) {
}
export async function readAttachment(filePath: string, maxBytes = MAX_ATTACHMENT_BYTES) {
return readBoundedFile(
filePath,
maxBytes,
`Selected attachments exceed the ${MAX_ATTACHMENT_BYTES / 1024 / 1024} MB limit`,
)
}
export async function readBoundedFile(filePath: string, maxBytes: number, message: string) {
const file = await open(filePath, "r")
try {
const info = await file.stat()
if (info.size > 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) {
@@ -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, "<svg />")
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")
})
@@ -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 })))
}
+29
View File
@@ -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),
+7
View File
@@ -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)) {
+8
View File
@@ -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<typeof cb>[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),
}
+8
View File
@@ -41,6 +41,10 @@ export type FatalRendererError = {
os?: string
}
export type BackgroundImage = {
revision: string
}
export type ElectronAPI = {
killSidecar: () => Promise<void>
installCli: () => Promise<string>
@@ -103,6 +107,10 @@ export type ElectronAPI = {
setTitlebar: (theme: TitlebarTheme) => Promise<void>
runDesktopMenuAction: (action: DesktopMenuAction) => Promise<void>
setBackgroundColor: (color: string) => Promise<void>
loadBackgroundImage: () => Promise<BackgroundImage | null>
selectBackgroundImage: () => Promise<BackgroundImage | null>
clearBackgroundImage: () => Promise<void>
onBackgroundImageChanged: (cb: (image: BackgroundImage | null) => void) => () => void
exportDebugLogs: () => Promise<string>
recordFatalRendererError: (error: FatalRendererError) => Promise<void>
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

+41
View File
@@ -21,6 +21,7 @@ import { createMemoryHistory, MemoryRouter, type BaseRouterProps } from "@solidj
import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, Show } from "solid-js"
import { render } from "solid-js/web"
import pkg from "../../package.json"
import defaultBackground from "./default-background.avif"
import { initI18n, t } from "./i18n"
import { initializationData, initializationReady } from "./initialization"
import { DesktopFirstLaunchOnboarding } from "./onboarding"
@@ -112,6 +113,26 @@ function DesktopMemoryRouter(props: BaseRouterProps & { windowID: string }) {
const createPlatform = (windowState: DesktopWindowState): Platform => {
const attachmentPaths = new WeakMap<File, string>()
const [backgroundImage, setBackgroundImage] = createSignal(true)
const applyBackgroundImage = (image: Awaited<ReturnType<typeof window.api.loadBackgroundImage>>) => {
const active = !!image || localStorage.getItem("background-image.default.dismissed") !== "true"
setBackgroundImage(active)
document.documentElement.toggleAttribute("data-background-image", active)
if (image) {
document.documentElement.style.setProperty(
"--app-background-image",
`url("oc://renderer/background-image?revision=${encodeURIComponent(image.revision)}")`,
)
return true
}
if (active) {
document.documentElement.style.setProperty("--app-background-image", `url("${defaultBackground}")`)
return true
}
document.documentElement.style.removeProperty("--app-background-image")
return false
}
window.api.onBackgroundImageChanged(applyBackgroundImage)
const os = (() => {
const ua = navigator.userAgent
if (ua.includes("Mac")) return "macos"
@@ -310,6 +331,25 @@ 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()
localStorage.removeItem("background-image.default.dismissed")
return applyBackgroundImage(image)
},
async clearBackgroundImage() {
await window.api.clearBackgroundImage()
localStorage.setItem("background-image.default.dismissed", "true")
applyBackgroundImage(null)
},
backgroundImage,
}
}
@@ -329,6 +369,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")