refactor(tui): centralize application exit (#31524)

This commit is contained in:
Dax
2026-06-09 11:46:02 -04:00
committed by GitHub
parent 960eacebcf
commit 37522185d3
32 changed files with 176 additions and 1217 deletions
+111 -92
View File
@@ -5,6 +5,7 @@ import { Global } from "@opencode-ai/core/global"
import { Flag } from "@opencode-ai/core/flag/flag"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { ClipboardProvider, useClipboard } from "./context/clipboard"
import { ExitProvider, useExit } from "./context/exit"
import { EpilogueProvider } from "./context/epilogue"
import * as Selection from "./util/selection"
import { createCliRenderer, MouseButton, type CliRenderer } from "@opentui/core"
@@ -80,6 +81,7 @@ import { createTuiAttention } from "./attention"
import * as TuiAudio from "./audio"
import { win32DisableProcessedInput, win32FlushInputBuffer } from "./terminal-win32"
import { destroyRenderer } from "./util/renderer"
import { cliErrorMessage, errorFormat } from "./util/error"
const appGlobalBindingCommands = [
"session.list",
@@ -175,8 +177,8 @@ function isVersionGreater(left: string, right: string) {
export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
const global = yield* Global.Service
const epilogue = { value: undefined as string | undefined }
const output = yield* Effect.scoped(
const exit = { epilogue: undefined as string | undefined, reason: undefined as unknown }
yield* Effect.scoped(
Effect.gen(function* () {
const renderer = yield* Effect.acquireRelease(
Effect.tryPromise(() =>
@@ -194,7 +196,10 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
},
}),
),
(renderer) => Effect.sync(() => destroyRenderer(renderer)),
(renderer) =>
Effect.sync(() => {
destroyRenderer(renderer)
}),
)
win32DisableProcessedInput()
const keymap = createDefaultOpenTuiKeymap(renderer)
@@ -212,7 +217,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
}),
)
yield* Effect.addFinalizer(() => Effect.sync(TuiAudio.dispose))
const shutdown = yield* Deferred.make<void>()
const shutdown = yield* Deferred.make<unknown>()
const onSighup = () => destroyRenderer(renderer)
yield* Effect.acquireRelease(
Effect.sync(() => process.on("SIGHUP", onSighup)),
@@ -229,103 +234,116 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
await render(() => {
return (
<ErrorBoundary fallback={(error, reset) => <ErrorComponent error={error} reset={reset} mode={mode} />}>
<TuiPathsProvider
value={{
cwd: process.cwd(),
home: global.home,
state: global.state,
worktree: global.data + "/worktree",
}}
>
<TuiTerminalEnvironmentProvider
value={{
platform: process.platform,
multiplexer: process.env.TMUX ? "tmux" : process.env.STY ? "screen" : undefined,
displayServer: process.env.WAYLAND_DISPLAY ? "wayland" : process.env.DISPLAY ? "x11" : undefined,
}}
>
<TuiStartupProvider
<ExitProvider
exit={(reason) => {
if (renderer.isDestroyed) return
exit.reason = reason
destroyRenderer(renderer)
}}
>
<EpilogueProvider set={(value) => (exit.epilogue = value)}>
<ErrorBoundary fallback={(error, reset) => <ErrorComponent error={error} reset={reset} mode={mode} />}>
<TuiPathsProvider
value={{
initialRoute: process.env.OPENCODE_ROUTE ? JSON.parse(process.env.OPENCODE_ROUTE) : undefined,
skipInitialLoading: Boolean(process.env.OPENCODE_FAST_BOOT),
cwd: process.cwd(),
home: global.home,
state: global.state,
worktree: global.data + "/worktree",
}}
>
<ClipboardProvider>
<EpilogueProvider set={(value) => (epilogue.value = value)}>
<OpencodeKeymapProvider keymap={keymap}>
<ArgsProvider {...input.args}>
<KVProvider>
<ToastProvider>
<RouteProvider
initialRoute={
input.args.continue
? {
type: "session",
sessionID: "dummy",
}
: undefined
}
>
<TuiConfigProvider config={input.config}>
<PluginRuntimeProvider value={pluginRuntime}>
<SDKProvider
url={input.url}
directory={input.directory}
fetch={input.fetch}
headers={input.headers}
events={input.events}
>
<ProjectProvider>
<SyncProvider>
<SyncProviderV2>
<ThemeProvider mode={mode}>
<LocalProvider>
<PromptStashProvider>
<DialogProvider>
<FrecencyProvider>
<PromptHistoryProvider>
<PromptRefProvider>
<EditorContextProvider>
<App
onSnapshot={input.onSnapshot}
pluginHost={input.pluginHost}
/>
</EditorContextProvider>
</PromptRefProvider>
</PromptHistoryProvider>
</FrecencyProvider>
</DialogProvider>
</PromptStashProvider>
</LocalProvider>
</ThemeProvider>
</SyncProviderV2>
</SyncProvider>
</ProjectProvider>
</SDKProvider>
</PluginRuntimeProvider>
</TuiConfigProvider>
</RouteProvider>
</ToastProvider>
</KVProvider>
</ArgsProvider>
</OpencodeKeymapProvider>
</EpilogueProvider>
</ClipboardProvider>
</TuiStartupProvider>
</TuiTerminalEnvironmentProvider>
</TuiPathsProvider>
</ErrorBoundary>
<TuiTerminalEnvironmentProvider
value={{
platform: process.platform,
multiplexer: process.env.TMUX ? "tmux" : process.env.STY ? "screen" : undefined,
displayServer: process.env.WAYLAND_DISPLAY
? "wayland"
: process.env.DISPLAY
? "x11"
: undefined,
}}
>
<TuiStartupProvider
value={{
initialRoute: process.env.OPENCODE_ROUTE ? JSON.parse(process.env.OPENCODE_ROUTE) : undefined,
skipInitialLoading: Boolean(process.env.OPENCODE_FAST_BOOT),
}}
>
<ClipboardProvider>
<OpencodeKeymapProvider keymap={keymap}>
<ArgsProvider {...input.args}>
<KVProvider>
<ToastProvider>
<RouteProvider
initialRoute={
input.args.continue
? {
type: "session",
sessionID: "dummy",
}
: undefined
}
>
<TuiConfigProvider config={input.config}>
<PluginRuntimeProvider value={pluginRuntime}>
<SDKProvider
url={input.url}
directory={input.directory}
fetch={input.fetch}
headers={input.headers}
events={input.events}
>
<ProjectProvider>
<SyncProvider>
<SyncProviderV2>
<ThemeProvider mode={mode}>
<LocalProvider>
<PromptStashProvider>
<DialogProvider>
<FrecencyProvider>
<PromptHistoryProvider>
<PromptRefProvider>
<EditorContextProvider>
<App
onSnapshot={input.onSnapshot}
pluginHost={input.pluginHost}
/>
</EditorContextProvider>
</PromptRefProvider>
</PromptHistoryProvider>
</FrecencyProvider>
</DialogProvider>
</PromptStashProvider>
</LocalProvider>
</ThemeProvider>
</SyncProviderV2>
</SyncProvider>
</ProjectProvider>
</SDKProvider>
</PluginRuntimeProvider>
</TuiConfigProvider>
</RouteProvider>
</ToastProvider>
</KVProvider>
</ArgsProvider>
</OpencodeKeymapProvider>
</ClipboardProvider>
</TuiStartupProvider>
</TuiTerminalEnvironmentProvider>
</TuiPathsProvider>
</ErrorBoundary>
</EpilogueProvider>
</ExitProvider>
)
}, renderer)
})
yield* Deferred.await(shutdown)
return epilogue.value
}),
)
yield* Effect.sync(() => {
win32FlushInputBuffer()
if (output) process.stdout.write(output + "\n")
if (exit.reason !== undefined)
process.stderr.write((cliErrorMessage(exit.reason) ?? errorFormat(exit.reason)) + "\n")
if (exit.epilogue) process.stdout.write(exit.epilogue + "\n")
})
})
@@ -346,6 +364,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
const { theme, mode, setMode, locked, lock, unlock } = themeState
const sync = useSync()
const project = useProject()
const exit = useExit()
const promptRef = usePromptRef()
const pluginRuntime = usePluginRuntime()
const attention = createTuiAttention({ renderer, config: tuiConfig, kv })
@@ -786,7 +805,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
title: "Exit the app",
slashName: "exit",
slashAliases: ["quit", "q"],
run: () => destroyRenderer(renderer),
run: () => exit(),
category: "System",
},
{
@@ -1020,7 +1039,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
`Successfully updated to OpenCode v${result.data.version}. Please restart the application.`,
)
destroyRenderer(renderer)
void exit()
})
const plugin = createMemo(() => {