From 56dfbbbc93b2450f80023d7134b838b6c2bcf55d Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:57:01 +1000 Subject: [PATCH] hook better instead of 100ms loop lol --- packages/opencode/src/cli/cmd/tui/app.tsx | 8 ++- packages/opencode/src/cli/cmd/tui/thread.ts | 6 ++- packages/opencode/src/cli/cmd/tui/win32.ts | 58 ++++++++++++++++----- 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index bec17d48ef..b5914f17ee 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -3,7 +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 { win32DisableProcessedInput, win32IgnoreCtrlC, win32InstallCtrlCGuard } from "./win32" import { Installation } from "@/installation" import { Flag } from "@/flag/flag" import { DialogProvider, useDialog } from "@tui/ui/dialog" @@ -111,6 +111,7 @@ export function tui(input: { }) { // promise to prevent immediate exit return new Promise(async (resolve) => { + const unguard = win32InstallCtrlCGuard() win32DisableProcessedInput() win32IgnoreCtrlC() @@ -121,6 +122,7 @@ export function tui(input: { win32DisableProcessedInput() const onExit = async () => { + unguard?.() await input.onExit?.() resolve() } @@ -247,10 +249,6 @@ 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) { diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 87987a45f0..4b1fcd39a8 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -9,7 +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" +import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" declare global { const OPENCODE_WORKER_PATH: string @@ -78,6 +78,10 @@ export const TuiThreadCommand = cmd({ describe: "agent to use", }), handler: async (args) => { + // Keep ENABLE_PROCESSED_INPUT cleared even if other code flips it. + // (Important when running under `bun run` wrappers on Windows.) + win32InstallCtrlCGuard() + // 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() diff --git a/packages/opencode/src/cli/cmd/tui/win32.ts b/packages/opencode/src/cli/cmd/tui/win32.ts index ddd8913f19..43d18569ac 100644 --- a/packages/opencode/src/cli/cmd/tui/win32.ts +++ b/packages/opencode/src/cli/cmd/tui/win32.ts @@ -55,32 +55,64 @@ export function win32IgnoreCtrlC() { k32!.symbols.SetConsoleCtrlHandler(null, 1) } +let unhook: (() => void) | undefined + /** - * Continuously enforce ENABLE_PROCESSED_INPUT=off on the console. + * Keep ENABLE_PROCESSED_INPUT disabled. * - * 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. + * On Windows, Ctrl+C becomes a CTRL_C_EVENT (instead of stdin input) when + * ENABLE_PROCESSED_INPUT is set. Various runtimes can re-apply console modes + * (sometimes on a later tick), and the flag is console-global, not per-process. * - * 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. + * We combine: + * - A `setRawMode(...)` hook to re-clear after known raw-mode toggles. + * - A low-frequency poll as a backstop for native/external mode changes. */ -export function win32EnforceCtrlCGuard() { +export function win32InstallCtrlCGuard() { if (process.platform !== "win32") return if (!process.stdin.isTTY) return if (!load()) return + if (unhook) return unhook + + const stdin = process.stdin as any + const original = stdin.setRawMode const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) const buf = new Uint32Array(1) - setInterval(() => { + const enforce = () => { 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) + } + + // Some runtimes can re-apply console modes on the next tick; enforce twice. + const later = () => { + enforce() + setImmediate(enforce) + } + + if (typeof original === "function") { + stdin.setRawMode = (mode: boolean) => { + const result = original.call(stdin, mode) + later() + return result + } + } + + // Ensure it's cleared immediately too (covers any earlier mode changes). + later() + + const interval = setInterval(enforce, 100) + + unhook = () => { + clearInterval(interval) + if (typeof original === "function") { + stdin.setRawMode = original + } + unhook = undefined + } + + return unhook }