fix(tui): show config error details on startup (#27803)

This commit is contained in:
Kit Langton
2026-05-15 20:42:56 -04:00
committed by GitHub
parent 2385123f03
commit 5911bd532d
6 changed files with 185 additions and 18 deletions

View File

@@ -1,3 +1,5 @@
import { FormatError } from "@/cli/error"
/**
* Aggregate Promise.allSettled results into a single Error that names every
* failed endpoint, or return null when all fulfilled. Used at TUI bootstrap
@@ -15,7 +17,19 @@ export function aggregateFailures(labeled: LabeledSettled[]): Error | null {
)
if (failed.length === 0) return null
const reasons = failed.map((f) => `${f.name}: ${reasonMessage(f.result.reason)}`).join("; ")
const reasons = Array.from(
failed
.map((f) => ({ name: f.name, message: reasonMessage(f.result.reason) }))
.reduce((grouped, failure) => {
grouped.set(failure.message, [...(grouped.get(failure.message) ?? []), failure.name])
return grouped
}, new Map<string, string[]>())
.entries(),
)
.map(([message, names]) =>
names.length === 1 ? `${names[0]}: ${message}` : `${message}\nAffected startup requests: ${names.join(", ")}`,
)
.join("; ")
const summary = `${failed.length} of ${labeled.length} requests failed: ${reasons}`
const err = new Error(summary)
err.cause = { failures: failed.map((f) => ({ name: f.name, reason: f.result.reason })) }
@@ -23,6 +37,9 @@ export function aggregateFailures(labeled: LabeledSettled[]): Error | null {
}
function reasonMessage(reason: unknown): string {
const formatted = FormatError(reason)
if (formatted) return formatted
if (reason instanceof Error) return reason.message
if (typeof reason === "string") return reason
if (reason && typeof reason === "object") {

View File

@@ -2,16 +2,9 @@ import { NamedError } from "@opencode-ai/core/util/error"
import { errorFormat } from "@/util/error"
import { isRecord } from "@/util/record"
interface ErrorLike {
name?: string
_tag?: string
message?: string
data?: Record<string, unknown>
}
type ConfigIssue = { message: string; path: string[] }
function isTaggedError(error: unknown, tag: string): boolean {
function isTaggedError(error: unknown, tag: string): error is Record<string, unknown> {
return isRecord(error) && error._tag === tag
}
@@ -39,22 +32,27 @@ function configIssues(input: Record<string, unknown>): ConfigIssue[] {
: []
}
export function FormatError(input: unknown) {
export function FormatError(input: unknown): string | undefined {
if (input instanceof Error && isRecord(input.cause) && "body" in input.cause) {
const formatted = FormatError(input.cause.body)
if (formatted) return formatted
}
// CliError: domain failure surfaced from an effectCmd handler via fail("...")
if (isTaggedError(input, "CliError")) {
const data = input as ErrorLike & { exitCode?: number }
if (data.exitCode != null) process.exitCode = data.exitCode
return data.message ?? ""
if (typeof input.exitCode === "number") process.exitCode = input.exitCode
return stringField(input, "message") ?? ""
}
// MCPFailed: { name: string }
if (NamedError.hasName(input, "MCPFailed")) {
return `MCP server "${(input as ErrorLike).data?.name}" failed. Note, opencode does not support MCP authentication yet.`
const data = isRecord(input) && isRecord(input.data) ? stringField(input.data, "name") : undefined
return `MCP server "${data}" failed. Note, opencode does not support MCP authentication yet.`
}
// AccountServiceError, AccountTransportError: TaggedErrorClass
if (isTaggedError(input, "AccountServiceError") || isTaggedError(input, "AccountTransportError")) {
return (input as ErrorLike).message ?? ""
return stringField(input, "message") ?? ""
}
// ProviderModelNotFoundError: { providerID: string, modelID: string, suggestions?: string[] }
@@ -64,7 +62,7 @@ export function FormatError(input: unknown) {
? providerModelNotFound.suggestions.filter((x) => typeof x === "string")
: []
return [
`Model not found: ${providerModelNotFound.providerID}/${providerModelNotFound.modelID}`,
`Model not found: ${stringField(providerModelNotFound, "providerID")}/${stringField(providerModelNotFound, "modelID")}`,
...(suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []),
`Try: \`opencode models\` to list available models`,
`Or check your config (opencode.json) provider/model names`,
@@ -112,6 +110,7 @@ export function FormatError(input: unknown) {
if (isTaggedError(input, "UICancelledError") || NamedError.hasName(input, "UICancelledError")) {
return ""
}
return undefined
}
export function FormatUnknownError(input: unknown): string {