Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
388 changes: 11 additions & 377 deletions packages/app/src/components/settings-general.tsx

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions packages/app/src/components/settings-notifications-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { type Component } from "solid-js"
import { Switch } from "@opencode-ai/ui/switch"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { SettingsList } from "./settings-list"
import { SettingsRow } from "./settings-row"

export const SettingsNotificationsSection: Component = () => {
const language = useLanguage()
const settings = useSettings()

return (
<div class="flex flex-col gap-1">
<h3 class="text-h3 text-fg-strong pb-2">{language.t("settings.general.section.notifications")}</h3>

<SettingsList>
<SettingsRow
title={language.t("settings.general.notifications.agent.title")}
description={language.t("settings.general.notifications.agent.description")}
>
<div data-action="settings-notifications-agent">
<Switch
checked={settings.notifications.agent()}
onChange={(checked) => settings.notifications.setAgent(checked)}
/>
</div>
</SettingsRow>

<SettingsRow
title={language.t("settings.general.notifications.permissions.title")}
description={language.t("settings.general.notifications.permissions.description")}
>
<div data-action="settings-notifications-permissions">
<Switch
checked={settings.notifications.permissions()}
onChange={(checked) => settings.notifications.setPermissions(checked)}
/>
</div>
</SettingsRow>

<SettingsRow
title={language.t("settings.general.notifications.errors.title")}
description={language.t("settings.general.notifications.errors.description")}
>
<div data-action="settings-notifications-errors">
<Switch
checked={settings.notifications.errors()}
onChange={(checked) => settings.notifications.setErrors(checked)}
/>
</div>
</SettingsRow>
</SettingsList>
</div>
)
}
19 changes: 19 additions & 0 deletions packages/app/src/components/settings-row.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { type Component, type JSX } from "solid-js"

interface SettingsRowProps {
title: string | JSX.Element
description: string | JSX.Element
children: JSX.Element
}

export const SettingsRow: Component<SettingsRowProps> = (props) => {
return (
<div class="flex flex-wrap items-center gap-4 py-3 border-b border-border-weak last:border-none sm:flex-nowrap">
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
<span class="text-h3 text-fg-strong">{props.title}</span>
<span class="text-body text-fg-weak">{props.description}</span>
</div>
<div class="flex w-full justify-end sm:w-auto sm:shrink-0">{props.children}</div>
</div>
)
}
129 changes: 129 additions & 0 deletions packages/app/src/components/settings-sounds-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { type Component } from "solid-js"
import { Select } from "@opencode-ai/ui/select"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
import { SettingsList } from "./settings-list"
import { SettingsRow } from "./settings-row"

let demoSoundState = {
cleanup: undefined as (() => void) | undefined,
timeout: undefined as NodeJS.Timeout | undefined,
run: 0,
}

const noneSound = { id: "none", label: "sound.option.none" } as const
const soundOptions = [noneSound, ...SOUND_OPTIONS]

const stopDemoSound = () => {
demoSoundState.run += 1
if (demoSoundState.cleanup) {
demoSoundState.cleanup()
}
clearTimeout(demoSoundState.timeout)
demoSoundState.cleanup = undefined
}

const playDemoSound = (id: string | undefined) => {
stopDemoSound()
if (!id) return

const run = ++demoSoundState.run
demoSoundState.timeout = setTimeout(() => {
void playSoundById(id).then((cleanup) => {
if (demoSoundState.run !== run) {
cleanup?.()
return
}
demoSoundState.cleanup = cleanup
})
}, 100)
}

export const SettingsSoundsSection: Component = () => {
const language = useLanguage()
const settings = useSettings()

const soundSelectProps = (
enabled: () => boolean,
current: () => string,
setEnabled: (value: boolean) => void,
set: (id: string) => void,
) => ({
options: soundOptions,
current: enabled() ? (soundOptions.find((o) => o.id === current()) ?? noneSound) : noneSound,
value: (o: (typeof soundOptions)[number]) => o.id,
label: (o: (typeof soundOptions)[number]) => language.t(o.label),
onHighlight: (option: (typeof soundOptions)[number] | undefined) => {
if (!option) return
playDemoSound(option.id === "none" ? undefined : option.id)
},
onSelect: (option: (typeof soundOptions)[number] | undefined) => {
if (!option) return
if (option.id === "none") {
setEnabled(false)
stopDemoSound()
return
}
setEnabled(true)
set(option.id)
playDemoSound(option.id)
},
variant: "secondary" as const,
size: "small" as const,
triggerVariant: "settings" as const,
})

return (
<div class="flex flex-col gap-1">
<h3 class="text-h3 text-fg-strong pb-2">{language.t("settings.general.section.sounds")}</h3>

<SettingsList>
<SettingsRow
title={language.t("settings.general.sounds.agent.title")}
description={language.t("settings.general.sounds.agent.description")}
>
<Select
data-action="settings-sounds-agent"
{...soundSelectProps(
() => settings.sounds.agentEnabled(),
() => settings.sounds.agent(),
(value) => settings.sounds.setAgentEnabled(value),
(id) => settings.sounds.setAgent(id),
)}
/>
</SettingsRow>

<SettingsRow
title={language.t("settings.general.sounds.permissions.title")}
description={language.t("settings.general.sounds.permissions.description")}
>
<Select
data-action="settings-sounds-permissions"
{...soundSelectProps(
() => settings.sounds.permissionsEnabled(),
() => settings.sounds.permissions(),
(value) => settings.sounds.setPermissionsEnabled(value),
(id) => settings.sounds.setPermissions(id),
)}
/>
</SettingsRow>

<SettingsRow
title={language.t("settings.general.sounds.errors.title")}
description={language.t("settings.general.sounds.errors.description")}
>
<Select
data-action="settings-sounds-errors"
{...soundSelectProps(
() => settings.sounds.errorsEnabled(),
() => settings.sounds.errors(),
(value) => settings.sounds.setErrorsEnabled(value),
(id) => settings.sounds.setErrors(id),
)}
/>
</SettingsRow>
</SettingsList>
</div>
)
}
139 changes: 139 additions & 0 deletions packages/app/src/components/settings-updates-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { type Component } from "solid-js"
import { createStore } from "solid-js/store"
import { Button } from "@opencode-ai/ui/button"
import { Switch } from "@opencode-ai/ui/switch"
import { showToast } from "@opencode-ai/ui/toast"
import { useLanguage } from "@/context/language"
import { canCheckUpdate, usePlatform } from "@/context/platform"
import { useSettings } from "@/context/settings"
import { SettingsList } from "./settings-list"
import { SettingsRow } from "./settings-row"

export const SettingsUpdatesSection: Component = () => {
const language = useLanguage()
const platform = usePlatform()
const settings = useSettings()
const [store, setStore] = createStore({
checking: false,
})

const check = () => {
const checkUpdate = platform.checkUpdate
if (!canCheckUpdate(platform) || !checkUpdate) return
setStore("checking", true)

void checkUpdate()
.then((result) => {
if (result.status === "busy") {
showToast({
title: language.t("settings.updates.toast.busy.title"),
description: language.t("settings.updates.toast.busy.description"),
})
return
}

if (result.status === "disabled") {
showToast({
title: language.t("settings.updates.toast.disabled.title"),
description: language.t("settings.updates.toast.disabled.description"),
})
return
}

if (result.status === "failed") {
showToast({
title: language.t("common.requestFailed"),
description: result.message || language.t("settings.updates.toast.failed.description"),
})
return
}

if (!result.updateAvailable) {
showToast({
variant: "success",
icon: "circle-check",
title: language.t("settings.updates.toast.latest.title"),
description: language.t("settings.updates.toast.latest.description", { version: platform.version ?? "" }),
})
return
}

const actions = platform.update
? [
{
label: language.t("toast.update.action.installRestart"),
onClick: async () => {
await platform.update!()
},
},
{
label: language.t("toast.update.action.notYet"),
onClick: "dismiss" as const,
},
]
: [
{
label: language.t("toast.update.action.notYet"),
onClick: "dismiss" as const,
},
]

showToast({
persistent: true,
icon: "download",
title: language.t("toast.update.title"),
description: language.t("toast.update.description", { version: result.version ?? "" }),
actions,
})
})
.catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
})
.finally(() => setStore("checking", false))
}

return (
<div class="flex flex-col gap-1">
<h3 class="text-h3 text-fg-strong pb-2">{language.t("settings.general.section.updates")}</h3>

<SettingsList>
<SettingsRow
title={language.t("settings.updates.row.startup.title")}
description={language.t("settings.updates.row.startup.description")}
>
<div data-action="settings-updates-startup">
<Switch
checked={settings.updates.startup()}
disabled={!canCheckUpdate(platform)}
onChange={(checked) => settings.updates.setStartup(checked)}
/>
</div>
</SettingsRow>

<SettingsRow
title={language.t("settings.general.row.releaseNotes.title")}
description={language.t("settings.general.row.releaseNotes.description")}
>
<div data-action="settings-release-notes">
<Switch
checked={settings.general.releaseNotes()}
onChange={(checked) => settings.general.setReleaseNotes(checked)}
/>
</div>
</SettingsRow>

<SettingsRow
title={language.t("settings.updates.row.check.title")}
description={language.t("settings.updates.row.check.description")}
>
<Button variant="secondary" disabled={store.checking || !canCheckUpdate(platform)} onClick={check}>
{store.checking
? language.t("settings.updates.action.checking")
: language.t("settings.updates.action.checkNow")}
</Button>
</SettingsRow>
</SettingsList>
</div>
)
}
Loading
Loading