diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index ea742f699708..b650881afddd 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -15,7 +15,7 @@ import { Show, on, } from "solid-js" -import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" +import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./console" import { Flag } from "@opencode-ai/core/flag/flag" import semver from "semver" import { DialogProvider, useDialog } from "@tui/ui/dialog" diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts index 5de937fdcc19..441a98bb9a3f 100644 --- a/packages/opencode/src/cli/cmd/tui/attach.ts +++ b/packages/opencode/src/cli/cmd/tui/attach.ts @@ -1,7 +1,7 @@ import { cmd } from "../cmd" import { UI } from "@/cli/ui" import { tui } from "./app" -import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" +import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./console" import { TuiConfig } from "@/cli/cmd/tui/config/tui" import { errorMessage } from "@/util/error" import { validateSession } from "./validate-session" diff --git a/packages/opencode/src/cli/cmd/tui/component/error-component.tsx b/packages/opencode/src/cli/cmd/tui/component/error-component.tsx index fcbd27ca9bd0..8ccb24ce88f7 100644 --- a/packages/opencode/src/cli/cmd/tui/component/error-component.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/error-component.tsx @@ -2,8 +2,8 @@ import { TextAttributes } from "@opentui/core" import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import * as Clipboard from "@tui/util/clipboard" import { createSignal } from "solid-js" +import { flushInputBuffer } from "../console" import { InstallationVersion } from "@opencode-ai/core/installation/version" -import { win32FlushInputBuffer } from "../win32" import { getScrollAcceleration } from "../util/scroll" export function ErrorComponent(props: { @@ -20,7 +20,7 @@ export function ErrorComponent(props: { await props.onBeforeExit?.() renderer.setTerminalTitle("") renderer.destroy() - win32FlushInputBuffer() + flushInputBuffer() await props.onExit() } diff --git a/packages/opencode/src/cli/cmd/tui/win32.ts b/packages/opencode/src/cli/cmd/tui/console.ts similarity index 73% rename from packages/opencode/src/cli/cmd/tui/win32.ts rename to packages/opencode/src/cli/cmd/tui/console.ts index 1aaa80aecd69..b630dfaaf35f 100644 --- a/packages/opencode/src/cli/cmd/tui/win32.ts +++ b/packages/opencode/src/cli/cmd/tui/console.ts @@ -1,6 +1,8 @@ import { dlopen, ptr } from "bun:ffi" import type { ReadStream } from "node:tty" +// -- Windows (kernel32) ------------------------------------------------------- + const STD_INPUT_HANDLE = -10 const ENABLE_PROCESSED_INPUT = 0x0001 @@ -14,16 +16,32 @@ const kernel = () => let k32: ReturnType | undefined +// -- POSIX (libc) ------------------------------------------------------------- + +const libc = () => + dlopen(process.platform === "darwin" ? "libSystem.B.dylib" : "libc.so.6", { + tcflush: { args: ["i32", "i32"], returns: "i32" }, + }) + +let lc: ReturnType | undefined + +// ----------------------------------------------------------------------------- + function load() { - if (process.platform !== "win32") return false try { - k32 ??= kernel() + if (process.platform === "win32") k32 ??= kernel() + else lc ??= libc() return true } catch { return false } } +// TCIFLUSH: discard received-but-unread input +const TCIFLUSH = process.platform === "darwin" ? 1 : 0 + +// -- Exports ------------------------------------------------------------------ + /** * Clear ENABLE_PROCESSED_INPUT on the console stdin handle. */ @@ -42,15 +60,21 @@ export function win32DisableProcessedInput() { } /** - * Discard any queued console input (mouse events, key presses, etc.). + * Discard any queued console/tty input (mouse events, key presses, etc.). + * + * On Windows this calls FlushConsoleInputBuffer via kernel32. + * On POSIX this calls tcflush(STDIN_FILENO, TCIFLUSH) via libc. */ -export function win32FlushInputBuffer() { - if (process.platform !== "win32") return +export function flushInputBuffer() { if (!process.stdin.isTTY) return if (!load()) return - const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) - k32!.symbols.FlushConsoleInputBuffer(handle) + if (process.platform === "win32") { + const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) + k32!.symbols.FlushConsoleInputBuffer(handle) + } else { + lc!.symbols.tcflush(0, TCIFLUSH) + } } let unhook: (() => void) | undefined diff --git a/packages/opencode/src/cli/cmd/tui/context/exit.tsx b/packages/opencode/src/cli/cmd/tui/context/exit.tsx index 205025f867de..8db86277b3cb 100644 --- a/packages/opencode/src/cli/cmd/tui/context/exit.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/exit.tsx @@ -1,7 +1,7 @@ import { useRenderer } from "@opentui/solid" import { createSimpleContext } from "./helper" import { FormatError, FormatUnknownError } from "@/cli/error" -import { win32FlushInputBuffer } from "../win32" +import { flushInputBuffer } from "../console" type Exit = ((reason?: unknown) => Promise) & { message: { set: (value?: string) => () => void @@ -37,7 +37,7 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({ // Reset window title before destroying renderer renderer.setTerminalTitle("") renderer.destroy() - win32FlushInputBuffer() + flushInputBuffer() if (reason) { const formatted = FormatError(reason) ?? FormatUnknownError(reason) if (formatted) { diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 384b6fc4ff57..45a24a8a4d4f 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -12,7 +12,7 @@ import { withNetworkOptions, resolveNetworkOptionsNoConfig } from "@/cli/network import { Filesystem } from "@/util/filesystem" import type { GlobalEvent } from "@opencode-ai/sdk/v2" import type { EventSource } from "./context/sdk" -import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" +import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./console" import { writeHeapSnapshot } from "v8" import { TuiConfig } from "./config/tui" import {