Преглед на файлове

fix(desktop): error handling

Adam преди 3 месеца
родител
ревизия
e1ad2a355c

+ 3 - 0
bun.lock

@@ -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=="],

+ 1 - 1
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}>

+ 5 - 7
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 }
   },
 })

+ 4 - 1
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 })
       })
   }
 

+ 9 - 3
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({

+ 4 - 7
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 }
   },
 })

+ 3 - 0
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(

+ 23 - 3
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

+ 5 - 2
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",

+ 1 - 0
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:"
   },

+ 153 - 3
packages/tauri/src-tauri/Cargo.lock

@@ -553,10 +553,39 @@ 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"
@@ -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",
 ]
 
@@ -718,6 +747,12 @@ dependencies = [
  "syn 2.0.110",
 ]
 
+[[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"
@@ -844,6 +879,15 @@ dependencies = [
  "syn 2.0.110",
 ]
 
+[[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"
@@ -1532,6 +1576,25 @@ dependencies = [
  "syn 2.0.110",
 ]
 
+[[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"
@@ -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]]
@@ -2111,6 +2177,12 @@ version = "0.8.1"
 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"
@@ -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",
@@ -3143,6 +3216,22 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[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"
@@ -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",
@@ -4113,6 +4207,27 @@ dependencies = [
  "libc",
 ]
 
+[[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"
@@ -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",
@@ -4380,6 +4495,30 @@ dependencies = [
  "url",
 ]
 
+[[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"
@@ -5621,6 +5760,17 @@ dependencies = [
  "windows-link 0.1.3",
 ]
 
+[[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"

+ 1 - 0
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"

+ 5 - 1
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://*:*/*" }]
+    }
   ]
 }

+ 1 - 0
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![
             kill_sidecar,

+ 9 - 1
packages/tauri/src/index.tsx

@@ -5,6 +5,8 @@ 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 { fetch as tauriFetch } from "@tauri-apps/plugin-http"
+import { Store } from "@tauri-apps/plugin-store"
 
 import { UPDATER_ENABLED } from "./updater"
 import { createMenu } from "./menu"
@@ -57,7 +59,7 @@ const platform: Platform = {
   storage: (name = "default.dat") => {
     const api: AsyncStorage = {
       _store: null,
-      _getStore: async () => api._store || (api._store = (await import("@tauri-apps/plugin-store")).Store.load(name)),
+      _getStore: async () => api._store || (api._store = Store.load(name)),
       getItem: async (key: string) => (await (await api._getStore()).get(key)) ?? null,
       setItem: async (key: string, value: string) => await (await api._getStore()).set(key, value),
       removeItem: async (key: string) => await (await api._getStore()).delete(key),
@@ -82,9 +84,15 @@ const platform: Platform = {
   update: async () => {
     if (!UPDATER_ENABLED || !update) return
     await update.install()
+  },
+
+  restart: async () => {
     await invoke("kill_sidecar")
     await relaunch()
   },
+
+  // @ts-expect-error
+  fetch: tauriFetch,
 }
 
 createMenu()