diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index 57efcfdfa..180a99c73 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -1,8 +1,12 @@ import { Component, createMemo, type JSX } from "solid-js" +import { createStore } from "solid-js/store" +import { Button } from "@opencode-ai/ui/button" import { Select } from "@opencode-ai/ui/select" import { Switch } from "@opencode-ai/ui/switch" import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme" +import { showToast } from "@opencode-ai/ui/toast" import { useLanguage } from "@/context/language" +import { usePlatform } from "@/context/platform" import { useSettings, monoFontFamily } from "@/context/settings" import { playSound, SOUND_OPTIONS } from "@/utils/sound" import { Link } from "./link" @@ -29,8 +33,67 @@ const playDemoSound = (src: string) => { export const SettingsGeneral: Component = () => { const theme = useTheme() const language = useLanguage() + const platform = usePlatform() const settings = useSettings() + const [store, setStore] = createStore({ + checking: false, + }) + + const check = () => { + if (!platform.checkUpdate) return + setStore("checking", true) + + void platform + .checkUpdate() + .then((result) => { + 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 && platform.restart + ? [ + { + label: language.t("toast.update.action.installRestart"), + onClick: async () => { + await platform.update!() + await platform.restart!() + }, + }, + { + 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)) + } + const themeOptions = createMemo(() => Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })), ) @@ -208,23 +271,6 @@ export const SettingsGeneral: Component = () => { - {/* Updates Section */} -
-

{language.t("settings.general.section.updates")}

- -
- - settings.general.setReleaseNotes(checked)} - /> - -
-
- {/* Sound effects Section */}

{language.t("settings.general.section.sounds")}

@@ -303,6 +349,50 @@ export const SettingsGeneral: Component = () => {
+ + {/* Updates Section */} +
+

{language.t("settings.general.section.updates")}

+ +
+ + settings.updates.setStartup(checked)} + /> + + + + settings.general.setReleaseNotes(checked)} + /> + + + + + +
+
) diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index 67e907a63..19b3846f8 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -20,6 +20,9 @@ export interface Settings { autoSave: boolean releaseNotes: boolean } + updates: { + startup: boolean + } appearance: { fontSize: number font: string @@ -37,6 +40,9 @@ const defaultSettings: Settings = { autoSave: true, releaseNotes: true, }, + updates: { + startup: true, + }, appearance: { fontSize: 14, font: "ibm-plex-mono", @@ -104,6 +110,12 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont setStore("general", "releaseNotes", value) }, }, + updates: { + startup: createMemo(() => store.updates?.startup ?? defaultSettings.updates.startup), + setStartup(value: boolean) { + setStore("updates", "startup", value) + }, + }, appearance: { fontSize: createMemo(() => store.appearance?.fontSize ?? defaultSettings.appearance.fontSize), setFontSize(value: number) { diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index abbe497dc..8fb819798 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -540,6 +540,15 @@ export const dict = { "settings.general.row.releaseNotes.title": "Release notes", "settings.general.row.releaseNotes.description": "Show What's New popups after updates", + + "settings.updates.row.startup.title": "Check for updates on startup", + "settings.updates.row.startup.description": "Automatically check for updates when OpenCode launches", + "settings.updates.row.check.title": "Check for updates", + "settings.updates.row.check.description": "Manually check for updates and install if available", + "settings.updates.action.checkNow": "Check now", + "settings.updates.action.checking": "Checking...", + "settings.updates.toast.latest.title": "You're up to date", + "settings.updates.toast.latest.description": "You're running the latest version of OpenCode.", "font.option.ibmPlexMono": "IBM Plex Mono", "font.option.cascadiaCode": "Cascadia Code", "font.option.firaCode": "Fira Code", diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 15557dedb..82a3fa6c9 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -332,6 +332,7 @@ export default function Layout(props: ParentProps) { if (!platform.checkUpdate || !platform.update || !platform.restart) return let toastId: number | undefined + let interval: ReturnType | undefined async function pollUpdate() { const { updateAvailable, version } = await platform.checkUpdate!() @@ -358,9 +359,25 @@ export default function Layout(props: ParentProps) { } } - pollUpdate() - const interval = setInterval(pollUpdate, 10 * 60 * 1000) - onCleanup(() => clearInterval(interval)) + createEffect(() => { + if (!settings.ready()) return + + if (!settings.updates.startup()) { + if (interval === undefined) return + clearInterval(interval) + interval = undefined + return + } + + if (interval !== undefined) return + void pollUpdate() + interval = setInterval(pollUpdate, 10 * 60 * 1000) + }) + + onCleanup(() => { + if (interval === undefined) return + clearInterval(interval) + }) }) onMount(() => {