summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-18 04:26:17 -0600
committerAdam <[email protected]>2025-12-18 04:26:21 -0600
commite37a75a411c584d3e463662a0219fd342d4247ab (patch)
treeeb3944722e942b3244c4f13466855fca712bc3c6
parent194ff4919cd1e68caaaa8336da0b13b815c74072 (diff)
downloadopencode-e37a75a411c584d3e463662a0219fd342d4247ab.tar.gz
opencode-e37a75a411c584d3e463662a0219fd342d4247ab.zip
feat(desktop): custom update toast
-rw-r--r--packages/desktop/src/app.tsx91
-rw-r--r--packages/desktop/src/context/platform.tsx6
-rw-r--r--packages/desktop/src/pages/layout.tsx55
-rw-r--r--packages/tauri/src/index.tsx27
-rw-r--r--packages/ui/src/components/icon.tsx1
-rw-r--r--packages/ui/src/components/toast.css6
-rw-r--r--packages/ui/src/components/toast.tsx15
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>
))}