diff options
| author | Adam <[email protected]> | 2025-12-18 04:26:17 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-18 04:26:21 -0600 |
| commit | e37a75a411c584d3e463662a0219fd342d4247ab (patch) | |
| tree | eb3944722e942b3244c4f13466855fca712bc3c6 | |
| parent | 194ff4919cd1e68caaaa8336da0b13b815c74072 (diff) | |
| download | opencode-e37a75a411c584d3e463662a0219fd342d4247ab.tar.gz opencode-e37a75a411c584d3e463662a0219fd342d4247ab.zip | |
feat(desktop): custom update toast
| -rw-r--r-- | packages/desktop/src/app.tsx | 91 | ||||
| -rw-r--r-- | packages/desktop/src/context/platform.tsx | 6 | ||||
| -rw-r--r-- | packages/desktop/src/pages/layout.tsx | 55 | ||||
| -rw-r--r-- | packages/tauri/src/index.tsx | 27 | ||||
| -rw-r--r-- | packages/ui/src/components/icon.tsx | 1 | ||||
| -rw-r--r-- | packages/ui/src/components/toast.css | 6 | ||||
| -rw-r--r-- | packages/ui/src/components/toast.tsx | 15 |
7 files changed, 135 insertions, 66 deletions
diff --git a/packages/desktop/src/app.tsx b/packages/desktop/src/app.tsx index 10bde2202..91952fc9d 100644 --- a/packages/desktop/src/app.tsx +++ b/packages/desktop/src/app.tsx @@ -1,5 +1,5 @@ import "@/index.css" -import { Show } from "solid-js" +import { ErrorBoundary, Show } from "solid-js" import { Router, Route, Navigate } from "@solidjs/router" import { MetaProvider } from "@solidjs/meta" import { Font } from "@opencode-ai/ui/font" @@ -20,6 +20,7 @@ import Layout from "@/pages/layout" import Home from "@/pages/home" import DirectoryLayout from "@/pages/directory-layout" import Session from "@/pages/session" +import { ErrorPage } from "./pages/error" declare global { interface Window { @@ -38,48 +39,50 @@ const url = export function App() { return ( - <MetaProvider> - <Font /> - <DialogProvider> - <MarkedProvider> - <DiffComponentProvider component={Diff}> - <CodeComponentProvider component={Code}> - <GlobalSDKProvider url={url}> - <GlobalSyncProvider> - <LayoutProvider> - <NotificationProvider> - <Router - root={(props) => ( - <CommandProvider> - <Layout>{props.children}</Layout> - </CommandProvider> - )} - > - <Route path="/" component={Home} /> - <Route path="/:dir" component={DirectoryLayout}> - <Route path="/" component={() => <Navigate href="session" />} /> - <Route - path="/session/:id?" - component={(p) => ( - <Show when={p.params.id || true} keyed> - <TerminalProvider> - <PromptProvider> - <Session /> - </PromptProvider> - </TerminalProvider> - </Show> - )} - /> - </Route> - </Router> - </NotificationProvider> - </LayoutProvider> - </GlobalSyncProvider> - </GlobalSDKProvider> - </CodeComponentProvider> - </DiffComponentProvider> - </MarkedProvider> - </DialogProvider> - </MetaProvider> + <ErrorBoundary fallback={ErrorPage}> + <MetaProvider> + <Font /> + <DialogProvider> + <MarkedProvider> + <DiffComponentProvider component={Diff}> + <CodeComponentProvider component={Code}> + <GlobalSDKProvider url={url}> + <GlobalSyncProvider> + <LayoutProvider> + <NotificationProvider> + <Router + root={(props) => ( + <CommandProvider> + <Layout>{props.children}</Layout> + </CommandProvider> + )} + > + <Route path="/" component={Home} /> + <Route path="/:dir" component={DirectoryLayout}> + <Route path="/" component={() => <Navigate href="session" />} /> + <Route + path="/session/:id?" + component={(p) => ( + <Show when={p.params.id || true} keyed> + <TerminalProvider> + <PromptProvider> + <Session /> + </PromptProvider> + </TerminalProvider> + </Show> + )} + /> + </Route> + </Router> + </NotificationProvider> + </LayoutProvider> + </GlobalSyncProvider> + </GlobalSDKProvider> + </CodeComponentProvider> + </DiffComponentProvider> + </MarkedProvider> + </DialogProvider> + </MetaProvider> + </ErrorBoundary> ) } diff --git a/packages/desktop/src/context/platform.tsx b/packages/desktop/src/context/platform.tsx index 92bb2ba15..2ac9f64d4 100644 --- a/packages/desktop/src/context/platform.tsx +++ b/packages/desktop/src/context/platform.tsx @@ -19,6 +19,12 @@ export type Platform = { /** Storage mechanism, defaults to localStorage */ storage?: (name?: string) => SyncStorage | AsyncStorage + + /** Check for updates (Tauri only) */ + checkUpdate?(): Promise<{ updateAvailable: boolean; version?: string }> + + /** Install updates (Tauri only) */ + update?(): Promise<void> } export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({ diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index 540c5d778..fccb6d595 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -1,4 +1,15 @@ -import { createEffect, createMemo, createSignal, For, Match, ParentProps, Show, Switch, type JSX } from "solid-js" +import { + createEffect, + createMemo, + createSignal, + For, + Match, + onMount, + ParentProps, + Show, + Switch, + type JSX, +} from "solid-js" import { DateTime } from "luxon" import { A, useNavigate, useParams } from "@solidjs/router" import { useLayout, getAvatarColors } from "@/context/layout" @@ -28,7 +39,7 @@ import { } from "@thisbeyond/solid-dnd" import type { DragEvent } from "@thisbeyond/solid-dnd" import { useProviders } from "@/hooks/use-providers" -import { Toast } from "@opencode-ai/ui/toast" +import { showToast, Toast } from "@opencode-ai/ui/toast" import { useGlobalSDK } from "@/context/global-sdk" import { useNotification } from "@/context/notification" import { Binary } from "@opencode-ai/util/binary" @@ -46,14 +57,6 @@ export default function Layout(props: ParentProps) { let scrollContainerRef: HTMLDivElement | undefined - function scrollToSession(sessionId: string) { - if (!scrollContainerRef) return - const element = scrollContainerRef.querySelector(`[data-session-id="${sessionId}"]`) - if (element) { - element.scrollIntoView({ block: "center", behavior: "smooth" }) - } - } - const params = useParams() const globalSDK = useGlobalSDK() const globalSync = useGlobalSync() @@ -65,6 +68,30 @@ export default function Layout(props: ParentProps) { const dialog = useDialog() const command = useCommand() + onMount(async () => { + if (platform.checkUpdate && platform.update) { + const { updateAvailable, version } = await platform.checkUpdate() + if (updateAvailable) { + showToast({ + persistent: true, + icon: "download", + title: "Update available", + description: `A new version of OpenCode (${version}) is now available to install.`, + actions: [ + { + label: "Install and restart", + onClick: () => platform!.update!(), + }, + { + label: "Not yet", + onClick: "dismiss", + }, + ], + }) + } + } + }) + function flattenSessions(sessions: Session[]): Session[] { const childrenMap = new Map<string, Session[]>() for (const session of sessions) { @@ -87,6 +114,14 @@ export default function Layout(props: ParentProps) { return result } + function scrollToSession(sessionId: string) { + if (!scrollContainerRef) return + const element = scrollContainerRef.querySelector(`[data-session-id="${sessionId}"]`) + if (element) { + element.scrollIntoView({ block: "center", behavior: "smooth" }) + } + } + const currentSessions = createMemo(() => { if (!params.dir) return [] const directory = base64Decode(params.dir) diff --git a/packages/tauri/src/index.tsx b/packages/tauri/src/index.tsx index c77058eb9..025dfd540 100644 --- a/packages/tauri/src/index.tsx +++ b/packages/tauri/src/index.tsx @@ -1,14 +1,16 @@ // @refresh reload import { render } from "solid-js/web" import { App, PlatformProvider, Platform } from "@opencode-ai/desktop" -import { onMount } from "solid-js" import { open, save } from "@tauri-apps/plugin-dialog" import { open as shellOpen } from "@tauri-apps/plugin-shell" import { type as ostype } from "@tauri-apps/plugin-os" import { AsyncStorage } from "@solid-primitives/storage" -import { runUpdater, UPDATER_ENABLED } from "./updater" +import { UPDATER_ENABLED } from "./updater" import { createMenu } from "./menu" +import { check, Update } from "@tauri-apps/plugin-updater" +import { invoke } from "@tauri-apps/api/core" +import { relaunch } from "@tauri-apps/plugin-process" const root = document.getElementById("root") if (import.meta.env.DEV && !(root instanceof HTMLElement)) { @@ -17,6 +19,8 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) { ) } +let update: Update | null = null + const platform: Platform = { platform: "tauri", @@ -66,15 +70,26 @@ const platform: Platform = { } return api }, + + checkUpdate: async () => { + if (!UPDATER_ENABLED) return { updateAvailable: false } + update = await check() + if (!update) return { updateAvailable: false } + await update.download() + return { updateAvailable: true, version: update.version } + }, + + update: async () => { + if (!UPDATER_ENABLED || !update) return + await update.install() + await invoke("kill_sidecar") + await relaunch() + }, } createMenu() render(() => { - onMount(() => { - if (UPDATER_ENABLED) runUpdater({ alertOnFail: false }) - }) - return ( <PlatformProvider value={platform}> {ostype() === "macos" && ( diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index 94d9544d6..8642be0f8 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -53,6 +53,7 @@ const icons = { check: `<path d="M5 11.9657L8.37838 14.7529L15 5.83398" stroke="currentColor" stroke-linecap="square"/>`, photo: `<path d="M16.6665 16.6666L11.6665 11.6666L9.99984 13.3333L6.6665 9.99996L3.08317 13.5833M2.9165 2.91663H17.0832V17.0833H2.9165V2.91663ZM13.3332 7.49996C13.3332 8.30537 12.6803 8.95829 11.8748 8.95829C11.0694 8.95829 10.4165 8.30537 10.4165 7.49996C10.4165 6.69454 11.0694 6.04163 11.8748 6.04163C12.6803 6.04163 13.3332 6.69454 13.3332 7.49996Z" stroke="currentColor" stroke-linecap="square"/>`, share: `<path d="M10.0013 12.0846L10.0013 3.33464M13.7513 6.66797L10.0013 2.91797L6.2513 6.66797M17.0846 10.418V17.0846H2.91797V10.418" stroke="currentColor" stroke-linecap="square"/>`, + download: `<path d="M13.9583 10.6257L10 14.584L6.04167 10.6257M10 2.08398V13.959M16.25 17.9173H3.75" stroke="currentColor" stroke-linecap="square"/>`, } export interface IconProps extends ComponentProps<"svg"> { diff --git a/packages/ui/src/components/toast.css b/packages/ui/src/components/toast.css index 374dd6523..8f6cf1941 100644 --- a/packages/ui/src/components/toast.css +++ b/packages/ui/src/components/toast.css @@ -134,7 +134,7 @@ padding: 0; cursor: pointer; - color: var(--text-invert-strong); + color: var(--text-invert-weak); font-family: var(--font-family-sans); font-size: var(--font-size-base); font-weight: var(--font-weight-medium); @@ -145,8 +145,8 @@ text-decoration: underline; } - &:last-child { - color: var(--text-invert-weak); + &:first-child { + color: var(--text-invert-strong); } } diff --git a/packages/ui/src/components/toast.tsx b/packages/ui/src/components/toast.tsx index 5869f8a6b..c1a29cd04 100644 --- a/packages/ui/src/components/toast.tsx +++ b/packages/ui/src/components/toast.tsx @@ -91,7 +91,7 @@ export type ToastVariant = "default" | "success" | "error" | "loading" export interface ToastAction { label: string - onClick: () => void + onClick: "dismiss" | (() => void) } export interface ToastOptions { @@ -100,13 +100,19 @@ export interface ToastOptions { icon?: IconProps["name"] variant?: ToastVariant duration?: number + persistent?: boolean actions?: ToastAction[] } export function showToast(options: ToastOptions | string) { const opts = typeof options === "string" ? { description: options } : options return toaster.show((props) => ( - <Toast toastId={props.toastId} duration={opts.duration} data-variant={opts.variant ?? "default"}> + <Toast + toastId={props.toastId} + duration={opts.duration} + persistent={opts.persistent} + data-variant={opts.variant ?? "default"} + > <Show when={opts.icon}> <Toast.Icon name={opts.icon!} /> </Show> @@ -120,7 +126,10 @@ export function showToast(options: ToastOptions | string) { <Show when={opts.actions?.length}> <Toast.Actions> {opts.actions!.map((action) => ( - <button data-slot="toast-action" onClick={action.onClick}> + <button + data-slot="toast-action" + onClick={typeof action.onClick === "function" ? action.onClick : () => toaster.dismiss(props.toastId)} + > {action.label} </button> ))} |
