Skip to content

Commit de3641e

Browse files
committed
wip(app): settings
1 parent 8bcbfd6 commit de3641e

6 files changed

Lines changed: 208 additions & 27 deletions

File tree

packages/app/src/components/settings-general.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Select } from "@opencode-ai/ui/select"
33
import { Switch } from "@opencode-ai/ui/switch"
44
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
55
import { useSettings } from "@/context/settings"
6+
import { playSound, SOUND_OPTIONS } from "@/utils/sound"
67

78
export const SettingsGeneral: Component = () => {
89
const theme = useTheme()
@@ -20,11 +21,20 @@ export const SettingsGeneral: Component = () => {
2021

2122
const fontOptions = [
2223
{ value: "ibm-plex-mono", label: "IBM Plex Mono" },
24+
{ value: "cascadia-code", label: "Cascadia Code" },
2325
{ value: "fira-code", label: "Fira Code" },
26+
{ value: "hack", label: "Hack" },
27+
{ value: "inconsolata", label: "Inconsolata" },
28+
{ value: "intel-one-mono", label: "Intel One Mono" },
2429
{ value: "jetbrains-mono", label: "JetBrains Mono" },
30+
{ value: "meslo-lgs", label: "Meslo LGS" },
31+
{ value: "roboto-mono", label: "Roboto Mono" },
2532
{ value: "source-code-pro", label: "Source Code Pro" },
33+
{ value: "ubuntu-mono", label: "Ubuntu Mono" },
2634
]
2735

36+
const soundOptions = [...SOUND_OPTIONS]
37+
2838
return (
2939
<div class="flex flex-col h-full overflow-y-auto no-scrollbar">
3040
<div class="flex flex-col gap-8 p-8 max-w-[720px]">
@@ -110,6 +120,59 @@ export const SettingsGeneral: Component = () => {
110120
/>
111121
</SettingsRow>
112122
</div>
123+
124+
{/* Sound effects Section */}
125+
<div class="flex flex-col gap-1">
126+
<h3 class="text-14-medium text-text-strong pb-2">Sound effects</h3>
127+
128+
<SettingsRow title="Agent" description="Play sound when the agent is complete or needs attention">
129+
<Select
130+
options={soundOptions}
131+
current={soundOptions.find((o) => o.id === settings.sounds.agent())}
132+
value={(o) => o.id}
133+
label={(o) => o.label}
134+
onSelect={(option) => {
135+
if (!option) return
136+
settings.sounds.setAgent(option.id)
137+
playSound(option.src)
138+
}}
139+
variant="secondary"
140+
size="small"
141+
/>
142+
</SettingsRow>
143+
144+
<SettingsRow title="Permissions" description="Play sound when a permission is required">
145+
<Select
146+
options={soundOptions}
147+
current={soundOptions.find((o) => o.id === settings.sounds.permissions())}
148+
value={(o) => o.id}
149+
label={(o) => o.label}
150+
onSelect={(option) => {
151+
if (!option) return
152+
settings.sounds.setPermissions(option.id)
153+
playSound(option.src)
154+
}}
155+
variant="secondary"
156+
size="small"
157+
/>
158+
</SettingsRow>
159+
160+
<SettingsRow title="Errors" description="Play sound when an error occurs">
161+
<Select
162+
options={soundOptions}
163+
current={soundOptions.find((o) => o.id === settings.sounds.errors())}
164+
value={(o) => o.id}
165+
label={(o) => o.label}
166+
onSelect={(option) => {
167+
if (!option) return
168+
settings.sounds.setErrors(option.id)
169+
playSound(option.src)
170+
}}
171+
variant="secondary"
172+
size="small"
173+
/>
174+
</SettingsRow>
175+
</div>
113176
</div>
114177
</div>
115178
)

packages/app/src/components/terminal.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
22
import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
33
import { useSDK } from "@/context/sdk"
4+
import { monoFontFamily, useSettings } from "@/context/settings"
45
import { SerializeAddon } from "@/addons/serialize"
56
import { LocalPTY } from "@/context/terminal"
67
import { resolveThemeVariant, useTheme, withAlpha, type HexColor } from "@opencode-ai/ui/theme"
@@ -36,6 +37,7 @@ const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = {
3637

3738
export const Terminal = (props: TerminalProps) => {
3839
const sdk = useSDK()
40+
const settings = useSettings()
3941
const theme = useTheme()
4042
let container!: HTMLDivElement
4143
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
@@ -82,6 +84,14 @@ export const Terminal = (props: TerminalProps) => {
8284
setOption("theme", colors)
8385
})
8486

87+
createEffect(() => {
88+
const font = monoFontFamily(settings.appearance.font())
89+
if (!term) return
90+
const setOption = (term as unknown as { setOption?: (key: string, value: string) => void }).setOption
91+
if (!setOption) return
92+
setOption("fontFamily", font)
93+
})
94+
8595
const focusTerminal = () => {
8696
const t = term
8797
if (!t) return
@@ -112,7 +122,7 @@ export const Terminal = (props: TerminalProps) => {
112122
cursorBlink: true,
113123
cursorStyle: "bar",
114124
fontSize: 14,
115-
fontFamily: "IBM Plex Mono, monospace",
125+
fontFamily: monoFontFamily(settings.appearance.font()),
116126
allowTransparency: true,
117127
theme: terminalColors(),
118128
scrollback: 10_000,

packages/app/src/context/notification.tsx

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
44
import { useGlobalSDK } from "./global-sdk"
55
import { useGlobalSync } from "./global-sync"
66
import { usePlatform } from "@/context/platform"
7+
import { useSettings } from "@/context/settings"
78
import { Binary } from "@opencode-ai/util/binary"
89
import { base64Encode } from "@opencode-ai/util/encode"
910
import { EventSessionError } from "@opencode-ai/sdk/v2"
10-
import { makeAudioPlayer } from "@solid-primitives/audio"
11-
import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac"
12-
import errorSound from "@opencode-ai/ui/audio/nope-03.aac"
1311
import { Persist, persisted } from "@/utils/persist"
12+
import { playSound, soundSrc } from "@/utils/sound"
1413

1514
type NotificationBase = {
1615
directory?: string
@@ -44,19 +43,10 @@ function pruneNotifications(list: Notification[]) {
4443
export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
4544
name: "Notification",
4645
init: () => {
47-
let idlePlayer: ReturnType<typeof makeAudioPlayer> | undefined
48-
let errorPlayer: ReturnType<typeof makeAudioPlayer> | undefined
49-
50-
try {
51-
idlePlayer = makeAudioPlayer(idleSound)
52-
errorPlayer = makeAudioPlayer(errorSound)
53-
} catch (err) {
54-
console.log("Failed to load audio", err)
55-
}
56-
5746
const globalSDK = useGlobalSDK()
5847
const globalSync = useGlobalSync()
5948
const platform = usePlatform()
49+
const settings = useSettings()
6050

6151
const [store, setStore, _, ready] = persisted(
6252
Persist.global("notification", ["notification.v1"]),
@@ -93,16 +83,20 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
9383
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
9484
const session = match.found ? syncStore.session[match.index] : undefined
9585
if (session?.parentID) break
96-
try {
97-
idlePlayer?.play()
98-
} catch {}
86+
87+
playSound(soundSrc(settings.sounds.agent()))
88+
9989
append({
10090
...base,
10191
type: "turn-complete",
10292
session: sessionID,
10393
})
94+
10495
const href = `/${base64Encode(directory)}/session/${sessionID}`
105-
void platform.notify("Response ready", session?.title ?? sessionID, href)
96+
if (settings.notifications.agent()) {
97+
void platform.notify("Response ready", session?.title ?? sessionID, href)
98+
}
99+
106100
break
107101
}
108102
case "session.error": {
@@ -111,19 +105,23 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
111105
const match = sessionID ? Binary.search(syncStore.session, sessionID, (s) => s.id) : undefined
112106
const session = sessionID && match?.found ? syncStore.session[match.index] : undefined
113107
if (session?.parentID) break
114-
try {
115-
errorPlayer?.play()
116-
} catch {}
108+
109+
playSound(soundSrc(settings.sounds.errors()))
110+
117111
const error = "error" in event.properties ? event.properties.error : undefined
118112
append({
119113
...base,
120114
type: "error",
121115
session: sessionID ?? "global",
122116
error,
123117
})
118+
124119
const description = session?.title ?? (typeof error === "string" ? error : "An error occurred")
125120
const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}`
126-
void platform.notify("Session error", description, href)
121+
if (settings.notifications.errors()) {
122+
void platform.notify("Session error", description, href)
123+
}
124+
127125
break
128126
}
129127
}

packages/app/src/context/settings.tsx

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createStore } from "solid-js/store"
2-
import { createMemo } from "solid-js"
2+
import { createEffect, createMemo } from "solid-js"
33
import { createSimpleContext } from "@opencode-ai/ui/context"
44
import { persisted } from "@/utils/persist"
55

@@ -9,6 +9,12 @@ export interface NotificationSettings {
99
errors: boolean
1010
}
1111

12+
export interface SoundSettings {
13+
agent: string
14+
permissions: string
15+
errors: string
16+
}
17+
1218
export interface Settings {
1319
general: {
1420
autoSave: boolean
@@ -22,6 +28,7 @@ export interface Settings {
2228
autoApprove: boolean
2329
}
2430
notifications: NotificationSettings
31+
sounds: SoundSettings
2532
}
2633

2734
const defaultSettings: Settings = {
@@ -37,16 +44,47 @@ const defaultSettings: Settings = {
3744
autoApprove: false,
3845
},
3946
notifications: {
40-
agent: false,
41-
permissions: false,
47+
agent: true,
48+
permissions: true,
4249
errors: false,
4350
},
51+
sounds: {
52+
agent: "staplebops-01",
53+
permissions: "staplebops-02",
54+
errors: "nope-03",
55+
},
56+
}
57+
58+
const monoFallback =
59+
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
60+
61+
const monoFonts: Record<string, string> = {
62+
"ibm-plex-mono": `"IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
63+
"cascadia-code": `"Cascadia Code Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
64+
"fira-code": `"Fira Code Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
65+
hack: `"Hack Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
66+
inconsolata: `"Inconsolata Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
67+
"intel-one-mono": `"Intel One Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
68+
"jetbrains-mono": `"JetBrains Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
69+
"meslo-lgs": `"Meslo LGS Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
70+
"roboto-mono": `"Roboto Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
71+
"source-code-pro": `"Source Code Pro Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
72+
"ubuntu-mono": `"Ubuntu Mono Nerd Font", "IBM Plex Mono", "IBM Plex Mono Fallback", ${monoFallback}`,
73+
}
74+
75+
export function monoFontFamily(font: string | undefined) {
76+
return monoFonts[font ?? defaultSettings.appearance.font] ?? monoFonts[defaultSettings.appearance.font]
4477
}
4578

4679
export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({
4780
name: "Settings",
4881
init: () => {
49-
const [store, setStore, _, ready] = persisted("settings.v1", createStore<Settings>(defaultSettings))
82+
const [store, setStore, _, ready] = persisted("settings.v3", createStore<Settings>(defaultSettings))
83+
84+
createEffect(() => {
85+
if (typeof document === "undefined") return
86+
document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font))
87+
})
5088

5189
return {
5290
ready,
@@ -98,6 +136,20 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
98136
setStore("notifications", "errors", value)
99137
},
100138
},
139+
sounds: {
140+
agent: createMemo(() => store.sounds?.agent ?? defaultSettings.sounds.agent),
141+
setAgent(value: string) {
142+
setStore("sounds", "agent", value)
143+
},
144+
permissions: createMemo(() => store.sounds?.permissions ?? defaultSettings.sounds.permissions),
145+
setPermissions(value: string) {
146+
setStore("sounds", "permissions", value)
147+
},
148+
errors: createMemo(() => store.sounds?.errors ?? defaultSettings.sounds.errors),
149+
setErrors(value: string) {
150+
setStore("sounds", "errors", value)
151+
},
152+
},
101153
}
102154
},
103155
})

packages/app/src/pages/layout.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
3737
import { getFilename } from "@opencode-ai/util/path"
3838
import { Session, type Message, type TextPart } from "@opencode-ai/sdk/v2/client"
3939
import { usePlatform } from "@/context/platform"
40+
import { useSettings } from "@/context/settings"
4041
import { createStore, produce, reconcile } from "solid-js/store"
4142
import {
4243
DragDropProvider,
@@ -54,6 +55,7 @@ import { useNotification } from "@/context/notification"
5455
import { usePermission } from "@/context/permission"
5556
import { Binary } from "@opencode-ai/util/binary"
5657
import { retry } from "@opencode-ai/util/retry"
58+
import { playSound, soundSrc } from "@/utils/sound"
5759

5860
import { useDialog } from "@opencode-ai/ui/context/dialog"
5961
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
@@ -98,6 +100,7 @@ export default function Layout(props: ParentProps) {
98100
const layout = useLayout()
99101
const layoutReady = createMemo(() => layout.ready())
100102
const platform = usePlatform()
103+
const settings = useSettings()
101104
const server = useServer()
102105
const notification = useNotification()
103106
const permission = usePermission()
@@ -329,7 +332,18 @@ export default function Layout(props: ParentProps) {
329332
if (now - lastAlerted < cooldownMs) return
330333
alertedAtBySession.set(sessionKey, now)
331334

332-
void platform.notify(config.title, description, href)
335+
if (e.details.type === "permission.asked") {
336+
playSound(soundSrc(settings.sounds.permissions()))
337+
if (settings.notifications.permissions()) {
338+
void platform.notify(config.title, description, href)
339+
}
340+
}
341+
342+
if (e.details.type === "question.asked") {
343+
if (settings.notifications.agent()) {
344+
void platform.notify(config.title, description, href)
345+
}
346+
}
333347

334348
const currentDir = params.dir ? base64Decode(params.dir) : undefined
335349
const currentSession = params.id

0 commit comments

Comments
 (0)