refactor(tui): centralize application exit (#31524)
This commit is contained in:
parent
960eacebcf
commit
37522185d3
32 changed files with 176 additions and 1217 deletions
|
|
@ -1,51 +0,0 @@
|
|||
import { cliErrorMessage } from "../util/error"
|
||||
|
||||
/**
|
||||
* Aggregate Promise.allSettled results into a single Error that names every
|
||||
* failed endpoint, or return null when all fulfilled. Used at TUI bootstrap
|
||||
* boundaries so a single 4xx doesn't drown its parallel siblings as
|
||||
* unhandled rejections — every failure surfaces in one labeled message.
|
||||
*/
|
||||
export type LabeledSettled = {
|
||||
name: string
|
||||
result: PromiseSettledResult<unknown>
|
||||
}
|
||||
|
||||
export function aggregateFailures(labeled: LabeledSettled[]): Error | null {
|
||||
const failed = labeled.filter(
|
||||
(x): x is { name: string; result: PromiseRejectedResult } => x.result.status === "rejected",
|
||||
)
|
||||
if (failed.length === 0) return null
|
||||
|
||||
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 })) }
|
||||
return err
|
||||
}
|
||||
|
||||
function reasonMessage(reason: unknown): string {
|
||||
const formatted = cliErrorMessage(reason)
|
||||
if (formatted) return formatted
|
||||
|
||||
if (reason instanceof Error) return reason.message
|
||||
if (typeof reason === "string") return reason
|
||||
if (reason && typeof reason === "object") {
|
||||
const obj = reason as { message?: unknown; name?: unknown }
|
||||
if (typeof obj.message === "string") return obj.message
|
||||
if (typeof obj.name === "string") return obj.name
|
||||
}
|
||||
return String(reason)
|
||||
}
|
||||
8
packages/tui/src/context/exit.tsx
Normal file
8
packages/tui/src/context/exit.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { createSimpleContext } from "./helper"
|
||||
|
||||
export type Exit = (reason?: unknown) => void
|
||||
|
||||
export const { use: useExit, provider: ExitProvider } = createSimpleContext({
|
||||
name: "Exit",
|
||||
init: (input: { exit: Exit }) => input.exit,
|
||||
})
|
||||
|
|
@ -26,13 +26,11 @@ import { useEvent } from "./event"
|
|||
import { useSDK } from "./sdk"
|
||||
import { useTuiStartup } from "./runtime"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { useRenderer } from "@opentui/solid"
|
||||
import { useExit } from "./exit"
|
||||
import { useArgs } from "./args"
|
||||
import { batch, onMount } from "solid-js"
|
||||
import path from "path"
|
||||
import { aggregateFailures } from "./aggregate-failures"
|
||||
import { useKV } from "./kv"
|
||||
import { destroyRenderer } from "../util/renderer"
|
||||
|
||||
const emptyConsoleState: ConsoleState = {
|
||||
consoleManagedProviders: [],
|
||||
|
|
@ -424,7 +422,7 @@ export const {
|
|||
}
|
||||
})
|
||||
|
||||
const renderer = useRenderer()
|
||||
const exit = useExit()
|
||||
const args = useArgs()
|
||||
|
||||
async function bootstrap(input: { fatal?: boolean } = {}) {
|
||||
|
|
@ -442,23 +440,14 @@ export const {
|
|||
.catch(() => emptyConsoleState)
|
||||
const agentsPromise = sdk.client.app.agents({ workspace }, { throwOnError: true })
|
||||
const configPromise = sdk.client.config.get({ workspace }, { throwOnError: true })
|
||||
const blockingRequests: { name: string; promise: Promise<unknown> }[] = [
|
||||
{ name: "config.providers", promise: providersPromise },
|
||||
{ name: "provider.list", promise: providerListPromise },
|
||||
{ name: "app.agents", promise: agentsPromise },
|
||||
{ name: "config.get", promise: configPromise },
|
||||
{ name: "project.sync", promise: projectPromise },
|
||||
...(args.continue ? [{ name: "session.list", promise: sessionListPromise }] : []),
|
||||
]
|
||||
|
||||
await Promise.allSettled(blockingRequests.map((r) => r.promise))
|
||||
.then((settled) => {
|
||||
// Surface every failed endpoint in one labeled message instead of
|
||||
// letting the first rejection drown its siblings as unhandled
|
||||
// rejections.
|
||||
const failure = aggregateFailures(blockingRequests.map((r, i) => ({ name: r.name, result: settled[i] })))
|
||||
if (failure) throw failure
|
||||
})
|
||||
await Promise.all([
|
||||
providersPromise,
|
||||
providerListPromise,
|
||||
agentsPromise,
|
||||
configPromise,
|
||||
projectPromise,
|
||||
...(args.continue ? [sessionListPromise] : []),
|
||||
])
|
||||
.then(async () => {
|
||||
const providersResponse = providersPromise.then((x) => x.data!)
|
||||
const providerListResponse = providerListPromise.then((x) => x.data!)
|
||||
|
|
@ -523,7 +512,7 @@ export const {
|
|||
stack: e instanceof Error ? e.stack : undefined,
|
||||
})
|
||||
if (fatal) {
|
||||
destroyRenderer(renderer)
|
||||
exit(e)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue