From 656572c2cfeff16034769381acfb60f9f85091a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Fri, 22 May 2026 08:18:48 -0300 Subject: [PATCH] fix: fix kitty proto on old terminals --- .changeset/mingw-terminal-keyboard.md | 5 ++ packages/opencode/src/cli/cmd/tui/app.tsx | 5 +- .../src/kilocode/cli/cmd/tui/util/terminal.ts | 39 ++++++-- .../opencode/test/kilocode/terminal.test.ts | 89 +++++++++++++++++++ .../ui/src/components/provider-icons/types.ts | 30 +++---- 5 files changed, 145 insertions(+), 23 deletions(-) create mode 100644 .changeset/mingw-terminal-keyboard.md create mode 100644 packages/opencode/test/kilocode/terminal.test.ts diff --git a/.changeset/mingw-terminal-keyboard.md b/.changeset/mingw-terminal-keyboard.md new file mode 100644 index 00000000000..d35e517934d --- /dev/null +++ b/.changeset/mingw-terminal-keyboard.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Avoid leaving advanced keyboard modes enabled after exiting the TUI in mintty and MINGW terminals. diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 4ec2172f197..a9e04cd57e7 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -67,20 +67,21 @@ import { createTuiApi } from "@/cli/cmd/tui/plugin/api" import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime" import type { RouteMap } from "@/cli/cmd/tui/plugin/api" import { FormatError, FormatUnknownError } from "@/cli/error" -import { resetTerminalState } from "@/kilocode/cli/cmd/tui/util/terminal" // kilocode_change +import { kitty, resetTerminalState } from "@/kilocode/cli/cmd/tui/util/terminal" // kilocode_change import type { EventSource } from "./context/sdk" import { DialogVariant } from "./component/dialog-variant" function rendererConfig(_config: TuiConfig.Info): CliRendererConfig { const mouseEnabled = !Flag.KILO_DISABLE_MOUSE && (_config.mouse ?? true) + const keyboard = kitty() // kilocode_change return { externalOutputMode: "passthrough", targetFps: 60, gatherStats: false, exitOnCtrlC: false, - useKittyKeyboard: {}, + ...(keyboard ? { useKittyKeyboard: {} } : {}), // kilocode_change autoFocus: false, openConsoleOnError: false, useMouse: mouseEnabled, diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/util/terminal.ts b/packages/opencode/src/kilocode/cli/cmd/tui/util/terminal.ts index c7c0e260a8a..9a6c775f923 100644 --- a/packages/opencode/src/kilocode/cli/cmd/tui/util/terminal.ts +++ b/packages/opencode/src/kilocode/cli/cmd/tui/util/terminal.ts @@ -1,21 +1,48 @@ -// kilocode_change - new file /** - * Write escape sequences to disable all mouse tracking modes and reset terminal state. + * Write escape sequences to disable terminal input modes and reset terminal state. * This is a safety net to ensure the terminal is clean after exit, even if the renderer's * cleanup didn't flush properly (e.g. on Windows). */ -export function resetTerminalState() { - const sequences = [ +function truthy(key: string) { + const value = process.env[key]?.toLowerCase() + return value === "true" || value === "1" +} + +export function kitty() { + if (truthy("KILO_DISABLE_KITTY_KEYBOARD")) return false + if (truthy("KILO_ENABLE_KITTY_KEYBOARD")) return true + + const term = process.env.TERM_PROGRAM?.toLowerCase() + const system = process.env.MSYSTEM?.toLowerCase() + + if (term === "mintty") return false + if (system) return false + + return true +} + +export function sequences() { + return [ "\x1b[?1000l", // disable normal mouse tracking "\x1b[?1002l", // disable button-event mouse tracking "\x1b[?1003l", // disable any-event mouse tracking (all movement) "\x1b[?1006l", // disable SGR extended mouse mode "\x1b[?1015l", // disable RXVT mouse mode - "\x1b[", // disable application keypad mode + "\x1b[?66l", // disable numeric keypad application mode + "\x1b[>4;0m", // reset xterm modifyOtherKeys + ...(kitty() ? ["\x1b[ [key, process.env[key]])) as Record + +function env(input: Partial>) { + for (const key of keys) { + const value = input[key] + if (value === undefined) { + delete process.env[key] + continue + } + process.env[key] = value + } +} + +function restore() { + for (const key of keys) { + const value = saved[key] + if (value === undefined) { + delete process.env[key] + continue + } + process.env[key] = value + } +} + +afterEach(() => { + restore() +}) + +test("enables Kitty keyboard reset by default", () => { + env({}) + + expect(kitty()).toBe(true) + expect(sequences()).toContain("\x1b[ { + env({ TERM_PROGRAM: "mintty" }) + + expect(kitty()).toBe(false) + expect(sequences()).not.toContain("\x1b[ { + env({ MSYSTEM: "MINGW64" }) + + expect(kitty()).toBe(false) + expect(sequences()).not.toContain("\x1b[ { + env({ KILO_ENABLE_KITTY_KEYBOARD: "1", MSYSTEM: "MINGW64" }) + + expect(kitty()).toBe(true) + expect(sequences()).toContain("\x1b[ { + env({ KILO_DISABLE_KITTY_KEYBOARD: "1", KILO_ENABLE_KITTY_KEYBOARD: "1" }) + + expect(kitty()).toBe(false) + expect(sequences()).not.toContain("\x1b[ { + env({ KILO_DISABLE_KITTY_KEYBOARD: "1" }) + + expect(sequences()).toEqual( + expect.arrayContaining([ + "\x1b[?1000l", + "\x1b[?1002l", + "\x1b[?1003l", + "\x1b[?1006l", + "\x1b[?1015l", + "\x1b[?2004l", + "\x1b[?1004l", + "\x1b[?1l", + "\x1b>", + "\x1b[?66l", + "\x1b[>4;0m", + "\x1b[?25h", + "\x1b[0m", + ]), + ) +}) diff --git a/packages/ui/src/components/provider-icons/types.ts b/packages/ui/src/components/provider-icons/types.ts index bb8a45e660d..2f22f2e1bb6 100644 --- a/packages/ui/src/components/provider-icons/types.ts +++ b/packages/ui/src/components/provider-icons/types.ts @@ -7,12 +7,12 @@ export const iconNames = [ "zai", "zai-coding-plan", "xiaomi", - "xiaomi-token-plan-sgp", // kilocode_change - "xiaomi-token-plan-cn", // kilocode_change - "xiaomi-token-plan-ams", // kilocode_change + "xiaomi-token-plan-sgp", + "xiaomi-token-plan-cn", + "xiaomi-token-plan-ams", "xai", "wandb", - "wafer.ai", // kilocode_change + "wafer.ai", "vultr", "vivgrid", "vercel", @@ -20,8 +20,8 @@ export const iconNames = [ "v0", "upstage", "togetherai", - "the-grid-ai", // kilocode_change - "tencent-tokenhub", // kilocode_change + "the-grid-ai", + "tencent-tokenhub", "tencent-coding-plan", "synthetic", "submodel", @@ -32,7 +32,7 @@ export const iconNames = [ "scaleway", "sap-ai-core", "requesty", - "regolo-ai", // kilocode_change + "regolo-ai", "qiniu-ai", "qihang-ai", "privatemode-ai", @@ -48,7 +48,7 @@ export const iconNames = [ "nvidia", "novita-ai", "nova", - "neuralwatt", // kilocode_change + "neuralwatt", "nebius", "nano-gpt", "morph", @@ -56,7 +56,7 @@ export const iconNames = [ "moonshotai-cn", "modelscope", "moark", - "mixlayer", // kilocode_change + "mixlayer", "mistral", "minimax", "minimax-coding-plan", @@ -65,10 +65,10 @@ export const iconNames = [ "meganova", "lucidquery", "lmstudio", - "llmgateway", // kilocode_change + "llmgateway", "llama", "kuae-cloud-coding-plan", - "kiro", // kilocode_change + "kiro", "kimi-for-coding", "kilo", "jiekou", @@ -77,7 +77,7 @@ export const iconNames = [ "inception", "iflowcn", "huggingface", - "hpc-ai", // kilocode_change + "hpc-ai", "helicone", "groq", "google", @@ -86,7 +86,7 @@ export const iconNames = [ "gitlab", "github-models", "github-copilot", - "frogbot", // kilocode_change + "frogbot", "friendli", "firmware", "fireworks-ai", @@ -94,7 +94,7 @@ export const iconNames = [ "evroc", "drun", "dinference", - "digitalocean", // kilocode_change + "digitalocean", "deepseek", "deepinfra", "cortecs", @@ -117,7 +117,7 @@ export const iconNames = [ "alibaba-coding-plan-cn", "alibaba-cn", "aihubmix", - "abliteration-ai", // kilocode_change + "abliteration-ai", "abacus", "302ai", ] as const