diff options
| author | Adam <[email protected]> | 2025-12-19 07:38:33 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-19 07:38:38 -0600 |
| commit | e1ad2a355c84422ef8c6a20f998bc71f2881713a (patch) | |
| tree | b37c2dc4ff16bf6d9fcfd83f35a2ff5aea37d5aa | |
| parent | 4f318f913e034ccd2fe4456461bf56b22a37eed9 (diff) | |
| download | opencode-e1ad2a355c84422ef8c6a20f998bc71f2881713a.tar.gz opencode-e1ad2a355c84422ef8c6a20f998bc71f2881713a.zip | |
fix(desktop): error handling
| -rw-r--r-- | bun.lock | 3 | ||||
| -rw-r--r-- | packages/desktop/src/app.tsx | 2 | ||||
| -rw-r--r-- | packages/desktop/src/context/global-sdk.tsx | 12 | ||||
| -rw-r--r-- | packages/desktop/src/context/global-sync.tsx | 5 | ||||
| -rw-r--r-- | packages/desktop/src/context/platform.tsx | 12 | ||||
| -rw-r--r-- | packages/desktop/src/context/sdk.tsx | 11 | ||||
| -rw-r--r-- | packages/desktop/src/entry.tsx | 3 | ||||
| -rw-r--r-- | packages/desktop/src/pages/error.tsx | 26 | ||||
| -rw-r--r-- | packages/desktop/src/pages/layout.tsx | 7 | ||||
| -rw-r--r-- | packages/tauri/package.json | 1 | ||||
| -rw-r--r-- | packages/tauri/src-tauri/Cargo.lock | 156 | ||||
| -rw-r--r-- | packages/tauri/src-tauri/Cargo.toml | 1 | ||||
| -rw-r--r-- | packages/tauri/src-tauri/capabilities/default.json | 6 | ||||
| -rw-r--r-- | packages/tauri/src-tauri/src/lib.rs | 1 | ||||
| -rw-r--r-- | packages/tauri/src/index.tsx | 10 |
15 files changed, 227 insertions, 29 deletions
@@ -359,6 +359,7 @@ "@solid-primitives/storage": "catalog:", "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "~2", + "@tauri-apps/plugin-http": "~2", "@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-os": "~2", "@tauri-apps/plugin-process": "~2", @@ -1673,6 +1674,8 @@ "@tauri-apps/plugin-dialog": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ=="], + "@tauri-apps/plugin-http": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-/i4U/9za3mrytTgfRn5RHneKubZE/dwRmshYwyMvNRlkWjvu1m4Ma72kcbVJMZFGXpkbl+qLyWMGrihtWB76Zg=="], + "@tauri-apps/plugin-opener": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="], "@tauri-apps/plugin-os": ["@tauri-apps/[email protected]", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="], diff --git a/packages/desktop/src/app.tsx b/packages/desktop/src/app.tsx index 4edbfd8f9..2ed529bbc 100644 --- a/packages/desktop/src/app.tsx +++ b/packages/desktop/src/app.tsx @@ -41,7 +41,7 @@ export function App() { return ( <MetaProvider> <Font /> - <ErrorBoundary fallback={ErrorPage}> + <ErrorBoundary fallback={(error) => <ErrorPage error={error} />}> <DialogProvider> <MarkedProvider> <DiffComponentProvider component={Diff}> diff --git a/packages/desktop/src/context/global-sdk.tsx b/packages/desktop/src/context/global-sdk.tsx index 0d301d2f3..ac6697093 100644 --- a/packages/desktop/src/context/global-sdk.tsx +++ b/packages/desktop/src/context/global-sdk.tsx @@ -1,15 +1,17 @@ import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client" import { createSimpleContext } from "@opencode-ai/ui/context" import { createGlobalEmitter } from "@solid-primitives/event-bus" -import { onCleanup } from "solid-js" +import { usePlatform } from "./platform" export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({ name: "GlobalSDK", init: (props: { url: string }) => { - const abort = new AbortController() + const platform = usePlatform() + const sdk = createOpencodeClient({ baseUrl: props.url, - signal: abort.signal, + signal: AbortSignal.timeout(1000 * 60 * 10), + fetch: platform.fetch, throwOnError: true, }) @@ -24,10 +26,6 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo } }) - onCleanup(() => { - abort.abort() - }) - return { url: props.url, client: sdk, event: emitter } }, }) diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx index 6c6a02432..fffef5b5f 100644 --- a/packages/desktop/src/context/global-sync.tsx +++ b/packages/desktop/src/context/global-sync.tsx @@ -21,6 +21,8 @@ import { Binary } from "@opencode-ai/util/binary" import { useGlobalSDK } from "./global-sdk" import { ErrorPage, type InitError } from "../pages/error" import { createContext, useContext, onMount, type ParentProps, Switch, Match } from "solid-js" +import { showToast } from "@opencode-ai/ui/toast" +import { getFilename } from "@opencode-ai/util/path" type State = { ready: boolean @@ -118,7 +120,8 @@ function createGlobalSync() { }) .catch((err) => { console.error("Failed to load sessions", err) - setGlobalStore("error", err) + const project = getFilename(directory) + showToast({ title: `Failed to load sessions for ${project}`, description: err.message }) }) } diff --git a/packages/desktop/src/context/platform.tsx b/packages/desktop/src/context/platform.tsx index 2ac9f64d4..73d4c7f3e 100644 --- a/packages/desktop/src/context/platform.tsx +++ b/packages/desktop/src/context/platform.tsx @@ -5,6 +5,12 @@ export type Platform = { /** Platform discriminator */ platform: "web" | "tauri" + /** Open a URL in the default browser */ + openLink(url: string): void + + /** Restart the app */ + restart(): Promise<void> + /** Open native directory picker dialog (Tauri only) */ openDirectoryPickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise<string | string[] | null> @@ -14,9 +20,6 @@ export type Platform = { /** Save file picker dialog (Tauri only) */ saveFilePickerDialog?(opts?: { title?: string; defaultPath?: string }): Promise<string | null> - /** Open a URL in the default browser */ - openLink(url: string): void - /** Storage mechanism, defaults to localStorage */ storage?: (name?: string) => SyncStorage | AsyncStorage @@ -25,6 +28,9 @@ export type Platform = { /** Install updates (Tauri only) */ update?(): Promise<void> + + /** Fetch override */ + fetch?: typeof fetch } export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({ diff --git a/packages/desktop/src/context/sdk.tsx b/packages/desktop/src/context/sdk.tsx index 0e556167b..4d1c797c9 100644 --- a/packages/desktop/src/context/sdk.tsx +++ b/packages/desktop/src/context/sdk.tsx @@ -1,17 +1,18 @@ import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client" import { createSimpleContext } from "@opencode-ai/ui/context" import { createGlobalEmitter } from "@solid-primitives/event-bus" -import { onCleanup } from "solid-js" import { useGlobalSDK } from "./global-sdk" +import { usePlatform } from "./platform" export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ name: "SDK", init: (props: { directory: string }) => { + const platform = usePlatform() const globalSDK = useGlobalSDK() - const abort = new AbortController() const sdk = createOpencodeClient({ baseUrl: globalSDK.url, - signal: abort.signal, + signal: AbortSignal.timeout(1000 * 60 * 10), + fetch: platform.fetch, directory: props.directory, throwOnError: true, }) @@ -24,10 +25,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ emitter.emit(event.type, event) }) - onCleanup(() => { - abort.abort() - }) - return { directory: props.directory, client: sdk, event: emitter, url: globalSDK.url } }, }) diff --git a/packages/desktop/src/entry.tsx b/packages/desktop/src/entry.tsx index eec6396e9..ecbce9815 100644 --- a/packages/desktop/src/entry.tsx +++ b/packages/desktop/src/entry.tsx @@ -15,6 +15,9 @@ const platform: Platform = { openLink(url: string) { window.open(url, "_blank") }, + restart: async () => { + window.location.reload() + }, } render( diff --git a/packages/desktop/src/pages/error.tsx b/packages/desktop/src/pages/error.tsx index 66fc81d98..352b9f3e8 100644 --- a/packages/desktop/src/pages/error.tsx +++ b/packages/desktop/src/pages/error.tsx @@ -1,5 +1,6 @@ import { TextField } from "@opencode-ai/ui/text-field" import { Logo } from "@opencode-ai/ui/logo" +import { Button } from "@opencode-ai/ui/button" import { Component } from "solid-js" import { usePlatform } from "@/context/platform" import { Icon } from "@opencode-ai/ui/icon" @@ -9,9 +10,17 @@ export type InitError = { data: Record<string, unknown> } -function formatError(error: InitError | undefined): string { - if (!error) return "Unknown error" +function isInitError(error: unknown): error is InitError { + return ( + typeof error === "object" && + error !== null && + "name" in error && + "data" in error && + typeof (error as InitError).data === "object" + ) +} +function formatInitError(error: InitError): string { const data = error.data switch (error.name) { case "MCPFailed": @@ -53,8 +62,16 @@ function formatError(error: InitError | undefined): string { } } +function formatError(error: unknown): string { + if (!error) return "Unknown error" + if (isInitError(error)) return formatInitError(error) + if (error instanceof Error) return `${error.name}: ${error.message}\n\n${error.stack}` + if (typeof error === "string") return error + return JSON.stringify(error, null, 2) +} + interface ErrorPageProps { - error: InitError | undefined + error: unknown } export const ErrorPage: Component<ErrorPageProps> = (props) => { @@ -76,6 +93,9 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => { label="Error Details" hideLabel /> + <Button size="large" onClick={platform.restart}> + Restart + </Button> <div class="flex items-center justify-center gap-1"> Please report this error to the OpenCode team <button diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index 5f4a5d797..626bceb22 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -69,7 +69,7 @@ export default function Layout(props: ParentProps) { const command = useCommand() onMount(async () => { - if (platform.checkUpdate && platform.update) { + if (platform.checkUpdate && platform.update && platform.restart) { const { updateAvailable, version } = await platform.checkUpdate() if (updateAvailable) { showToast({ @@ -80,7 +80,10 @@ export default function Layout(props: ParentProps) { actions: [ { label: "Install and restart", - onClick: () => platform!.update!(), + onClick: async () => { + await platform.update!() + await platform.restart!() + }, }, { label: "Not yet", diff --git a/packages/tauri/package.json b/packages/tauri/package.json index b23257732..2c920b2e5 100644 --- a/packages/tauri/package.json +++ b/packages/tauri/package.json @@ -22,6 +22,7 @@ "@tauri-apps/plugin-shell": "~2", "@tauri-apps/plugin-store": "~2", "@tauri-apps/plugin-updater": "~2", + "@tauri-apps/plugin-http": "~2", "@tauri-apps/plugin-window-state": "~2", "solid-js": "catalog:" }, diff --git a/packages/tauri/src-tauri/Cargo.lock b/packages/tauri/src-tauri/Cargo.lock index fc578470b..0bf5f7013 100644 --- a/packages/tauri/src-tauri/Cargo.lock +++ b/packages/tauri/src-tauri/Cargo.lock @@ -553,11 +553,40 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ + "percent-encoding", "time", "version_check", ] [[package]] +name = "cookie_store" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] name = "core-foundation" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -580,7 +609,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", "foreign-types", "libc", @@ -593,7 +622,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -719,6 +748,12 @@ dependencies = [ ] [[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] name = "deranged" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -845,6 +880,15 @@ dependencies = [ ] [[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] name = "downcast-rs" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1533,6 +1577,25 @@ dependencies = [ ] [[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.12.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] name = "half" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1650,6 +1713,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1697,9 +1761,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2112,6 +2178,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2685,6 +2757,7 @@ dependencies = [ "tauri-build", "tauri-plugin-clipboard-manager", "tauri-plugin-dialog", + "tauri-plugin-http", "tauri-plugin-opener", "tauri-plugin-os", "tauri-plugin-process", @@ -3144,6 +3217,22 @@ dependencies = [ ] [[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + +[[package]] name = "pxfm" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3439,8 +3528,12 @@ checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64 0.22.1", "bytes", + "cookie", + "cookie_store", + "encoding_rs", "futures-core", "futures-util", + "h2", "http", "http-body", "http-body-util", @@ -3449,6 +3542,7 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime", "percent-encoding", "pin-project-lite", "quinn", @@ -4114,6 +4208,27 @@ dependencies = [ ] [[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] name = "system-deps" version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4134,7 +4249,7 @@ checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" dependencies = [ "bitflags 2.10.0", "block2 0.6.2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dispatch", @@ -4381,6 +4496,30 @@ dependencies = [ ] [[package]] +name = "tauri-plugin-http" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00685aceab12643cf024f712ab0448ba8fcadf86f2391d49d2e5aa732aacc70" +dependencies = [ + "bytes", + "cookie_store", + "data-url", + "http", + "regex", + "reqwest", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.17", + "tokio", + "url", + "urlpattern", +] + +[[package]] name = "tauri-plugin-opener" version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5622,6 +5761,17 @@ dependencies = [ ] [[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/packages/tauri/src-tauri/Cargo.toml b/packages/tauri/src-tauri/Cargo.toml index 58b3d107b..c6208190b 100644 --- a/packages/tauri/src-tauri/Cargo.toml +++ b/packages/tauri/src-tauri/Cargo.toml @@ -27,6 +27,7 @@ tauri-plugin-process = "2" tauri-plugin-store = "2" tauri-plugin-window-state = "2" tauri-plugin-clipboard-manager = "2" +tauri-plugin-http = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/packages/tauri/src-tauri/capabilities/default.json b/packages/tauri/src-tauri/capabilities/default.json index 91af8cbdc..c805f623b 100644 --- a/packages/tauri/src-tauri/capabilities/default.json +++ b/packages/tauri/src-tauri/capabilities/default.json @@ -14,6 +14,10 @@ "process:default", "store:default", "window-state:default", - "os:default" + "os:default", + { + "identifier": "http:default", + "allow": [{ "url": "http://*" }, { "url": "https://*" }, { "url": "http://*:*/*" }] + } ] } diff --git a/packages/tauri/src-tauri/src/lib.rs b/packages/tauri/src-tauri/src/lib.rs index 1bf1427a2..ffefbabf6 100644 --- a/packages/tauri/src-tauri/src/lib.rs +++ b/packages/tauri/src-tauri/src/lib.rs @@ -190,6 +190,7 @@ pub fn run() { .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_clipboard_manager::init()) + .plugin(tauri_plugin_http::init()) .plugin(PinchZoomDisablePlugin) .invoke_handler(tauri::generate_handler