{
- variant?: "normal" | "alt"
+ variant?: "normal" | "alt" | "settings"
orientation?: "horizontal" | "vertical"
}
export interface TabsListProps extends ComponentProps {}
@@ -106,8 +106,13 @@ function TabsContent(props: ParentProps) {
)
}
+const TabsSectionTitle: Component = (props) => {
+ return {props.children}
+}
+
export const Tabs = Object.assign(TabsRoot, {
List: TabsList,
Trigger: TabsTrigger,
Content: TabsContent,
+ SectionTitle: TabsSectionTitle,
})
From de3641e8ebfd6d6d0262289136e970b1ddea54b2 Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Tue, 6 Jan 2026 16:03:39 -0600
Subject: [PATCH 093/426] wip(app): settings
---
.../app/src/components/settings-general.tsx | 63 +++++++++++++++++++
packages/app/src/components/terminal.tsx | 12 +++-
packages/app/src/context/notification.tsx | 40 ++++++------
packages/app/src/context/settings.tsx | 60 ++++++++++++++++--
packages/app/src/pages/layout.tsx | 16 ++++-
packages/app/src/utils/sound.ts | 44 +++++++++++++
6 files changed, 208 insertions(+), 27 deletions(-)
create mode 100644 packages/app/src/utils/sound.ts
diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx
index e9965b0fa0d..52672d01f47 100644
--- a/packages/app/src/components/settings-general.tsx
+++ b/packages/app/src/components/settings-general.tsx
@@ -3,6 +3,7 @@ import { Select } from "@opencode-ai/ui/select"
import { Switch } from "@opencode-ai/ui/switch"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { useSettings } from "@/context/settings"
+import { playSound, SOUND_OPTIONS } from "@/utils/sound"
export const SettingsGeneral: Component = () => {
const theme = useTheme()
@@ -20,11 +21,20 @@ export const SettingsGeneral: Component = () => {
const fontOptions = [
{ value: "ibm-plex-mono", label: "IBM Plex Mono" },
+ { value: "cascadia-code", label: "Cascadia Code" },
{ value: "fira-code", label: "Fira Code" },
+ { value: "hack", label: "Hack" },
+ { value: "inconsolata", label: "Inconsolata" },
+ { value: "intel-one-mono", label: "Intel One Mono" },
{ value: "jetbrains-mono", label: "JetBrains Mono" },
+ { value: "meslo-lgs", label: "Meslo LGS" },
+ { value: "roboto-mono", label: "Roboto Mono" },
{ value: "source-code-pro", label: "Source Code Pro" },
+ { value: "ubuntu-mono", label: "Ubuntu Mono" },
]
+ const soundOptions = [...SOUND_OPTIONS]
+
return (
)
diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx
index 8001e2caadc..f19366b8ab9 100644
--- a/packages/app/src/components/terminal.tsx
+++ b/packages/app/src/components/terminal.tsx
@@ -1,6 +1,7 @@
import type { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
import { useSDK } from "@/context/sdk"
+import { monoFontFamily, useSettings } from "@/context/settings"
import { SerializeAddon } from "@/addons/serialize"
import { LocalPTY } from "@/context/terminal"
import { resolveThemeVariant, useTheme, withAlpha, type HexColor } from "@opencode-ai/ui/theme"
@@ -36,6 +37,7 @@ const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = {
export const Terminal = (props: TerminalProps) => {
const sdk = useSDK()
+ const settings = useSettings()
const theme = useTheme()
let container!: HTMLDivElement
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
@@ -82,6 +84,14 @@ export const Terminal = (props: TerminalProps) => {
setOption("theme", colors)
})
+ createEffect(() => {
+ const font = monoFontFamily(settings.appearance.font())
+ if (!term) return
+ const setOption = (term as unknown as { setOption?: (key: string, value: string) => void }).setOption
+ if (!setOption) return
+ setOption("fontFamily", font)
+ })
+
const focusTerminal = () => {
const t = term
if (!t) return
@@ -112,7 +122,7 @@ export const Terminal = (props: TerminalProps) => {
cursorBlink: true,
cursorStyle: "bar",
fontSize: 14,
- fontFamily: "IBM Plex Mono, monospace",
+ fontFamily: monoFontFamily(settings.appearance.font()),
allowTransparency: true,
theme: terminalColors(),
scrollback: 10_000,
diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx
index 16b3d306c2d..8b108851949 100644
--- a/packages/app/src/context/notification.tsx
+++ b/packages/app/src/context/notification.tsx
@@ -4,13 +4,12 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSDK } from "./global-sdk"
import { useGlobalSync } from "./global-sync"
import { usePlatform } from "@/context/platform"
+import { useSettings } from "@/context/settings"
import { Binary } from "@opencode-ai/util/binary"
import { base64Encode } from "@opencode-ai/util/encode"
import { EventSessionError } from "@opencode-ai/sdk/v2"
-import { makeAudioPlayer } from "@solid-primitives/audio"
-import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac"
-import errorSound from "@opencode-ai/ui/audio/nope-03.aac"
import { Persist, persisted } from "@/utils/persist"
+import { playSound, soundSrc } from "@/utils/sound"
type NotificationBase = {
directory?: string
@@ -44,19 +43,10 @@ function pruneNotifications(list: Notification[]) {
export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
name: "Notification",
init: () => {
- let idlePlayer: ReturnType
| undefined
- let errorPlayer: ReturnType | undefined
-
- try {
- idlePlayer = makeAudioPlayer(idleSound)
- errorPlayer = makeAudioPlayer(errorSound)
- } catch (err) {
- console.log("Failed to load audio", err)
- }
-
const globalSDK = useGlobalSDK()
const globalSync = useGlobalSync()
const platform = usePlatform()
+ const settings = useSettings()
const [store, setStore, _, ready] = persisted(
Persist.global("notification", ["notification.v1"]),
@@ -93,16 +83,20 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
const session = match.found ? syncStore.session[match.index] : undefined
if (session?.parentID) break
- try {
- idlePlayer?.play()
- } catch {}
+
+ playSound(soundSrc(settings.sounds.agent()))
+
append({
...base,
type: "turn-complete",
session: sessionID,
})
+
const href = `/${base64Encode(directory)}/session/${sessionID}`
- void platform.notify("Response ready", session?.title ?? sessionID, href)
+ if (settings.notifications.agent()) {
+ void platform.notify("Response ready", session?.title ?? sessionID, href)
+ }
+
break
}
case "session.error": {
@@ -111,9 +105,9 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
const match = sessionID ? Binary.search(syncStore.session, sessionID, (s) => s.id) : undefined
const session = sessionID && match?.found ? syncStore.session[match.index] : undefined
if (session?.parentID) break
- try {
- errorPlayer?.play()
- } catch {}
+
+ playSound(soundSrc(settings.sounds.errors()))
+
const error = "error" in event.properties ? event.properties.error : undefined
append({
...base,
@@ -121,9 +115,13 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
session: sessionID ?? "global",
error,
})
+
const description = session?.title ?? (typeof error === "string" ? error : "An error occurred")
const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}`
- void platform.notify("Session error", description, href)
+ if (settings.notifications.errors()) {
+ void platform.notify("Session error", description, href)
+ }
+
break
}
}
diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx
index 6aca57ae2c9..4160d1b70a1 100644
--- a/packages/app/src/context/settings.tsx
+++ b/packages/app/src/context/settings.tsx
@@ -1,5 +1,5 @@
import { createStore } from "solid-js/store"
-import { createMemo } from "solid-js"
+import { createEffect, createMemo } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { persisted } from "@/utils/persist"
@@ -9,6 +9,12 @@ export interface NotificationSettings {
errors: boolean
}
+export interface SoundSettings {
+ agent: string
+ permissions: string
+ errors: string
+}
+
export interface Settings {
general: {
autoSave: boolean
@@ -22,6 +28,7 @@ export interface Settings {
autoApprove: boolean
}
notifications: NotificationSettings
+ sounds: SoundSettings
}
const defaultSettings: Settings = {
@@ -37,16 +44,47 @@ const defaultSettings: Settings = {
autoApprove: false,
},
notifications: {
- agent: false,
- permissions: false,
+ agent: true,
+ permissions: true,
errors: false,
},
+ sounds: {
+ agent: "staplebops-01",
+ permissions: "staplebops-02",
+ errors: "nope-03",
+ },
+}
+
+const monoFallback =
+ 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
+
+const monoFonts: Record = {
+ "ibm-plex-mono": `"IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
+ "cascadia-code": `"Cascadia Code Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
+ "fira-code": `"Fira Code Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
+ hack: `"Hack Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
+ inconsolata: `"Inconsolata Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
+ "intel-one-mono": `"Intel One Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
+ "jetbrains-mono": `"JetBrains Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
+ "meslo-lgs": `"Meslo LGS Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
+ "roboto-mono": `"Roboto Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
+ "source-code-pro": `"Source Code Pro Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
+ "ubuntu-mono": `"Ubuntu Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
+}
+
+export function monoFontFamily(font: string | undefined) {
+ return monoFonts[font ?? defaultSettings.appearance.font] ?? monoFonts[defaultSettings.appearance.font]
}
export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({
name: "Settings",
init: () => {
- const [store, setStore, _, ready] = persisted("settings.v1", createStore(defaultSettings))
+ const [store, setStore, _, ready] = persisted("settings.v3", createStore(defaultSettings))
+
+ createEffect(() => {
+ if (typeof document === "undefined") return
+ document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font))
+ })
return {
ready,
@@ -98,6 +136,20 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setStore("notifications", "errors", value)
},
},
+ sounds: {
+ agent: createMemo(() => store.sounds?.agent ?? defaultSettings.sounds.agent),
+ setAgent(value: string) {
+ setStore("sounds", "agent", value)
+ },
+ permissions: createMemo(() => store.sounds?.permissions ?? defaultSettings.sounds.permissions),
+ setPermissions(value: string) {
+ setStore("sounds", "permissions", value)
+ },
+ errors: createMemo(() => store.sounds?.errors ?? defaultSettings.sounds.errors),
+ setErrors(value: string) {
+ setStore("sounds", "errors", value)
+ },
+ },
}
},
})
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index 8c04f10dbac..f4e202b6ea7 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -37,6 +37,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
import { getFilename } from "@opencode-ai/util/path"
import { Session, type Message, type TextPart } from "@opencode-ai/sdk/v2/client"
import { usePlatform } from "@/context/platform"
+import { useSettings } from "@/context/settings"
import { createStore, produce, reconcile } from "solid-js/store"
import {
DragDropProvider,
@@ -54,6 +55,7 @@ import { useNotification } from "@/context/notification"
import { usePermission } from "@/context/permission"
import { Binary } from "@opencode-ai/util/binary"
import { retry } from "@opencode-ai/util/retry"
+import { playSound, soundSrc } from "@/utils/sound"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
@@ -98,6 +100,7 @@ export default function Layout(props: ParentProps) {
const layout = useLayout()
const layoutReady = createMemo(() => layout.ready())
const platform = usePlatform()
+ const settings = useSettings()
const server = useServer()
const notification = useNotification()
const permission = usePermission()
@@ -329,7 +332,18 @@ export default function Layout(props: ParentProps) {
if (now - lastAlerted < cooldownMs) return
alertedAtBySession.set(sessionKey, now)
- void platform.notify(config.title, description, href)
+ if (e.details.type === "permission.asked") {
+ playSound(soundSrc(settings.sounds.permissions()))
+ if (settings.notifications.permissions()) {
+ void platform.notify(config.title, description, href)
+ }
+ }
+
+ if (e.details.type === "question.asked") {
+ if (settings.notifications.agent()) {
+ void platform.notify(config.title, description, href)
+ }
+ }
const currentDir = params.dir ? base64Decode(params.dir) : undefined
const currentSession = params.id
diff --git a/packages/app/src/utils/sound.ts b/packages/app/src/utils/sound.ts
new file mode 100644
index 00000000000..e8db0bf7b9a
--- /dev/null
+++ b/packages/app/src/utils/sound.ts
@@ -0,0 +1,44 @@
+import nope01 from "@opencode-ai/ui/audio/nope-01.aac"
+import nope02 from "@opencode-ai/ui/audio/nope-02.aac"
+import nope03 from "@opencode-ai/ui/audio/nope-03.aac"
+import nope04 from "@opencode-ai/ui/audio/nope-04.aac"
+import nope05 from "@opencode-ai/ui/audio/nope-05.aac"
+import staplebops01 from "@opencode-ai/ui/audio/staplebops-01.aac"
+import staplebops02 from "@opencode-ai/ui/audio/staplebops-02.aac"
+import staplebops03 from "@opencode-ai/ui/audio/staplebops-03.aac"
+import staplebops04 from "@opencode-ai/ui/audio/staplebops-04.aac"
+import staplebops05 from "@opencode-ai/ui/audio/staplebops-05.aac"
+import staplebops06 from "@opencode-ai/ui/audio/staplebops-06.aac"
+import staplebops07 from "@opencode-ai/ui/audio/staplebops-07.aac"
+
+export const SOUND_OPTIONS = [
+ { id: "staplebops-01", label: "Boopy", src: staplebops01 },
+ { id: "staplebops-02", label: "Beepy", src: staplebops02 },
+ { id: "staplebops-03", label: "Staplebops 03", src: staplebops03 },
+ { id: "staplebops-04", label: "Staplebops 04", src: staplebops04 },
+ { id: "staplebops-05", label: "Staplebops 05", src: staplebops05 },
+ { id: "staplebops-06", label: "Staplebops 06", src: staplebops06 },
+ { id: "staplebops-07", label: "Staplebops 07", src: staplebops07 },
+ { id: "nope-01", label: "Nope 01", src: nope01 },
+ { id: "nope-02", label: "Nope 02", src: nope02 },
+ { id: "nope-03", label: "Oopsie", src: nope03 },
+ { id: "nope-04", label: "Nope 04", src: nope04 },
+ { id: "nope-05", label: "Nope 05", src: nope05 },
+] as const
+
+export type SoundOption = (typeof SOUND_OPTIONS)[number]
+export type SoundID = SoundOption["id"]
+
+const soundById = Object.fromEntries(SOUND_OPTIONS.map((s) => [s.id, s.src])) as Record
+
+export function soundSrc(id: string | undefined) {
+ if (!id) return
+ if (!(id in soundById)) return
+ return soundById[id as SoundID]
+}
+
+export function playSound(src: string | undefined) {
+ if (typeof Audio === "undefined") return
+ if (!src) return
+ void new Audio(src).play().catch(() => undefined)
+}
From df094a10ff1f1a95f66abc6bdccfa69080480afa Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Wed, 7 Jan 2026 06:54:48 -0600
Subject: [PATCH 094/426] wip(app): settings
---
.../app/src/components/settings-general.tsx | 10 +-
.../app/src/components/settings-keybinds.tsx | 309 +++++++++++++++++-
packages/app/src/context/command.tsx | 55 +++-
packages/app/src/context/settings.tsx | 5 +-
4 files changed, 361 insertions(+), 18 deletions(-)
diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx
index 52672d01f47..15dc98bfbed 100644
--- a/packages/app/src/components/settings-general.tsx
+++ b/packages/app/src/components/settings-general.tsx
@@ -37,10 +37,14 @@ export const SettingsGeneral: Component = () => {
return (