diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index ec048f86b2f1..aba55da368f4 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, win32InstallCtrlCGuard, win32ResetTerminal } from "./win32" import { Flag } from "@/flag/flag" import semver from "semver" import { DialogProvider, useDialog } from "@tui/ui/dialog" @@ -182,6 +182,8 @@ export function tui(input: { win32DisableProcessedInput() const onExit = async () => { + // Reset terminal on Windows to prevent ANSI corruption (issue #20224) + win32ResetTerminal() unguard?.() resolve() } diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts index e892f9922d1b..77e1153fd14e 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, win32ResetTerminal } from "./win32" import { TuiConfig } from "@/config/tui" import { Instance } from "@/project/instance" import { existsSync } from "fs" @@ -82,6 +82,8 @@ export const AttachCommand = cmd({ headers, }) } finally { + // Reset terminal on Windows to prevent ANSI corruption (issue #20224) + win32ResetTerminal() unguard?.() } }, diff --git a/packages/opencode/src/cli/cmd/tui/win32.ts b/packages/opencode/src/cli/cmd/tui/win32.ts index 23e9f448574f..5b8515bb1433 100644 --- a/packages/opencode/src/cli/cmd/tui/win32.ts +++ b/packages/opencode/src/cli/cmd/tui/win32.ts @@ -1,7 +1,11 @@ import { dlopen, ptr } from "bun:ffi" +const STD_OUTPUT_HANDLE = -11 +const STD_ERROR_HANDLE = -12 const STD_INPUT_HANDLE = -10 const ENABLE_PROCESSED_INPUT = 0x0001 +const ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 +const ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002 const kernel = () => dlopen("kernel32.dll", { @@ -23,6 +27,41 @@ function load() { } } +/** + * Reset terminal to a clean state on Windows. + * This clears any pending output and restores proper console modes. + */ +export function win32ResetTerminal() { + if (process.platform !== "win32") return + if (!load()) return + + try { + // Clear any pending output buffers + const outputHandle = k32!.symbols.GetStdHandle(STD_OUTPUT_HANDLE) + const errorHandle = k32!.symbols.GetStdHandle(STD_ERROR_HANDLE) + + // Reset output console modes to enable proper ANSI processing + const buf = new Uint32Array(1) + if (k32!.symbols.GetConsoleMode(outputHandle, ptr(buf)) !== 0) { + const mode = buf[0]! | ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_WRAP_AT_EOL_OUTPUT + k32!.symbols.SetConsoleMode(outputHandle, mode) + } + if (k32!.symbols.GetConsoleMode(errorHandle, ptr(buf)) !== 0) { + const mode = buf[0]! | ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_WRAP_AT_EOL_OUTPUT + k32!.symbols.SetConsoleMode(errorHandle, mode) + } + + // Clear the screen and reset cursor position + process.stdout.write("\x1b[2J\x1b[H") + // Reset all SGR (Select Graphic Rendition) attributes + process.stdout.write("\x1b[0m") + // Disable any alternate screen buffer + process.stdout.write("\x1b[?1049l") + } catch { + // Ignore errors during terminal reset + } +} + /** * Clear ENABLE_PROCESSED_INPUT on the console stdin handle. */