import { TextField } from "@opencode-ai/ui/text-field" import { Logo } from "@opencode-ai/ui/logo" import { Button } from "@opencode-ai/ui/button" import { Component, Show } from "solid-js" import { createStore } from "solid-js/store" import { usePlatform } from "@/context/platform" import { useLanguage } from "@/context/language" import { Icon } from "@opencode-ai/ui/icon" export type InitError = { name: string data: Record } type Translator = ReturnType["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): string { const seen = new WeakSet() 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 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 : "unknown" const message = typeof data.message === "string" ? data.message : safeJson(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 : "unknown" return t("error.chain.providerInitFailed", { provider: providerID }) } case "ConfigJsonError": { const path = typeof data.path === "string" ? data.path : safeJson(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 : safeJson(data.path) const dir = typeof data.dir === "string" ? data.dir : safeJson(data.dir) const suggestion = typeof data.suggestion === "string" ? data.suggestion : safeJson(data.suggestion) return t("error.chain.configDirectoryTypo", { dir, path, suggestion }) } case "ConfigFrontmatterError": { const path = typeof data.path === "string" ? data.path : safeJson(data.path) const message = typeof data.message === "string" ? data.message : safeJson(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 : safeJson(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 : safeJson(data) default: if (typeof data.message === "string") return data.message return safeJson(data) } } function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessage?: string): string { 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 + safeJson(error) } function formatError(error: unknown, t: Translator): string { return formatErrorChain(error, t, 0) } interface ErrorPageProps { error: unknown } export const ErrorPage: Component = (props) => { const platform = usePlatform() const language = useLanguage() const [store, setStore] = createStore({ checking: false, version: undefined as string | undefined, actionError: undefined as string | undefined, }) async function checkForUpdates() { if (!platform.checkUpdate) return setStore("checking", true) await platform .checkUpdate() .then((result) => { setStore("actionError", undefined) if (result.updateAvailable && result.version) setStore("version", result.version) }) .catch((err) => { setStore("actionError", formatError(err, language.t)) }) .finally(() => { setStore("checking", false) }) } async function installUpdate() { if (!platform.update || !platform.restart) return await platform .update() .then(() => platform.restart!()) .then(() => setStore("actionError", undefined)) .catch((err) => { setStore("actionError", formatError(err, language.t)) }) } return (

{language.t("error.page.title")}

{language.t("error.page.description")}

{store.checking ? language.t("error.page.action.checking") : language.t("error.page.action.checkUpdates")} } >
{(message) =>

{message()}

}
{language.t("error.page.report.prefix")}
{(version) => (

{language.t("error.page.version", { version: version() })}

)}
) }