it works but its dumb
This commit is contained in:
@@ -3,6 +3,7 @@ import { Clipboard } from "@tui/util/clipboard"
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { RouteProvider, useRoute } from "@tui/context/route"
|
||||
import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js"
|
||||
import { win32DisableProcessedInput, win32IgnoreCtrlC, win32EnforceCtrlCGuard } from "./win32"
|
||||
import { Installation } from "@/installation"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { DialogProvider, useDialog } from "@tui/ui/dialog"
|
||||
@@ -110,7 +111,15 @@ export function tui(input: {
|
||||
}) {
|
||||
// promise to prevent immediate exit
|
||||
return new Promise<void>(async (resolve) => {
|
||||
win32DisableProcessedInput()
|
||||
win32IgnoreCtrlC()
|
||||
|
||||
const mode = await getTerminalBackgroundColor()
|
||||
|
||||
// Re-clear after getTerminalBackgroundColor() — setRawMode(false) restores
|
||||
// the original console mode which re-enables ENABLE_PROCESSED_INPUT.
|
||||
win32DisableProcessedInput()
|
||||
|
||||
const onExit = async () => {
|
||||
await input.onExit?.()
|
||||
resolve()
|
||||
@@ -238,6 +247,10 @@ function App() {
|
||||
|
||||
const args = useArgs()
|
||||
onMount(() => {
|
||||
// opentui reconfigures console mode via native calls which re-enable
|
||||
// ENABLE_PROCESSED_INPUT. Poll and re-clear it so the parent `bun run`
|
||||
// wrapper isn't killed by CTRL_C_EVENT.
|
||||
win32EnforceCtrlCGuard()
|
||||
batch(() => {
|
||||
if (args.agent) local.agent.set(args.agent)
|
||||
if (args.model) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Log } from "@/util/log"
|
||||
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
|
||||
import type { Event } from "@opencode-ai/sdk/v2"
|
||||
import type { EventSource } from "./context/sdk"
|
||||
import { win32DisableProcessedInput } from "./win32"
|
||||
|
||||
declare global {
|
||||
const OPENCODE_WORKER_PATH: string
|
||||
@@ -77,6 +78,10 @@ export const TuiThreadCommand = cmd({
|
||||
describe: "agent to use",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
// Must be the very first thing — disables CTRL_C_EVENT before any Worker
|
||||
// spawn or async work so the OS cannot kill the process group.
|
||||
win32DisableProcessedInput()
|
||||
|
||||
if (args.fork && !args.continue && !args.session) {
|
||||
UI.error("--fork requires --continue or --session")
|
||||
process.exit(1)
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { dlopen, ptr } from "bun:ffi"
|
||||
|
||||
const STD_INPUT_HANDLE = -10
|
||||
const ENABLE_PROCESSED_INPUT = 0x0001
|
||||
|
||||
const kernel = () =>
|
||||
dlopen("kernel32.dll", {
|
||||
GetStdHandle: { args: ["i32"], returns: "ptr" },
|
||||
GetConsoleMode: { args: ["ptr", "ptr"], returns: "i32" },
|
||||
SetConsoleMode: { args: ["ptr", "u32"], returns: "i32" },
|
||||
SetConsoleCtrlHandler: { args: ["ptr", "i32"], returns: "i32" },
|
||||
})
|
||||
|
||||
let k32: ReturnType<typeof kernel> | undefined
|
||||
|
||||
function load() {
|
||||
if (process.platform !== "win32") return false
|
||||
try {
|
||||
k32 ??= kernel()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear ENABLE_PROCESSED_INPUT on the console stdin handle.
|
||||
*/
|
||||
export function win32DisableProcessedInput() {
|
||||
if (process.platform !== "win32") return
|
||||
if (!process.stdin.isTTY) return
|
||||
if (!load()) return
|
||||
|
||||
const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
|
||||
const buf = new Uint32Array(1)
|
||||
if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return
|
||||
|
||||
const mode = buf[0]!
|
||||
if ((mode & ENABLE_PROCESSED_INPUT) === 0) return
|
||||
k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell Windows to ignore CTRL_C_EVENT for this process.
|
||||
*
|
||||
* SetConsoleCtrlHandler(NULL, TRUE) makes the process ignore Ctrl+C
|
||||
* signals at the OS level. Belt-and-suspenders alongside disabling
|
||||
* ENABLE_PROCESSED_INPUT.
|
||||
*/
|
||||
export function win32IgnoreCtrlC() {
|
||||
if (process.platform !== "win32") return
|
||||
if (!process.stdin.isTTY) return
|
||||
if (!load()) return
|
||||
|
||||
k32!.symbols.SetConsoleCtrlHandler(null, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Continuously enforce ENABLE_PROCESSED_INPUT=off on the console.
|
||||
*
|
||||
* opentui reconfigures the console mode through native calls (not
|
||||
* process.stdin.setRawMode) so we cannot intercept them. Instead we
|
||||
* poll at a low frequency and re-clear the flag when needed.
|
||||
*
|
||||
* Because ENABLE_PROCESSED_INPUT is a console-level flag (not per-process),
|
||||
* keeping it cleared protects every process attached to this console,
|
||||
* including the parent `bun run` wrapper that we can't otherwise control.
|
||||
*
|
||||
* The fast-path (GetConsoleMode + bitmask check) is sub-microsecond;
|
||||
* SetConsoleMode only fires when something re-enabled the flag.
|
||||
*/
|
||||
export function win32EnforceCtrlCGuard() {
|
||||
if (process.platform !== "win32") return
|
||||
if (!process.stdin.isTTY) return
|
||||
if (!load()) return
|
||||
|
||||
const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
|
||||
const buf = new Uint32Array(1)
|
||||
|
||||
setInterval(() => {
|
||||
if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return
|
||||
const mode = buf[0]!
|
||||
if ((mode & ENABLE_PROCESSED_INPUT) === 0) return
|
||||
k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT)
|
||||
}, 100)
|
||||
}
|
||||
@@ -26,6 +26,15 @@ import { EOL } from "os"
|
||||
import { WebCommand } from "./cli/cmd/web"
|
||||
import { PrCommand } from "./cli/cmd/pr"
|
||||
import { SessionCommand } from "./cli/cmd/session"
|
||||
import { win32DisableProcessedInput, win32IgnoreCtrlC } from "./cli/cmd/tui/win32"
|
||||
|
||||
// Disable Windows CTRL_C_EVENT as early as possible. When running under
|
||||
// `bun run` (e.g. `bun dev`), the parent bun process shares this console
|
||||
// and would be killed by the OS before any JS signal handler fires.
|
||||
win32DisableProcessedInput()
|
||||
// Belt-and-suspenders: even if something re-enables ENABLE_PROCESSED_INPUT
|
||||
// later (opentui raw mode, libuv, etc.), ignore the generated event.
|
||||
win32IgnoreCtrlC()
|
||||
|
||||
process.on("unhandledRejection", (e) => {
|
||||
Log.Default.error("rejection", {
|
||||
|
||||
Reference in New Issue
Block a user