From 85ad7c5b554ddac2d6861ffca352c883ab90747d Mon Sep 17 00:00:00 2001 From: SAMMY Date: Thu, 2 Apr 2026 23:48:01 +0000 Subject: [PATCH 1/4] fix(tui): lazy load bun ffi on windows --- packages/opencode/src/cli/cmd/tui/app.tsx | 3 +- packages/opencode/src/cli/cmd/tui/win32.ts | 39 +++++++++++----------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 93d1fc19ae..6e86300ea9 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -16,7 +16,7 @@ import { on, onCleanup, } from "solid-js" -import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" +import { win32DisableProcessedInput, win32Init, win32InstallCtrlCGuard } from "./win32" import { Flag } from "@/flag/flag" import semver from "semver" import { DialogProvider, useDialog } from "@tui/ui/dialog" @@ -172,6 +172,7 @@ export function tui(input: { }) { // promise to prevent immediate exit return new Promise(async (resolve) => { + await win32Init() const unguard = win32InstallCtrlCGuard() win32DisableProcessedInput() diff --git a/packages/opencode/src/cli/cmd/tui/win32.ts b/packages/opencode/src/cli/cmd/tui/win32.ts index 23e9f44857..6fd8638ffe 100644 --- a/packages/opencode/src/cli/cmd/tui/win32.ts +++ b/packages/opencode/src/cli/cmd/tui/win32.ts @@ -1,24 +1,25 @@ -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" }, - FlushConsoleInputBuffer: { args: ["ptr"], returns: "i32" }, - }) +let mod: typeof import("bun:ffi") | undefined +let k32: ReturnType | undefined +let ready: boolean | undefined -let k32: ReturnType | undefined - -function load() { +export async function win32Init() { if (process.platform !== "win32") return false + if (ready !== undefined) return ready try { - k32 ??= kernel() + mod = await import("bun:ffi") + k32 = mod.dlopen("kernel32.dll", { + GetStdHandle: { args: ["i32"], returns: "ptr" }, + GetConsoleMode: { args: ["ptr", "ptr"], returns: "i32" }, + SetConsoleMode: { args: ["ptr", "u32"], returns: "i32" }, + FlushConsoleInputBuffer: { args: ["ptr"], returns: "i32" }, + }) + ready = true return true } catch { + ready = false return false } } @@ -29,11 +30,11 @@ function load() { export function win32DisableProcessedInput() { if (process.platform !== "win32") return if (!process.stdin.isTTY) return - if (!load()) return + if (!k32 || !mod) return const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) const buf = new Uint32Array(1) - if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return + if (k32!.symbols.GetConsoleMode(handle, mod.ptr(buf)) === 0) return const mode = buf[0]! if ((mode & ENABLE_PROCESSED_INPUT) === 0) return @@ -46,7 +47,7 @@ export function win32DisableProcessedInput() { export function win32FlushInputBuffer() { if (process.platform !== "win32") return if (!process.stdin.isTTY) return - if (!load()) return + if (!k32) return const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) k32!.symbols.FlushConsoleInputBuffer(handle) @@ -68,7 +69,7 @@ let unhook: (() => void) | undefined export function win32InstallCtrlCGuard() { if (process.platform !== "win32") return if (!process.stdin.isTTY) return - if (!load()) return + if (!k32 || !mod) return if (unhook) return unhook const stdin = process.stdin as any @@ -77,11 +78,11 @@ export function win32InstallCtrlCGuard() { const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) const buf = new Uint32Array(1) - if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return + if (k32!.symbols.GetConsoleMode(handle, mod.ptr(buf)) === 0) return const initial = buf[0]! const enforce = () => { - if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return + if (k32!.symbols.GetConsoleMode(handle, mod.ptr(buf)) === 0) return const mode = buf[0]! if ((mode & ENABLE_PROCESSED_INPUT) === 0) return k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT) From be22a4507a20d58998f362a46072e93bea1b76d2 Mon Sep 17 00:00:00 2001 From: SAMMY Date: Thu, 2 Apr 2026 23:51:39 +0000 Subject: [PATCH 2/4] fix(tui): type lazy win32 ffi bindings --- packages/opencode/src/cli/cmd/tui/win32.ts | 37 +++++++++++++++------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/win32.ts b/packages/opencode/src/cli/cmd/tui/win32.ts index 6fd8638ffe..1d643525f3 100644 --- a/packages/opencode/src/cli/cmd/tui/win32.ts +++ b/packages/opencode/src/cli/cmd/tui/win32.ts @@ -2,9 +2,20 @@ const STD_INPUT_HANDLE = -10 const ENABLE_PROCESSED_INPUT = 0x0001 let mod: typeof import("bun:ffi") | undefined -let k32: ReturnType | undefined +type K32 = { + symbols: { + GetStdHandle: (n: number) => bigint | number + GetConsoleMode: (h: bigint | number, mode: unknown) => number + SetConsoleMode: (h: bigint | number, mode: number) => number + FlushConsoleInputBuffer: (h: bigint | number) => number + } +} + +let k32: K32 | undefined let ready: boolean | undefined +type Ptr = typeof import("bun:ffi").ptr + export async function win32Init() { if (process.platform !== "win32") return false if (ready !== undefined) return ready @@ -15,7 +26,7 @@ export async function win32Init() { GetConsoleMode: { args: ["ptr", "ptr"], returns: "i32" }, SetConsoleMode: { args: ["ptr", "u32"], returns: "i32" }, FlushConsoleInputBuffer: { args: ["ptr"], returns: "i32" }, - }) + }) as K32 ready = true return true } catch { @@ -31,14 +42,15 @@ export function win32DisableProcessedInput() { if (process.platform !== "win32") return if (!process.stdin.isTTY) return if (!k32 || !mod) return + const ptr = mod.ptr as Ptr - const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) + const handle = k32.symbols.GetStdHandle(STD_INPUT_HANDLE) const buf = new Uint32Array(1) - if (k32!.symbols.GetConsoleMode(handle, mod.ptr(buf)) === 0) return + 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) + k32.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT) } /** @@ -49,8 +61,8 @@ export function win32FlushInputBuffer() { if (!process.stdin.isTTY) return if (!k32) return - const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) - k32!.symbols.FlushConsoleInputBuffer(handle) + const handle = k32.symbols.GetStdHandle(STD_INPUT_HANDLE) + k32.symbols.FlushConsoleInputBuffer(handle) } let unhook: (() => void) | undefined @@ -71,21 +83,22 @@ export function win32InstallCtrlCGuard() { if (!process.stdin.isTTY) return if (!k32 || !mod) return if (unhook) return unhook + const ptr = mod.ptr as Ptr const stdin = process.stdin as any const original = stdin.setRawMode - const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE) + const handle = k32.symbols.GetStdHandle(STD_INPUT_HANDLE) const buf = new Uint32Array(1) - if (k32!.symbols.GetConsoleMode(handle, mod.ptr(buf)) === 0) return + if (k32.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return const initial = buf[0]! const enforce = () => { - if (k32!.symbols.GetConsoleMode(handle, mod.ptr(buf)) === 0) return + 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) + k32.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT) } // Some runtimes can re-apply console modes on the next tick; enforce twice. @@ -122,7 +135,7 @@ export function win32InstallCtrlCGuard() { stdin.setRawMode = original } - k32!.symbols.SetConsoleMode(handle, initial) + k32.symbols.SetConsoleMode(handle, initial) unhook = undefined } From efe62aef81ba7c39223b69ff5f1e9e377dc59a33 Mon Sep 17 00:00:00 2001 From: SAMMY Date: Thu, 2 Apr 2026 23:53:58 +0000 Subject: [PATCH 3/4] fix(tui): narrow lazy win32 ffi symbols --- packages/opencode/src/cli/cmd/tui/win32.ts | 37 +++++++++++----------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/win32.ts b/packages/opencode/src/cli/cmd/tui/win32.ts index 1d643525f3..fd4f3acd0a 100644 --- a/packages/opencode/src/cli/cmd/tui/win32.ts +++ b/packages/opencode/src/cli/cmd/tui/win32.ts @@ -3,12 +3,10 @@ const ENABLE_PROCESSED_INPUT = 0x0001 let mod: typeof import("bun:ffi") | undefined type K32 = { - symbols: { - GetStdHandle: (n: number) => bigint | number - GetConsoleMode: (h: bigint | number, mode: unknown) => number - SetConsoleMode: (h: bigint | number, mode: number) => number - FlushConsoleInputBuffer: (h: bigint | number) => number - } + GetStdHandle: (n: number) => bigint | number + GetConsoleMode: (h: bigint | number, mode: unknown) => number + SetConsoleMode: (h: bigint | number, mode: number) => number + FlushConsoleInputBuffer: (h: bigint | number) => number } let k32: K32 | undefined @@ -21,12 +19,12 @@ export async function win32Init() { if (ready !== undefined) return ready try { mod = await import("bun:ffi") - k32 = mod.dlopen("kernel32.dll", { + k32 = (mod.dlopen("kernel32.dll", { GetStdHandle: { args: ["i32"], returns: "ptr" }, GetConsoleMode: { args: ["ptr", "ptr"], returns: "i32" }, SetConsoleMode: { args: ["ptr", "u32"], returns: "i32" }, FlushConsoleInputBuffer: { args: ["ptr"], returns: "i32" }, - }) as K32 + }).symbols as unknown as K32 ready = true return true } catch { @@ -44,13 +42,14 @@ export function win32DisableProcessedInput() { if (!k32 || !mod) return const ptr = mod.ptr as Ptr - const handle = k32.symbols.GetStdHandle(STD_INPUT_HANDLE) + const lib = k32 + const handle = lib.GetStdHandle(STD_INPUT_HANDLE) const buf = new Uint32Array(1) - if (k32.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return + if (lib.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) + lib.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT) } /** @@ -61,8 +60,9 @@ export function win32FlushInputBuffer() { if (!process.stdin.isTTY) return if (!k32) return - const handle = k32.symbols.GetStdHandle(STD_INPUT_HANDLE) - k32.symbols.FlushConsoleInputBuffer(handle) + const lib = k32 + const handle = lib.GetStdHandle(STD_INPUT_HANDLE) + lib.FlushConsoleInputBuffer(handle) } let unhook: (() => void) | undefined @@ -84,21 +84,22 @@ export function win32InstallCtrlCGuard() { if (!k32 || !mod) return if (unhook) return unhook const ptr = mod.ptr as Ptr + const lib = k32 const stdin = process.stdin as any const original = stdin.setRawMode - const handle = k32.symbols.GetStdHandle(STD_INPUT_HANDLE) + const handle = lib.GetStdHandle(STD_INPUT_HANDLE) const buf = new Uint32Array(1) - if (k32.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return + if (lib.GetConsoleMode(handle, ptr(buf)) === 0) return const initial = buf[0]! const enforce = () => { - if (k32.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return + if (lib.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) + lib.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT) } // Some runtimes can re-apply console modes on the next tick; enforce twice. @@ -135,7 +136,7 @@ export function win32InstallCtrlCGuard() { stdin.setRawMode = original } - k32.symbols.SetConsoleMode(handle, initial) + lib.SetConsoleMode(handle, initial) unhook = undefined } From 6e942c5933171864df304cec5a365fc261f1356f Mon Sep 17 00:00:00 2001 From: SAMMY Date: Thu, 2 Apr 2026 23:56:37 +0000 Subject: [PATCH 4/4] fix(tui): correct win32 ffi cast syntax --- packages/opencode/src/cli/cmd/tui/win32.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/win32.ts b/packages/opencode/src/cli/cmd/tui/win32.ts index fd4f3acd0a..11659a7d64 100644 --- a/packages/opencode/src/cli/cmd/tui/win32.ts +++ b/packages/opencode/src/cli/cmd/tui/win32.ts @@ -24,7 +24,7 @@ export async function win32Init() { GetConsoleMode: { args: ["ptr", "ptr"], returns: "i32" }, SetConsoleMode: { args: ["ptr", "u32"], returns: "i32" }, FlushConsoleInputBuffer: { args: ["ptr"], returns: "i32" }, - }).symbols as unknown as K32 + }).symbols as unknown as K32) ready = true return true } catch {