Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385import { TextField } from "@opencode/ui/text-field"import type { captureException } from "@sentry/solid"import { Logo } from "@opencode/ui/logo"import { Button } from "@opencode/ui/button"import { Component, createSignal, onMount, Show } from "solid-js"import { createStore } from "solid-js/store"import { usePlatform } from "@/runtime/platform/platform"import { useLanguage } from "@/runtime/i18n/language"import { Icon } from "@opencode/ui/icon"import { errorDescriptionKey, errorStatus } from "./description"
export type InitError = { name: string data: Record<string, unknown>}
type Translator = ReturnType<typeof useLanguage>["t"]const CHAIN_SEPARATOR = "\n" + "─".repeat(40) + "\n"
function isIssue(value: unknown): value is { message: string; path: string[] } { if (!value || typeof value !== "object") return false if (!("message" in value) || !("path" in value)) return false const message = (value as { message: unknown }).message const path = (value as { path: unknown }).path if (typeof message !== "string") return false if (!Array.isArray(path)) return false return path.every((part) => typeof part === "string")}
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 safeJson(value: unknown, circular: string): string { const seen = new WeakSet<object>() const json = JSON.stringify( value, (_key, val) => { if (typeof val === "bigint") return val.toString() if (typeof val === "object" && val) { if (seen.has(val)) return circular seen.add(val) } return val }, 2, ) return json ?? String(value)}
function formatInitError(error: InitError, t: Translator): string { const data = error.data const json = (value: unknown) => safeJson(value, t("error.page.circular")) switch (error.name) { case "MCPFailed": { const name = typeof data.name === "string" ? data.name : "" return t("error.chain.mcpFailed", { name }) } case "ProviderAuthError": { const providerID = typeof data.providerID === "string" ? data.providerID : t("common.unknown") const message = typeof data.message === "string" ? data.message : json(data.message) return t("error.chain.providerAuthFailed", { provider: providerID, message }) } case "APIError": { const message = typeof data.message === "string" ? data.message : t("error.chain.apiError") const lines: string[] = [message]
if (typeof data.statusCode === "number") { lines.push(t("error.chain.status", { status: data.statusCode })) }
if (typeof data.isRetryable === "boolean") { lines.push(t("error.chain.retryable", { retryable: data.isRetryable })) }
if (typeof data.responseBody === "string" && data.responseBody) { lines.push(t("error.chain.responseBody", { body: data.responseBody })) }
return lines.join("\n") } case "ProviderModelNotFoundError": { const { providerID, modelID, suggestions } = data as { providerID: string modelID: string suggestions?: string[] }
const suggestionsLine = Array.isArray(suggestions) && suggestions.length ? [t("error.chain.didYouMean", { suggestions: suggestions.join(", ") })] : []
return [ t("error.chain.modelNotFound", { provider: providerID, model: modelID }), ...suggestionsLine, t("error.chain.checkConfig"), ].join("\n") } case "ProviderInitError": { const providerID = typeof data.providerID === "string" ? data.providerID : t("common.unknown") return t("error.chain.providerInitFailed", { provider: providerID }) } case "ConfigJsonError": { const path = typeof data.path === "string" ? data.path : json(data.path) const message = typeof data.message === "string" ? data.message : "" if (message) return t("error.chain.configJsonInvalidWithMessage", { path, message }) return t("error.chain.configJsonInvalid", { path }) } case "ConfigDirectoryTypoError": { const path = typeof data.path === "string" ? data.path : json(data.path) const dir = typeof data.dir === "string" ? data.dir : json(data.dir) const suggestion = typeof data.suggestion === "string" ? data.suggestion : json(data.suggestion) return t("error.chain.configDirectoryTypo", { dir, path, suggestion }) } case "ConfigFrontmatterError": { const path = typeof data.path === "string" ? data.path : json(data.path) const message = typeof data.message === "string" ? data.message : json(data.message) return t("error.chain.configFrontmatterError", { path, message }) } case "ConfigInvalidError": { const issues = Array.isArray(data.issues) ? data.issues.filter(isIssue).map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) : [] const message = typeof data.message === "string" ? data.message : "" const path = typeof data.path === "string" ? data.path : json(data.path)
const line = message ? t("error.chain.configInvalidWithMessage", { path, message }) : t("error.chain.configInvalid", { path })
return [line, ...issues].join("\n") } case "UnknownError": return typeof data.message === "string" ? data.message : json(data) default: if (typeof data.message === "string") return data.message return json(data) }}
function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessage?: string): string { const json = (value: unknown) => safeJson(value, t("error.page.circular")) if (!error) return t("error.chain.unknown")
if (isInitError(error)) { const message = formatInitError(error, t) if (depth > 0 && parentMessage === message) return "" const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" return indent + `${error.name}\n${message}` }
if (error instanceof Error) { const isDuplicate = depth > 0 && parentMessage === error.message const parts: string[] = [] const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : ""
const header = `${error.name}${error.message ? `: ${error.message}` : ""}` const stack = error.stack?.trim()
if (stack) { const startsWithHeader = stack.startsWith(header)
if (isDuplicate && startsWithHeader) { const trace = stack.split("\n").slice(1).join("\n").trim() if (trace) { parts.push(indent + trace) } }
if (isDuplicate && !startsWithHeader) { parts.push(indent + stack) }
if (!isDuplicate && startsWithHeader) { parts.push(indent + stack) }
if (!isDuplicate && !startsWithHeader) { parts.push(indent + `${header}\n${stack}`) } }
if (!stack && !isDuplicate) { parts.push(indent + header) }
if (error.cause) { const causeResult = formatErrorChain(error.cause, t, depth + 1, error.message) if (causeResult) { parts.push(causeResult) } }
return parts.join("\n\n") }
if (typeof error === "string") { if (depth > 0 && parentMessage === error) return "" const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" return indent + error }
const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" return indent + json(error)}
function formatError(error: unknown, t: Translator): string { return formatErrorChain(error, t, 0)}
interface ErrorPageProps { error: unknown}
export const ErrorPage: Component<ErrorPageProps> = (props) => { const platform = usePlatform() const language = useLanguage() const formattedError = () => formatError(props.error, language.t) const status = () => errorStatus(props.error) let recordedFatalError: Promise<void> | undefined const [store, setStore] = createStore({ actionError: undefined as string | undefined, captureException: undefined as typeof captureException | undefined, })
function ensureFatalErrorRecorded() { recordedFatalError ??= platform.recordFatalRendererError?.({ error: formattedError(), url: location.href, version: platform.version, platform: platform.platform, os: platform.os, }) ?? Promise.resolve() return recordedFatalError }
onMount(() => { void ensureFatalErrorRecorded().catch(() => undefined) void import("@sentry/solid") .then(({ captureException, isEnabled }) => { if (isEnabled()) setStore("captureException", () => captureException) }) .catch(() => undefined) })
async function checkForUpdates() { const state = await platform.updater?.check() setStore("actionError", state?.status === "error" ? state.message : undefined) }
async function installUpdate() { await platform.updater ?.install() .then(() => setStore("actionError", undefined)) .catch((err) => { setStore("actionError", formatError(err, language.t)) }) }
const updateVersion = () => { const state = platform.updater?.state() return state?.status === "ready" || state?.status === "download-required" ? state.version : undefined }
async function exportDebugLogs() { const exportLogs = platform.exportDebugLogs if (!exportLogs) return await ensureFatalErrorRecorded() .then(() => exportLogs()) .then(() => setStore("actionError", undefined)) .catch((err) => { setStore("actionError", formatError(err, language.t)) }) }
return ( <div class="relative flex-1 h-full w-full min-h-0 min-w-0 overflow-y-auto flex flex-col items-center justify-start sm:justify-center p-4 sm:p-8 font-sans" data-tauri-drag-region > <div class="w-full max-w-3xl flex flex-col items-center justify-center gap-6 sm:gap-8 my-auto"> <Logo class="w-48 sm:w-58.5 opacity-12 shrink-0" /> <div class="flex flex-col items-center gap-2 text-center"> <h1 class="text-lg font-medium text-text-strong"> {language.t(status() ? "error.page.title.status" : "error.page.title")} </h1> <p class="text-sm text-text-weak"> {status() ? language.t("error.page.description.status", { status: status()! }) : language.t(errorDescriptionKey(props.error))} </p> </div> <TextField value={formattedError()} readOnly copyable multiline class="max-h-96 w-full font-mono text-xs no-scrollbar" label={language.t("error.page.details.label")} hideLabel /> <div class="flex flex-row items-center justify-center gap-3 flex-wrap max-w-64"> <Button size="large" onClick={platform.restart}> {language.t(platform.platform === "web" ? "error.page.action.reload" : "error.page.action.restart")} </Button> <Show when={platform.platform === "desktop" && platform.exportDebugLogs}> <Button size="large" variant="ghost" onClick={exportDebugLogs}> {language.t("error.page.action.exportLogs")} </Button> </Show> <Show when={store.captureException}> {(capture) => { const [reported, setReported] = createSignal(false) return ( <Button size="large" disabled={reported()} onClick={() => { capture()(props.error) setReported(true) }} > {language.t(reported() ? "error.page.action.reported" : "error.page.action.report")} </Button> ) }} </Show> <Show when={platform.updater}> <Show when={updateVersion()} fallback={ <Button size="large" variant="ghost" onClick={checkForUpdates} disabled={["checking", "downloading", "installing"].includes(platform.updater?.state().status ?? "")} > {platform.updater?.state().status === "checking" ? language.t("error.page.action.checking") : language.t("error.page.action.checkUpdates")} </Button> } > {(version) => ( <Button size="large" onClick={installUpdate}> {language.t("error.page.action.updateTo", { version: version() })} </Button> )} </Show> </Show> </div> <Show when={store.actionError}> {(message) => <p class="text-xs text-text-danger-base text-center max-w-2xl">{message()}</p>} </Show> <div class="flex flex-col items-center gap-2 text-xs text-center"> <div class="flex flex-wrap items-center justify-center gap-1"> {language.t("error.page.report.prefix")} <button type="button" class="flex items-center text-text-interactive-base gap-1" onClick={() => platform.openExternal("https://opencode.ai/desktop-feedback")} > <div>{language.t("error.page.report.discord")}</div> <Icon name="discord" class="text-text-interactive-base" /> </button> </div> <Show when={platform.version}> {(version) => ( <p class="text-xs text-text-weak">{language.t("error.page.version", { version: version() })}</p> )} </Show> </div> </div> </div> )}