fix(tui): preserve exit epilogue during scoped shutdown (#31805)
This commit is contained in:
@@ -178,7 +178,7 @@ function isVersionGreater(left: string, right: string) {
|
|||||||
export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||||
const global = yield* Global.Service
|
const global = yield* Global.Service
|
||||||
const exit = { epilogue: undefined as string | undefined, reason: undefined as unknown }
|
const exit = { epilogue: undefined as string | undefined, reason: undefined as unknown }
|
||||||
yield* Effect.scoped(
|
const result = yield* Effect.scoped(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const renderer = yield* Effect.acquireRelease(
|
const renderer = yield* Effect.acquireRelease(
|
||||||
Effect.tryPromise(() =>
|
Effect.tryPromise(() =>
|
||||||
@@ -337,13 +337,14 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
|||||||
}, renderer)
|
}, renderer)
|
||||||
})
|
})
|
||||||
yield* Deferred.await(shutdown)
|
yield* Deferred.await(shutdown)
|
||||||
|
return { epilogue: exit.epilogue, reason: exit.reason }
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
yield* Effect.sync(() => {
|
yield* Effect.sync(() => {
|
||||||
win32FlushInputBuffer()
|
win32FlushInputBuffer()
|
||||||
if (exit.reason !== undefined)
|
if (result.reason !== undefined)
|
||||||
process.stderr.write((cliErrorMessage(exit.reason) ?? errorFormat(exit.reason)) + "\n")
|
process.stderr.write((cliErrorMessage(result.reason) ?? errorFormat(result.reason)) + "\n")
|
||||||
if (exit.epilogue) process.stdout.write(exit.epilogue + "\n")
|
if (result.epilogue) process.stdout.write(result.epilogue + "\n")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { CliRenderer } from "@opentui/core"
|
import type { CliRenderer } from "@opentui/core"
|
||||||
|
|
||||||
export function destroyRenderer(renderer: Pick<CliRenderer, "isDestroyed" | "setTerminalTitle" | "destroy">) {
|
export function destroyRenderer(renderer: Pick<CliRenderer, "isDestroyed" | "setTerminalTitle" | "destroy">) {
|
||||||
if (!renderer.isDestroyed) {
|
renderer.setTerminalTitle("")
|
||||||
renderer.setTerminalTitle("")
|
if (renderer.isDestroyed) return
|
||||||
renderer.destroy()
|
renderer.destroy()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { expect, mock, test } from "bun:test"
|
import { expect, mock, test } from "bun:test"
|
||||||
|
import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
|
||||||
import { createTestRenderer } from "@opentui/core/testing"
|
import { createTestRenderer } from "@opentui/core/testing"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
import { createTuiResolvedConfig } from "./fixture/tui-runtime"
|
import { createTuiResolvedConfig } from "./fixture/tui-runtime"
|
||||||
import { createEventSource, createFetch, directory } from "./fixture/tui-sdk"
|
import { createEventSource, createFetch, directory, json } from "./fixture/tui-sdk"
|
||||||
|
|
||||||
test("SIGHUP clears title and disposes scoped resources once", async () => {
|
test("SIGHUP clears title and disposes scoped resources once", async () => {
|
||||||
const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
|
const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
|
||||||
@@ -57,3 +58,70 @@ test("SIGHUP clears title and disposes scoped resources once", async () => {
|
|||||||
mock.restore()
|
mock.restore()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("app.exit prints the session epilogue after scoped cleanup", async () => {
|
||||||
|
const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
|
||||||
|
const core = await import("@opentui/core")
|
||||||
|
mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer }))
|
||||||
|
const events = createEventSource()
|
||||||
|
const calls = createFetch((url) => {
|
||||||
|
if (url.pathname === "/session")
|
||||||
|
return json([
|
||||||
|
{
|
||||||
|
id: "dummy",
|
||||||
|
title: "Demo session",
|
||||||
|
slug: "dummy",
|
||||||
|
projectID: "project",
|
||||||
|
directory,
|
||||||
|
version: "0.0.0-test",
|
||||||
|
time: { created: 0, updated: 0 },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
const originalWrite = process.stdout.write.bind(process.stdout)
|
||||||
|
let stdout = ""
|
||||||
|
let api: TuiPluginApi | undefined
|
||||||
|
let started!: () => void
|
||||||
|
const ready = new Promise<void>((resolve) => {
|
||||||
|
started = resolve
|
||||||
|
})
|
||||||
|
|
||||||
|
process.stdout.write = ((chunk: string | Uint8Array) => {
|
||||||
|
stdout += String(chunk)
|
||||||
|
return true
|
||||||
|
}) as typeof process.stdout.write
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { run } = await import("../src/app")
|
||||||
|
const task = Effect.runPromise(
|
||||||
|
run({
|
||||||
|
url: "http://test",
|
||||||
|
directory,
|
||||||
|
config: createTuiResolvedConfig({ plugin_enabled: {} }),
|
||||||
|
fetch: calls.fetch,
|
||||||
|
events: events.source,
|
||||||
|
args: { continue: true },
|
||||||
|
pluginHost: {
|
||||||
|
async start(input) {
|
||||||
|
api = input.api
|
||||||
|
started()
|
||||||
|
},
|
||||||
|
async dispose() {},
|
||||||
|
},
|
||||||
|
}).pipe(Effect.provide(Global.defaultLayer)),
|
||||||
|
)
|
||||||
|
|
||||||
|
await ready
|
||||||
|
await setup.renderOnce()
|
||||||
|
await setup.renderOnce()
|
||||||
|
api?.keymap.dispatchCommand("app.exit")
|
||||||
|
await task
|
||||||
|
|
||||||
|
expect(stdout).toContain("Demo session")
|
||||||
|
expect(stdout).toContain("opencode -s dummy")
|
||||||
|
} finally {
|
||||||
|
process.stdout.write = originalWrite
|
||||||
|
if (!setup.renderer.isDestroyed) setup.renderer.destroy()
|
||||||
|
mock.restore()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user