From e98339cbec9123abb62acca41f11683a57b79d2c Mon Sep 17 00:00:00 2001 From: ToastyTheBot Date: Wed, 1 Apr 2026 09:15:27 +0800 Subject: [PATCH] fix: reset Windows terminal on exit to prevent ANSI corruption Add win32ResetTerminal function that properly resets the terminal state on Windows before exiting. This prevents raw ANSI escape sequences from being printed to the terminal when Node processes are killed. Changes: - Added win32ResetTerminal() that clears output buffers and restores proper console modes with ENABLE_VIRTUAL_TERMINAL_PROCESSING - Clears screen and resets cursor position - Resets SGR attributes and disables alternate screen buffer - Called on exit in both TUI app and attach command Refs: #20224 --- packages/opencode/src/cli/cmd/tui/app.tsx | 4 ++- packages/opencode/src/cli/cmd/tui/attach.ts | 4 ++- packages/opencode/src/cli/cmd/tui/win32.ts | 39 +++++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) 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. */