it works but its dumb

This commit is contained in:
LukeParkerDev
2026-02-11 11:37:17 +10:00
parent a90b62267f
commit 1f0346725a
4 changed files with 113 additions and 0 deletions
+13
View File
@@ -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)
}
+9
View File
@@ -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", {